@augustdigital/sdk 8.11.0 → 8.13.0

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.
@@ -1,4 +1,5 @@
1
- import type { IAddress } from '../../types';
1
+ import { type Signer, type Wallet } from 'ethers';
2
+ import type { IAddress, ISwapRouterDepositResult, ISwapRouterDepositResultOptions } from '../../types';
2
3
  /**
3
4
  * Resolve the `SwapRouter` address for a chain.
4
5
  *
@@ -11,6 +12,26 @@ import type { IAddress } from '../../types';
11
12
  * ```
12
13
  */
13
14
  export declare function getSwapRouterAddress(chainId: number): IAddress | undefined;
15
+ /**
16
+ * Resolve the human-readable display name of the `SwapRouter` periphery
17
+ * contract for a chain.
18
+ *
19
+ * The contract exposes no name on-chain, so this reads the SDK-maintained
20
+ * {@link SWAP_ROUTER_NAMES} registry. Intended for UI surfaces that label the
21
+ * contract a swap-and-deposit routes through (e.g. a "Router" row in a
22
+ * transaction overview) — display metadata only, never routing logic.
23
+ *
24
+ * @param chainId - EVM chain ID.
25
+ * @returns The display name (e.g. `'Upshift Swap Router'`), or `undefined`
26
+ * when no SwapRouter is deployed on the chain — callers should omit the
27
+ * label rather than show an address.
28
+ * @example
29
+ * ```ts
30
+ * const name = getSwapRouterName(1);
31
+ * // 'Upshift Swap Router'
32
+ * ```
33
+ */
34
+ export declare function getSwapRouterName(chainId: number): string | undefined;
14
35
  /**
15
36
  * Whether a vault is eligible for the any-token `SwapRouter` deposit surface
16
37
  * (the SwapRouter is `enableVault`-ed for it on-chain). Address comparison is
@@ -22,13 +43,20 @@ export declare function getSwapRouterAddress(chainId: number): IAddress | undefi
22
43
  *
23
44
  * @param vaultAddress - August vault address.
24
45
  * @returns `true` when the vault is in {@link SWAP_ROUTER_ELIGIBLE_VAULTS}.
46
+ * @deprecated Superseded by {@link getSwapRouterEligibleVaults}, which resolves
47
+ * eligibility from the router's on-chain `VaultEnabled` history + current
48
+ * `vaultInfo` registration instead of this static snapshot. Kept functional as
49
+ * the zero-RPC sync fallback (seed data, fail-fast checks); migrate UI gates
50
+ * to the async resolver.
25
51
  */
26
52
  export declare function isSwapRouterEligible(vaultAddress: IAddress): boolean;
27
53
  /**
28
- * @deprecated Renamed to {@link isSwapRouterEligible}. The meaning also changed:
29
- * it no longer implies `vaultDeposit` routes the vault through the SwapRouter
30
- * (that behavior was removed) it only reports swap-router *eligibility* for
31
- * the opt-in deposit surface. Kept as an alias for one release.
54
+ * @deprecated Renamed to {@link isSwapRouterEligible} (itself now superseded by
55
+ * {@link getSwapRouterEligibleVaults}, the on-chain-derived resolver). The
56
+ * meaning also changed: it no longer implies `vaultDeposit` routes the vault
57
+ * through the SwapRouter (that behavior was removed) it only reports
58
+ * swap-router *eligibility* for the opt-in deposit surface. Kept as an alias
59
+ * for one release; migrate to the async resolver.
32
60
  */
33
61
  export declare const vaultUsesSwapRouter: typeof isSwapRouterEligible;
34
62
  /**
@@ -46,3 +74,138 @@ export declare const vaultUsesSwapRouter: typeof isSwapRouterEligible;
46
74
  * @throws {@link AugustValidationError} when `provided` is not a 32-byte hex string.
47
75
  */
48
76
  export declare function resolveOriginCode(provided?: `0x${string}`): `0x${string}`;
