@talismn/balances 1.1.0 → 1.2.1

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.
@@ -6,6 +6,7 @@ type BalancesStatus = "initialising" | "live";
6
6
  export type BalancesResult = {
7
7
  status: BalancesStatus;
8
8
  balances: IBalance[];
9
+ failedBalanceIds: string[];
9
10
  };
10
11
  export type BalancesStorage = {
11
12
  balances: IBalance[];
@@ -320,6 +320,7 @@ export declare class Balance {
320
320
  type: "substrate-dtao";
321
321
  platform: "polkadot";
322
322
  netuid: number;
323
+ isTransferable: boolean;
323
324
  isDefault?: boolean | undefined;
324
325
  name?: string | undefined;
325
326
  logo?: string | undefined;
@@ -2432,7 +2432,7 @@ const fetchTokens$5 = async ({
2432
2432
  }) => {
2433
2433
  const anyMiniMetadata = miniMetadata;
2434
2434
  if (!anyMiniMetadata?.data) return [];
2435
- const dynamicInfos = await fetchRuntimeCallResult(connector, networkId, anyMiniMetadata.data, "SubnetInfoRuntimeApi", "get_all_dynamic_info", []);
2435
+ const [transferableTokensMap, dynamicInfos] = await Promise.all([fetchTransferableTokensMap(connector, anyMiniMetadata.data, networkId), fetchRuntimeCallResult(connector, networkId, anyMiniMetadata.data, "SubnetInfoRuntimeApi", "get_all_dynamic_info", [])]);
2436
2436
  return dynamicInfos.filter(util.isNotNil).map(info => {
2437
2437
  const config = tokens.find(t => t.netuid === info.netuid);
2438
2438
  let symbol = new TextDecoder().decode(Uint8Array.from(info.token_symbol));
@@ -2441,7 +2441,10 @@ const fetchTokens$5 = async ({
2441
2441
 
2442
2442
  // for root we want same symbol as native so they can be grouped together in portfolio
2443
2443
  if (info.netuid === 0 && NATIVE_TOKEN_SYMBOLS[networkId]) symbol = NATIVE_TOKEN_SYMBOLS[networkId];
2444
- return Object.assign({}, {
2444
+
2445
+ // if not stage in storage, consider transferable
2446
+ const isTransferable = transferableTokensMap[info.netuid] ?? true;
2447
+ const token = {
2445
2448
  id: chaindataProvider.subDTaoTokenId(networkId, info.netuid),
2446
2449
  type: MODULE_TYPE$5,
2447
2450
  platform: PLATFORM$5,
@@ -2451,13 +2454,29 @@ const fetchTokens$5 = async ({
2451
2454
  symbol,
2452
2455
  decimals: 9,
2453
2456
  name,
2454
- subnetName
2455
- }, config);
2457
+ subnetName,
2458
+ isTransferable
2459
+ };
2460
+ return Object.assign({}, token, config);
2456
2461
  }).filter(t => {
2457
2462
  const parsed = chaindataProvider.SubDTaoTokenSchema.safeParse(t);
2458
2463
  return parsed.success;
2459
2464
  });
2460
2465
  };
2466
+ const fetchTransferableTokensMap = async (connector, metadata, networkId) => {
2467
+ const {
2468
+ builder
2469
+ } = scale.parseMetadataRpc(metadata);
2470
+ const transferToggleCodec = builder.buildStorage("SubtensorModule", "TransferToggle");
2471
+ const transferToggleKeys = await connector.send(networkId, "state_getKeys", [scale.getStorageKeyPrefix("SubtensorModule", "TransferToggle")]);
2472
+ const transferToggleResults = await connector.send(networkId, "state_queryStorageAt", [transferToggleKeys]);
2473
+ const transferToggleEntries = transferToggleResults.length ? transferToggleResults[0].changes : [];
2474
+ return lodashEs.fromPairs(transferToggleEntries.map(([key, value]) => {
2475
+ const [netuid] = transferToggleCodec.keys.dec(key);
2476
+ const isTransferable = transferToggleCodec.value.dec(value);
2477
+ return [netuid, isTransferable];
2478
+ }));
2479
+ };
2461
2480
 
2462
2481
  const getMiniMetadata$6 = ({
2463
2482
  networkId,
@@ -2496,9 +2515,10 @@ const getData$4 = metadataRpc => {
2496
2515
  name
2497
2516
  }) => name === "SubtensorModule");
2498
2517
  if (!isBittensor) return null;
2499
- scale.compactMetadata(metadata, [
2500
- // { pallet: "SubtensorModule", items: ["StakingHotkeys", "Stake"] }
2501
- ], [{
2518
+ scale.compactMetadata(metadata, [{
2519
+ pallet: "SubtensorModule",
2520
+ items: ["TransferToggle"]
2521
+ }], [{
2502
2522
  runtimeApi: "StakeInfoRuntimeApi",
2503
2523
  methods: ["get_stake_info_for_coldkeys"]
2504
2524
  }, {
@@ -6947,7 +6967,8 @@ class BalancesProvider {
6947
6967
  balances: results.flatMap(result => result.balances.map(b => isStale && b.status !== "live" ? {
6948
6968
  ...b,
6949
6969
  status: "stale"
6950
- } : b)).sort(sortByBalanceId)
6970
+ } : b)).sort(sortByBalanceId),
6971
+ failedBalanceIds: results.flatMap(result => result.failedBalanceIds)
6951
6972
  })), rxjs.distinctUntilChanged(lodashEs.isEqual));
6952
6973
  }
6953
6974
  fetchBalances(addressesByTokenId) {
@@ -6988,17 +7009,25 @@ class BalancesProvider {
6988
7009
  });
6989
7010
  return rxjs.of({
6990
7011
  status: "live",
6991
- balances: []
7012
+ balances: [],
7013
+ failedBalanceIds: []
6992
7014
  });
6993
7015
  }
6994
7016
  }
6995
7017
  }));
