Skip to content

Commit 579337b

Browse files
authored
Merge pull request #65 from nulllpc/npc/fetch-balance-for-multi-indices
Support fetching token balances for multiple account indices
2 parents fba56a5 + 331c747 commit 579337b

4 files changed

Lines changed: 194 additions & 171 deletions

File tree

src/hooks/useBalance.ts

Lines changed: 134 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,48 @@ export const balanceQueryKeys = {
155155
QUERY_KEY_TAGS.TOKEN,
156156
assetId,
157157
] as const,
158+
byTokensAndIndices: (walletId: string, accountIndices: number[], assetIds: string[]) => {
159+
const sortedIndices = [...accountIndices].sort((a, b) => a - b)
160+
const sortedAssetIds = [...assetIds].sort()
161+
162+
return [
163+
QUERY_KEY_TAGS.BALANCES,
164+
QUERY_KEY_TAGS.WALLET,
165+
walletId,
166+
sortedIndices,
167+
sortedAssetIds,
168+
'multi'
169+
]
170+
}
171+
}
172+
173+
async function promisePool<T>(
174+
promiseFns: (() => Promise<T>)[],
175+
concurrency = 2
176+
): Promise<T[]> {
177+
const results: T[] = new Array(promiseFns.length)
178+
let index = 0
179+
180+
const runNext = async (): Promise<void> => {
181+
if (index >= promiseFns.length) {
182+
return
183+
}
184+
185+
const currentIndex = index++
186+
const promiseFn = promiseFns[currentIndex]!
187+
188+
results[currentIndex] = await promiseFn()
189+
190+
await runNext()
191+
}
192+
193+
const workers = Array(Math.min(concurrency, promiseFns.length))
194+
.fill(null)
195+
.map(() => runNext())
196+
197+
await Promise.all(workers)
198+
199+
return results
158200
}
159201

160202
/**
@@ -283,7 +325,7 @@ export function useBalancesForWallet(
283325
error: addressesError,
284326
} = useMultiAddressLoader({
285327
networks: uniqueNetworks,
286-
accountIndex,
328+
accountIndices: [accountIndex],
287329
enabled: options?.enabled,
288330
});
289331

@@ -332,6 +374,97 @@ export function useBalancesForWallet(
332374
return { ...query, isLoading, error: error as Error | null };
333375
}
334376

377+
export type UseBalancesForWalletsResult = Omit<
378+
UseQueryResult<BalanceFetchResult[], Error>,
379+
'isLoading' | 'error'
380+
> & {
381+
isLoading: boolean;
382+
error: Error | null;
383+
};
384+
385+
export function useBalancesForWallets(
386+
accountIndices: number[],
387+
assetConfigs: IAsset[],
388+
options?: BalanceQueryOptions & { concurrencyLimit?: number }
389+
): UseBalancesForWalletsResult {
390+
const uniqueNetworks = useMemo(
391+
() => [...new Set(assetConfigs.map((asset) => asset.getNetwork()))],
392+
[assetConfigs],
393+
);
394+
395+
const {
396+
isLoading: areAddressesLoading,
397+
error: addressesError,
398+
} = useMultiAddressLoader({
399+
networks: uniqueNetworks,
400+
accountIndices: accountIndices,
401+
enabled: options?.enabled,
402+
});
403+
404+
const walletId = getWalletStore()((state) => state.activeWalletId);
405+
406+
const initialData: BalanceFetchResult[] | undefined = (() => {
407+
if (!walletId || assetConfigs.length === 0) {
408+
return undefined;
409+
}
410+
411+
return accountIndices.flatMap((accountIndex) => {
412+
return assetConfigs.map((asset) => {
413+
const balance = BalanceService.getBalance(
414+
accountIndex,
415+
asset.getNetwork(),
416+
asset.getId(),
417+
walletId,
418+
);
419+
420+
return {
421+
success: true,
422+
network: asset.getNetwork(),
423+
accountIndex: accountIndex,
424+
assetId: asset.getId(),
425+
balance,
426+
};
427+
})
428+
});
429+
})();
430+
431+
const query = useQuery({
432+
queryKey: balanceQueryKeys.byTokensAndIndices(walletId || '', accountIndices, assetConfigs.map((asset) => asset.getId())),
433+
queryFn: async () => {
434+
const concurrency = options?.concurrencyLimit ?? 2;
435+
const tasks = accountIndices.map((accountIndex) => async (): Promise<BalanceFetchResult[]> => {
436+
try {
437+
return await fetchBalances(accountIndex, assetConfigs);
438+
} catch (error) {
439+
return assetConfigs.map((asset) => ({
440+
success: false,
441+
network: asset.getNetwork(),
442+
accountIndex,
443+
assetId: asset.getId(),
444+
balance: null,
445+
error: error instanceof Error ? error.message : String(error),
446+
}));
447+
}
448+
});
449+
const results = await promisePool(tasks, concurrency);
450+
return results.flat();
451+
},
452+
enabled: isQueryEnabled(
453+
options?.enabled,
454+
!!walletId && !areAddressesLoading && assetConfigs.length > 0,
455+
),
456+
refetchInterval: options?.refetchInterval,
457+
staleTime: options?.staleTime ?? DEFAULT_QUERY_STALE_TIME_MS,
458+
gcTime: DEFAULT_QUERY_GC_TIME_MS,
459+
initialData,
460+
});
461+
462+
const isLoading = areAddressesLoading || query.isLoading;
463+
const error = addressesError || query.error;
464+
465+
return { ...query, isLoading, error: error as Error | null };
466+
}
467+
335468
/**
336469
* Invalidate balance queries based on refresh type
337470
*/