77
+ /**
78
+ * Resolves the real, post-execution outcome of a SwapRouter deposit from its
79
+ * mined transaction receipt. The SwapRouter contract itself emits no
80
+ * deposit/amount-out event — only the vault does, via its own `Deposit` event
81
+ * — so this decodes that log directly rather than trusting the pre-trade quote
82
+ * a UI shows before the user submits (the true post-swap amount depends on
83
+ * execution-time price and can differ from that estimate).
84
+ *
85
+ * Works for both vault generations without the caller resolving the version
86
+ * first: evm-1 (ERC-4626-style `Deposit(sender, owner, assets, shares)`) and
87
+ * evm-2 (`Deposit(assetIn, amountIn, shares, senderAddr, receiverAddr)`) have
88
+ * different event signatures (different topic0), so both are registered and
89
+ * whichever matches the vault's actual log is decoded.
90
+ *
91
+ * @param signer - ethers `Signer` or `Wallet` connected to a provider (used
92
+ * read-only here — no transaction is sent).
93
+ * @param options - {@link ISwapRouterDepositResultOptions}.
94
+ * @returns The decoded {@link ISwapRouterDepositResult}, or `null` if the
95
+ * receipt isn't found yet or carries no `Deposit` log from `vault` (e.g. the
96
+ * hash is wrong, or the tx hasn't mined).
97
+ * @throws {@link AugustValidationError} if `signer` has no connected provider.
98
+ * @worstCaseRpcCalls 1 — a single `getTransactionReceipt` call; decoding is
99
+ * local.
100
+ * @example
101
+ * ```ts
102
+ * const hash = await augustSdk.evm.swapRouterDeposit({ ... });
103
+ * const result = await augustSdk.evm.getSwapRouterDepositResult({
104
+ * txHash: hash,
105
+ * vault: '0x74ad2f789ed583dbd141bbdafc673fe1f033718b',
106
+ * });
107
+ * // result?.amountOut — actual reference-asset amount received after the swap
108
+ * ```
109
+ */
110
+ export declare function getSwapRouterDepositResult(signer: Signer | Wallet, { txHash, vault }: ISwapRouterDepositResultOptions): Promise<ISwapRouterDepositResult | null>;
111
+ /**
112
+ * Resolve the ERC-20 tokens currently whitelisted for the `SwapRouter` on a
113
+ * chain — the any-token set a UI can offer as swap-and-deposit inputs.
114
+ *
115
+ * The router exposes its allowlist only as `mapping(address => bool)
116
+ * whitelistedTokens` with **no on-chain enumeration**, so this cannot be a
117
+ * single view call. It runs in two stages:
118
+ * 1. Scan the router's `TokenEnabled(address)` event history (from the chain's
119
+ * {@link SWAP_ROUTER_DEPLOY_BLOCKS} floor, in `eth_getLogs` chunks sized by
120
+ * {@link determineBlockSkipInternal}) to reconstruct the candidate set of
121
+ * every token ever enabled.
122
+ * 2. Re-read `whitelistedTokens(token)` for each candidate — batched via
123
+ * Multicall3 where verified, else per-call — and keep only those the
124
+ * mapping currently returns `true` for. This step makes later disables (and
125
+ * disable-then-re-enable) correct without replaying `TokenDisabled`, and is
126
+ * the fail-closed source of truth: a token surfaces only on a definite
127
+ * on-chain `true`.
128
+ *
129
+ * The resolved list is cached per chain for 5 minutes — the allowlist changes
130
+ * only via an admin `enableToken`/`disableToken` transaction, so a short TTL
131
+ * keeps the (multi-RPC) scan off the hot path without going stale for long.
132
+ *
133
+ * @param chainId - EVM chain ID. Chains without a {@link SWAP_ROUTER_ADDRESSES}
134
+ * entry resolve to `[]` with no RPC.
135
+ * @param options - `rpcUrl` (required) must point at `chainId`'s network;
136
+ * `fromBlock` overrides the default deploy-block scan floor (e.g. to widen a
137
+ * scan on a chain missing from {@link SWAP_ROUTER_DEPLOY_BLOCKS}).
138
+ * @returns Checksummed addresses of the currently-whitelisted tokens, deduped.
139
+ * Empty when no router is deployed or none are enabled. Callers still filter
140
+ * out a given vault's natively-accepted assets (those deposit without a swap).
141
+ * @throws AugustValidationError when `rpcUrl` is missing/empty.
142
+ * @throws AugustSDKError when an `eth_getLogs` chunk fails — surfaced (not
143
+ * swallowed) so a partial scan never masquerades as a complete allowlist.
144
+ * @worstCaseRpcCalls On mainnet, 1 `eth_blockNumber` + ~4 `eth_getLogs` chunks
145
+ * (router-lifetime ÷ 50k-block window) + 1 Multicall3 `eth_call` ≈ 6; scales
146
+ * with the router's on-chain age, not with candidate count.
147
+ * @example
148
+ * ```ts
149
+ * const tokens = await getSwapRouterWhitelistedTokens(1, {
150
+ * rpcUrl: 'https://eth-mainnet.example/v2/KEY',
151
+ * });
152
+ * // ['0xA0b8…', '0xdAC1…', '0xC02a…', '0x2260…'] (USDC, USDT, WETH, WBTC)
153
+ * ```
154
+ */
155
+ export declare function getSwapRouterWhitelistedTokens(chainId: number, options: {
156
+ rpcUrl: string;
157
+ fromBlock?: number;
158
+ }): Promise<IAddress[]>;
159
+ /**
160
+ * Resolve the vaults currently eligible for the any-token `SwapRouter` deposit
161
+ * surface on a chain — i.e. every vault the router has `enableVault`-ed that is
162
+ * still registered on-chain.
163
+ *
164
+ * The router stores registrations as `mapping(address => VaultInfo) vaultInfo`
165
+ * with **no on-chain enumeration**, so (exactly like
166
+ * {@link getSwapRouterWhitelistedTokens}) this runs in two stages:
167
+ * 1. Scan the router's `VaultEnabled(address)` event history (from the chain's
168
+ * {@link SWAP_ROUTER_DEPLOY_BLOCKS} floor, in `eth_getLogs` chunks sized by
169
+ * {@link determineBlockSkipInternal}) to reconstruct the candidate set of
170
+ * every vault ever enabled.
171
+ * 2. Re-read `vaultInfo(vault)` for each candidate — batched via Multicall3
172
+ * where verified, else per-call — and keep only those whose
173
+ * `referenceAsset != address(0)` (an unregistered vault reverts with
174
+ * `InvalidVault` at deposit time). This makes later disables correct
175
+ * without replaying `VaultDisabled`, and is the fail-closed source of
176
+ * truth: a vault surfaces only on a definite on-chain registration.
177
+ *
178
+ * This is the dynamic successor to the static
179
+ * {@link SWAP_ROUTER_ELIGIBLE_VAULTS} snapshot / {@link isSwapRouterEligible}.
180
+ * It reports **on-chain eligibility only** — app-level policy (e.g. excluding
181
+ * OVault-enabled vaults that have their own deposit flow) remains the caller's
182
+ * responsibility, as does the flag gating of the UI surface.
183
+ *
184
+ * The resolved list is cached per chain for 5 minutes — registrations change
185
+ * only via an admin `enableVault`/`disableVault` transaction, so a short TTL
186
+ * keeps the (multi-RPC) scan off the hot path without going stale for long.
187
+ *
188
+ * @param chainId - EVM chain ID. Chains without a {@link SWAP_ROUTER_ADDRESSES}
189
+ * entry resolve to `[]` with no RPC.
190
+ * @param options - `rpcUrl` (required) must point at `chainId`'s network;
191
+ * `fromBlock` overrides the default deploy-block scan floor.
192
+ * @returns Checksummed addresses of the currently-registered eligible vaults,
193
+ * deduped. Empty when no router is deployed or none are registered.
194
+ * @throws AugustValidationError when `rpcUrl` is missing/empty.
195
+ * @throws AugustSDKError when an `eth_getLogs` chunk fails — surfaced (not
196
+ * swallowed) so a partial scan never masquerades as the complete set.
197
+ * @worstCaseRpcCalls On mainnet, 1 `eth_blockNumber` + ~4 `eth_getLogs` chunks
198
+ * (router-lifetime ÷ 50k-block window) + 1 Multicall3 `eth_call` ≈ 6; scales
199
+ * with the router's on-chain age, not with candidate count.
200
+ * @example
201
+ * ```ts
202
+ * const vaults = await getSwapRouterEligibleVaults(1, {
203
+ * rpcUrl: 'https://eth-mainnet.example/v2/KEY',
204
+ * });
205
+ * // ['0x74aD…718b', '0xE9B7…9D32'] (Sentora USD, Upshift Core USDC)
206
+ * ```
207
+ */
208
+ export declare function getSwapRouterEligibleVaults(chainId: number, options: {
209
+ rpcUrl: string;
210
+ fromBlock?: number;
211
+ }): Promise<IAddress[]>;
@@ -2,10 +2,20 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.vaultUsesSwapRouter = void 0;
4
4
  exports.getSwapRouterAddress = getSwapRouterAddress;