6996
7018
  }), rxjs.map(results => {
7019
+ // for each balance that could not be fetched, see if we have a stored balance and return it, marked as stale
7020
+ const errorBalanceIds = results.flatMap(result => result.failedBalanceIds);
7021
+ const staleBalances = errorBalanceIds.map(balanceId => this.#storage.value.balances[balanceId]).filter(util.isNotNil).map(b => ({
7022
+ ...b,
7023
+ status: "stale"
7024
+ }));
6997
7025
  return {
6998
7026
  status: results.some(({
6999
7027
  status
7000
7028
  }) => status === "initialising") ? "initialising" : "live",
7001
- balances: results.flatMap(result => result.balances).sort(sortByBalanceId)
7029
+ balances: results.flatMap(result => result.balances).concat(staleBalances).sort(sortByBalanceId),
7030
+ failedBalanceIds: []
7002
7031
  };
7003
7032
  }), rxjs.distinctUntilChanged(lodashEs.isEqual));
7004
7033
  }
@@ -7010,7 +7039,8 @@ class BalancesProvider {
7010
7039
  }, () => {
7011
7040
  if (!tokensWithAddresses.length) return rxjs.of({
7012
7041
  status: "live",
7013
- balances: []
7042
+ balances: [],
7043
+ failedBalanceIds: []
7014
7044
  });
7015
7045
  const moduleAddressesByTokenId = lodashEs.fromPairs(tokensWithAddresses.map(([token, addresses]) => [token.id, addresses]));
7016
7046
 
@@ -7023,7 +7053,8 @@ class BalancesProvider {
7023
7053
  log.warn("[balances] no substrate connector or miniMetadata for module", mod.type);
7024
7054
  return rxjs.defer(() => rxjs.of({
7025
7055
  status: "initialising",
7026
- balances: this.getStoredBalances(moduleAddressesByTokenId)
7056
+ balances: this.getStoredBalances(moduleAddressesByTokenId),
7057
+ failedBalanceIds: []
7027
7058
  }));
7028
7059
  }
7029
7060
  const moduleBalances$ = this.getNetworkMiniMetadatas$(networkId).pipe(rxjs.map(miniMetadatas => miniMetadatas.find(m => m.source === mod.type)), rxjs.switchMap(miniMetadata => mod.subscribeBalances({
@@ -7041,7 +7072,14 @@ class BalancesProvider {
7041
7072
  }), rxjs.map(results => ({
7042
7073
  status: "live",
7043
7074
  // exclude zero balances
7044
- balances: results.success.filter(b => new Balance(b).total.planck > 0n)
7075
+ balances: results.success.filter(b => new Balance(b).total.planck > 0n),
7076
+ failedBalanceIds: results.errors.map(({
7077
+ tokenId,
7078
+ address
7079
+ }) => getBalanceId({
7080
+ tokenId,
7081
+ address
7082
+ }))
7045
7083
  })), rxjs.tap(results => {
7046
7084
  this.updateStorage$(balanceIds, results);
7047
7085
  }),
@@ -7054,7 +7092,8 @@ class BalancesProvider {
7054
7092
  // defer the startWith call to start with up to date balances each time the observable is re-subscribed to
7055
7093
  return rxjs.defer(() => moduleBalances$.pipe(rxjs.startWith({
7056
7094
  status: "initialising",
7057
- balances: this.getStoredBalances(moduleAddressesByTokenId)
7095
+ balances: this.getStoredBalances(moduleAddressesByTokenId),
7096
+ failedBalanceIds: []
7058
7097
  })));
7059
7098
  });
7060
7099
  }
@@ -7066,7 +7105,8 @@ class BalancesProvider {
7066
7105
  }, () => {
7067
7106
  if (!tokensWithAddresses.length) return rxjs.of({
7068
7107
  status: "live",
7069
- balances: []
7108
+ balances: [],
7109
+ failedBalanceIds: []
7070
7110
  });
7071
7111
  const moduleAddressesByTokenId = lodashEs.fromPairs(tokensWithAddresses.map(([token, addresses]) => [token.id, addresses]));
7072
7112
 
@@ -7079,7 +7119,8 @@ class BalancesProvider {
7079
7119
  log.warn("[balances] no ethereum connector for module", mod.type);
7080
7120
  return rxjs.defer(() => rxjs.of({
7081
7121
  status: "initialising",
7082
- balances: this.getStoredBalances(moduleAddressesByTokenId)
7122
+ balances: this.getStoredBalances(moduleAddressesByTokenId),
7123
+ failedBalanceIds: []
7083
7124
  }));
7084
7125
  }
7085
7126
  const moduleBalances$ = mod.subscribeBalances({
@@ -7091,7 +7132,14 @@ class BalancesProvider {
7091
7132
  rxjs.map(results => ({
7092
7133
  status: "live",
7093
7134
  // exclude zero balances
7094
- balances: results.success.filter(b => new Balance(b).total.planck > 0n)
7135
+ balances: results.success.filter(b => new Balance(b).total.planck > 0n),
7136
+ failedBalanceIds: results.errors.map(({
7137
+ tokenId,
7138
+ address
7139
+ }) => getBalanceId({
7140
+ tokenId,
7141
+ address
7142
+ }))
7095
7143
  })), rxjs.tap(results => {
7096
7144
  this.updateStorage$(balanceIds, results);
7097
7145
  }),
@@ -7104,7 +7152,8 @@ class BalancesProvider {
7104
7152
  // defer the startWith call to start with up to date balances each time the observable is re-subscribed to
7105
7153
  return rxjs.defer(() => moduleBalances$.pipe(rxjs.startWith({
7106
7154
  status: "initialising",
7107
- balances: this.getStoredBalances(moduleAddressesByTokenId)
7155
+ balances: this.getStoredBalances(moduleAddressesByTokenId),
7156
+ failedBalanceIds: []
7108
7157
  })));
7109
7158
  });
7110
7159
  }
@@ -7116,7 +7165,8 @@ class BalancesProvider {
7116
7165
  }, () => {
7117
7166
  if (!tokensWithAddresses.length) return rxjs.of({
7118
7167
  status: "live",
7119
- balances: []
7168
+ balances: [],
7169
+ failedBalanceIds: []
7120
7170
  });
7121
7171
  const moduleAddressesByTokenId = lodashEs.fromPairs(tokensWithAddresses.map(([token, addresses]) => [token.id, addresses]));
7122
7172
 
@@ -7129,7 +7179,8 @@ class BalancesProvider {
7129
7179
  log.warn("[balances] no solana connector for module", mod.type);
7130
7180
  return rxjs.defer(() => rxjs.of({
7131
7181
  status: "initialising",
7132
- balances: this.getStoredBalances(moduleAddressesByTokenId)
7182
+ balances: this.getStoredBalances(moduleAddressesByTokenId),
7183
+ failedBalanceIds: []
7133
7184
  }));
7134
7185
  }
7135
7186
  const moduleBalances$ = mod.subscribeBalances({
@@ -7141,7 +7192,14 @@ class BalancesProvider {
7141
7192
  rxjs.map(results => ({
7142
7193
  status: "live",
7143
7194
  // exclude zero balances
7144
- balances: results.success.filter(b => new Balance(b).total.planck > 0n)
7195
+ balances: results.success.filter(b => new Balance(b).total.planck > 0n),
7196
+ failedBalanceIds: results.errors.map(({
7197
+ tokenId,
7198
+ address
7199
+ }) => getBalanceId({
7200
+ tokenId,
7201
+ address
7202
+ }))
7145
7203
  })), rxjs.tap(results => {
7146
7204
  this.updateStorage$(balanceIds, results);
7147
7205
  }),
@@ -7154,7 +7212,8 @@ class BalancesProvider {
7154
7212
  // defer the startWith call to start with up to date balances each time the observable is re-subscribed to
7155
7213
  return rxjs.defer(() => moduleBalances$.pipe(rxjs.startWith({
7156
7214
  status: "initialising",
7157
- balances: this.getStoredBalances(moduleAddressesByTokenId)
7215
+ balances: this.getStoredBalances(moduleAddressesByTokenId),
7216
+ failedBalanceIds: []
7158
7217
  })));
7159
7218
  });
7160
7219
  }
@@ -7162,13 +7221,19 @@ class BalancesProvider {
7162
7221
  if (balancesResult.status !== "live") return;
7163
7222
  const storage = this.#storage.getValue();
7164
7223
  const balances = lodashEs.assign({}, storage.balances,
7165
- // delete all balances expected in the result set. because if they are not present it means they are empty.
7166
- lodashEs.fromPairs(balanceIds.map(balanceId => [balanceId, undefined])), lodashEs.keyBy(
7224
+ // delete all balances expected in the result set (except the ones that failed). because if they are not present it means they are empty.
7225
+ lodashEs.fromPairs(balanceIds.filter(bid => !balancesResult.failedBalanceIds.includes(bid)).map(balanceId => [balanceId, undefined])), lodashEs.keyBy(
7167
7226
  // storage balances must have status "cache", because they are used as start value when initialising subsequent subscriptions
7168
7227
  balancesResult.balances.map(b => ({
7169
7228
  ...b,
7170
7229
  status: "cache"
7171
7230
  })), b => getBalanceId(b)));
7231
+
7232
+ // update status of stale balances
7233
+ for (const errorBalanceId of balancesResult.failedBalanceIds) {
7234
+ const balance = balances[errorBalanceId];
7235
+ if (balance) balance.status = "stale";
7236
+ }
7172
7237
  this.#storage.next(lodashEs.assign({}, storage, {
7173
7238
  balances
7174
7239
  }));
@@ -2432,7 +2432,7 @@ const fetchTokens$5 = async ({
2432
2432
  }) => {
2433
2433
  const anyMiniMetadata = miniMetadata;
2434
2434
  if (!anyMiniMetadata?.data) return [];
2435
- const dynamicInfos = await fetchRuntimeCallResult(connector, networkId, anyMiniMetadata.data, "SubnetInfoRuntimeApi", "get_all_dynamic_info", []);
2435
+ const [transferableTokensMap, dynamicInfos] = await Promise.all([fetchTransferableTokensMap(connector, anyMiniMetadata.data, networkId), fetchRuntimeCallResult(connector, networkId, anyMiniMetadata.data, "SubnetInfoRuntimeApi", "get_all_dynamic_info", [])]);
2436
2436
  return dynamicInfos.filter(util.isNotNil).map(info => {
2437
2437
  const config = tokens.find(t => t.netuid === info.netuid);
2438
2438
  let symbol = new TextDecoder().decode(Uint8Array.from(info.token_symbol));
@@ -2441,7 +2441,10 @@ const fetchTokens$5 = async ({
2441
2441
 
2442
2442
  // for root we want same symbol as native so they can be grouped together in portfolio
2443
2443
  if (info.netuid === 0 && NATIVE_TOKEN_SYMBOLS[networkId]) symbol = NATIVE_TOKEN_SYMBOLS[networkId];
2444
- return Object.assign({}, {
2444
+
2445
+ // if not stage in storage, consider transferable
2446
+ const isTransferable = transferableTokensMap[info.netuid] ?? true;
2447
+ const token = {
2445
2448
  id: chaindataProvider.subDTaoTokenId(networkId, info.netuid),
2446
2449
  type: MODULE_TYPE$5,
2447
2450
  platform: PLATFORM$5,
@@ -2451,13 +2454,29 @@ const fetchTokens$5 = async ({
2451
2454
  symbol,
2452
2455
  decimals: 9,
2453
2456
  name,
2454
- subnetName
2455
- }, config);
2457
+ subnetName,
2458
+ isTransferable
2459
+ };
2460
+ return Object.assign({}, token, config);
2456
2461
  }).filter(t => {
2457
2462
  const parsed = chaindataProvider.SubDTaoTokenSchema.safeParse(t);
2458
2463
  return parsed.success;
2459
2464
  });
2460
2465
  };
2466
+ const fetchTransferableTokensMap = async (connector, metadata, networkId) => {
2467
+ const {
2468
+ builder
2469
+ } = scale.parseMetadataRpc(metadata);
2470
+ const transferToggleCodec = builder.buildStorage("SubtensorModule", "TransferToggle");
2471
+ const transferToggleKeys = await connector.send(networkId, "state_getKeys", [scale.getStorageKeyPrefix("SubtensorModule", "TransferToggle")]);
2472
+ const transferToggleResults = await connector.send(networkId, "state_queryStorageAt", [transferToggleKeys]);
2473
+ const transferToggleEntries = transferToggleResults.length ? transferToggleResults[0].changes : [];
2474
+ return lodashEs.fromPairs(transferToggleEntries.map(([key, value]) => {
2475
+ const [netuid] = transferToggleCodec.keys.dec(key);
2476
+ const isTransferable = transferToggleCodec.value.dec(value);
2477
+ return [netuid, isTransferable];
2478
+ }));
2479
+ };
2461
2480
 
2462
2481
  const getMiniMetadata$6 = ({
2463
2482
  networkId,
@@ -2496,9 +2515,10 @@ const getData$4 = metadataRpc => {
2496
2515
  name
2497
2516
  }) => name === "SubtensorModule");
2498
2517
  if (!isBittensor) return null;
2499
- scale.compactMetadata(metadata, [
2500
- // { pallet: "SubtensorModule", items: ["StakingHotkeys", "Stake"] }
2501
- ], [{
2518
+ scale.compactMetadata(metadata, [{
2519
+ pallet: "SubtensorModule",
2520
+ items: ["TransferToggle"]
2521
+ }], [{
2502
2522
  runtimeApi: "StakeInfoRuntimeApi",
2503
2523
  methods: ["get_stake_info_for_coldkeys"]
2504
2524
  }, {
@@ -6947,7 +6967,8 @@ class BalancesProvider {
6947
6967
  balances: results.flatMap(result => result.balances.map(b => isStale && b.status !== "live" ? {
6948
6968
  ...b,
6949
6969
  status: "stale"
6950
- } : b)).sort(sortByBalanceId)
6970
+ } : b)).sort(sortByBalanceId),
6971
+ failedBalanceIds: results.flatMap(result => result.failedBalanceIds)
6951
6972
  })), rxjs.distinctUntilChanged(lodashEs.isEqual));
6952
6973
  }
6953
6974
  fetchBalances(addressesByTokenId) {
@@ -6988,17 +7009,25 @@ class BalancesProvider {
6988
7009
  });
6989
7010
  return rxjs.of({
6990
7011
  status: "live",
6991
- balances: []
7012
+ balances: [],
7013
+ failedBalanceIds: []
6992
7014
  });
6993
7015
  }
6994
7016
  }
6995
7017
  }));
6996
7018
  }), rxjs.map(results => {
7019
+ // for each balance that could not be fetched, see if we have a stored balance and return it, marked as stale
7020
+ const errorBalanceIds = results.flatMap(result => result.failedBalanceIds);
7021
+ const staleBalances = errorBalanceIds.map(balanceId => this.#storage.value.balances[balanceId]).filter(util.isNotNil).map(b => ({
7022
+ ...b,
7023
+ status: "stale"
7024
+ }));
6997
7025
  return {
6998
7026
  status: results.some(({
6999
7027
  status
7000
7028
  }) => status === "initialising") ? "initialising" : "live",
7001
- balances: results.flatMap(result => result.balances).sort(sortByBalanceId)
7029
+ balances: results.flatMap(result => result.balances).concat(staleBalances).sort(sortByBalanceId),
7030
+ failedBalanceIds: []
7002
7031
  };
7003
7032
  }), rxjs.distinctUntilChanged(lodashEs.isEqual));
