@talismn/balances 3.1.0 → 3.1.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.
package/dist/index.js CHANGED
@@ -1694,18 +1694,31 @@ const fetchRpcQueryPack = async (connector, networkId, queries, at) => {
1694
1694
  return decodeRpcQueryPackChunked(queries, new Map(result.changes), { label: `rpcQueryPack decode ${networkId}` });
1695
1695
  };
1696
1696
  /**
1697
+ * The connector reports a chain that has been unreachable for a while with a StaleRpcError,
1698
+ * but keeps the subscription registered and re-issues it when a socket reconnects (the
1699
+ * first notification after that is a full snapshot again). Matched by its `type` rather
1700
+ * than `instanceof`, so a second copy of @talismn/chain-connectors still matches.
1701
+ */
1702
+ const isStaleRpcError = (error) => typeof error === "object" && error !== null && error.type === "STALE_RPC_ERROR";
1703
+ /**
1697
1704
  * Wraps the raw state_subscribeStorage subscription and emits, for every storage change
1698
1705
  * callback, a SNAPSHOT of the accumulated changes keyed by state key.
1699
1706
  *
1700
1707
  * A snapshot (shallow copy of string refs, cheap) is required because decoding is chunked
1701
1708
  * and asynchronous: a later websocket callback must not mutate the map that an in-flight
1702
1709
  * decode is reading.
1710
+ *
1711
+ * A StaleRpcError does not end the stream. Erroring would unsubscribe, and with no
1712
+ * subscription holding the chain's socket, every later request (eg the dtao poll) would
1713
+ * open and close a socket of its own, starting from the network's first RPC each time.
1703
1714
  */