5
+ exports.getSwapRouterName = getSwapRouterName;
5
6
  exports.isSwapRouterEligible = isSwapRouterEligible;
6
7
  exports.resolveOriginCode = resolveOriginCode;
8
+ exports.getSwapRouterDepositResult = getSwapRouterDepositResult;
9
+ exports.getSwapRouterWhitelistedTokens = getSwapRouterWhitelistedTokens;
10
+ exports.getSwapRouterEligibleVaults = getSwapRouterEligibleVaults;
11
+ const ethers_1 = require("ethers");
7
12
  const swap_router_1 = require("../constants/swap-router");
13
+ const abis_1 = require("../../abis");
8
14
  const errors_1 = require("../errors");
15
+ const cache_1 = require("../cache");
16
+ const logger_1 = require("../logger");
17
+ const web3_1 = require("./web3");
18
+ const multicall_1 = require("./multicall");
9
19
  /**
10
20
  * Resolve the `SwapRouter` address for a chain.
11
21
  *
@@ -20,6 +30,28 @@ const errors_1 = require("../errors");
20
30
  function getSwapRouterAddress(chainId) {
21
31
  return swap_router_1.SWAP_ROUTER_ADDRESSES[chainId];
22
32
  }
33
+ /**
34
+ * Resolve the human-readable display name of the `SwapRouter` periphery
35
+ * contract for a chain.
36
+ *
37
+ * The contract exposes no name on-chain, so this reads the SDK-maintained
38
+ * {@link SWAP_ROUTER_NAMES} registry. Intended for UI surfaces that label the
39
+ * contract a swap-and-deposit routes through (e.g. a "Router" row in a
40
+ * transaction overview) — display metadata only, never routing logic.
41
+ *
42
+ * @param chainId - EVM chain ID.
43
+ * @returns The display name (e.g. `'Upshift Swap Router'`), or `undefined`
44
+ * when no SwapRouter is deployed on the chain — callers should omit the
45
+ * label rather than show an address.
46
+ * @example
47
+ * ```ts
48
+ * const name = getSwapRouterName(1);
49
+ * // 'Upshift Swap Router'
50
+ * ```
51
+ */
52
+ function getSwapRouterName(chainId) {
53
+ return swap_router_1.SWAP_ROUTER_NAMES[chainId];
54
+ }
23
55
  /**
24
56
  * Whether a vault is eligible for the any-token `SwapRouter` deposit surface
25
57
  * (the SwapRouter is `enableVault`-ed for it on-chain). Address comparison is
@@ -31,15 +63,22 @@ function getSwapRouterAddress(chainId) {
31
63
  *
32
64
  * @param vaultAddress - August vault address.
33
65
  * @returns `true` when the vault is in {@link SWAP_ROUTER_ELIGIBLE_VAULTS}.
66
+ * @deprecated Superseded by {@link getSwapRouterEligibleVaults}, which resolves
67
+ * eligibility from the router's on-chain `VaultEnabled` history + current
68
+ * `vaultInfo` registration instead of this static snapshot. Kept functional as
69
+ * the zero-RPC sync fallback (seed data, fail-fast checks); migrate UI gates
70
+ * to the async resolver.
34
71
  */