7004
7033
  }
@@ -7010,7 +7039,8 @@ class BalancesProvider {
7010
7039
  }, () => {
7011
7040
  if (!tokensWithAddresses.length) return rxjs.of({
7012
7041
  status: "live",
7013
- balances: []
7042
+ balances: [],
7043
+ failedBalanceIds: []
7014
7044
  });
7015
7045
  const moduleAddressesByTokenId = lodashEs.fromPairs(tokensWithAddresses.map(([token, addresses]) => [token.id, addresses]));
7016
7046
 
@@ -7023,7 +7053,8 @@ class BalancesProvider {
7023
7053
  log.warn("[balances] no substrate connector or miniMetadata for module", mod.type);
7024
7054
  return rxjs.defer(() => rxjs.of({
7025
7055
  status: "initialising",
7026
- balances: this.getStoredBalances(moduleAddressesByTokenId)
7056
+ balances: this.getStoredBalances(moduleAddressesByTokenId),
7057
+ failedBalanceIds: []
7027
7058
  }));
7028
7059
  }
7029
7060
  const moduleBalances$ = this.getNetworkMiniMetadatas$(networkId).pipe(rxjs.map(miniMetadatas => miniMetadatas.find(m => m.source === mod.type)), rxjs.switchMap(miniMetadata => mod.subscribeBalances({
@@ -7041,7 +7072,14 @@ class BalancesProvider {
7041
7072
  }), rxjs.map(results => ({
7042
7073
  status: "live",
7043
7074
  // exclude zero balances
7044
- balances: results.success.filter(b => new Balance(b).total.planck > 0n)
7075
+ balances: results.success.filter(b => new Balance(b).total.planck > 0n),
7076
+ failedBalanceIds: results.errors.map(({
7077
+ tokenId,
7078
+ address
7079
+ }) => getBalanceId({
7080
+ tokenId,
7081
+ address
7082
+ }))
7045
7083
  })), rxjs.tap(results => {
7046
7084
  this.updateStorage$(balanceIds, results);
7047
7085
  }),
@@ -7054,7 +7092,8 @@ class BalancesProvider {
7054
7092
  // defer the startWith call to start with up to date balances each time the observable is re-subscribed to
7055
7093
  return rxjs.defer(() => moduleBalances$.pipe(rxjs.startWith({
7056
7094
  status: "initialising",
7057
- balances: this.getStoredBalances(moduleAddressesByTokenId)
7095
+ balances: this.getStoredBalances(moduleAddressesByTokenId),
7096
+ failedBalanceIds: []
7058
7097
  })));
7059
7098
  });
7060
7099
  }
@@ -7066,7 +7105,8 @@ class BalancesProvider {
7066
7105
  }, () => {
7067
7106
  if (!tokensWithAddresses.length) return rxjs.of({
7068
7107
  status: "live",
7069
- balances: []
7108
+ balances: [],
7109
+ failedBalanceIds: []
7070
7110
  });
7071
7111
  const moduleAddressesByTokenId = lodashEs.fromPairs(tokensWithAddresses.map(([token, addresses]) => [token.id, addresses]));
7072
7112
 
@@ -7079,7 +7119,8 @@ class BalancesProvider {
7079
7119
  log.warn("[balances] no ethereum connector for module", mod.type);
7080
7120
  return rxjs.defer(() => rxjs.of({
7081
7121
  status: "initialising",
7082
- balances: this.getStoredBalances(moduleAddressesByTokenId)
7122
+ balances: this.getStoredBalances(moduleAddressesByTokenId),
7123
+ failedBalanceIds: []
7083
7124
  }));
7084
7125
  }
7085
7126
  const moduleBalances$ = mod.subscribeBalances({
@@ -7091,7 +7132,14 @@ class BalancesProvider {
7091
7132
  rxjs.map(results => ({
7092
7133
  status: "live",
7093
7134
  // exclude zero balances
7094
- balances: results.success.filter(b => new Balance(b).total.planck > 0n)
7135
+ balances: results.success.filter(b => new Balance(b).total.planck > 0n),
7136
+ failedBalanceIds: results.errors.map(({
7137
+ tokenId,
7138
+ address
7139
+ }) => getBalanceId({
7140
+ tokenId,
7141
+ address
7142
+ }))
7095
7143
  })), rxjs.tap(results => {
7096
7144
  this.updateStorage$(balanceIds, results);
7097
7145
  }),
@@ -7104,7 +7152,8 @@ class BalancesProvider {
7104
7152
  // defer the startWith call to start with up to date balances each time the observable is re-subscribed to
7105
7153
  return rxjs.defer(() => moduleBalances$.pipe(rxjs.startWith({
7106
7154
  status: "initialising",
7107
- balances: this.getStoredBalances(moduleAddressesByTokenId)
7155
+ balances: this.getStoredBalances(moduleAddressesByTokenId),
7156
+ failedBalanceIds: []
7108
7157
  })));
7109
7158
  });
7110
7159
  }
@@ -7116,7 +7165,8 @@ class BalancesProvider {
7116
7165
  }, () => {
7117
7166
  if (!tokensWithAddresses.length) return rxjs.of({
7118
7167
  status: "live",
7119
- balances: []
7168
+ balances: [],
7169
+ failedBalanceIds: []
7120
7170
  });
7121
7171
  const moduleAddressesByTokenId = lodashEs.fromPairs(tokensWithAddresses.map(([token, addresses]) => [token.id, addresses]));
7122
7172
 
@@ -7129,7 +7179,8 @@ class BalancesProvider {
7129
7179
  log.warn("[balances] no solana connector for module", mod.type);
7130
7180
  return rxjs.defer(() => rxjs.of({
7131
7181
  status: "initialising",
7132
- balances: this.getStoredBalances(moduleAddressesByTokenId)
7182
+ balances: this.getStoredBalances(moduleAddressesByTokenId),
7183
+ failedBalanceIds: []
7133
7184
  }));
7134
7185
  }
7135
7186
  const moduleBalances$ = mod.subscribeBalances({
@@ -7141,7 +7192,14 @@ class BalancesProvider {
7141
7192
  rxjs.map(results => ({
7142
7193
  status: "live",
7143
7194
  // exclude zero balances
7144
- balances: results.success.filter(b => new Balance(b).total.planck > 0n)
7195
+ balances: results.success.filter(b => new Balance(b).total.planck > 0n),
7196
+ failedBalanceIds: results.errors.map(({
7197
+ tokenId,
7198
+ address
7199
+ }) => getBalanceId({
7200
+ tokenId,
7201
+ address
7202
+ }))
7145
7203
  })), rxjs.tap(results => {
7146
7204
  this.updateStorage$(balanceIds, results);
7147
7205
  }),
@@ -7154,7 +7212,8 @@ class BalancesProvider {
7154
7212
  // defer the startWith call to start with up to date balances each time the observable is re-subscribed to
7155
7213
  return rxjs.defer(() => moduleBalances$.pipe(rxjs.startWith({
7156
7214
  status: "initialising",
7157
- balances: this.getStoredBalances(moduleAddressesByTokenId)
7215
+ balances: this.getStoredBalances(moduleAddressesByTokenId),
7216
+ failedBalanceIds: []
7158
7217
  })));
7159
7218
  });
7160
7219
  }
@@ -7162,13 +7221,19 @@ class BalancesProvider {
7162
7221
  if (balancesResult.status !== "live") return;
7163
7222
  const storage = this.#storage.getValue();
7164
7223
  const balances = lodashEs.assign({}, storage.balances,
7165
- // delete all balances expected in the result set. because if they are not present it means they are empty.
7166
- lodashEs.fromPairs(balanceIds.map(balanceId => [balanceId, undefined])), lodashEs.keyBy(
7224
+ // delete all balances expected in the result set (except the ones that failed). because if they are not present it means they are empty.
7225
+ lodashEs.fromPairs(balanceIds.filter(bid => !balancesResult.failedBalanceIds.includes(bid)).map(balanceId => [balanceId, undefined])), lodashEs.keyBy(
7167
7226
  // storage balances must have status "cache", because they are used as start value when initialising subsequent subscriptions
7168
7227
  balancesResult.balances.map(b => ({
7169
7228
  ...b,
7170
7229
  status: "cache"
7171
7230
  })), b => getBalanceId(b)));
7231
+
7232
+ // update status of stale balances
7233
+ for (const errorBalanceId of balancesResult.failedBalanceIds) {
7234
+ const balance = balances[errorBalanceId];
7235
+ if (balance) balance.status = "stale";
7236
+ }
7172
7237
  this.#storage.next(lodashEs.assign({}, storage, {
7173
7238
  balances
7174
7239
  }));
@@ -2,7 +2,7 @@ import { EvmErc20TokenSchema, parseTokenId, parseEvmErc20TokenId, evmErc20TokenI
2
2
  export { MINIMETADATA_VERSION } from '@talismn/chaindata-provider';
3
3
  import { isEthereumAddress, isSolanaAddress, normalizeAddress, getAccountPlatformFromAddress } from '@talismn/crypto';
4
4
  import { parseAbi, erc20Abi, getContract, ContractFunctionExecutionError, hexToString, erc20Abi_bytes32, encodeFunctionData, withRetry } from 'viem';
5
- import { assign, omit, isEqual, uniq, keyBy, toPairs, keys, values, fromPairs } from 'lodash-es';
5
+ import { assign, omit, isEqual, uniq, keyBy, toPairs, keys, fromPairs, values } from 'lodash-es';
6
6
  import z from 'zod/v4';
7
7
  import anylogger from 'anylogger';
8
8
  import { of, Observable, distinctUntilChanged, BehaviorSubject, map, switchMap, shareReplay, combineLatest, timer, startWith, firstValueFrom, filter, defer, catchError, EMPTY, tap, from } from 'rxjs';
@@ -2423,7 +2423,7 @@ const fetchTokens$5 = async ({
2423
2423
  }) => {
2424
2424
  const anyMiniMetadata = miniMetadata;
2425
2425
  if (!anyMiniMetadata?.data) return [];
2426
- const dynamicInfos = await fetchRuntimeCallResult(connector, networkId, anyMiniMetadata.data, "SubnetInfoRuntimeApi", "get_all_dynamic_info", []);
2426
+ const [transferableTokensMap, dynamicInfos] = await Promise.all([fetchTransferableTokensMap(connector, anyMiniMetadata.data, networkId), fetchRuntimeCallResult(connector, networkId, anyMiniMetadata.data, "SubnetInfoRuntimeApi", "get_all_dynamic_info", [])]);
2427
2427
  return dynamicInfos.filter(isNotNil).map(info => {
2428
2428
  const config = tokens.find(t => t.netuid === info.netuid);
2429
2429
  let symbol = new TextDecoder().decode(Uint8Array.from(info.token_symbol));
@@ -2432,7 +2432,10 @@ const fetchTokens$5 = async ({
2432
2432
 
2433
2433
  // for root we want same symbol as native so they can be grouped together in portfolio
2434
2434
  if (info.netuid === 0 && NATIVE_TOKEN_SYMBOLS[networkId]) symbol = NATIVE_TOKEN_SYMBOLS[networkId];
2435
- return Object.assign({}, {
2435
+
2436
+ // if not stage in storage, consider transferable
2437
+ const isTransferable = transferableTokensMap[info.netuid] ?? true;
2438
+ const token = {
2436
2439
  id: subDTaoTokenId(networkId, info.netuid),
2437
2440
  type: MODULE_TYPE$5,
2438
2441
  platform: PLATFORM$5,
@@ -2442,13 +2445,29 @@ const fetchTokens$5 = async ({
2442
2445
  symbol,
2443
2446
  decimals: 9,
2444
2447
  name,
2445
- subnetName
2446
- }, config);
2448
+ subnetName,
2449
+ isTransferable
2450
+ };
2451
+ return Object.assign({}, token, config);
2447
2452
  }).filter(t => {
2448
2453
  const parsed = SubDTaoTokenSchema.safeParse(t);
2449
2454
  return parsed.success;
2450
2455
  });
2451
2456
  };
2457
+ const fetchTransferableTokensMap = async (connector, metadata, networkId) => {
2458
+ const {
2459
+ builder
2460
+ } = parseMetadataRpc(metadata);
2461
+ const transferToggleCodec = builder.buildStorage("SubtensorModule", "TransferToggle");
2462
+ const transferToggleKeys = await connector.send(networkId, "state_getKeys", [getStorageKeyPrefix("SubtensorModule", "TransferToggle")]);
2463
+ const transferToggleResults = await connector.send(networkId, "state_queryStorageAt", [transferToggleKeys]);
2464
+ const transferToggleEntries = transferToggleResults.length ? transferToggleResults[0].changes : [];
2465
+ return fromPairs(transferToggleEntries.map(([key, value]) => {
2466
+ const [netuid] = transferToggleCodec.keys.dec(key);
2467
+ const isTransferable = transferToggleCodec.value.dec(value);
2468
+ return [netuid, isTransferable];
2469
+ }));
2470
+ };
2452
2471
 
2453
2472
  const getMiniMetadata$6 = ({
2454
2473
  networkId,
@@ -2487,9 +2506,10 @@ const getData$4 = metadataRpc => {
2487
2506
  name
2488
2507
  }) => name === "SubtensorModule");
2489
2508
  if (!isBittensor) return null;
2490
- compactMetadata(metadata, [
2491
- // { pallet: "SubtensorModule", items: ["StakingHotkeys", "Stake"] }
2492
- ], [{
2509
+ compactMetadata(metadata, [{
2510
+ pallet: "SubtensorModule",
2511
+ items: ["TransferToggle"]
2512
+ }], [{
2493
2513
  runtimeApi: "StakeInfoRuntimeApi",
2494
2514
  methods: ["get_stake_info_for_coldkeys"]
2495
2515
  }, {
@@ -6938,7 +6958,8 @@ class BalancesProvider {
6938
6958
  balances: results.flatMap(result => result.balances.map(b => isStale && b.status !== "live" ? {
6939
6959
  ...b,
6940
6960
  status: "stale"
6941
- } : b)).sort(sortByBalanceId)
6961
+ } : b)).sort(sortByBalanceId),
6962
+ failedBalanceIds: results.flatMap(result => result.failedBalanceIds)
6942
6963
  })), distinctUntilChanged(isEqual));
6943
6964
  }
6944
6965
  fetchBalances(addressesByTokenId) {
@@ -6979,17 +7000,25 @@ class BalancesProvider {
6979
7000
  });
6980
7001
  return of({
6981
7002
  status: "live",
6982
- balances: []
7003
+ balances: [],
7004
+ failedBalanceIds: []
6983
7005
  });
6984
7006
  }
6985
7007
  }
6986
7008
  }));
6987
7009
  }), map(results => {
7010
+ // for each balance that could not be fetched, see if we have a stored balance and return it, marked as stale
7011
+ const errorBalanceIds = results.flatMap(result => result.failedBalanceIds);
7012
+ const staleBalances = errorBalanceIds.map(balanceId => this.#storage.value.balances[balanceId]).filter(isNotNil).map(b => ({
7013
+ ...b,
7014
+ status: "stale"
7015
+ }));
6988
7016
  return {
6989
7017
  status: results.some(({
6990
7018
  status
6991
7019
  }) => status === "initialising") ? "initialising" : "live",
6992
- balances: results.flatMap(result => result.balances).sort(sortByBalanceId)
7020
+ balances: results.flatMap(result => result.balances).concat(staleBalances).sort(sortByBalanceId),
7021
+ failedBalanceIds: []
6993
7022
  };
6994
7023
  }), distinctUntilChanged(isEqual));
