@talismn/balances 0.0.0-pr2075-20250714011912 → 0.0.0-pr2075-20250714084622

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -19,6 +19,9 @@ export declare class BalancesProvider {
19
19
  getBalances$(addressesByTokenId: Record<TokenId, Address[]>): Observable<BalancesResult>;
20
20
  fetchBalances(addressesByTokenId: Record<TokenId, Address[]>): Promise<IBalance[]>;
21
21
  private getNetworkBalances$;
22
+ private getPolkadotNetworkModuleBalances$;
23
+ private getEthereumNetworkModuleBalances$;
24
+ private updateStorage$;
22
25
  private getNetworkMiniMetadatas$;
23
26
  private getNetworkSpecVersion$;
24
27
  private getMiniMetadatas$;
@@ -6296,7 +6296,6 @@ class BalancesProvider {
6296
6296
  })), rxjs.distinctUntilChanged(lodashEs.isEqual));
6297
6297
  }
6298
6298
  fetchBalances(addressesByTokenId) {
6299
- // TODO: better
6300
6299
  return rxjs.firstValueFrom(this.getBalances$(addressesByTokenId).pipe(rxjs.filter(({
6301
6300
  status
6302
6301
  }) => status === "live"), rxjs.map(({
@@ -6304,103 +6303,156 @@ class BalancesProvider {
6304
6303
  }) => balances)));
6305
6304
  }
6306
6305
  getNetworkBalances$(networkId, addressesByTokenId) {
6307
- return util.getSharedObservable(`BalancesProvider.getNetorkBalances$`, {
6306
+ const network$ = this.#chaindataProvider.getNetworkById$(networkId);
6307
+ const tokensMapById$ = this.#chaindataProvider.getTokensMapById$();
6308
+ const networkBalances$ = rxjs.combineLatest([network$, tokensMapById$]).pipe(rxjs.switchMap(([network, tokensMapById]) => {
6309
+ const tokensAndAddresses = lodashEs.toPairs(addressesByTokenId).map(([tokenId, addresses]) => [tokensMapById[tokenId], addresses]);
6310
+ return rxjs.combineLatest(BALANCE_MODULES.filter(mod => mod.platform === network?.platform).map(mod => {
6311
+ const tokensWithAddresses = tokensAndAddresses.filter(([token]) => token.type === mod.type);
6312
+ switch (mod.platform) {
6313
+ case "ethereum":
6314
+ {
6315
+ return this.getEthereumNetworkModuleBalances$(networkId, tokensWithAddresses, mod);
6316
+ }
6317
+ case "polkadot":
6318
+ {
6319
+ return this.getPolkadotNetworkModuleBalances$(networkId, tokensWithAddresses, mod);
6320
+ }
6321
+ }
6322
+ }));
6323
+ }), rxjs.map(results => {
6324
+ return {
6325
+ status: results.some(({
6326
+ status
6327
+ }) => status === "initialising") ? "initialising" : "live",
6328
+ balances: results.flatMap(result => result.balances).sort(sortByBalanceId)
6329
+ };
6330
+ }), rxjs.distinctUntilChanged(lodashEs.isEqual));
6331
+
6332
+ // defer the startWith call to start with up to date balances each time the observable is re-subscribed to
6333
+ return rxjs.defer(() => networkBalances$.pipe(rxjs.startWith({
6334
+ status: "initialising",
6335
+ balances: this.getStoredBalances(addressesByTokenId)
6336
+ })));
6337
+ }
6338
+ getPolkadotNetworkModuleBalances$(networkId, tokensWithAddresses, mod) {
6339
+ return util.getSharedObservable(`BalancesProvider.getPolkadotNetworkModuleBalances$`, {
6308
6340
  networkId,
6309
- addressesByTokenId
6341
+ mod,
6342
+ tokensWithAddresses
6310
6343
  }, () => {
6311
- const network$ = this.#chaindataProvider.getNetworkById$(networkId);
6312
- const tokensMapById$ = this.#chaindataProvider.getTokensMapById$();
6313
- const miniMetadatas$ = this.getNetworkMiniMetadatas$(networkId);
6314
- return rxjs.combineLatest([network$, miniMetadatas$, tokensMapById$]).pipe(rxjs.switchMap(([network, miniMetadatas, tokensMapById]) => {
6315
- const tokensAndAddresses = lodashEs.toPairs(addressesByTokenId).map(([tokenId, addresses]) => [tokensMapById[tokenId], addresses]);
6316
- return rxjs.combineLatest(BALANCE_MODULES.filter(mod => mod.platform === network?.platform).map(mod => {
6317
- const tokensWithAddresses = tokensAndAddresses.filter(([token]) => token.type === mod.type);
6318
- const moduleAddressesByTokenId = lodashEs.fromPairs(tokensWithAddresses.map(([token, addresses]) => [token.id, addresses]));
6319
- const miniMetadata = miniMetadatas.find(m => m.source === mod.type);
6320
-
6321
- // all balance ids expected in result set
6322
- const balanceIds = lodashEs.toPairs(moduleAddressesByTokenId).flatMap(([tokenId, addresses]) => addresses.map(address => getBalanceId({
6323
- tokenId,
6324
- address
6325
- })));
6326
- const initValue = {
6327
- status: "initialising",
6328
- balances: this.getStoredBalances(moduleAddressesByTokenId)
6329
- };
6344
+ if (!tokensWithAddresses.length) return rxjs.of({
6345
+ status: "live",
6346
+ balances: []
6347
+ });
6348
+ const moduleAddressesByTokenId = lodashEs.fromPairs(tokensWithAddresses.map(([token, addresses]) => [token.id, addresses]));
6330
6349
 
6331
- // updating storage has to be done on a per-module basis, so we know which balances can be deleted
6332
- const updateStorage = results => {
6333
- if (results.status !== "live") return;
6334
- const storage = this.#storage.getValue();
6335
- const balances = lodashEs.assign({}, storage.balances,
6336
- // delete all balances expected in the result set. because if they are not present it means they are empty.
6337
- lodashEs.fromPairs(balanceIds.map(balanceId => [balanceId, undefined])), lodashEs.keyBy(
6338
- // storage balances must have status "cache", because they are used as start value when initialising subsequent subscriptions
6339
- results.balances.map(b => ({
6340
- ...b,
6341
- status: "cache"
6342
- })), b => getBalanceId(b)));
6343
- this.#storage.next(lodashEs.assign({}, storage, {
6344
- balances
6345
- }));
6346
- };
6347
- switch (mod.platform) {
6348
- case "ethereum":
6349
- {
6350
- if (!this.#chainConnectors.evm) return rxjs.of(initValue);
6351
- return mod.subscribeBalances({
6352
- networkId,
6353
- tokensWithAddresses,
6354
- connector: this.#chainConnectors.evm
6355
- }).pipe(rxjs.catchError(() => rxjs.EMPTY),
6356
- // don't emit, let provider mark balances stale
6357
- rxjs.map(results => ({
6358
- status: "live",
6359
- // exclude zero balances
6360
- balances: results.success.filter(b => new Balance(b).total.planck > 0n)
6361
- })), rxjs.tap(updateStorage), rxjs.startWith(initValue));
6362
- }
6363
- case "polkadot":
6364
- if (!this.#chainConnectors.substrate || !miniMetadata) {
6365
- log.debug("[balances] no substrate connector or miniMetadata for polkadot", mod.type);
6366
- return rxjs.of(initValue);
6367
- }
6368
- return mod.subscribeBalances({
6369
- networkId,
6370
- tokensWithAddresses,
6371
- connector: this.#chainConnectors.substrate,
6372
- miniMetadata: miniMetadata
6373
- }).pipe(rxjs.catchError(() => rxjs.EMPTY),
6374
- // don't emit, let provider mark balances stale
6375
- rxjs.map(results => ({
6376
- status: "live",
6377
- // exclude zero balances
6378
- balances: results.success.filter(b => new Balance(b).total.planck > 0n)
6379
- })), rxjs.tap(updateStorage), rxjs.startWith(initValue));
6380
- }
6350
+ // all balance ids expected in result set
6351
+ const balanceIds = lodashEs.toPairs(moduleAddressesByTokenId).flatMap(([tokenId, addresses]) => addresses.map(address => getBalanceId({
6352
+ tokenId,
6353
+ address
6354
+ })));
6355
+ if (!this.#chainConnectors.substrate) {
6356
+ log.debug("[balances] no substrate connector or miniMetadata for module", mod.type);
6357
+ return rxjs.defer(() => rxjs.of({
6358
+ status: "initialising",
6359
+ balances: this.getStoredBalances(moduleAddressesByTokenId)
6381
6360
  }));
6382
- }), rxjs.map(results => {
6383
- return {
6384
- status: results.some(({
6385
- status
6386
- }) => status === "initialising") ? "initialising" : "live",
6387
- balances: results.flatMap(result => result.balances).sort(sortByBalanceId)
6388
- };
6389
- }), rxjs.distinctUntilChanged(lodashEs.isEqual), rxjs.startWith({
6361
+ }
6362
+ const moduleBalances$ = this.getNetworkMiniMetadatas$(networkId).pipe(rxjs.map(miniMetadatas => miniMetadatas.find(m => m.source === mod.type)), rxjs.switchMap(miniMetadata => mod.subscribeBalances({
6363
+ networkId,
6364
+ tokensWithAddresses,
6365
+ connector: this.#chainConnectors.substrate,
6366
+ miniMetadata: miniMetadata
6367
+ })), rxjs.catchError(() => rxjs.EMPTY),
6368
+ // don't emit, let provider mark balances stale
6369
+ rxjs.map(results => ({
6370
+ status: "live",
6371
+ // exclude zero balances
6372
+ balances: results.success.filter(b => new Balance(b).total.planck > 0n)
6373
+ })), rxjs.tap(results => {
6374
+ this.updateStorage$(balanceIds, results);
6375
+ }), rxjs.shareReplay({
6376
+ refCount: true,
6377
+ bufferSize: 1
6378
+ }), util.keepAlive(0));
6379
+
6380
+ // defer the startWith call to start with up to date balances each time the observable is re-subscribed to
6381
+ return rxjs.defer(() => moduleBalances$.pipe(rxjs.startWith({
6390
6382
  status: "initialising",
6391
- balances: this.getStoredBalances(addressesByTokenId)
6392
- }),
6393
- // shareReplay + keepAlive allow for this network subscription to not restart as long as the inputs don't change
6394
- // for example if another network is enabled/disabled, we don't want this subscription to be restarted
6395
- // the unsubscribe/resubscribe is instantaneous when parameters parameters change, so keepAlive 0ms does the job
6396
- rxjs.shareReplay({
6383
+ balances: this.getStoredBalances(moduleAddressesByTokenId)
6384
+ })));
6385
+ });
6386
+ }
6387
+ getEthereumNetworkModuleBalances$(networkId, tokensWithAddresses, mod) {
6388
+ return util.getSharedObservable(`BalancesProvider.getEthereumNetworkModuleBalances$`, {
6389
+ networkId,
6390
+ mod,
6391
+ tokensWithAddresses
6392
+ }, () => {
6393
+ if (!tokensWithAddresses.length) return rxjs.of({
6394
+ status: "live",
6395
+ balances: []
6396
+ });
6397
+ const moduleAddressesByTokenId = lodashEs.fromPairs(tokensWithAddresses.map(([token, addresses]) => [token.id, addresses]));
6398
+
6399
+ // all balance ids expected in result set
6400
+ const balanceIds = lodashEs.toPairs(moduleAddressesByTokenId).flatMap(([tokenId, addresses]) => addresses.map(address => getBalanceId({
6401
+ tokenId,
6402
+ address
6403
+ })));
6404
+ if (!this.#chainConnectors.evm) {
6405
+ log.debug("[balances] no ethereum connector for module", mod.type);
6406
+ return rxjs.defer(() => rxjs.of({
6407
+ status: "initialising",
6408
+ balances: this.getStoredBalances(moduleAddressesByTokenId)
6409
+ }));
6410
+ }
6411
+ const moduleBalances$ = mod.subscribeBalances({
6412
+ networkId,
6413
+ tokensWithAddresses,
6414
+ connector: this.#chainConnectors.evm
6415
+ }).pipe(rxjs.catchError(() => rxjs.EMPTY),
6416
+ // don't emit, let provider mark balances stale
6417
+ rxjs.map(results => ({
6418
+ status: "live",
6419
+ // exclude zero balances
6420
+ balances: results.success.filter(b => new Balance(b).total.planck > 0n)
6421
+ })), rxjs.tap(results => {
6422
+ this.updateStorage$(balanceIds, results);
6423
+ }), rxjs.shareReplay({
6397
6424
  refCount: true,
6398
6425
  bufferSize: 1
6399
6426
  }), util.keepAlive(0));
6427
+
6428
+ // defer the startWith call to start with up to date balances each time the observable is re-subscribed to
6429
+ return rxjs.defer(() => moduleBalances$.pipe(rxjs.startWith({
6430
+ status: "initialising",
6431
+ balances: this.getStoredBalances(moduleAddressesByTokenId)
6432
+ })));
6400
6433
  });
6401
6434
  }
6435
+ updateStorage$(balanceIds, balancesResult) {
6436
+ if (balancesResult.status !== "live") return;
6437
+ const storage = this.#storage.getValue();
6438
+ const balances = lodashEs.assign({}, storage.balances,
6439
+ // delete all balances expected in the result set. because if they are not present it means they are empty.
6440
+ lodashEs.fromPairs(balanceIds.map(balanceId => [balanceId, undefined])), lodashEs.keyBy(
6441
+ // storage balances must have status "cache", because they are used as start value when initialising subsequent subscriptions
6442
+ balancesResult.balances.map(b => ({
6443
+ ...b,
6444
+ status: "cache"
6445
+ })), b => getBalanceId(b)));
6446
+ this.#storage.next(lodashEs.assign({}, storage, {
6447
+ balances
6448
+ }));
6449
+ }
6402
6450
  getNetworkMiniMetadatas$(networkId) {
6403
- return this.#chaindataProvider.getNetworkById$(networkId).pipe(rxjs.switchMap(network => chaindataProvider.isNetworkDot(network) ? this.getNetworkSpecVersion$(networkId).pipe(rxjs.switchMap(specVersion => specVersion === null ? rxjs.of([]) : this.getMiniMetadatas$(networkId, specVersion))) : rxjs.of([])), rxjs.distinctUntilChanged(lodashEs.isEqual));
6451
+ return util.getSharedObservable(`BalancesProvider.getNetworkMiniMetadatas$`, {
6452
+ networkId
6453
+ }, () => {
6454
+ return this.#chaindataProvider.getNetworkById$(networkId).pipe(rxjs.switchMap(network => chaindataProvider.isNetworkDot(network) ? this.getNetworkSpecVersion$(networkId).pipe(rxjs.switchMap(specVersion => specVersion === null ? rxjs.of([]) : this.getMiniMetadatas$(networkId, specVersion))) : rxjs.of([])), rxjs.distinctUntilChanged(lodashEs.isEqual));
6455
+ });
6404
6456
  }
6405
6457
  getNetworkSpecVersion$(networkId) {
6406
6458
  return rxjs.from(viem.withRetry(() => getSpecVersion(this.#chainConnectors.substrate, networkId), {
@@ -6296,7 +6296,6 @@ class BalancesProvider {
6296
6296
  })), rxjs.distinctUntilChanged(lodashEs.isEqual));
6297
6297
  }
6298
6298
  fetchBalances(addressesByTokenId) {
6299
- // TODO: better
6300
6299
  return rxjs.firstValueFrom(this.getBalances$(addressesByTokenId).pipe(rxjs.filter(({
6301
6300
  status
6302
6301
  }) => status === "live"), rxjs.map(({
@@ -6304,103 +6303,156 @@ class BalancesProvider {
6304
6303
  }) => balances)));
6305
6304
  }
6306
6305
  getNetworkBalances$(networkId, addressesByTokenId) {
6307
- return util.getSharedObservable(`BalancesProvider.getNetorkBalances$`, {
6306
+ const network$ = this.#chaindataProvider.getNetworkById$(networkId);
6307
+ const tokensMapById$ = this.#chaindataProvider.getTokensMapById$();
6308
+ const networkBalances$ = rxjs.combineLatest([network$, tokensMapById$]).pipe(rxjs.switchMap(([network, tokensMapById]) => {
6309
+ const tokensAndAddresses = lodashEs.toPairs(addressesByTokenId).map(([tokenId, addresses]) => [tokensMapById[tokenId], addresses]);
6310
+ return rxjs.combineLatest(BALANCE_MODULES.filter(mod => mod.platform === network?.platform).map(mod => {
6311
+ const tokensWithAddresses = tokensAndAddresses.filter(([token]) => token.type === mod.type);
6312
+ switch (mod.platform) {
6313
+ case "ethereum":
6314
+ {
6315
+ return this.getEthereumNetworkModuleBalances$(networkId, tokensWithAddresses, mod);
6316
+ }
6317
+ case "polkadot":
6318
+ {
6319
+ return this.getPolkadotNetworkModuleBalances$(networkId, tokensWithAddresses, mod);
6320
+ }
6321
+ }
6322
+ }));
6323
+ }), rxjs.map(results => {
6324
+ return {
6325
+ status: results.some(({
6326
+ status
6327
+ }) => status === "initialising") ? "initialising" : "live",
6328
+ balances: results.flatMap(result => result.balances).sort(sortByBalanceId)
6329
+ };
6330
+ }), rxjs.distinctUntilChanged(lodashEs.isEqual));
6331
+
6332
+ // defer the startWith call to start with up to date balances each time the observable is re-subscribed to
6333
+ return rxjs.defer(() => networkBalances$.pipe(rxjs.startWith({
6334
+ status: "initialising",
6335
+ balances: this.getStoredBalances(addressesByTokenId)
6336
+ })));
6337
+ }
6338
+ getPolkadotNetworkModuleBalances$(networkId, tokensWithAddresses, mod) {
6339
+ return util.getSharedObservable(`BalancesProvider.getPolkadotNetworkModuleBalances$`, {
6308
6340
  networkId,
6309
- addressesByTokenId
6341
+ mod,
6342
+ tokensWithAddresses
6310
6343
  }, () => {
6311
- const network$ = this.#chaindataProvider.getNetworkById$(networkId);
6312
- const tokensMapById$ = this.#chaindataProvider.getTokensMapById$();
6313
- const miniMetadatas$ = this.getNetworkMiniMetadatas$(networkId);
6314
- return rxjs.combineLatest([network$, miniMetadatas$, tokensMapById$]).pipe(rxjs.switchMap(([network, miniMetadatas, tokensMapById]) => {
6315
- const tokensAndAddresses = lodashEs.toPairs(addressesByTokenId).map(([tokenId, addresses]) => [tokensMapById[tokenId], addresses]);
6316
- return rxjs.combineLatest(BALANCE_MODULES.filter(mod => mod.platform === network?.platform).map(mod => {
6317
- const tokensWithAddresses = tokensAndAddresses.filter(([token]) => token.type === mod.type);
6318
- const moduleAddressesByTokenId = lodashEs.fromPairs(tokensWithAddresses.map(([token, addresses]) => [token.id, addresses]));
6319
- const miniMetadata = miniMetadatas.find(m => m.source === mod.type);
6320
-
6321
- // all balance ids expected in result set
6322
- const balanceIds = lodashEs.toPairs(moduleAddressesByTokenId).flatMap(([tokenId, addresses]) => addresses.map(address => getBalanceId({
6323
- tokenId,
6324
- address
6325
- })));
6326
- const initValue = {
6327
- status: "initialising",
6328
- balances: this.getStoredBalances(moduleAddressesByTokenId)
6329
- };
6344
+ if (!tokensWithAddresses.length) return rxjs.of({
6345
+ status: "live",
6346
+ balances: []
6347
+ });
6348
+ const moduleAddressesByTokenId = lodashEs.fromPairs(tokensWithAddresses.map(([token, addresses]) => [token.id, addresses]));
6330
6349
 
6331
- // updating storage has to be done on a per-module basis, so we know which balances can be deleted
6332
- const updateStorage = results => {
6333
- if (results.status !== "live") return;
6334
- const storage = this.#storage.getValue();
6335
- const balances = lodashEs.assign({}, storage.balances,
6336
- // delete all balances expected in the result set. because if they are not present it means they are empty.
6337
- lodashEs.fromPairs(balanceIds.map(balanceId => [balanceId, undefined])), lodashEs.keyBy(
6338
- // storage balances must have status "cache", because they are used as start value when initialising subsequent subscriptions
6339
- results.balances.map(b => ({
6340
- ...b,
6341
- status: "cache"
6342
- })), b => getBalanceId(b)));
6343
- this.#storage.next(lodashEs.assign({}, storage, {
6344
- balances
6345
- }));
6346
- };
6347
- switch (mod.platform) {
6348
- case "ethereum":
6349
- {
6350
- if (!this.#chainConnectors.evm) return rxjs.of(initValue);
6351
- return mod.subscribeBalances({
6352
- networkId,
6353
- tokensWithAddresses,
6354
- connector: this.#chainConnectors.evm
6355
- }).pipe(rxjs.catchError(() => rxjs.EMPTY),
6356
- // don't emit, let provider mark balances stale
6357
- rxjs.map(results => ({
6358
- status: "live",
6359
- // exclude zero balances
6360
- balances: results.success.filter(b => new Balance(b).total.planck > 0n)
6361
- })), rxjs.tap(updateStorage), rxjs.startWith(initValue));
6362
- }
6363
- case "polkadot":
6364
- if (!this.#chainConnectors.substrate || !miniMetadata) {
6365
- log.debug("[balances] no substrate connector or miniMetadata for polkadot", mod.type);
6366
- return rxjs.of(initValue);
6367
- }
6368
- return mod.subscribeBalances({
6369
- networkId,
6370
- tokensWithAddresses,
6371
- connector: this.#chainConnectors.substrate,
6372
- miniMetadata: miniMetadata
6373
- }).pipe(rxjs.catchError(() => rxjs.EMPTY),
6374
- // don't emit, let provider mark balances stale
6375
- rxjs.map(results => ({
6376
- status: "live",
6377
- // exclude zero balances
6378
- balances: results.success.filter(b => new Balance(b).total.planck > 0n)
6379
- })), rxjs.tap(updateStorage), rxjs.startWith(initValue));
6380
- }
6350
+ // all balance ids expected in result set
6351
+ const balanceIds = lodashEs.toPairs(moduleAddressesByTokenId).flatMap(([tokenId, addresses]) => addresses.map(address => getBalanceId({
6352
+ tokenId,
6353
+ address
6354
+ })));
6355
+ if (!this.#chainConnectors.substrate) {
6356
+ log.debug("[balances] no substrate connector or miniMetadata for module", mod.type);
6357
+ return rxjs.defer(() => rxjs.of({
6358
+ status: "initialising",
6359
+ balances: this.getStoredBalances(moduleAddressesByTokenId)
6381
6360
  }));
6382
- }), rxjs.map(results => {
6383
- return {
6384
- status: results.some(({
6385
- status
6386
- }) => status === "initialising") ? "initialising" : "live",
6387
- balances: results.flatMap(result => result.balances).sort(sortByBalanceId)
6388
- };
6389
- }), rxjs.distinctUntilChanged(lodashEs.isEqual), rxjs.startWith({
6361
+ }
6362
+ const moduleBalances$ = this.getNetworkMiniMetadatas$(networkId).pipe(rxjs.map(miniMetadatas => miniMetadatas.find(m => m.source === mod.type)), rxjs.switchMap(miniMetadata => mod.subscribeBalances({
6363
+ networkId,
6364
+ tokensWithAddresses,
6365
+ connector: this.#chainConnectors.substrate,
6366
+ miniMetadata: miniMetadata
6367
+ })), rxjs.catchError(() => rxjs.EMPTY),
6368
+ // don't emit, let provider mark balances stale
6369
+ rxjs.map(results => ({
6370
+ status: "live",
6371
+ // exclude zero balances
6372
+ balances: results.success.filter(b => new Balance(b).total.planck > 0n)
6373
+ })), rxjs.tap(results => {
6374
+ this.updateStorage$(balanceIds, results);
6375
+ }), rxjs.shareReplay({
6376
+ refCount: true,
6377
+ bufferSize: 1
6378
+ }), util.keepAlive(0));
6379
+
6380
+ // defer the startWith call to start with up to date balances each time the observable is re-subscribed to
6381
+ return rxjs.defer(() => moduleBalances$.pipe(rxjs.startWith({
6390
6382
  status: "initialising",
6391
- balances: this.getStoredBalances(addressesByTokenId)
6392
- }),
6393
- // shareReplay + keepAlive allow for this network subscription to not restart as long as the inputs don't change
6394
- // for example if another network is enabled/disabled, we don't want this subscription to be restarted
6395
- // the unsubscribe/resubscribe is instantaneous when parameters parameters change, so keepAlive 0ms does the job
6396
- rxjs.shareReplay({
6383
+ balances: this.getStoredBalances(moduleAddressesByTokenId)
6384
+ })));
6385
+ });
6386
+ }
6387
+ getEthereumNetworkModuleBalances$(networkId, tokensWithAddresses, mod) {
6388
+ return util.getSharedObservable(`BalancesProvider.getEthereumNetworkModuleBalances$`, {
6389
+ networkId,
6390
+ mod,
6391
+ tokensWithAddresses
6392
+ }, () => {
6393
+ if (!tokensWithAddresses.length) return rxjs.of({
6394
+ status: "live",
6395
+ balances: []
6396
+ });
6397
+ const moduleAddressesByTokenId = lodashEs.fromPairs(tokensWithAddresses.map(([token, addresses]) => [token.id, addresses]));
6398
+
6399
+ // all balance ids expected in result set
6400
+ const balanceIds = lodashEs.toPairs(moduleAddressesByTokenId).flatMap(([tokenId, addresses]) => addresses.map(address => getBalanceId({
6401
+ tokenId,
6402
+ address
6403
+ })));
6404
+ if (!this.#chainConnectors.evm) {
6405
+ log.debug("[balances] no ethereum connector for module", mod.type);
6406
+ return rxjs.defer(() => rxjs.of({
6407
+ status: "initialising",
6408
+ balances: this.getStoredBalances(moduleAddressesByTokenId)
6409
+ }));
6410
+ }
6411
+ const moduleBalances$ = mod.subscribeBalances({
6412
+ networkId,
6413
+ tokensWithAddresses,
6414
+ connector: this.#chainConnectors.evm
6415
+ }).pipe(rxjs.catchError(() => rxjs.EMPTY),
6416
+ // don't emit, let provider mark balances stale
6417
+ rxjs.map(results => ({
6418
+ status: "live",
6419
+ // exclude zero balances
6420
+ balances: results.success.filter(b => new Balance(b).total.planck > 0n)
6421
+ })), rxjs.tap(results => {
6422
+ this.updateStorage$(balanceIds, results);
6423
+ }), rxjs.shareReplay({
6397
6424
  refCount: true,
6398
6425
  bufferSize: 1
6399
6426
  }), util.keepAlive(0));
6427
+
6428
+ // defer the startWith call to start with up to date balances each time the observable is re-subscribed to
6429
+ return rxjs.defer(() => moduleBalances$.pipe(rxjs.startWith({
6430
+ status: "initialising",
6431
+ balances: this.getStoredBalances(moduleAddressesByTokenId)
6432
+ })));
6400
6433
  });
6401
6434
  }
6435
+ updateStorage$(balanceIds, balancesResult) {
6436
+ if (balancesResult.status !== "live") return;
6437
+ const storage = this.#storage.getValue();
6438
+ const balances = lodashEs.assign({}, storage.balances,
6439
+ // delete all balances expected in the result set. because if they are not present it means they are empty.
6440
+ lodashEs.fromPairs(balanceIds.map(balanceId => [balanceId, undefined])), lodashEs.keyBy(
6441
+ // storage balances must have status "cache", because they are used as start value when initialising subsequent subscriptions
6442
+ balancesResult.balances.map(b => ({
6443
+ ...b,
6444
+ status: "cache"
6445
+ })), b => getBalanceId(b)));
6446
+ this.#storage.next(lodashEs.assign({}, storage, {
6447
+ balances
6448
+ }));
6449
+ }
6402
6450
  getNetworkMiniMetadatas$(networkId) {
6403
- return this.#chaindataProvider.getNetworkById$(networkId).pipe(rxjs.switchMap(network => chaindataProvider.isNetworkDot(network) ? this.getNetworkSpecVersion$(networkId).pipe(rxjs.switchMap(specVersion => specVersion === null ? rxjs.of([]) : this.getMiniMetadatas$(networkId, specVersion))) : rxjs.of([])), rxjs.distinctUntilChanged(lodashEs.isEqual));
6451
+ return util.getSharedObservable(`BalancesProvider.getNetworkMiniMetadatas$`, {
6452
+ networkId
6453
+ }, () => {
6454
+ return this.#chaindataProvider.getNetworkById$(networkId).pipe(rxjs.switchMap(network => chaindataProvider.isNetworkDot(network) ? this.getNetworkSpecVersion$(networkId).pipe(rxjs.switchMap(specVersion => specVersion === null ? rxjs.of([]) : this.getMiniMetadatas$(networkId, specVersion))) : rxjs.of([])), rxjs.distinctUntilChanged(lodashEs.isEqual));
6455
+ });
6404
6456
  }
6405
6457
  getNetworkSpecVersion$(networkId) {
6406
6458
  return rxjs.from(viem.withRetry(() => getSpecVersion(this.#chainConnectors.substrate, networkId), {
@@ -5,7 +5,7 @@ import { parseAbi, erc20Abi, getContract, ContractFunctionExecutionError, hexToS
5
5
  import { assign, omit, isEqual, uniq, keyBy, keys, fromPairs, values, toPairs } from 'lodash-es';
6
6
  import z from 'zod/v4';
7
7
  import anylogger from 'anylogger';
8
- import { of, Observable, distinctUntilChanged, map, timer, switchMap, from, firstValueFrom, combineLatest, BehaviorSubject, shareReplay, startWith, filter, catchError, EMPTY, tap } from 'rxjs';
8
+ import { of, Observable, distinctUntilChanged, map, timer, switchMap, from, firstValueFrom, combineLatest, BehaviorSubject, shareReplay, startWith, filter, defer, catchError, EMPTY, tap } from 'rxjs';
9
9
  import BigNumber from 'bignumber.js';
10
10
  import { parseMetadataRpc, toHex, unifyMetadata, decAnyMetadata, getDynamicBuilder, getLookupFn, decodeScale, getStorageKeyPrefix, Twox128, compactMetadata, encodeMetadata, papiParse, papiStringify } from '@talismn/scale';
11
11
  import { newTokenRates } from '@talismn/token-rates';
@@ -6287,7 +6287,6 @@ class BalancesProvider {
6287
6287
  })), distinctUntilChanged(isEqual));
6288
6288
  }
6289
6289
  fetchBalances(addressesByTokenId) {
6290
- // TODO: better
6291
6290
  return firstValueFrom(this.getBalances$(addressesByTokenId).pipe(filter(({
6292
6291
  status
6293
6292
  }) => status === "live"), map(({
@@ -6295,103 +6294,156 @@ class BalancesProvider {
6295
6294
  }) => balances)));
6296
6295
  }
6297
6296
  getNetworkBalances$(networkId, addressesByTokenId) {
6298
- return getSharedObservable(`BalancesProvider.getNetorkBalances$`, {
6297
+ const network$ = this.#chaindataProvider.getNetworkById$(networkId);
6298
+ const tokensMapById$ = this.#chaindataProvider.getTokensMapById$();
6299
+ const networkBalances$ = combineLatest([network$, tokensMapById$]).pipe(switchMap(([network, tokensMapById]) => {
6300
+ const tokensAndAddresses = toPairs(addressesByTokenId).map(([tokenId, addresses]) => [tokensMapById[tokenId], addresses]);
6301
+ return combineLatest(BALANCE_MODULES.filter(mod => mod.platform === network?.platform).map(mod => {
6302
+ const tokensWithAddresses = tokensAndAddresses.filter(([token]) => token.type === mod.type);
6303
+ switch (mod.platform) {
6304
+ case "ethereum":
6305
+ {
6306
+ return this.getEthereumNetworkModuleBalances$(networkId, tokensWithAddresses, mod);
6307
+ }
6308
+ case "polkadot":
6309
+ {
6310
+ return this.getPolkadotNetworkModuleBalances$(networkId, tokensWithAddresses, mod);
6311
+ }
6312
+ }
6313
+ }));
6314
+ }), map(results => {
6315
+ return {
6316
+ status: results.some(({
6317
+ status
6318
+ }) => status === "initialising") ? "initialising" : "live",
6319
+ balances: results.flatMap(result => result.balances).sort(sortByBalanceId)
6320
+ };
6321
+ }), distinctUntilChanged(isEqual));
6322
+
6323
+ // defer the startWith call to start with up to date balances each time the observable is re-subscribed to
6324
+ return defer(() => networkBalances$.pipe(startWith({
6325
+ status: "initialising",
6326
+ balances: this.getStoredBalances(addressesByTokenId)
6327
+ })));
6328
+ }
6329
+ getPolkadotNetworkModuleBalances$(networkId, tokensWithAddresses, mod) {
6330
+ return getSharedObservable(`BalancesProvider.getPolkadotNetworkModuleBalances$`, {
6299
6331
  networkId,
6300
- addressesByTokenId
6332
+ mod,
6333
+ tokensWithAddresses
6301
6334
  }, () => {
6302
- const network$ = this.#chaindataProvider.getNetworkById$(networkId);
6303
- const tokensMapById$ = this.#chaindataProvider.getTokensMapById$();
6304
- const miniMetadatas$ = this.getNetworkMiniMetadatas$(networkId);
6305
- return combineLatest([network$, miniMetadatas$, tokensMapById$]).pipe(switchMap(([network, miniMetadatas, tokensMapById]) => {
6306
- const tokensAndAddresses = toPairs(addressesByTokenId).map(([tokenId, addresses]) => [tokensMapById[tokenId], addresses]);
6307
- return combineLatest(BALANCE_MODULES.filter(mod => mod.platform === network?.platform).map(mod => {
6308
- const tokensWithAddresses = tokensAndAddresses.filter(([token]) => token.type === mod.type);
6309
- const moduleAddressesByTokenId = fromPairs(tokensWithAddresses.map(([token, addresses]) => [token.id, addresses]));
6310
- const miniMetadata = miniMetadatas.find(m => m.source === mod.type);
6311
-
6312
- // all balance ids expected in result set
6313
- const balanceIds = toPairs(moduleAddressesByTokenId).flatMap(([tokenId, addresses]) => addresses.map(address => getBalanceId({
6314
- tokenId,
6315
- address
6316
- })));
6317
- const initValue = {
6318
- status: "initialising",
6319
- balances: this.getStoredBalances(moduleAddressesByTokenId)
6320
- };
6335
+ if (!tokensWithAddresses.length) return of({
6336
+ status: "live",
6337
+ balances: []
6338
+ });
6339
+ const moduleAddressesByTokenId = fromPairs(tokensWithAddresses.map(([token, addresses]) => [token.id, addresses]));
6321
6340
 
6322
- // updating storage has to be done on a per-module basis, so we know which balances can be deleted
6323
- const updateStorage = results => {
6324
- if (results.status !== "live") return;
6325
- const storage = this.#storage.getValue();
6326
- const balances = assign({}, storage.balances,
6327
- // delete all balances expected in the result set. because if they are not present it means they are empty.
6328
- fromPairs(balanceIds.map(balanceId => [balanceId, undefined])), keyBy(
6329
- // storage balances must have status "cache", because they are used as start value when initialising subsequent subscriptions
6330
- results.balances.map(b => ({
6331
- ...b,
6332
- status: "cache"
6333
- })), b => getBalanceId(b)));
6334
- this.#storage.next(assign({}, storage, {
6335
- balances
6336
- }));
6337
- };
6338
- switch (mod.platform) {
6339
- case "ethereum":
6340
- {
6341
- if (!this.#chainConnectors.evm) return of(initValue);
6342
- return mod.subscribeBalances({
6343
- networkId,
6344
- tokensWithAddresses,
6345
- connector: this.#chainConnectors.evm
6346
- }).pipe(catchError(() => EMPTY),
6347
- // don't emit, let provider mark balances stale
6348
- map(results => ({
6349
- status: "live",
6350
- // exclude zero balances
6351
- balances: results.success.filter(b => new Balance(b).total.planck > 0n)
6352
- })), tap(updateStorage), startWith(initValue));
6353
- }
6354
- case "polkadot":
6355
- if (!this.#chainConnectors.substrate || !miniMetadata) {
6356
- log.debug("[balances] no substrate connector or miniMetadata for polkadot", mod.type);
6357
- return of(initValue);
6358
- }
6359
- return mod.subscribeBalances({
6360
- networkId,
6361
- tokensWithAddresses,
6362
- connector: this.#chainConnectors.substrate,
6363
- miniMetadata: miniMetadata
6364
- }).pipe(catchError(() => EMPTY),
6365
- // don't emit, let provider mark balances stale
6366
- map(results => ({
6367
- status: "live",
6368
- // exclude zero balances
6369
- balances: results.success.filter(b => new Balance(b).total.planck > 0n)
6370
- })), tap(updateStorage), startWith(initValue));
6371
- }
6341
+ // all balance ids expected in result set
6342
+ const balanceIds = toPairs(moduleAddressesByTokenId).flatMap(([tokenId, addresses]) => addresses.map(address => getBalanceId({
6343
+ tokenId,
6344
+ address
6345
+ })));
6346
+ if (!this.#chainConnectors.substrate) {
6347
+ log.debug("[balances] no substrate connector or miniMetadata for module", mod.type);
6348
+ return defer(() => of({
6349
+ status: "initialising",
6350
+ balances: this.getStoredBalances(moduleAddressesByTokenId)
6372
6351
  }));
6373
- }), map(results => {
6374
- return {
6375
- status: results.some(({
6376
- status
6377
- }) => status === "initialising") ? "initialising" : "live",
6378
- balances: results.flatMap(result => result.balances).sort(sortByBalanceId)
6379
- };
6380
- }), distinctUntilChanged(isEqual), startWith({
6352
+ }
6353
+ const moduleBalances$ = this.getNetworkMiniMetadatas$(networkId).pipe(map(miniMetadatas => miniMetadatas.find(m => m.source === mod.type)), switchMap(miniMetadata => mod.subscribeBalances({
6354
+ networkId,
6355
+ tokensWithAddresses,
6356
+ connector: this.#chainConnectors.substrate,
6357
+ miniMetadata: miniMetadata
6358
+ })), catchError(() => EMPTY),
6359
+ // don't emit, let provider mark balances stale
6360
+ map(results => ({
6361
+ status: "live",
6362
+ // exclude zero balances
6363
+ balances: results.success.filter(b => new Balance(b).total.planck > 0n)
6364
+ })), tap(results => {
6365
+ this.updateStorage$(balanceIds, results);
6366
+ }), shareReplay({
6367
+ refCount: true,
6368
+ bufferSize: 1
6369
+ }), keepAlive(0));
6370
+
6371
+ // defer the startWith call to start with up to date balances each time the observable is re-subscribed to
6372
+ return defer(() => moduleBalances$.pipe(startWith({
6381
6373
  status: "initialising",
6382
- balances: this.getStoredBalances(addressesByTokenId)
6383
- }),
6384
- // shareReplay + keepAlive allow for this network subscription to not restart as long as the inputs don't change
6385
- // for example if another network is enabled/disabled, we don't want this subscription to be restarted
6386
- // the unsubscribe/resubscribe is instantaneous when parameters parameters change, so keepAlive 0ms does the job
6387
- shareReplay({
6374
+ balances: this.getStoredBalances(moduleAddressesByTokenId)
6375
+ })));
6376
+ });
6377
+ }
6378
+ getEthereumNetworkModuleBalances$(networkId, tokensWithAddresses, mod) {
6379
+ return getSharedObservable(`BalancesProvider.getEthereumNetworkModuleBalances$`, {
6380
+ networkId,
6381
+ mod,
6382
+ tokensWithAddresses
6383
+ }, () => {
6384
+ if (!tokensWithAddresses.length) return of({
6385
+ status: "live",
6386
+ balances: []
6387
+ });
6388
+ const moduleAddressesByTokenId = fromPairs(tokensWithAddresses.map(([token, addresses]) => [token.id, addresses]));
6389
+
6390
+ // all balance ids expected in result set
6391
+ const balanceIds = toPairs(moduleAddressesByTokenId).flatMap(([tokenId, addresses]) => addresses.map(address => getBalanceId({
6392
+ tokenId,
6393
+ address
6394
+ })));
6395
+ if (!this.#chainConnectors.evm) {
6396
+ log.debug("[balances] no ethereum connector for module", mod.type);
6397
+ return defer(() => of({
6398
+ status: "initialising",
6399
+ balances: this.getStoredBalances(moduleAddressesByTokenId)
6400
+ }));
6401
+ }
6402
+ const moduleBalances$ = mod.subscribeBalances({
6403
+ networkId,
6404
+ tokensWithAddresses,
6405
+ connector: this.#chainConnectors.evm
6406
+ }).pipe(catchError(() => EMPTY),
6407
+ // don't emit, let provider mark balances stale
6408
+ map(results => ({
6409
+ status: "live",
6410
+ // exclude zero balances
6411
+ balances: results.success.filter(b => new Balance(b).total.planck > 0n)
6412
+ })), tap(results => {
6413
+ this.updateStorage$(balanceIds, results);
6414
+ }), shareReplay({
6388
6415
  refCount: true,
6389
6416
  bufferSize: 1
6390
6417
  }), keepAlive(0));
6418
+
6419
+ // defer the startWith call to start with up to date balances each time the observable is re-subscribed to
6420
+ return defer(() => moduleBalances$.pipe(startWith({
6421
+ status: "initialising",
6422
+ balances: this.getStoredBalances(moduleAddressesByTokenId)
6423
+ })));
6391
6424
  });
6392
6425
  }
6426
+ updateStorage$(balanceIds, balancesResult) {
6427
+ if (balancesResult.status !== "live") return;
6428
+ const storage = this.#storage.getValue();
6429
+ const balances = assign({}, storage.balances,
6430
+ // delete all balances expected in the result set. because if they are not present it means they are empty.
6431
+ fromPairs(balanceIds.map(balanceId => [balanceId, undefined])), keyBy(
6432
+ // storage balances must have status "cache", because they are used as start value when initialising subsequent subscriptions
6433
+ balancesResult.balances.map(b => ({
6434
+ ...b,
6435
+ status: "cache"
6436
+ })), b => getBalanceId(b)));
6437
+ this.#storage.next(assign({}, storage, {
6438
+ balances
6439
+ }));
6440
+ }
6393
6441
  getNetworkMiniMetadatas$(networkId) {
6394
- return this.#chaindataProvider.getNetworkById$(networkId).pipe(switchMap(network => isNetworkDot(network) ? this.getNetworkSpecVersion$(networkId).pipe(switchMap(specVersion => specVersion === null ? of([]) : this.getMiniMetadatas$(networkId, specVersion))) : of([])), distinctUntilChanged(isEqual));
6442
+ return getSharedObservable(`BalancesProvider.getNetworkMiniMetadatas$`, {
6443
+ networkId
6444
+ }, () => {
6445
+ return this.#chaindataProvider.getNetworkById$(networkId).pipe(switchMap(network => isNetworkDot(network) ? this.getNetworkSpecVersion$(networkId).pipe(switchMap(specVersion => specVersion === null ? of([]) : this.getMiniMetadatas$(networkId, specVersion))) : of([])), distinctUntilChanged(isEqual));
6446
+ });
6395
6447
  }
6396
6448
  getNetworkSpecVersion$(networkId) {
6397
6449
  return from(withRetry(() => getSpecVersion(this.#chainConnectors.substrate, networkId), {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@talismn/balances",
3
- "version": "0.0.0-pr2075-20250714011912",
3
+ "version": "0.0.0-pr2075-20250714084622",
4
4
  "author": "Talisman",
5
5
  "homepage": "https://talisman.xyz",
6
6
  "license": "GPL-3.0-or-later",
@@ -34,13 +34,13 @@
34
34
  "scale-ts": "^1.6.1",
35
35
  "viem": "^2.27.3",
36
36
  "zod": "^3.25.62",
37
- "@talismn/chain-connector": "0.0.0-pr2075-20250714011912",
38
- "@talismn/chain-connector-evm": "0.0.0-pr2075-20250714011912",
39
- "@talismn/chaindata-provider": "0.0.0-pr2075-20250714011912",
40
- "@talismn/sapi": "0.0.0-pr2075-20250714011912",
41
- "@talismn/scale": "0.0.0-pr2075-20250714011912",
42
- "@talismn/token-rates": "0.0.0-pr2075-20250714011912",
43
- "@talismn/util": "0.0.0-pr2075-20250714011912"
37
+ "@talismn/chain-connector": "0.0.0-pr2075-20250714084622",
38
+ "@talismn/token-rates": "0.0.0-pr2075-20250714084622",
39
+ "@talismn/chaindata-provider": "0.0.0-pr2075-20250714084622",
40
+ "@talismn/chain-connector-evm": "0.0.0-pr2075-20250714084622",
41
+ "@talismn/sapi": "0.0.0-pr2075-20250714084622",
42
+ "@talismn/util": "0.0.0-pr2075-20250714084622",
43
+ "@talismn/scale": "0.0.0-pr2075-20250714084622"
44
44
  },
45
45
  "devDependencies": {
46
46
  "@polkadot/api-contract": "16.1.2",