35
72
  function isSwapRouterEligible(vaultAddress) {
36
73
  return swap_router_1.SWAP_ROUTER_ELIGIBLE_VAULTS.has(vaultAddress.toLowerCase());
37
74
  }
38
75
  /**
39
- * @deprecated Renamed to {@link isSwapRouterEligible}. The meaning also changed:
40
- * it no longer implies `vaultDeposit` routes the vault through the SwapRouter
41
- * (that behavior was removed) it only reports swap-router *eligibility* for
42
- * the opt-in deposit surface. Kept as an alias for one release.
76
+ * @deprecated Renamed to {@link isSwapRouterEligible} (itself now superseded by
77
+ * {@link getSwapRouterEligibleVaults}, the on-chain-derived resolver). The
78
+ * meaning also changed: it no longer implies `vaultDeposit` routes the vault
79
+ * through the SwapRouter (that behavior was removed) it only reports
80
+ * swap-router *eligibility* for the opt-in deposit surface. Kept as an alias
81
+ * for one release; migrate to the async resolver.
43
82
  */
44
83
  exports.vaultUsesSwapRouter = isSwapRouterEligible;
45
84
  /**
@@ -64,4 +103,378 @@ function resolveOriginCode(provided) {
64
103
  }
65
104
  return provided;
66
105
  }
106
+ /**
107
+ * Resolves the real, post-execution outcome of a SwapRouter deposit from its
108
+ * mined transaction receipt. The SwapRouter contract itself emits no
109
+ * deposit/amount-out event — only the vault does, via its own `Deposit` event
110
+ * — so this decodes that log directly rather than trusting the pre-trade quote
111
+ * a UI shows before the user submits (the true post-swap amount depends on
112
+ * execution-time price and can differ from that estimate).
113
+ *
114
+ * Works for both vault generations without the caller resolving the version
115
+ * first: evm-1 (ERC-4626-style `Deposit(sender, owner, assets, shares)`) and
116
+ * evm-2 (`Deposit(assetIn, amountIn, shares, senderAddr, receiverAddr)`) have
117
+ * different event signatures (different topic0), so both are registered and
118
+ * whichever matches the vault's actual log is decoded.
119
+ *
120
+ * @param signer - ethers `Signer` or `Wallet` connected to a provider (used
121
+ * read-only here — no transaction is sent).
122
+ * @param options - {@link ISwapRouterDepositResultOptions}.
123
+ * @returns The decoded {@link ISwapRouterDepositResult}, or `null` if the
124
+ * receipt isn't found yet or carries no `Deposit` log from `vault` (e.g. the
125
+ * hash is wrong, or the tx hasn't mined).
126
+ * @throws {@link AugustValidationError} if `signer` has no connected provider.
127
+ * @worstCaseRpcCalls 1 — a single `getTransactionReceipt` call; decoding is
128
+ * local.
129
+ * @example
130
+ * ```ts
131
+ * const hash = await augustSdk.evm.swapRouterDeposit({ ... });
132
+ * const result = await augustSdk.evm.getSwapRouterDepositResult({
133
+ * txHash: hash,
134
+ * vault: '0x74ad2f789ed583dbd141bbdafc673fe1f033718b',
135
+ * });
136
+ * // result?.amountOut — actual reference-asset amount received after the swap
137
+ * ```
138
+ */
139
+ async function getSwapRouterDepositResult(signer, { txHash, vault }) {
140
+ const provider = signer.provider;
141
+ if (!provider) {
142
+ throw new errors_1.AugustValidationError('INVALID_INPUT', 'getSwapRouterDepositResult: signer has no connected provider');
143
+ }
144
+ const receipt = await provider.getTransactionReceipt(txHash);
145
+ if (!receipt)
146
+ return null;
147
+ const iface = new ethers_1.ethers.Interface([
148
+ 'event Deposit(address indexed sender, address indexed owner, uint256 assets, uint256 shares)',
149
+ 'event Deposit(address assetIn, uint256 amountIn, uint256 shares, address indexed senderAddr, address indexed receiverAddr)',
150
+ ]);
151
+ for (const log of receipt.logs) {
152
+ if (log.address.toLowerCase() !== vault.toLowerCase())
153
+ continue;
154
+ let parsed;
155
+ try {
156
+ parsed = iface.parseLog(log);
157
+ }
158
+ catch {
159
+ continue;
160
+ }
161
+ if (parsed?.name !== 'Deposit')
162
+ continue;
163
+ // Discriminate on the matched event fragment, not on probing a
164
+ // possibly-absent named arg. Only the evm-2 signature declares `assetIn`,
165
+ // so its presence in the fragment inputs uniquely identifies the version.
166
+ // (Reading `parsed.args.assetIn` on the evm-1 log would couple correctness
167
+ // to ethers' `Result`-proxy behavior for unknown names.)
168
+ const isEvm2 = parsed.fragment.inputs.some((i) => i.name === 'assetIn');
169
+ if (isEvm2) {
170
+ // evm-2 TokenizedVault: Deposit(assetIn, amountIn, shares, sender, receiver)
171
+ return {
172
+ amountOut: BigInt(parsed.args.amountIn),
173
+ sharesOut: BigInt(parsed.args.shares),
174
+ };
175
+ }
176
+ // evm-1 ERC-4626-style: Deposit(sender, owner, assets, shares)
177
+ return {
178
+ amountOut: BigInt(parsed.args.assets),
179
+ sharesOut: BigInt(parsed.args.shares),
180
+ };
181
+ }
182
+ return null;
183
+ }
184
+ /** Cache TTL for a chain's resolved SwapRouter token allowlist (5 minutes). */
185
+ const SWAP_ROUTER_WHITELIST_TTL_MS = 5 * 60 * 1000;
186
+ /**
187
+ * Resolve the ERC-20 tokens currently whitelisted for the `SwapRouter` on a
188
+ * chain — the any-token set a UI can offer as swap-and-deposit inputs.
189
+ *
190
+ * The router exposes its allowlist only as `mapping(address => bool)
191
+ * whitelistedTokens` with **no on-chain enumeration**, so this cannot be a
192
+ * single view call. It runs in two stages:
193
+ * 1. Scan the router's `TokenEnabled(address)` event history (from the chain's
194
+ * {@link SWAP_ROUTER_DEPLOY_BLOCKS} floor, in `eth_getLogs` chunks sized by
195
+ * {@link determineBlockSkipInternal}) to reconstruct the candidate set of
196
+ * every token ever enabled.
197
+ * 2. Re-read `whitelistedTokens(token)` for each candidate — batched via
198
+ * Multicall3 where verified, else per-call — and keep only those the
199
+ * mapping currently returns `true` for. This step makes later disables (and
200
+ * disable-then-re-enable) correct without replaying `TokenDisabled`, and is
201
+ * the fail-closed source of truth: a token surfaces only on a definite
202
+ * on-chain `true`.
203
+ *
204
+ * The resolved list is cached per chain for 5 minutes — the allowlist changes
205
+ * only via an admin `enableToken`/`disableToken` transaction, so a short TTL
206
+ * keeps the (multi-RPC) scan off the hot path without going stale for long.
207
+ *
208
+ * @param chainId - EVM chain ID. Chains without a {@link SWAP_ROUTER_ADDRESSES}
209
+ * entry resolve to `[]` with no RPC.
210
+ * @param options - `rpcUrl` (required) must point at `chainId`'s network;
211
+ * `fromBlock` overrides the default deploy-block scan floor (e.g. to widen a
212
+ * scan on a chain missing from {@link SWAP_ROUTER_DEPLOY_BLOCKS}).
213
+ * @returns Checksummed addresses of the currently-whitelisted tokens, deduped.
214
+ * Empty when no router is deployed or none are enabled. Callers still filter
215
+ * out a given vault's natively-accepted assets (those deposit without a swap).
216
+ * @throws AugustValidationError when `rpcUrl` is missing/empty.
217
+ * @throws AugustSDKError when an `eth_getLogs` chunk fails — surfaced (not
218
+ * swallowed) so a partial scan never masquerades as a complete allowlist.
219
+ * @worstCaseRpcCalls On mainnet, 1 `eth_blockNumber` + ~4 `eth_getLogs` chunks
220
+ * (router-lifetime ÷ 50k-block window) + 1 Multicall3 `eth_call` ≈ 6; scales
221
+ * with the router's on-chain age, not with candidate count.
222
+ * @example
223
+ * ```ts
224
+ * const tokens = await getSwapRouterWhitelistedTokens(1, {
225
+ * rpcUrl: 'https://eth-mainnet.example/v2/KEY',
226
+ * });
227
+ * // ['0xA0b8…', '0xdAC1…', '0xC02a…', '0x2260…'] (USDC, USDT, WETH, WBTC)
228
+ * ```
229
+ */
230
+ async function getSwapRouterWhitelistedTokens(chainId, options) {
231
+ const router = getSwapRouterAddress(chainId);
232
+ if (!router)
233
+ return [];
234
+ const cacheKey = `swap-router-whitelisted-tokens-${chainId}-${router.toLowerCase()}`;
235
+ const cached = await (0, cache_1.getSafeCache)(cacheKey);
236
+ if (cached)
237
+ return cached;
238
+ const provider = (0, web3_1.createProvider)(options.rpcUrl, chainId);
239
+ const contract = (0, web3_1.createContract)({
240
+ address: router,
241
+ provider,
242
+ abi: abis_1.ABI_SWAP_ROUTER,
243
+ });
244
+ if (!contract)
245
+ return [];
246
+ // Stage 1 — reconstruct the candidate set from TokenEnabled logs, scanning
247
+ // the router's lifetime in chunks to respect per-request eth_getLogs limits.
248
+ const currentBlock = await provider.getBlockNumber();
249
+ const fromBlock = Math.max(0, options.fromBlock ?? swap_router_1.SWAP_ROUTER_DEPLOY_BLOCKS[chainId] ?? 0);
250
+ const blockSkip = (0, web3_1.determineBlockSkipInternal)(chainId);
251
+ const ranges = [];
252
+ for (let end = currentBlock; end >= fromBlock; end -= blockSkip + 1) {
253
+ ranges.push({ from: Math.max(end - blockSkip, fromBlock), to: end });
254
+ }
255
+ const settled = await Promise.allSettled(ranges.map((r) => contract.queryFilter('TokenEnabled', BigInt(r.from), BigInt(r.to))));
256
+ const failed = settled.find((s) => s.status === 'rejected');
257
+ if (failed) {
258
+ const reason = failed.reason instanceof Error
259
+ ? failed.reason.message
260
+ : String(failed.reason);
261
+ throw new errors_1.AugustSDKError('NETWORK_ERROR', `getSwapRouterWhitelistedTokens: TokenEnabled log scan failed on chain ${chainId}: ${reason}`);
262
+ }
263
+ const candidates = new Set();
264
+ for (const s of settled) {
265
+ if (s.status !== 'fulfilled')
266
+ continue;
267
+ for (const log of s.value) {
268
+ const raw = log.args?.tokenAddr;
269
+ if (typeof raw === 'string' && (0, ethers_1.isAddress)(raw)) {
270
+ candidates.add((0, ethers_1.getAddress)(raw));
271
+ }
272
+ }
273
+ }
274
+ if (candidates.size === 0) {
275
+ cache_1.CACHE.set(cacheKey, [], { ttl: SWAP_ROUTER_WHITELIST_TTL_MS });
276
+ return [];
277
+ }
278
+ // Stage 2 — keep only tokens the mapping currently reports as whitelisted.
279
+ const list = [...candidates];
280
+ let enabled;
281
+ if ((0, multicall_1.isMulticall3Supported)(chainId)) {
282
+ const calls = list.map((token) => ({
283
+ target: router,
284
+ abi: abis_1.ABI_SWAP_ROUTER,
285
+ functionName: 'whitelistedTokens',
286
+ args: [token],
287
+ }));
288
+ const results = await (0, multicall_1.multicall3Aggregate)(provider, chainId, calls);
289
+ enabled = list.filter((_, i) => results[i]?.success && results[i]?.value === true);
290
+ }
291
+ else {
292
+ const bools = await Promise.all(list.map(async (token) => {
293
+ try {
294
+ return (await contract.whitelistedTokens(token));
295
+ }
296
+ catch (e) {
297
+ logger_1.Logger.log.warn('getSwapRouterWhitelistedTokens', `whitelistedTokens(${token}) read failed — omitting`, e);
298
+ return false;
299
+ }
300
+ }));
301
+ enabled = list.filter((_, i) => bools[i] === true);
302
+ }
303
+ cache_1.CACHE.set(cacheKey, enabled, { ttl: SWAP_ROUTER_WHITELIST_TTL_MS });
304
+ return enabled;
305
+ }
306
+ /** Cache TTL for a chain's resolved SwapRouter eligible-vault set (5 minutes). */
307
+ const SWAP_ROUTER_ELIGIBLE_VAULTS_TTL_MS = 5 * 60 * 1000;
308
+ /**
309
+ * Minimal `vaultInfo` fragment declaring the four static outputs as a single
310
+ * named tuple. The ABI encoding of `(uint256,uint8,address,address)` returned
311
+ * as four values is byte-identical to the same four static types returned as
312
+ * one tuple, and {@link multicall3Aggregate} surfaces only the FIRST decoded
313
+ * output — so decoding against this fragment makes that first output the whole
314
+ * struct, giving batched access to `referenceAsset` (the registration marker)
315
+ * without changing the shared multicall helper.
316
+ */
317
+ const ABI_SWAP_ROUTER_VAULT_INFO_TUPLE = [
318
+ {
319
+ inputs: [{ internalType: 'address', name: '', type: 'address' }],
320
+ name: 'vaultInfo',
321
+ outputs: [
322
+ {
323
+ components: [
324
+ { internalType: 'uint256', name: 'swapFee', type: 'uint256' },
325
+ { internalType: 'uint8', name: 'vaultType', type: 'uint8' },
326
+ { internalType: 'address', name: 'referenceAsset', type: 'address' },
327
+ { internalType: 'address', name: 'lpTokenAddress', type: 'address' },
328
+ ],
329
+ internalType: 'struct SwapRouter.VaultInfo',
330
+ name: 'info',
331
+ type: 'tuple',
332
+ },
333
+ ],
334
+ stateMutability: 'view',
335
+ type: 'function',
336
+ },
337
+ ];
338
+ /**
339
+ * Extract `referenceAsset` from a decoded `vaultInfo` result (ethers `Result`
340
+ * proxies expose named tuple components as properties). Returns `undefined`
341
+ * for anything that doesn't carry a string `referenceAsset` — treated as
342
+ * unregistered by the caller (fail-closed).
343
+ */
344
+ function referenceAssetOf(value) {
345
+ const ref = value
346
+ ?.referenceAsset;
347
+ return typeof ref === 'string' ? ref : undefined;
348
+ }
349
+ /**
350
+ * Resolve the vaults currently eligible for the any-token `SwapRouter` deposit
351
+ * surface on a chain — i.e. every vault the router has `enableVault`-ed that is
352
+ * still registered on-chain.
353
+ *
354
+ * The router stores registrations as `mapping(address => VaultInfo) vaultInfo`
355
+ * with **no on-chain enumeration**, so (exactly like
356
+ * {@link getSwapRouterWhitelistedTokens}) this runs in two stages:
357
+ * 1. Scan the router's `VaultEnabled(address)` event history (from the chain's
358
+ * {@link SWAP_ROUTER_DEPLOY_BLOCKS} floor, in `eth_getLogs` chunks sized by
359
+ * {@link determineBlockSkipInternal}) to reconstruct the candidate set of
360
+ * every vault ever enabled.
361
+ * 2. Re-read `vaultInfo(vault)` for each candidate — batched via Multicall3
362
+ * where verified, else per-call — and keep only those whose
363
+ * `referenceAsset != address(0)` (an unregistered vault reverts with
364
+ * `InvalidVault` at deposit time). This makes later disables correct
365
+ * without replaying `VaultDisabled`, and is the fail-closed source of
366
+ * truth: a vault surfaces only on a definite on-chain registration.
367
+ *
368
+ * This is the dynamic successor to the static
369
+ * {@link SWAP_ROUTER_ELIGIBLE_VAULTS} snapshot / {@link isSwapRouterEligible}.
370
+ * It reports **on-chain eligibility only** — app-level policy (e.g. excluding
371
+ * OVault-enabled vaults that have their own deposit flow) remains the caller's
372
+ * responsibility, as does the flag gating of the UI surface.
373
+ *
374
+ * The resolved list is cached per chain for 5 minutes — registrations change
375
+ * only via an admin `enableVault`/`disableVault` transaction, so a short TTL
376
+ * keeps the (multi-RPC) scan off the hot path without going stale for long.
377
+ *
378
+ * @param chainId - EVM chain ID. Chains without a {@link SWAP_ROUTER_ADDRESSES}
379
+ * entry resolve to `[]` with no RPC.
380
+ * @param options - `rpcUrl` (required) must point at `chainId`'s network;
381
+ * `fromBlock` overrides the default deploy-block scan floor.
382
+ * @returns Checksummed addresses of the currently-registered eligible vaults,
383
+ * deduped. Empty when no router is deployed or none are registered.
384
+ * @throws AugustValidationError when `rpcUrl` is missing/empty.
385
+ * @throws AugustSDKError when an `eth_getLogs` chunk fails — surfaced (not
386
+ * swallowed) so a partial scan never masquerades as the complete set.
387
+ * @worstCaseRpcCalls On mainnet, 1 `eth_blockNumber` + ~4 `eth_getLogs` chunks
388
+ * (router-lifetime ÷ 50k-block window) + 1 Multicall3 `eth_call` ≈ 6; scales
389
+ * with the router's on-chain age, not with candidate count.
390
+ * @example
391
+ * ```ts
392
+ * const vaults = await getSwapRouterEligibleVaults(1, {
393
+ * rpcUrl: 'https://eth-mainnet.example/v2/KEY',
394
+ * });
395
+ * // ['0x74aD…718b', '0xE9B7…9D32'] (Sentora USD, Upshift Core USDC)
396
+ * ```
397
+ */
398
+ async function getSwapRouterEligibleVaults(chainId, options) {
399
+ const router = getSwapRouterAddress(chainId);
400
+ if (!router)
401
+ return [];
402
+ const cacheKey = `swap-router-eligible-vaults-${chainId}-${router.toLowerCase()}`;
403
+ const cached = await (0, cache_1.getSafeCache)(cacheKey);
404
+ if (cached)
405
+ return cached;
406
+ const provider = (0, web3_1.createProvider)(options.rpcUrl, chainId);
407
+ const contract = (0, web3_1.createContract)({
408
+ address: router,
409
+ provider,
410
+ abi: abis_1.ABI_SWAP_ROUTER,
411
+ });
412
+ if (!contract)
413
+ return [];
414
+ // Stage 1 — reconstruct the candidate set from VaultEnabled logs, scanning
415
+ // the router's lifetime in chunks to respect per-request eth_getLogs limits.
416
+ const currentBlock = await provider.getBlockNumber();
417
+ const fromBlock = Math.max(0, options.fromBlock ?? swap_router_1.SWAP_ROUTER_DEPLOY_BLOCKS[chainId] ?? 0);
418
+ const blockSkip = (0, web3_1.determineBlockSkipInternal)(chainId);
419
+ const ranges = [];
420
+ for (let end = currentBlock; end >= fromBlock; end -= blockSkip + 1) {
421
+ ranges.push({ from: Math.max(end - blockSkip, fromBlock), to: end });
422
+ }
423
+ const settled = await Promise.allSettled(ranges.map((r) => contract.queryFilter('VaultEnabled', BigInt(r.from), BigInt(r.to))));
424
+ const failed = settled.find((s) => s.status === 'rejected');
425
+ if (failed) {
426
+ const reason = failed.reason instanceof Error
427
+ ? failed.reason.message
428
+ : String(failed.reason);
429
+ throw new errors_1.AugustSDKError('NETWORK_ERROR', `getSwapRouterEligibleVaults: VaultEnabled log scan failed on chain ${chainId}: ${reason}`);
430
+ }
431
+ const candidates = new Set();
432
+ for (const s of settled) {
433
+ if (s.status !== 'fulfilled')
434
+ continue;
435
+ for (const log of s.value) {
436
+ const raw = log.args?.vaultAddr;
437
+ if (typeof raw === 'string' && (0, ethers_1.isAddress)(raw)) {
438
+ candidates.add((0, ethers_1.getAddress)(raw));
439
+ }
440
+ }
441
+ }
442
+ if (candidates.size === 0) {
443
+ cache_1.CACHE.set(cacheKey, [], { ttl: SWAP_ROUTER_ELIGIBLE_VAULTS_TTL_MS });
444
+ return [];
445
+ }
446
+ // Stage 2 — keep only vaults whose current on-chain registration is live
447
+ // (referenceAsset set). Fail-closed: unreadable/zero → excluded.
448
+ const list = [...candidates];
449
+ let registered;
450
+ if ((0, multicall_1.isMulticall3Supported)(chainId)) {
451
+ const calls = list.map((vault) => ({
452
+ target: router,
453
+ abi: ABI_SWAP_ROUTER_VAULT_INFO_TUPLE,
454
+ functionName: 'vaultInfo',
455
+ args: [vault],
456
+ }));
457
+ const results = await (0, multicall_1.multicall3Aggregate)(provider, chainId, calls);
458
+ registered = list.filter((_, i) => {
459
+ if (!results[i]?.success)
460
+ return false;
461
+ const ref = referenceAssetOf(results[i]?.value);
462
+ return !!ref && ref !== ethers_1.ethers.ZeroAddress;
463
+ });
464
+ }
465
+ else {
466
+ const refs = await Promise.all(list.map(async (vault) => {
467
+ try {
468
+ return referenceAssetOf(await contract.vaultInfo(vault));
469
+ }
470
+ catch (e) {
471
+ logger_1.Logger.log.warn('getSwapRouterEligibleVaults', `vaultInfo(${vault}) read failed — omitting`, e);
472
+ return undefined;
473
+ }
474
+ }));
475
+ registered = list.filter((_, i) => !!refs[i] && refs[i] !== ethers_1.ethers.ZeroAddress);
476
+ }
477
+ cache_1.CACHE.set(cacheKey, registered, { ttl: SWAP_ROUTER_ELIGIBLE_VAULTS_TTL_MS });
478
+ return registered;
479
+ }
67
480
  //# sourceMappingURL=swap-router.js.map
