@talismn/balances 3.1.0 → 3.1.2
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.d.mts +20 -20
- package/dist/index.d.mts.map +1 -1
- package/dist/index.d.ts +17 -17
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +85 -36
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +86 -37
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -4
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)
|
|
1680
|
-
|
|
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
|
}
|
|
@@ -2482,6 +2495,12 @@ const createBalanceStabilizer = (isEffectivelyEqual, options) => {
|
|
|
2482
2495
|
};
|
|
2483
2496
|
};
|
|
2484
2497
|
/**
|
|
2498
|
+
* Cross-module drift tolerance (see classifySmallAmountDrift): amounts moving by no more
|
|
2499
|
+
* than this classify as drift and re-emit at most once per stabilizer refresh interval.
|
|
2500
|
+
* Kept tight — a real incoming transfer above 0.01% of the position emits immediately.
|
|
2501
|
+
*/
|
|
2502
|
+
const GENERIC_DRIFT_TOLERANCE_BPS = 1n;
|
|
2503
|
+
/**
|
|
2485
2504
|
* Default cross-module drift classifier: balances whose SHAPE is identical (same
|
|
2486
2505
|
* address/token/status, same value set with same types/labels/metas) but whose amounts
|
|
2487
2506
|
* moved by no more than `toleranceBps` classify as "drift" — continuously-moving
|
|
@@ -2706,7 +2725,7 @@ const fetchOnChainTokenData$1 = async (connector, tokenId) => {
|
|
|
2706
2725
|
//#region src/modules/sol-spl/fetchBalances.ts
|
|
2707
2726
|
const SPL_PROGRAM_ID = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA";
|
|
2708
2727
|
const dynamicTokenMetadataCache$1 = new LruMap(1024);
|
|
2709
|
-
const associatedTokenAccountCache = new LruMap(1024);
|
|
2728
|
+
const associatedTokenAccountCache$1 = new LruMap(1024);
|
|
2710
2729
|
const fetchBalances$8 = async ({ networkId, tokensWithAddresses, connector }) => {
|
|
2711
2730
|
if (!tokensWithAddresses.length) return {
|
|
2712
2731
|
success: [],
|
|
@@ -2726,7 +2745,7 @@ const fetchBalances$8 = async ({ networkId, tokensWithAddresses, connector }) =>
|
|
|
2726
2745
|
return {
|
|
2727
2746
|
mintAddress,
|
|
2728
2747
|
value,
|
|
2729
|
-
isSpendable: value !== 0n && state !== "frozen" && d.pubkey === await getAssociatedTokenAccount(address$2, mintAddress)
|
|
2748
|
+
isSpendable: value !== 0n && state !== "frozen" && d.pubkey === await getAssociatedTokenAccount$1(address$2, mintAddress)
|
|
2730
2749
|
};
|
|
2731
2750
|
} catch {
|
|
2732
2751
|
log_default.warn("Failed to parse token amount", {
|
|
@@ -2819,16 +2838,16 @@ const fetchBalances$8 = async ({ networkId, tokensWithAddresses, connector }) =>
|
|
|
2819
2838
|
};
|
|
2820
2839
|
};
|
|
2821
2840
|
const getBalanceKey$1 = (tokenId, address) => `${tokenId}:${address}`;
|
|
2822
|
-
const getAssociatedTokenAccount = async (owner, mint) => {
|
|
2841
|
+
const getAssociatedTokenAccount$1 = async (owner, mint) => {
|
|
2823
2842
|
const key = `${owner}:${mint}`;
|
|
2824
|
-
const cached = associatedTokenAccountCache.get(key);
|
|
2843
|
+
const cached = associatedTokenAccountCache$1.get(key);
|
|
2825
2844
|
if (cached) return cached;
|
|
2826
2845
|
const [associatedTokenAccount] = await findAssociatedTokenPda({
|
|
2827
2846
|
mint: address(mint),
|
|
2828
2847
|
owner: address(owner),
|
|
2829
2848
|
tokenProgram: TOKEN_PROGRAM_ADDRESS
|
|
2830
2849
|
});
|
|
2831
|
-
associatedTokenAccountCache.set(key, associatedTokenAccount);
|
|
2850
|
+
associatedTokenAccountCache$1.set(key, associatedTokenAccount);
|
|
2832
2851
|
return associatedTokenAccount;
|
|
2833
2852
|
};
|
|
2834
2853
|
//#endregion
|
|
@@ -3278,6 +3297,7 @@ const fetchOnChainTokenData = async (connector, tokenId) => {
|
|
|
3278
3297
|
//#region src/modules/sol-token2022/fetchBalances.ts
|
|
3279
3298
|
const TOKEN_2022_PROGRAM_ID = "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb";
|
|
3280
3299
|
const dynamicTokenMetadataCache = /* @__PURE__ */ new Map();
|
|
3300
|
+
const associatedTokenAccountCache = new LruMap(1024);
|
|
3281
3301
|
const fetchBalances$7 = async ({ networkId, tokensWithAddresses, connector }) => {
|
|
3282
3302
|
if (!tokensWithAddresses.length) return {
|
|
3283
3303
|
success: [],
|
|
@@ -3289,19 +3309,15 @@ const fetchBalances$7 = async ({ networkId, tokensWithAddresses, connector }) =>
|
|
|
3289
3309
|
const knownTokenIds = new Set(tokensWithAddresses.map(([token]) => token.id));
|
|
3290
3310
|
const unknownMints = /* @__PURE__ */ new Set();
|
|
3291
3311
|
const balancesPerAddress = await Promise.all(accountAddresses.map(async (address$1) => {
|
|
3292
|
-
const
|
|
3312
|
+
const tokenAccounts = await rpc.getTokenAccountsByOwner(address(address$1), { programId: address(TOKEN_2022_PROGRAM_ID) }, { encoding: "jsonParsed" }).send();
|
|
3313
|
+
const tokenAccountHoldings = await Promise.all(tokenAccounts.value.map(async (d) => {
|
|
3293
3314
|
try {
|
|
3294
|
-
const mintAddress = d.account.data.parsed.info
|
|
3295
|
-
const value =
|
|
3296
|
-
const tokenId = solToken2022TokenId(networkId, mintAddress);
|
|
3297
|
-
if (!knownTokenIds.has(tokenId) && value !== "0") unknownMints.add(mintAddress);
|
|
3315
|
+
const { mint: mintAddress, state, tokenAmount } = d.account.data.parsed.info;
|
|
3316
|
+
const value = BigInt(tokenAmount.amount ?? "0");
|
|
3298
3317
|
return {
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
source: MODULE_TYPE$7,
|
|
3303
|
-
status: "live",
|
|
3304
|
-
value
|
|
3318
|
+
mintAddress,
|
|
3319
|
+
value,
|
|
3320
|
+
isSpendable: value !== 0n && state !== "frozen" && d.pubkey === await getAssociatedTokenAccount(address$1, mintAddress)
|
|
3305
3321
|
};
|
|
3306
3322
|
} catch {
|
|
3307
3323
|
log_default.warn("Failed to parse token amount", {
|
|
@@ -3310,7 +3326,36 @@ const fetchBalances$7 = async ({ networkId, tokensWithAddresses, connector }) =>
|
|
|
3310
3326
|
});
|
|
3311
3327
|
return null;
|
|
3312
3328
|
}
|
|
3313
|
-
})
|
|
3329
|
+
}));
|
|
3330
|
+
const holdingsPerTokenId = /* @__PURE__ */ new Map();
|
|
3331
|
+
for (const { mintAddress, value, isSpendable } of tokenAccountHoldings.filter(isNotNil)) {
|
|
3332
|
+
const tokenId = solToken2022TokenId(networkId, mintAddress);
|
|
3333
|
+
if (!knownTokenIds.has(tokenId) && value !== 0n) unknownMints.add(mintAddress);
|
|
3334
|
+
const { total, spendable } = holdingsPerTokenId.get(tokenId) ?? {
|
|
3335
|
+
total: 0n,
|
|
3336
|
+
spendable: 0n
|
|
3337
|
+
};
|
|
3338
|
+
holdingsPerTokenId.set(tokenId, {
|
|
3339
|
+
total: total + value,
|
|
3340
|
+
spendable: spendable + (isSpendable ? value : 0n)
|
|
3341
|
+
});
|
|
3342
|
+
}
|
|
3343
|
+
const balances = [...holdingsPerTokenId].map(([tokenId, { total, spendable }]) => ({
|
|
3344
|
+
tokenId,
|
|
3345
|
+
networkId,
|
|
3346
|
+
address: address$1,
|
|
3347
|
+
source: MODULE_TYPE$7,
|
|
3348
|
+
status: "live",
|
|
3349
|
+
...spendable === total ? { value: total.toString() } : { values: [{
|
|
3350
|
+
type: "free",
|
|
3351
|
+
label: "free",
|
|
3352
|
+
amount: total.toString()
|
|
3353
|
+
}, {
|
|
3354
|
+
type: "locked",
|
|
3355
|
+
label: "locked",
|
|
3356
|
+
amount: (total - spendable).toString()
|
|
3357
|
+
}] }
|
|
3358
|
+
}));
|
|
3314
3359
|
setDetectedTokenIds(address$1, MODULE_TYPE$7, balances.map((b) => b.tokenId));
|
|
3315
3360
|
return [address$1, balances];
|
|
3316
3361
|
}));
|
|
@@ -3365,6 +3410,18 @@ const fetchBalances$7 = async ({ networkId, tokensWithAddresses, connector }) =>
|
|
|
3365
3410
|
};
|
|
3366
3411
|
};
|
|
3367
3412
|
const getBalanceKey = (tokenId, address) => `${tokenId}:${address}`;
|
|
3413
|
+
const getAssociatedTokenAccount = async (owner, mint) => {
|
|
3414
|
+
const key = `${owner}:${mint}`;
|
|
3415
|
+
const cached = associatedTokenAccountCache.get(key);
|
|
3416
|
+
if (cached) return cached;
|
|
3417
|
+
const [associatedTokenAccount] = await findAssociatedTokenPda({
|
|
3418
|
+
mint: address(mint),
|
|
3419
|
+
owner: address(owner),
|
|
3420
|
+
tokenProgram: TOKEN_2022_PROGRAM_ADDRESS
|
|
3421
|
+
});
|
|
3422
|
+
associatedTokenAccountCache.set(key, associatedTokenAccount);
|
|
3423
|
+
return associatedTokenAccount;
|
|
3424
|
+
};
|
|
3368
3425
|
//#endregion
|
|
3369
3426
|
//#region src/modules/sol-token2022/fetchTokens.ts
|
|
3370
3427
|
const fetchTokens$7 = async ({ networkId, tokens, connector, cache }) => {
|
|
@@ -4430,16 +4487,10 @@ const getTransferCallData$5 = ({ from, to, value, token, metadataRpc }) => {
|
|
|
4430
4487
|
* dividend accrual on (nearly) every block; the display is informational */
|
|
4431
4488
|
const CLAIM_DRIFT_TOLERANCE_BPS = 100n;
|
|
4432
4489
|
/**
|
|
4433
|
-
* subnet staking positions auto-compound: dividend injections land once per subnet tempo
|
|
4434
|
-
* (staggered across subnets, so with many positions SOME position bumps on nearly every
|
|
4435
|
-
* poll). Moves up to this size classify as drift (surfaced within the refresh interval);
|
|
4436
|
-
* anything larger (a real stake/unstake) emits immediately.
|
|
4437
|
-
*/
|
|
4438
|
-
const STAKE_DRIFT_TOLERANCE_BPS = 100n;
|
|
4439
|
-
/**
|
|
4440
4490
|
* dtao balances embed values that drift on (nearly) every block even when the user's
|
|
4441
|
-
* position is untouched:
|
|
4442
|
-
*
|
|
4491
|
+
* position is untouched: basket claimable quotes move and conviction locks decay.
|
|
4492
|
+
* Stake itself only moves on dividend injections, once per subnet tempo, so every stake
|
|
4493
|
+
* move emits immediately.
|
|
4443
4494
|
*
|
|
4444
4495
|
* Without special handling, every 6s poll re-emits the full result set, which defeats
|
|
4445
4496
|
* every distinctUntilChanged stage downstream and forces the whole pipeline
|
|
@@ -4490,7 +4541,6 @@ const classifyValue = (previous, next) => {
|
|
|
4490
4541
|
if (previous.label === "Claimable rewards") {
|
|
4491
4542
|
if (!isWithinDriftTolerance(previous.amount, next.amount, CLAIM_DRIFT_TOLERANCE_BPS)) drift = true;
|
|
4492
4543
|
} else if (previousLock) drift = true;
|
|
4493
|
-
else if (isWithinDriftTolerance(previous.amount, next.amount, STAKE_DRIFT_TOLERANCE_BPS)) drift = true;
|
|
4494
4544
|
else return changed(`amount ${previous.amount}→${next.amount}`);
|
|
4495
4545
|
}
|
|
4496
4546
|
return { equivalence: drift ? "drift" : "equal" };
|
|
@@ -6773,12 +6823,8 @@ const fetchMiniMetadatas = async (chainConnector, chaindataProvider, chainId, sp
|
|
|
6773
6823
|
};
|
|
6774
6824
|
//#endregion
|
|
6775
6825
|
//#region src/BalancesProvider.ts
|
|
6776
|
-
/**
|
|
6777
|
-
|
|
6778
|
-
* than this classify as drift and re-emit at most once per stabilizer refresh interval.
|
|
6779
|
-
* Kept tight — a real incoming transfer above 0.1% of the position emits immediately.
|
|
6780
|
-
*/
|
|
6781
|
-
const GENERIC_DRIFT_TOLERANCE_BPS = 10n;
|
|
6826
|
+
/** Delay before resubscribing a substrate module stream that errored: 2s, doubling, capped at 1min */
|
|
6827
|
+
const substrateModuleRetryDelay = (retryCount) => timer(Math.min(6e4, 2e3 * 2 ** (retryCount - 1)));
|
|
6782
6828
|
const DEFAULT_STORAGE = {
|
|
6783
6829
|
balances: [],
|
|
6784
6830
|
miniMetadatas: []
|
|
@@ -6916,7 +6962,10 @@ var BalancesProvider = class {
|
|
|
6916
6962
|
connector: this.#chainConnectors.substrate,
|
|
6917
6963
|
miniMetadata,
|
|
6918
6964
|
hasFrontierMirrorWithdraw
|
|
6919
|
-
})), stabilizeModuleResults(mod.type === "substrate-dtao" ? void 0 : classifySmallAmountDrift(GENERIC_DRIFT_TOLERANCE_BPS)),
|
|
6965
|
+
})), stabilizeModuleResults(mod.type === "substrate-dtao" ? void 0 : classifySmallAmountDrift(GENERIC_DRIFT_TOLERANCE_BPS)), retry({
|
|
6966
|
+
delay: (_error, retryCount) => substrateModuleRetryDelay(retryCount),
|
|
6967
|
+
resetOnSuccess: true
|
|
6968
|
+
}), tap((results) => {
|
|
6920
6969
|
reportJsActivity(`module ${mod.type} ${networkId} (${results.success.length})`);
|
|
6921
6970
|
if (results.dynamicTokens?.length) this.#chaindataProvider.registerDynamicTokens(results.dynamicTokens);
|
|
6922
6971
|
}), map((results) => ({
|