6995
7024
  }
@@ -7001,7 +7030,8 @@ class BalancesProvider {
7001
7030
  }, () => {
7002
7031
  if (!tokensWithAddresses.length) return of({
7003
7032
  status: "live",
7004
- balances: []
7033
+ balances: [],
7034
+ failedBalanceIds: []
7005
7035
  });
7006
7036
  const moduleAddressesByTokenId = fromPairs(tokensWithAddresses.map(([token, addresses]) => [token.id, addresses]));
7007
7037
 
@@ -7014,7 +7044,8 @@ class BalancesProvider {
7014
7044
  log.warn("[balances] no substrate connector or miniMetadata for module", mod.type);
7015
7045
  return defer(() => of({
7016
7046
  status: "initialising",
7017
- balances: this.getStoredBalances(moduleAddressesByTokenId)
7047
+ balances: this.getStoredBalances(moduleAddressesByTokenId),
7048
+ failedBalanceIds: []
7018
7049
  }));
7019
7050
  }
7020
7051
  const moduleBalances$ = this.getNetworkMiniMetadatas$(networkId).pipe(map(miniMetadatas => miniMetadatas.find(m => m.source === mod.type)), switchMap(miniMetadata => mod.subscribeBalances({
@@ -7032,7 +7063,14 @@ class BalancesProvider {
7032
7063
  }), map(results => ({
7033
7064
  status: "live",
7034
7065
  // exclude zero balances
7035
- balances: results.success.filter(b => new Balance(b).total.planck > 0n)
7066
+ balances: results.success.filter(b => new Balance(b).total.planck > 0n),
7067
+ failedBalanceIds: results.errors.map(({
7068
+ tokenId,
7069
+ address
7070
+ }) => getBalanceId({
7071
+ tokenId,
7072
+ address
7073
+ }))
7036
7074
  })), tap(results => {
7037
7075
  this.updateStorage$(balanceIds, results);
7038
7076
  }),
@@ -7045,7 +7083,8 @@ class BalancesProvider {
7045
7083
  // defer the startWith call to start with up to date balances each time the observable is re-subscribed to
7046
7084
  return defer(() => moduleBalances$.pipe(startWith({
7047
7085
  status: "initialising",
7048
- balances: this.getStoredBalances(moduleAddressesByTokenId)
7086
+ balances: this.getStoredBalances(moduleAddressesByTokenId),
7087
+ failedBalanceIds: []
7049
7088
  })));
7050
7089
  });
7051
7090
  }
@@ -7057,7 +7096,8 @@ class BalancesProvider {
7057
7096
  }, () => {
7058
7097
  if (!tokensWithAddresses.length) return of({
7059
7098
  status: "live",
7060
- balances: []
7099
+ balances: [],
7100
+ failedBalanceIds: []
7061
7101
  });
7062
7102
  const moduleAddressesByTokenId = fromPairs(tokensWithAddresses.map(([token, addresses]) => [token.id, addresses]));
7063
7103
 
@@ -7070,7 +7110,8 @@ class BalancesProvider {
7070
7110
  log.warn("[balances] no ethereum connector for module", mod.type);
7071
7111
  return defer(() => of({
7072
7112
  status: "initialising",
7073
- balances: this.getStoredBalances(moduleAddressesByTokenId)
7113
+ balances: this.getStoredBalances(moduleAddressesByTokenId),
7114
+ failedBalanceIds: []
7074
7115
  }));
7075
7116
  }
7076
7117
  const moduleBalances$ = mod.subscribeBalances({
@@ -7082,7 +7123,14 @@ class BalancesProvider {
7082
7123
  map(results => ({
7083
7124
  status: "live",
7084
7125
  // exclude zero balances
7085
- balances: results.success.filter(b => new Balance(b).total.planck > 0n)
7126
+ balances: results.success.filter(b => new Balance(b).total.planck > 0n),
7127
+ failedBalanceIds: results.errors.map(({
7128
+ tokenId,
7129
+ address
7130
+ }) => getBalanceId({
7131
+ tokenId,
7132
+ address
7133
+ }))
7086
7134
  })), tap(results => {
7087
7135
  this.updateStorage$(balanceIds, results);
7088
7136
  }),
@@ -7095,7 +7143,8 @@ class BalancesProvider {
7095
7143
  // defer the startWith call to start with up to date balances each time the observable is re-subscribed to
7096
7144
  return defer(() => moduleBalances$.pipe(startWith({
7097
7145
  status: "initialising",
7098
- balances: this.getStoredBalances(moduleAddressesByTokenId)
7146
+ balances: this.getStoredBalances(moduleAddressesByTokenId),
7147
+ failedBalanceIds: []
7099
7148
  })));
7100
7149
  });
7101
7150
  }
@@ -7107,7 +7156,8 @@ class BalancesProvider {
7107
7156
  }, () => {
7108
7157
  if (!tokensWithAddresses.length) return of({
7109
7158
  status: "live",
7110
- balances: []
7159
+ balances: [],
7160
+ failedBalanceIds: []
7111
7161
  });
7112
7162
  const moduleAddressesByTokenId = fromPairs(tokensWithAddresses.map(([token, addresses]) => [token.id, addresses]));
7113
7163
 
@@ -7120,7 +7170,8 @@ class BalancesProvider {
7120
7170
  log.warn("[balances] no solana connector for module", mod.type);
7121
7171
  return defer(() => of({
7122
7172
  status: "initialising",
7123
- balances: this.getStoredBalances(moduleAddressesByTokenId)
7173
+ balances: this.getStoredBalances(moduleAddressesByTokenId),
7174
+ failedBalanceIds: []
7124
7175
  }));
7125
7176
  }
7126
7177
  const moduleBalances$ = mod.subscribeBalances({
@@ -7132,7 +7183,14 @@ class BalancesProvider {
7132
7183
  map(results => ({
7133
7184
  status: "live",
7134
7185
  // exclude zero balances
7135
- balances: results.success.filter(b => new Balance(b).total.planck > 0n)
7186
+ balances: results.success.filter(b => new Balance(b).total.planck > 0n),
7187
+ failedBalanceIds: results.errors.map(({
7188
+ tokenId,
7189
+ address
7190
+ }) => getBalanceId({
7191
+ tokenId,
7192
+ address
7193
+ }))
7136
7194
  })), tap(results => {
7137
7195
  this.updateStorage$(balanceIds, results);
7138
7196
  }),
@@ -7145,7 +7203,8 @@ class BalancesProvider {
7145
7203
  // defer the startWith call to start with up to date balances each time the observable is re-subscribed to
7146
7204
  return defer(() => moduleBalances$.pipe(startWith({
7147
7205
  status: "initialising",
7148
- balances: this.getStoredBalances(moduleAddressesByTokenId)
7206
+ balances: this.getStoredBalances(moduleAddressesByTokenId),
7207
+ failedBalanceIds: []
7149
7208
  })));
7150
7209
  });
7151
7210
  }
@@ -7153,13 +7212,19 @@ class BalancesProvider {
7153
7212
  if (balancesResult.status !== "live") return;
7154
7213
  const storage = this.#storage.getValue();
7155
7214
  const balances = assign({}, storage.balances,
7156
- // delete all balances expected in the result set. because if they are not present it means they are empty.
7157
- fromPairs(balanceIds.map(balanceId => [balanceId, undefined])), keyBy(
7215
+ // delete all balances expected in the result set (except the ones that failed). because if they are not present it means they are empty.
7216
+ fromPairs(balanceIds.filter(bid => !balancesResult.failedBalanceIds.includes(bid)).map(balanceId => [balanceId, undefined])), keyBy(
7158
7217
  // storage balances must have status "cache", because they are used as start value when initialising subsequent subscriptions
7159
7218
  balancesResult.balances.map(b => ({
7160
7219
  ...b,
7161
7220
  status: "cache"
7162
7221
  })), b => getBalanceId(b)));
7222
+
7223
+ // update status of stale balances
7224
+ for (const errorBalanceId of balancesResult.failedBalanceIds) {
7225
+ const balance = balances[errorBalanceId];
7226
+ if (balance) balance.status = "stale";
7227
+ }
7163
7228
  this.#storage.next(assign({}, storage, {
7164
7229
  balances
7165
7230
  }));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@talismn/balances",
3
- "version": "1.1.0",
3
+ "version": "1.2.1",
4
4
  "author": "Talisman",
5
5
  "homepage": "https://talisman.xyz",
6
6
  "license": "GPL-3.0-or-later",
@@ -38,14 +38,14 @@
38
38
  "scale-ts": "^1.6.1",
39
39
  "viem": "^2.27.3",
40
40
  "zod": "^3.25.76",
41
- "@talismn/chain-connectors": "0.0.8",
42
- "@talismn/crypto": "0.2.3",
43
- "@talismn/chaindata-provider": "1.2.0",
41
+ "@talismn/chain-connectors": "0.0.10",
42
+ "@talismn/chaindata-provider": "1.3.1",
44
43
  "@talismn/scale": "0.2.2",
44
+ "@talismn/solana": "0.0.4",
45
+ "@talismn/crypto": "0.2.3",
45
46
  "@talismn/sapi": "0.0.12",
46
- "@talismn/token-rates": "3.0.10",
47
- "@talismn/solana": "0.0.3",
48
- "@talismn/util": "0.5.5"
47
+ "@talismn/token-rates": "3.0.12",
48
+ "@talismn/util": "0.5.6"
49
49
  },
50
50
  "devDependencies": {
51
51
  "@polkadot/api-contract": "16.1.2",
@@ -60,8 +60,8 @@
60
60
  "jest": "^29.7.0",
61
61
  "ts-jest": "^29.2.5",
62
62
  "typescript": "^5.6.3",
63
- "@talismn/eslint-config": "0.0.3",
64
- "@talismn/tsconfig": "0.0.3"
63
+ "@talismn/tsconfig": "0.0.3",
64
+ "@talismn/eslint-config": "0.0.3"
65
65
  },
66
66
  "peerDependencies": {
67
67
  "@polkadot/api-contract": "*",