@@ -1,5 +1,23 @@
1
1
  import type { ITokenizedVault, IVaultVersion, ChainType } from '../../types';
2
2
  /** Classify an address by chain family (evm / solana / stellar / sui). */
3
3
  export declare function getAddressChainType(address: string): ChainType;
4
+ /**
5
+ * Classify a vault's protocol version from its address alone.
6
+ *
7
+ * @deprecated Use {@link getVaultVersionV2} instead. This V1 classifier can only
8
+ * recognise a multi-asset vault (`evm-2`) if its address is hard-coded in the
9
+ * static `MULTI_ASSET_VAULTS` list. Any multi-asset vault missing from that list
10
+ * is silently misclassified as `evm-1`, which broke deposit preview/simulation in
11
+ * the 2026-07-06 Sentora incident. `getVaultVersionV2` reads the backend
12
+ * `internal_type` (falling back to the static list only as a safety net), so new
13
+ * vaults classify correctly without a code change. Retained as a non-breaking
14
+ * shim; scheduled for removal in the next major.
15
+ *
16
+ * @param vault - The vault contract address (EVM `0x…`, Solana, or Stellar).
17
+ * @returns The vault version slug, or `undefined` for empty/invalid input.
18
+ */
4
19
  export declare function getVaultVersion(vault: string): IVaultVersion | undefined;
5
- export declare function getVaultVersionV2(vault: ITokenizedVault): IVaultVersion | undefined;
20
+ export declare function getVaultVersionV2(vault: (Pick<ITokenizedVault, 'address'> & {
21
+ chain_type?: string;
22
+ internal_type?: string;
23
+ }) | undefined | null): IVaultVersion | undefined;