@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.mjs CHANGED
@@ -3,7 +3,7 @@ import { EvmErc20TokenSchema, EvmNativeTokenSchema, EvmUniswapV2TokenSchema, MIN
3
3
  import { frontierH160ToSs58Mirror, frontierSs58ToTruncatedH160, getAccountPlatformFromAddress, isAddressEqual, isEthereumAddress, isOnCurveSolanaAddress, isSolanaAddress, normalizeAddress } from "@talismn/crypto";
4
4
  import { BigMath, LruMap, createTimeSlicer, forEachWithYield, getErrorMessage, getSharedObservable, isAbortError, isArrayOf, isBigInt, isErrorOfName, isNotNil, isTruthy, keepAlive, mapWithYield, planckToTokens, reportJsActivity, switchMapChunked, yieldToEventLoop } from "@talismn/util";
5
5
  import { assign, fromPairs, isEqual, keyBy, keys, mergeWith, omit, toPairs, uniq, values } from "lodash-es";
6
- import { BehaviorSubject, EMPTY, Observable, ReplaySubject, auditTime, catchError, combineLatest, defer, distinctUntilChanged, filter, firstValueFrom, from, map, of, shareReplay, startWith, switchMap, tap, timer } from "rxjs";
6
+ import { BehaviorSubject, EMPTY, Observable, ReplaySubject, auditTime, catchError, combineLatest, defer, distinctUntilChanged, filter, firstValueFrom, from, map, of, retry, shareReplay, startWith, switchMap, tap, timer } from "rxjs";
7
7
  import { encodeFunctionData, erc20Abi, erc20Abi_bytes32, getContract, hexToString, parseAbi, withRetry } from "viem";
8
8
  import { newTokenRates } from "@talismn/token-rates";
9
9
  import anylogger from "anylogger";
@@ -1666,18 +1666,31 @@ const fetchRpcQueryPack = async (connector, networkId, queries, at) => {
1666
1666
  return decodeRpcQueryPackChunked(queries, new Map(result.changes), { label: `rpcQueryPack decode ${networkId}` });
1667
1667
  };
1668
1668
  /**
1669
+ * The connector reports a chain that has been unreachable for a while with a StaleRpcError,
1670
+ * but keeps the subscription registered and re-issues it when a socket reconnects (the
1671
+ * first notification after that is a full snapshot again). Matched by its `type` rather
1672
+ * than `instanceof`, so a second copy of @talismn/chain-connectors still matches.
1673
+ */
1674
+ const isStaleRpcError = (error) => typeof error === "object" && error !== null && error.type === "STALE_RPC_ERROR";
1675
+ /**
1669
1676
  * Wraps the raw state_subscribeStorage subscription and emits, for every storage change
1670
1677
  * callback, a SNAPSHOT of the accumulated changes keyed by state key.
1671
1678
  *
1672
1679
  * A snapshot (shallow copy of string refs, cheap) is required because decoding is chunked
1673
1680
  * and asynchronous: a later websocket callback must not mutate the map that an in-flight
1674
1681
  * decode is reading.
1682
+ *
1683
+ * A StaleRpcError does not end the stream. Erroring would unsubscribe, and with no
1684
+ * subscription holding the chain's socket, every later request (eg the dtao poll) would
1685
+ * open and close a socket of its own, starting from the network's first RPC each time.
1675
1686
  */
1676
1687
  const getRawStorageUpdates$ = (connector, networkId, allStateKeys, timeout) => new Observable((subscriber) => {
1677
1688
  const changesCache = /* @__PURE__ */ new Map();
1678
1689
  const promUnsub = connector.subscribe(networkId, "state_subscribeStorage", "state_storage", [allStateKeys], (error, result) => {
1679
- if (error) subscriber.error(error);
1680
- else if (result) {
1690
+ if (error) {
1691
+ if (isStaleRpcError(error)) log_default.warn(`Storage subscription stale on ${networkId}`);
1692
+ else subscriber.error(error);
1693
+ } else if (result) {
1681
1694
  for (const [stateKey, encodedResult] of result.changes) changesCache.set(stateKey, encodedResult);
1682
1695
  subscriber.next(new Map(changesCache));
1683
1696
  }
@@ -2706,7 +2719,7 @@ const fetchOnChainTokenData$1 = async (connector, tokenId) => {
2706
2719
  //#region src/modules/sol-spl/fetchBalances.ts
2707
2720
  const SPL_PROGRAM_ID = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA";
2708
2721
  const dynamicTokenMetadataCache$1 = new LruMap(1024);
2709
- const associatedTokenAccountCache = new LruMap(1024);
2722
+ const associatedTokenAccountCache$1 = new LruMap(1024);
2710
2723
  const fetchBalances$8 = async ({ networkId, tokensWithAddresses, connector }) => {
2711
2724
  if (!tokensWithAddresses.length) return {
2712
2725
  success: [],
@@ -2726,7 +2739,7 @@ const fetchBalances$8 = async ({ networkId, tokensWithAddresses, connector }) =>
2726
2739
  return {
2727
2740
  mintAddress,
2728
2741
  value,
2729
- isSpendable: value !== 0n && state !== "frozen" && d.pubkey === await getAssociatedTokenAccount(address$2, mintAddress)
2742
+ isSpendable: value !== 0n && state !== "frozen" && d.pubkey === await getAssociatedTokenAccount$1(address$2, mintAddress)
2730
2743
  };
2731
2744
  } catch {
2732
2745
  log_default.warn("Failed to parse token amount", {
@@ -2819,16 +2832,16 @@ const fetchBalances$8 = async ({ networkId, tokensWithAddresses, connector }) =>
2819
2832
  };
2820
2833
  };
2821
2834
  const getBalanceKey$1 = (tokenId, address) => `${tokenId}:${address}`;
2822
- const getAssociatedTokenAccount = async (owner, mint) => {
2835
+ const getAssociatedTokenAccount$1 = async (owner, mint) => {
2823
2836
  const key = `${owner}:${mint}`;
2824
- const cached = associatedTokenAccountCache.get(key);
2837
+ const cached = associatedTokenAccountCache$1.get(key);
2825
2838
  if (cached) return cached;
2826
2839
  const [associatedTokenAccount] = await findAssociatedTokenPda({
2827
2840
  mint: address(mint),
2828
2841
  owner: address(owner),
2829
2842
  tokenProgram: TOKEN_PROGRAM_ADDRESS
2830
2843
  });
2831
- associatedTokenAccountCache.set(key, associatedTokenAccount);
2844
+ associatedTokenAccountCache$1.set(key, associatedTokenAccount);
2832
2845
  return associatedTokenAccount;
2833
2846
  };
2834
2847
  //#endregion
@@ -3278,6 +3291,7 @@ const fetchOnChainTokenData = async (connector, tokenId) => {
3278
3291
  //#region src/modules/sol-token2022/fetchBalances.ts
3279
3292
  const TOKEN_2022_PROGRAM_ID = "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb";
3280
3293
  const dynamicTokenMetadataCache = /* @__PURE__ */ new Map();
3294
+ const associatedTokenAccountCache = new LruMap(1024);
3281
3295
  const fetchBalances$7 = async ({ networkId, tokensWithAddresses, connector }) => {
3282
3296
  if (!tokensWithAddresses.length) return {
3283
3297
  success: [],
@@ -3289,19 +3303,15 @@ const fetchBalances$7 = async ({ networkId, tokensWithAddresses, connector }) =>
3289
3303
  const knownTokenIds = new Set(tokensWithAddresses.map(([token]) => token.id));
3290
3304
  const unknownMints = /* @__PURE__ */ new Set();
3291
3305
  const balancesPerAddress = await Promise.all(accountAddresses.map(async (address$1) => {
3292
- const balances = (await rpc.getTokenAccountsByOwner(address(address$1), { programId: address(TOKEN_2022_PROGRAM_ID) }, { encoding: "jsonParsed" }).send()).value.map((d) => {
3306
+ const tokenAccounts = await rpc.getTokenAccountsByOwner(address(address$1), { programId: address(TOKEN_2022_PROGRAM_ID) }, { encoding: "jsonParsed" }).send();
3307
+ const tokenAccountHoldings = await Promise.all(tokenAccounts.value.map(async (d) => {
3293
3308
  try {
3294
- const mintAddress = d.account.data.parsed.info.mint;
3295
- const value = d.account.data.parsed.info.tokenAmount.amount ?? "0";
3296
- const tokenId = solToken2022TokenId(networkId, mintAddress);
3297
- if (!knownTokenIds.has(tokenId) && value !== "0") unknownMints.add(mintAddress);
3309
+ const { mint: mintAddress, state, tokenAmount } = d.account.data.parsed.info;
3310
+ const value = BigInt(tokenAmount.amount ?? "0");
3298
3311
  return {
3299
- tokenId,
3300
- networkId,
3301
- address: address$1,
3302
- source: MODULE_TYPE$7,
3303
- status: "live",
3304
- value
3312
+ mintAddress,
3313
+ value,
3314
+ isSpendable: value !== 0n && state !== "frozen" && d.pubkey === await getAssociatedTokenAccount(address$1, mintAddress)
3305
3315
  };
3306
3316
  } catch {
3307
3317
  log_default.warn("Failed to parse token amount", {
@@ -3310,7 +3320,36 @@ const fetchBalances$7 = async ({ networkId, tokensWithAddresses, connector }) =>
3310
3320
  });
3311
3321
  return null;
3312
3322
  }
3313
- }).filter(isNotNil);
3323
+ }));
3324
+ const holdingsPerTokenId = /* @__PURE__ */ new Map();
3325
+ for (const { mintAddress, value, isSpendable } of tokenAccountHoldings.filter(isNotNil)) {
3326
+ const tokenId = solToken2022TokenId(networkId, mintAddress);
3327
+ if (!knownTokenIds.has(tokenId) && value !== 0n) unknownMints.add(mintAddress);
3328
+ const { total, spendable } = holdingsPerTokenId.get(tokenId) ?? {
3329
+ total: 0n,
3330
+ spendable: 0n
3331
+ };
3332
+ holdingsPerTokenId.set(tokenId, {
3333
+ total: total + value,
3334
+ spendable: spendable + (isSpendable ? value : 0n)
3335
+ });
3336
+ }
3337
+ const balances = [...holdingsPerTokenId].map(([tokenId, { total, spendable }]) => ({
3338
+ tokenId,
3339
+ networkId,
3340
+ address: address$1,
3341
+ source: MODULE_TYPE$7,
3342
+ status: "live",
3343
+ ...spendable === total ? { value: total.toString() } : { values: [{
3344
+ type: "free",
3345
+ label: "free",
3346
+ amount: total.toString()
3347
+ }, {
3348
+ type: "locked",
3349
+ label: "locked",
3350
+ amount: (total - spendable).toString()
3351
+ }] }
3352
+ }));
3314
3353
  setDetectedTokenIds(address$1, MODULE_TYPE$7, balances.map((b) => b.tokenId));
3315
3354
  return [address$1, balances];
3316
3355
  }));
@@ -3365,6 +3404,18 @@ const fetchBalances$7 = async ({ networkId, tokensWithAddresses, connector }) =>
3365
3404
  };
3366
3405
  };
3367
3406
  const getBalanceKey = (tokenId, address) => `${tokenId}:${address}`;
3407
+ const getAssociatedTokenAccount = async (owner, mint) => {
3408
+ const key = `${owner}:${mint}`;
3409
+ const cached = associatedTokenAccountCache.get(key);
3410
+ if (cached) return cached;
3411
+ const [associatedTokenAccount] = await findAssociatedTokenPda({
3412
+ mint: address(mint),
3413
+ owner: address(owner),
3414
+ tokenProgram: TOKEN_2022_PROGRAM_ADDRESS
3415
+ });
3416
+ associatedTokenAccountCache.set(key, associatedTokenAccount);
3417
+ return associatedTokenAccount;
3418
+ };
3368
3419
  //#endregion
3369
3420
  //#region src/modules/sol-token2022/fetchTokens.ts
3370
3421
  const fetchTokens$7 = async ({ networkId, tokens, connector, cache }) => {
@@ -6779,6 +6830,8 @@ const fetchMiniMetadatas = async (chainConnector, chaindataProvider, chainId, sp
6779
6830
  * Kept tight — a real incoming transfer above 0.1% of the position emits immediately.
6780
6831
  */
6781
6832
  const GENERIC_DRIFT_TOLERANCE_BPS = 10n;
6833
+ /** Delay before resubscribing a substrate module stream that errored: 2s, doubling, capped at 1min */
6834
+ const substrateModuleRetryDelay = (retryCount) => timer(Math.min(6e4, 2e3 * 2 ** (retryCount - 1)));
6782
6835
  const DEFAULT_STORAGE = {
6783
6836
  balances: [],
6784
6837
  miniMetadatas: []
@@ -6916,7 +6969,10 @@ var BalancesProvider = class {
6916
6969
  connector: this.#chainConnectors.substrate,
6917
6970
  miniMetadata,
6918
6971
  hasFrontierMirrorWithdraw
6919
- })), stabilizeModuleResults(mod.type === "substrate-dtao" ? void 0 : classifySmallAmountDrift(GENERIC_DRIFT_TOLERANCE_BPS)), catchError(() => EMPTY), tap((results) => {
6972
+ })), stabilizeModuleResults(mod.type === "substrate-dtao" ? void 0 : classifySmallAmountDrift(GENERIC_DRIFT_TOLERANCE_BPS)), retry({
6973
+ delay: (_error, retryCount) => substrateModuleRetryDelay(retryCount),
6974
+ resetOnSuccess: true
6975
+ }), tap((results) => {
6920
6976
  reportJsActivity(`module ${mod.type} ${networkId} (${results.success.length})`);
6921
6977
  if (results.dynamicTokens?.length) this.#chaindataProvider.registerDynamicTokens(results.dynamicTokens);
6922
6978
  }), map((results) => ({