1704
1715
  const getRawStorageUpdates$ = (connector, networkId, allStateKeys, timeout) => new rxjs.Observable((subscriber) => {
1705
1716
  const changesCache = /* @__PURE__ */ new Map();
1706
1717
  const promUnsub = connector.subscribe(networkId, "state_subscribeStorage", "state_storage", [allStateKeys], (error, result) => {
1707
- if (error) subscriber.error(error);
1708
- else if (result) {
1718
+ if (error) {
1719
+ if (isStaleRpcError(error)) log_default.warn(`Storage subscription stale on ${networkId}`);
1720
+ else subscriber.error(error);
1721
+ } else if (result) {
1709
1722
  for (const [stateKey, encodedResult] of result.changes) changesCache.set(stateKey, encodedResult);
1710
1723
  subscriber.next(new Map(changesCache));
1711
1724
  }
@@ -2734,7 +2747,7 @@ const fetchOnChainTokenData$1 = async (connector, tokenId) => {
2734
2747
  //#region src/modules/sol-spl/fetchBalances.ts
2735
2748
  const SPL_PROGRAM_ID = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA";
2736
2749
  const dynamicTokenMetadataCache$1 = new _talismn_util.LruMap(1024);
2737
- const associatedTokenAccountCache = new _talismn_util.LruMap(1024);
2750
+ const associatedTokenAccountCache$1 = new _talismn_util.LruMap(1024);
2738
2751
  const fetchBalances$8 = async ({ networkId, tokensWithAddresses, connector }) => {
2739
2752
  if (!tokensWithAddresses.length) return {
2740
2753
  success: [],
@@ -2754,7 +2767,7 @@ const fetchBalances$8 = async ({ networkId, tokensWithAddresses, connector }) =>
2754
2767
  return {
2755
2768
  mintAddress,
2756
2769
  value,
2757
- isSpendable: value !== 0n && state !== "frozen" && d.pubkey === await getAssociatedTokenAccount(address, mintAddress)
2770
+ isSpendable: value !== 0n && state !== "frozen" && d.pubkey === await getAssociatedTokenAccount$1(address, mintAddress)
2758
2771
  };
2759
2772
  } catch {
2760
2773
  log_default.warn("Failed to parse token amount", {
@@ -2847,16 +2860,16 @@ const fetchBalances$8 = async ({ networkId, tokensWithAddresses, connector }) =>
2847
2860
  };
2848
2861
  };
2849
2862
  const getBalanceKey$1 = (tokenId, address) => `${tokenId}:${address}`;
2850
- const getAssociatedTokenAccount = async (owner, mint) => {
2863
+ const getAssociatedTokenAccount$1 = async (owner, mint) => {
2851
2864
  const key = `${owner}:${mint}`;
2852
- const cached = associatedTokenAccountCache.get(key);
2865
+ const cached = associatedTokenAccountCache$1.get(key);
2853
2866
  if (cached) return cached;
2854
2867
  const [associatedTokenAccount] = await (0, _solana_program_token.findAssociatedTokenPda)({
2855
2868
  mint: (0, _solana_kit.address)(mint),
2856
2869
  owner: (0, _solana_kit.address)(owner),
2857
2870
  tokenProgram: _solana_program_token.TOKEN_PROGRAM_ADDRESS
2858
2871
  });
2859
- associatedTokenAccountCache.set(key, associatedTokenAccount);
2872
+ associatedTokenAccountCache$1.set(key, associatedTokenAccount);
2860
2873
  return associatedTokenAccount;
2861
2874
  };
2862
2875
  //#endregion
@@ -3306,6 +3319,7 @@ const fetchOnChainTokenData = async (connector, tokenId) => {
3306
3319
  //#region src/modules/sol-token2022/fetchBalances.ts
3307
3320
  const TOKEN_2022_PROGRAM_ID = "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb";
3308
3321
  const dynamicTokenMetadataCache = /* @__PURE__ */ new Map();
3322
+ const associatedTokenAccountCache = new _talismn_util.LruMap(1024);
3309
3323
  const fetchBalances$7 = async ({ networkId, tokensWithAddresses, connector }) => {
3310
3324
  if (!tokensWithAddresses.length) return {
3311
3325
  success: [],
@@ -3317,19 +3331,15 @@ const fetchBalances$7 = async ({ networkId, tokensWithAddresses, connector }) =>
3317
3331
  const knownTokenIds = new Set(tokensWithAddresses.map(([token]) => token.id));
3318
3332
  const unknownMints = /* @__PURE__ */ new Set();
3319
3333
  const balancesPerAddress = await Promise.all(accountAddresses.map(async (address) => {
3320
- const balances = (await rpc.getTokenAccountsByOwner((0, _solana_kit.address)(address), { programId: (0, _solana_kit.address)(TOKEN_2022_PROGRAM_ID) }, { encoding: "jsonParsed" }).send()).value.map((d) => {
3334
+ const tokenAccounts = await rpc.getTokenAccountsByOwner((0, _solana_kit.address)(address), { programId: (0, _solana_kit.address)(TOKEN_2022_PROGRAM_ID) }, { encoding: "jsonParsed" }).send();
3335
+ const tokenAccountHoldings = await Promise.all(tokenAccounts.value.map(async (d) => {
3321
3336
  try {
3322
- const mintAddress = d.account.data.parsed.info.mint;
3323
- const value = d.account.data.parsed.info.tokenAmount.amount ?? "0";
3324
- const tokenId = (0, _talismn_chaindata_provider.solToken2022TokenId)(networkId, mintAddress);
3325
- if (!knownTokenIds.has(tokenId) && value !== "0") unknownMints.add(mintAddress);
3337
+ const { mint: mintAddress, state, tokenAmount } = d.account.data.parsed.info;
3338
+ const value = BigInt(tokenAmount.amount ?? "0");
3326
3339
  return {
3327
- tokenId,
3328
- networkId,
3329
- address,
3330
- source: MODULE_TYPE$7,
3331
- status: "live",
3332
- value
3340
+ mintAddress,
3341
+ value,
3342
+ isSpendable: value !== 0n && state !== "frozen" && d.pubkey === await getAssociatedTokenAccount(address, mintAddress)
3333
3343
  };
3334
3344
  } catch {
3335
3345
  log_default.warn("Failed to parse token amount", {
@@ -3338,7 +3348,36 @@ const fetchBalances$7 = async ({ networkId, tokensWithAddresses, connector }) =>
3338
3348
  });
3339
3349
  return null;
3340
3350
  }
3341
- }).filter(_talismn_util.isNotNil);
3351
+ }));
3352
+ const holdingsPerTokenId = /* @__PURE__ */ new Map();
3353
+ for (const { mintAddress, value, isSpendable } of tokenAccountHoldings.filter(_talismn_util.isNotNil)) {
3354
+ const tokenId = (0, _talismn_chaindata_provider.solToken2022TokenId)(networkId, mintAddress);
3355
+ if (!knownTokenIds.has(tokenId) && value !== 0n) unknownMints.add(mintAddress);
3356
+ const { total, spendable } = holdingsPerTokenId.get(tokenId) ?? {
3357
+ total: 0n,
3358
+ spendable: 0n
3359
+ };
3360
+ holdingsPerTokenId.set(tokenId, {
3361
+ total: total + value,
3362
+ spendable: spendable + (isSpendable ? value : 0n)
3363
+ });
3364
+ }
3365
+ const balances = [...holdingsPerTokenId].map(([tokenId, { total, spendable }]) => ({
3366
+ tokenId,
3367
+ networkId,
3368
+ address,
3369
+ source: MODULE_TYPE$7,
3370
+ status: "live",
3371
+ ...spendable === total ? { value: total.toString() } : { values: [{
3372
+ type: "free",
3373
+ label: "free",
3374
+ amount: total.toString()
3375
+ }, {
3376
+ type: "locked",
3377
+ label: "locked",
3378
+ amount: (total - spendable).toString()
3379
+ }] }
3380
+ }));
3342
3381
  setDetectedTokenIds(address, MODULE_TYPE$7, balances.map((b) => b.tokenId));
3343
3382
  return [address, balances];
3344
3383
  }));
@@ -3393,6 +3432,18 @@ const fetchBalances$7 = async ({ networkId, tokensWithAddresses, connector }) =>
3393
3432
  };
3394
3433
  };
3395
3434
  const getBalanceKey = (tokenId, address) => `${tokenId}:${address}`;
3435
+ const getAssociatedTokenAccount = async (owner, mint) => {
3436
+ const key = `${owner}:${mint}`;
3437
+ const cached = associatedTokenAccountCache.get(key);
3438
+ if (cached) return cached;
3439
+ const [associatedTokenAccount] = await (0, _solana_program_token.findAssociatedTokenPda)({
3440
+ mint: (0, _solana_kit.address)(mint),
3441
+ owner: (0, _solana_kit.address)(owner),
3442
+ tokenProgram: _solana_program_token_2022.TOKEN_2022_PROGRAM_ADDRESS
3443
+ });
3444
+ associatedTokenAccountCache.set(key, associatedTokenAccount);
3445
+ return associatedTokenAccount;
3446
+ };
3396
3447
  //#endregion
3397
3448
  //#region src/modules/sol-token2022/fetchTokens.ts
3398
3449
  const fetchTokens$7 = async ({ networkId, tokens, connector, cache }) => {
@@ -6807,6 +6858,8 @@ const fetchMiniMetadatas = async (chainConnector, chaindataProvider, chainId, sp
6807
6858
  * Kept tight — a real incoming transfer above 0.1% of the position emits immediately.
6808
6859
  */
6809
6860
  const GENERIC_DRIFT_TOLERANCE_BPS = 10n;
6861
+ /** Delay before resubscribing a substrate module stream that errored: 2s, doubling, capped at 1min */
6862
+ const substrateModuleRetryDelay = (retryCount) => (0, rxjs.timer)(Math.min(6e4, 2e3 * 2 ** (retryCount - 1)));
6810
6863
  const DEFAULT_STORAGE = {
6811
6864
  balances: [],
6812
6865
  miniMetadatas: []
@@ -6944,7 +6997,10 @@ var BalancesProvider = class {
6944
6997
  connector: this.#chainConnectors.substrate,
6945
6998
  miniMetadata,
6946
6999
  hasFrontierMirrorWithdraw
6947
- })), stabilizeModuleResults(mod.type === "substrate-dtao" ? void 0 : classifySmallAmountDrift(GENERIC_DRIFT_TOLERANCE_BPS)), (0, rxjs.catchError)(() => rxjs.EMPTY), (0, rxjs.tap)((results) => {
7000
+ })), stabilizeModuleResults(mod.type === "substrate-dtao" ? void 0 : classifySmallAmountDrift(GENERIC_DRIFT_TOLERANCE_BPS)), (0, rxjs.retry)({
7001
+ delay: (_error, retryCount) => substrateModuleRetryDelay(retryCount),
7002
+ resetOnSuccess: true
7003
+ }), (0, rxjs.tap)((results) => {
6948
7004
  (0, _talismn_util.reportJsActivity)(`module ${mod.type} ${networkId} (${results.success.length})`);
6949
7005
  if (results.dynamicTokens?.length) this.#chaindataProvider.registerDynamicTokens(results.dynamicTokens);
6950
7006
  }), (0, rxjs.map)((results) => ({