src/hooks/useMultiAddressLoader.ts

Lines changed: 41 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,19 @@
1313
// limitations under the License.
1414

1515
import { useState, useEffect } from 'react';
16-
import { AccountService } from '../services/accountService';
1716
import { getWalletStore } from '../store/walletStore';
1817
import { logError } from '../utils/logger';
18+
import { AddressService } from '../services/addressService';
1919

2020
export interface AddressResult {
2121
network: string;
22-
address: string;
22+
address: string | null;
23+
accountIndex: number;
2324
}
2425

2526
interface UseMultiAddressLoaderParams {
2627
networks: string[];
27-
accountIndex: number;
28+
accountIndices: number[];
2829
enabled?: boolean;
2930
}
3031

@@ -42,7 +43,7 @@ interface UseMultiAddressLoaderResult {
4243
*/
4344
export function useMultiAddressLoader({
4445
networks,
45-
accountIndex,
46+
accountIndices,
4647
enabled = true,
4748
}: UseMultiAddressLoaderParams): UseMultiAddressLoaderResult {
4849
const [addresses, setAddresses] = useState<AddressResult[] | null>(null);
@@ -51,13 +52,18 @@ export function useMultiAddressLoader({
5152
const activeWalletId = getWalletStore()((state) => state.activeWalletId);
5253

5354
const networksKey = JSON.stringify([...networks].sort());
55+
const activeIndices = JSON.stringify([...accountIndices].sort());
5456

5557
useEffect(() => {
58+
let isStale = false;
59+
5660
const loadAddresses = async () => {
5761
if (!enabled || networks.length === 0 || !activeWalletId) {
58-
setIsLoading(false);
59-
setError(null);
60-
setAddresses(null);
62+
if (!isStale) {
63+
setIsLoading(false);
64+
setError(null);
65+
setAddresses(null);
66+
}
6167
return;
6268
}
6369

@@ -66,40 +72,46 @@ export function useMultiAddressLoader({
6672
setAddresses(null);
6773

6874
try {
69-
const uniqueNetworks = [...new Set(networks)];
70-
const addressPromises = uniqueNetworks.map((network) =>
71-
AccountService.callAccountMethod<'getAddress'>(
72-
network,
73-
accountIndex,
74-
'getAddress'
75-
),
76-
);
77-
78-
const loadedAddresses = await Promise.all(addressPromises);
79-
80-
const addressMap = new Map<string, string>();
81-
uniqueNetworks.forEach((network, index) => {
82-
addressMap.set(network, loadedAddresses[index] as string);
83-
});
84-
85-
const finalAddresses = networks.map((network) => ({
86-
network,
87-
address: addressMap.get(network)!,
88-
}));
75+
const addressesResult = await AddressService.getAddresses(accountIndices, networks)
76+
77+
if (isStale) return;
78+
79+
const finalAddresses: AddressResult[] = addressesResult.map((addressInfo) => {
80+
if (addressInfo.success === true) {
81+
return {
82+
network: addressInfo.network,
83+
accountIndex: addressInfo.accountIndex,
84+
address: addressInfo.address
85+
}
86+
} else {
87+
return {
88+
network: addressInfo.network,
89+
accountIndex: addressInfo.accountIndex,
90+
address: null
91+
}
92+
}
93+
})
8994

9095
setAddresses(finalAddresses);
9196
} catch (e) {
97+
if (isStale) return;
9298
const err = e instanceof Error ? e : new Error('Failed to load addresses');
9399
logError('useMultiAddressLoader failed:', err);
94100
setError(err);
95101
} finally {
96-
setIsLoading(false);
102+
if (!isStale) {
103+
setIsLoading(false);
104+
}
97105
}
98106
};
99107

100108
loadAddresses();
109+
110+
return () => {
111+
isStale = true;
112+
};
101113
// eslint-disable-next-line react-hooks/exhaustive-deps
102-
}, [networksKey, accountIndex, enabled, activeWalletId]);
114+
}, [networksKey, activeIndices, enabled, activeWalletId]);
103115

104116
return { addresses, isLoading, error };
105117
}

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ export type { UseWalletManagerResult, WalletInfo } from './hooks/useWalletManage
4848
export {
4949
useBalance,
5050
useBalancesForWallet,
51+
useBalancesForWallets,
5152
useRefreshBalance,
5253
balanceQueryKeys,
5354
} from './hooks/useBalance'

0 commit comments

Comments
 (0)