mirror of
https://github.com/luxfi/explore.git
synced 2026-09-24 00:28:18 +00:00
Every never-transacted address showed `Balance Pending` instead of
`0 LUX`, permanently, on the live v1.1.23 build.
The RPC fallback was working the entire time. Captured on the wire:
eth_getBalance -> {"jsonrpc":"2.0","id":0,"result":"0x0"}. One line
disagreed. `if (!balance)` treats 0n as falsy, so a genuine zero balance
was reported as an RPC failure.
The consequence was not a broken path but a WRONG one, and the cascade
is why it looked like nothing: select() returned null -> isRpcQuery went
false and the fallback was discarded -> `query` reverted to the 404'd
apiQuery -> AddressDetails substituted error404Data (coin_balance: null)
-> AddressBalance's pruned-node guard fired. That guard's intent is
right ("coin_balance may be null while the address is known to exist,
show Pending instead of a misleading 0 LUX"); it just cannot tell a
genuine zero from a genuine failure when the caller has already
collapsed them. setRefetchEnabled(false) then made it permanent rather
than eventually-correct.
Nothing looked broken from outside: no hang, no skeleton, no error
alert, zero JS exceptions, and the counters path degraded correctly
under the very same 404 (Tokens/Transactions/Transfers all rendered 0).
Only the balance field was wrong.
`balance === null` is exact -- the type is already `bigint | null`.
Extracted the mapper as selectRpcAddress so the case is testable at all;
it shipped broken and was invisible from source. 5 tests, and they were
run against the OLD code first: reverting to `!balance` fails 3 of them.
A regression test that has never failed has not been tested.
Suite 322 -> 327.
Co-Authored-By: Hanzo Dev <[email protected]>
159 lines
5.1 KiB
TypeScript
159 lines
5.1 KiB
TypeScript
import type { UseQueryResult } from '@tanstack/react-query';
|
|
import { useQuery } from '@tanstack/react-query';
|
|
import React from 'react';
|
|
|
|
import type { Address } from 'types/api/address';
|
|
|
|
import type { ResourceError } from 'lib/api/resources';
|
|
import useApiQuery from 'lib/api/useApiQuery';
|
|
import { retry } from 'lib/api/useQueryClientConfig';
|
|
import { publicClient } from 'lib/web3/client';
|
|
import { ADDRESS_INFO } from 'stubs/address';
|
|
import { GET_BALANCE } from 'stubs/RPC';
|
|
import { SECOND } from 'toolkit/utils/consts';
|
|
|
|
type RpcResponseType = [
|
|
bigint | null,
|
|
];
|
|
|
|
export type AddressQuery = UseQueryResult<Address, ResourceError<{ status: number }>> & {
|
|
isDegradedData: boolean;
|
|
};
|
|
|
|
interface Params {
|
|
hash: string;
|
|
isEnabled?: boolean;
|
|
}
|
|
|
|
const NO_RPC_FALLBACK_ERROR_CODES = [ 403 ];
|
|
|
|
/**
|
|
* Maps the RPC fallback response to an Address, or null when the RPC could not
|
|
* answer. Exported so the zero-balance case is testable — it shipped broken and
|
|
* was invisible from source.
|
|
*/
|
|
export function selectRpcAddress(response: RpcResponseType, hash: string): Address | null {
|
|
const [ balance ] = response;
|
|
|
|
// `balance === null`, NOT `!balance`. getBalance() returns a bigint and 0n is
|
|
// FALSY, so the truthy check threw away every genuine zero balance and
|
|
// reported it as an RPC failure.
|
|
//
|
|
// The consequence was not a broken RPC path but a WRONG one: with the fallback
|
|
// discarded, `query` reverted to the 404'd apiQuery, AddressDetails substituted
|
|
// error404Data (coin_balance: null), and AddressBalance's pruned-node guard —
|
|
// "coin_balance may be null while the address is known to exist, show Pending
|
|
// instead of a misleading 0 LUX" — fired on a perfectly healthy address. Every
|
|
// never-transacted address rendered `Balance Pending` forever, and
|
|
// setRefetchEnabled(false) made it permanent rather than eventually-correct.
|
|
//
|
|
// Verified on the live v1.1.23 build: the RPC answered {"result":"0x0"}
|
|
// correctly the whole time. Only this line disagreed. The type is already
|
|
// `bigint | null`, so the null check is exact.
|
|
if (balance === null) {
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
hash,
|
|
block_number_balance_updated_at: null,
|
|
coin_balance: balance.toString(),
|
|
creator_address_hash: null,
|
|
creation_transaction_hash: null,
|
|
creation_status: null,
|
|
exchange_rate: null,
|
|
ens_domain_name: null,
|
|
has_logs: false,
|
|
has_token_transfers: false,
|
|
has_tokens: false,
|
|
has_validated_blocks: false,
|
|
implementations: null,
|
|
is_contract: false,
|
|
is_verified: false,
|
|
name: null,
|
|
token: null,
|
|
watchlist_address_id: null,
|
|
private_tags: null,
|
|
public_tags: null,
|
|
watchlist_names: null,
|
|
} as Address;
|
|
}
|
|
|
|
export default function useAddressQuery({ hash, isEnabled = true }: Params): AddressQuery {
|
|
const [ isRefetchEnabled, setRefetchEnabled ] = React.useState(false);
|
|
|
|
const apiQuery = useApiQuery<'general:address', { status: number }>('general:address', {
|
|
pathParams: { hash },
|
|
queryOptions: {
|
|
enabled: isEnabled && Boolean(hash),
|
|
placeholderData: ADDRESS_INFO,
|
|
refetchOnMount: false,
|
|
retry: (failureCount, error) => {
|
|
if (isRefetchEnabled) {
|
|
return false;
|
|
}
|
|
|
|
return retry(failureCount, error);
|
|
},
|
|
refetchInterval: (): number | false => {
|
|
return isRefetchEnabled ? 15 * SECOND : false;
|
|
},
|
|
},
|
|
});
|
|
|
|
const rpcQuery = useQuery<RpcResponseType, unknown, Address | null>({
|
|
queryKey: [ 'RPC', 'address', { hash } ],
|
|
queryFn: async() => {
|
|
if (!publicClient) {
|
|
throw new Error('No public RPC client');
|
|
}
|
|
|
|
const balance = publicClient.getBalance({ address: hash as `0x${ string }` }).catch(() => null);
|
|
|
|
return Promise.all([
|
|
balance,
|
|
]);
|
|
},
|
|
select: (response) => selectRpcAddress(response, hash),
|
|
placeholderData: [ GET_BALANCE ],
|
|
enabled: (apiQuery.isError || apiQuery.errorUpdateCount > 0) && !(apiQuery.error?.status && NO_RPC_FALLBACK_ERROR_CODES.includes(apiQuery.error?.status)),
|
|
retry: false,
|
|
refetchOnMount: false,
|
|
});
|
|
|
|
React.useEffect(() => {
|
|
if (apiQuery.isPlaceholderData || !publicClient) {
|
|
return;
|
|
}
|
|
|
|
if (apiQuery.isError && apiQuery.errorUpdateCount === 1) {
|
|
setRefetchEnabled(true);
|
|
} else if (!apiQuery.isError) {
|
|
setRefetchEnabled(false);
|
|
}
|
|
}, [ apiQuery.errorUpdateCount, apiQuery.isError, apiQuery.isPlaceholderData, apiQuery.error?.status ]);
|
|
|
|
React.useEffect(() => {
|
|
if (!rpcQuery.isPlaceholderData && !rpcQuery.data) {
|
|
setRefetchEnabled(false);
|
|
}
|
|
}, [ rpcQuery.data, rpcQuery.isPlaceholderData ]);
|
|
|
|
const isRpcQuery = Boolean(
|
|
|
|
(apiQuery.isError || apiQuery.isPlaceholderData) &&
|
|
!(apiQuery.error?.status && NO_RPC_FALLBACK_ERROR_CODES.includes(apiQuery.error?.status)) &&
|
|
!NO_RPC_FALLBACK_ERROR_CODES.includes(apiQuery.error?.status ?? 999) &&
|
|
apiQuery.errorUpdateCount > 0 &&
|
|
rpcQuery.data &&
|
|
publicClient,
|
|
);
|
|
|
|
const query = isRpcQuery ? rpcQuery as UseQueryResult<Address, ResourceError<{ status: number }>> : apiQuery;
|
|
|
|
return {
|
|
...query,
|
|
isDegradedData: isRpcQuery,
|
|
};
|
|
}
|