@augustdigital/sdk 8.16.1 → 8.19.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.
- package/lib/adapters/evm/index.js +4 -2
- package/lib/adapters/stellar/soroban.d.ts +8 -3
- package/lib/adapters/stellar/soroban.js +9 -4
- package/lib/core/analytics/constants.d.ts +1 -1
- package/lib/core/analytics/constants.js +1 -1
- package/lib/core/analytics/version.d.ts +1 -1
- package/lib/core/analytics/version.js +1 -1
- package/lib/core/attribution.d.ts +111 -0
- package/lib/core/attribution.js +142 -0
- package/lib/core/base.class.d.ts +17 -1
- package/lib/core/base.class.js +6 -1
- package/lib/core/fetcher.js +10 -1
- package/lib/core/helpers/chain-error.d.ts +140 -0
- package/lib/core/helpers/chain-error.js +412 -0
- package/lib/core/helpers/signer.d.ts +21 -0
- package/lib/core/helpers/signer.js +52 -0
- package/lib/core/helpers/web3.d.ts +152 -2
- package/lib/core/helpers/web3.js +307 -45
- package/lib/core/index.d.ts +1 -0
- package/lib/core/index.js +1 -0
- package/lib/evm/methods/crossChainVault.js +4 -0
- package/lib/modules/vaults/getters.js +6 -2
- package/lib/modules/vaults/utils.js +4 -0
- package/lib/modules/vaults/write.actions.d.ts +41 -1
- package/lib/modules/vaults/write.actions.js +302 -86
- package/lib/sdk.d.ts +461 -3
- package/lib/services/subgraph/vaults.js +85 -14
- package/lib/types/vaults.d.ts +12 -0
- package/lib/types/webserver.d.ts +11 -0
- package/package.json +1 -1
|
@@ -75,11 +75,84 @@ export { explorerLink } from './explorer-link';
|
|
|
75
75
|
/**
|
|
76
76
|
* Fetch token decimals from contract or Solana mint.
|
|
77
77
|
* Results are cached to minimize RPC calls.
|
|
78
|
+
*
|
|
79
|
+
* **Never throws** — a failed read logs at error level and resolves
|
|
80
|
+
* `undefined`. Callers that must not silently proceed on an unknown scale (any
|
|
81
|
+
* path that encodes an amount) should use {@link getDecimalsOrThrow} instead:
|
|
82
|
+
* feeding `undefined` into `toNormalizedBn` silently defaults to 18 decimals.
|
|
83
|
+
*
|
|
78
84
|
* @param provider Web3 provider
|
|
79
85
|
* @param address Token contract address or Solana mint
|
|
80
|
-
* @
|
|
86
|
+
* @param isVault Resolve `address` as a vault (evm-2 vaults redirect to the
|
|
87
|
+
* receipt token) rather than reading it as a plain ERC-20. Defaults to `true`.
|
|
88
|
+
* @returns Number of decimals for the token, or `undefined` when the read failed.
|
|
81
89
|
*/
|
|
82
90
|
export declare const getDecimals: (provider: IContractRunner, address: IAddress, isVault?: boolean) => Promise<number>;
|
|
91
|
+
/**
|
|
92
|
+
* Read a token's `decimals()` with the **same cache and stampede protection as
|
|
93
|
+
* {@link getDecimals}**, but surfacing failures instead of swallowing them, and
|
|
94
|
+
* retrying the transient ones.
|
|
95
|
+
*
|
|
96
|
+
* Two reasons this exists rather than a flag on `getDecimals`:
|
|
97
|
+
*
|
|
98
|
+
* 1. **`getDecimals` must keep returning `undefined` on failure** — a dozen
|
|
99
|
+
* read paths depend on that. Amount-encoding paths need the opposite: an
|
|
100
|
+
* `undefined` reaching `toNormalizedBn` silently means 18 decimals, which
|
|
101
|
+
* misencodes the transaction. Here the original error propagates, so the
|
|
102
|
+
* caller's `AugustSDKError` keeps its cause and its Sentry grouping.
|
|
103
|
+
* 2. **Retry.** Providers intermittently return an empty response to
|
|
104
|
+
* `decimals()`, which ethers reports as
|
|
105
|
+
* `missing revert data (action="call", data="0x313ce567", …)` — a shape a
|
|
106
|
+
* deployed ERC-20 cannot legitimately produce, since `decimals()` takes no
|
|
107
|
+
* arguments. That is retried here via {@link isEmptyViewResponse} scoped to
|
|
108
|
+
* this exact selector, alongside ordinary transport faults
|
|
109
|
+
* ({@link isRetryableRpcError}). The empty-view widening applies **only**
|
|
110
|
+
* here; everywhere else the strict transport definition stands. A genuine
|
|
111
|
+
* revert (`CALL_EXCEPTION` carrying revert data) is never retried.
|
|
112
|
+
*
|
|
113
|
+
* Cache is shared with `getDecimals`, so a read-then-write flow against the
|
|
114
|
+
* same token costs one `decimals()` RPC in total, and N concurrent callers for
|
|
115
|
+
* an uncached token collapse to one.
|
|
116
|
+
*
|
|
117
|
+
* **Exception — an unresolvable chain scope bypasses the cache entirely.** When
|
|
118
|
+
* {@link resolveProviderScope} cannot determine the chain (a runner with no
|
|
119
|
+
* resolved network and no connection URL, e.g. a browser provider before its
|
|
120
|
+
* first request), the only available key is the shared `unknown` bucket. Read
|
|
121
|
+
* paths happily use that bucket, and this function deliberately does not: the
|
|
122
|
+
* two paths have different blast radii. A wrong cached `decimals` on a read
|
|
123
|
+
* renders a wrong number on screen; on a write it misencodes an amount the user
|
|
124
|
+
* then *signs*. The collision needed — the same address being a different token
|
|
125
|
+
* with different decimals on two chains, within one process, across a
|
|
126
|
+
* mid-session chain switch — is narrow, but "no worse than the read path" is
|
|
127
|
+
* not the bar for a value that ends up in a transaction. In that case the read
|
|
128
|
+
* is neither served from nor written to the cache, and it also skips the
|
|
129
|
+
* in-flight dedup map, since joining another caller under an untrusted key
|
|
130
|
+
* would reintroduce exactly the collision being avoided. Retries still apply;
|
|
131
|
+
* the only thing forgone is the RPC saving.
|
|
132
|
+
*
|
|
133
|
+
* RPC cost: 1 on a cache miss, 0 on a hit, up to 3 on a miss that keeps
|
|
134
|
+
* faulting (~750ms of added latency in the worst case before it gives up).
|
|
135
|
+
* Always ≥1 when the chain scope is unresolvable.
|
|
136
|
+
*
|
|
137
|
+
* @param runner - Provider or signer to read through. A signer is unwrapped to
|
|
138
|
+
* its provider for cache scoping, so it shares entries with reads on the
|
|
139
|
+
* same chain.
|
|
140
|
+
* @param address - Token address. Read directly as an ERC-20 — pass the
|
|
141
|
+
* receipt-token address yourself for an `evm-2` vault's share scale.
|
|
142
|
+
* @param tag - Low-cardinality label for retry breadcrumbs (e.g.
|
|
143
|
+
* `'vaultDeposit:poolDecimals'`).
|
|
144
|
+
* @returns The token's decimals.
|
|
145
|
+
* @throws The underlying read error once retries are exhausted, or immediately
|
|
146
|
+
* when the failure is neither a transport fault nor an empty view response.
|
|
147
|
+
*
|
|
148
|
+
* @example
|
|
149
|
+
* ```ts
|
|
150
|
+
* // Amount encoding must not proceed on a guessed scale.
|
|
151
|
+
* const decimals = await getDecimalsOrThrow(signer, token, 'deposit:decimals');
|
|
152
|
+
* const amount = toNormalizedBn(userInput, decimals);
|
|
153
|
+
* ```
|
|
154
|
+
*/
|
|
155
|
+
export declare const getDecimalsOrThrow: (runner: IContractRunner, address: IAddress, tag: string) => Promise<number>;
|
|
83
156
|
/**
|
|
84
157
|
* Fetch the whitelisted deposit-asset addresses from a vault's whitelist
|
|
85
158
|
* contract. Cached with a 5-minute TTL; a fresh fetch fans out into N
|
|
@@ -87,12 +160,89 @@ export declare const getDecimals: (provider: IContractRunner, address: IAddress,
|
|
|
87
160
|
* @internal
|
|
88
161
|
*/
|
|
89
162
|
export declare const getWhitelistedAssets: (provider: IContractRunner, whitelistAddress: IAddress) => Promise<IAddress[]>;
|
|
163
|
+
/**
|
|
164
|
+
* Read an `evm-2` vault's receipt (LP) token address via `lpTokenAddress()`,
|
|
165
|
+
* retrying only the transient transport faults and surfacing everything else
|
|
166
|
+
* unchanged.
|
|
167
|
+
*
|
|
168
|
+
* Why this exists: `lpTokenAddress()` sits immediately before the receipt-token
|
|
169
|
+
* `decimals()` read on every `evm-2` write path (approve / deposit / request
|
|
170
|
+
* redeem / swap-router deposit). Those `decimals()` reads were hardened against
|
|
171
|
+
* provider blips ({@link getDecimalsOrThrow}); the `lpTokenAddress()` read one
|
|
172
|
+
* line above them was not, so a single truncated `eth_call` response still
|
|
173
|
+
* failed the whole write with
|
|
174
|
+
* `missing revert data (action="call", data="0xf5ae497a", …)` — observed in
|
|
175
|
+
* production against a mainnet vault whose `lpTokenAddress()` demonstrably
|
|
176
|
+
* returns a real address when the provider is healthy.
|
|
177
|
+
*
|
|
178
|
+
* **This must never hide a misrouted vault, and does not.** `lpTokenAddress()`
|
|
179
|
+
* exists *only* on `evm-2` vaults. A vault wrongly classified as `evm-2` returns
|
|
180
|
+
* empty returndata for it, deterministically and forever, and that empty
|
|
181
|
+
* response is byte-identical to the transient one — it is the *only* signal the
|
|
182
|
+
* SDK gets that its version routing was wrong. So the retry here is deliberately
|
|
183
|
+
* shaped to preserve that signal:
|
|
184
|
+
*
|
|
185
|
+
* - it is **bounded** (3 attempts, ~750ms of backoff in total — see
|
|
186
|
+
* `retryOnTransientRpc`), never open-ended;
|
|
187
|
+
* - on exhaustion it **rethrows the original error object**, with its identity,
|
|
188
|
+
* message, `code` and `transaction.data` intact, so the caller's
|
|
189
|
+
* `AugustSDKError` cause and Sentry grouping are exactly what they are today;
|
|
190
|
+
* - it has **no fallback**: it never substitutes another address, never resolves
|
|
191
|
+
* the vault address itself, and never resolves `null`/`undefined`. It either
|
|
192
|
+
* returns a real receipt-token address or throws.
|
|
193
|
+
*
|
|
194
|
+
* Net effect: a blip costs a retry, a misroute costs three `eth_call`s and then
|
|
195
|
+
* fails exactly as loudly as before.
|
|
196
|
+
*
|
|
197
|
+
* **Deliberately not cached.** Unlike `decimals()`, the vault→receipt-token
|
|
198
|
+
* mapping is not memoized here. Caching it would make a misroute's first
|
|
199
|
+
* (failed) probe and every subsequent one diverge, and would put a
|
|
200
|
+
* vault-identity mapping in the cache on the money path. The lenient, cached
|
|
201
|
+
* reader is {@link getReceiptTokenAddress} — use that on read paths that can
|
|
202
|
+
* tolerate `undefined`.
|
|
203
|
+
*
|
|
204
|
+
* RPC cost: exactly 1 `eth_call` on success; at most 3 when the provider keeps
|
|
205
|
+
* faulting.
|
|
206
|
+
*
|
|
207
|
+
* @param runner - Provider or signer to read through.
|
|
208
|
+
* @param vault - Address of the `evm-2` tokenized vault.
|
|
209
|
+
* @param tag - Low-cardinality label for retry breadcrumbs (e.g.
|
|
210
|
+
* `'vaultRequestRedeem:receiptToken'`).
|
|
211
|
+
* @returns The vault's receipt (LP) token address. Never `null`/`undefined`.
|
|
212
|
+
* @throws The underlying read error, unmodified, once the bounded retries are
|
|
213
|
+
* exhausted — or immediately when the failure is neither a transport fault nor
|
|
214
|
+
* an empty response to this exact selector (e.g. a genuine revert carrying
|
|
215
|
+
* revert data).
|
|
216
|
+
*
|
|
217
|
+
* @example
|
|
218
|
+
* ```ts
|
|
219
|
+
* const receiptToken = await getReceiptTokenAddressOrThrow(
|
|
220
|
+
* signer,
|
|
221
|
+
* vault,
|
|
222
|
+
* 'vaultRequestRedeem:receiptToken',
|
|
223
|
+
* );
|
|
224
|
+
* const decimals = await getDecimalsOrThrow(signer, receiptToken, 'tag');
|
|
225
|
+
* ```
|
|
226
|
+
*/
|
|
227
|
+
export declare const getReceiptTokenAddressOrThrow: (runner: IContractRunner, vault: IAddress, tag: string) => Promise<IAddress>;
|
|
90
228
|
/**
|
|
91
229
|
* Fetch receipt token address from tokenized vault contract.
|
|
92
230
|
* Results are cached to minimize RPC calls.
|
|
231
|
+
*
|
|
232
|
+
* **Never throws** — a failed read logs at error level and resolves
|
|
233
|
+
* `undefined`, which several read paths depend on. The underlying
|
|
234
|
+
* `lpTokenAddress()` call is made through
|
|
235
|
+
* {@link getReceiptTokenAddressOrThrow}, so a transient provider blip is
|
|
236
|
+
* absorbed by a bounded retry before that happens; a deterministic failure (a
|
|
237
|
+
* vault that has no `lpTokenAddress()` because it is not `evm-2`) still resolves
|
|
238
|
+
* `undefined` after the attempts are spent, exactly as before. Callers on a
|
|
239
|
+
* money path must use {@link getReceiptTokenAddressOrThrow} directly instead —
|
|
240
|
+
* an `undefined` receipt-token address there would silently mis-address a
|
|
241
|
+
* `decimals()` read.
|
|
242
|
+
*
|
|
93
243
|
* @param provider Web3 provider
|
|
94
244
|
* @param address Tokenized vault contract address
|
|
95
|
-
* @returns Receipt token address
|
|
245
|
+
* @returns Receipt token address, or `undefined` when the read failed.
|
|
96
246
|
*/
|
|
97
247
|
export declare const getReceiptTokenAddress: (provider: IContractRunner, address: IAddress) => Promise<`0x${string}`>;
|
|
98
248
|
/**
|
package/lib/core/helpers/web3.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.getManagementFeePercent = exports.getSymbol = exports.getReceiptTokenAddress = exports.getWhitelistedAssets = exports.getDecimals = exports.explorerLink = exports.getInfuraProvider = exports.createProvider = exports.getChainId = exports.determineBlockSkipInternal = exports.determineSecondsPerBlock = exports.determineBlockCutoff = void 0;
|
|
3
|
+
exports.getManagementFeePercent = exports.getSymbol = exports.getReceiptTokenAddress = exports.getReceiptTokenAddressOrThrow = exports.getWhitelistedAssets = exports.getDecimalsOrThrow = exports.getDecimals = exports.explorerLink = exports.getInfuraProvider = exports.createProvider = exports.getChainId = exports.determineBlockSkipInternal = exports.determineSecondsPerBlock = exports.determineBlockCutoff = void 0;
|
|
4
4
|
exports.createContract = createContract;
|
|
5
5
|
exports.getTokenMetadata = getTokenMetadata;
|
|
6
6
|
exports.simulateTransaction = simulateTransaction;
|
|
@@ -15,6 +15,7 @@ const logger_1 = require("../logger");
|
|
|
15
15
|
const web3_1 = require("../constants/web3");
|
|
16
16
|
const fetcher_1 = require("../fetcher");
|
|
17
17
|
const chain_address_1 = require("./chain-address");
|
|
18
|
+
const chain_error_1 = require("./chain-error");
|
|
18
19
|
const constants_1 = require("../../adapters/solana/constants");
|
|
19
20
|
const abis_1 = require("../../abis");
|
|
20
21
|
const TokenizedVaultV2_1 = require("../../abis/TokenizedVaultV2");
|
|
@@ -199,8 +200,9 @@ exports.getInfuraProvider = getInfuraProvider;
|
|
|
199
200
|
var explorer_link_1 = require("./explorer-link");
|
|
200
201
|
Object.defineProperty(exports, "explorerLink", { enumerable: true, get: function () { return explorer_link_1.explorerLink; } });
|
|
201
202
|
/**
|
|
202
|
-
* Stable per-provider identity for cache keys
|
|
203
|
-
*
|
|
203
|
+
* Stable per-provider identity for cache keys, or `null` when the chain cannot
|
|
204
|
+
* be determined at all — the distinction callers need to decide whether a cache
|
|
205
|
+
* key is trustworthy.
|
|
204
206
|
*
|
|
205
207
|
* `provider._network` is a *getter* in ethers v6 that throws
|
|
206
208
|
* `network is not available yet (NETWORK_ERROR)` until the provider has
|
|
@@ -210,8 +212,19 @@ Object.defineProperty(exports, "explorerLink", { enumerable: true, get: function
|
|
|
210
212
|
* `getReceiptTokenAddress`, …) and surface as a confusing TVL failure on
|
|
211
213
|
* fresh provider instances. We swallow the throw and fall back to the
|
|
212
214
|
* connection URL — slightly less precise as a cache key but always safe.
|
|
215
|
+
*
|
|
216
|
+
* The runner is unwrapped to its provider first. An `IContractRunner` is either
|
|
217
|
+
* a `Provider` or a `Signer`, and a `Signer` carries the network on
|
|
218
|
+
* `signer.provider`, not on itself — without the unwrap every signer-backed
|
|
219
|
+
* read scoped to `'unknown'` and could never share a cache entry with the
|
|
220
|
+
* provider-backed read of the same token. The unwrap is a **no-op for
|
|
221
|
+
* providers**: ethers' `AbstractProvider` defines `get provider() { return
|
|
222
|
+
* this; }`, so read callers resolve to exactly the scope they did before.
|
|
223
|
+
*
|
|
224
|
+
* @returns `chain:<id>`, `url:<endpoint>`, or `null` when neither is available.
|
|
213
225
|
*/
|
|
214
|
-
function
|
|
226
|
+
function resolveProviderScope(runner) {
|
|
227
|
+
const provider = runner?.provider ?? runner;
|
|
215
228
|
let chainId;
|
|
216
229
|
try {
|
|
217
230
|
const net = provider
|
|
@@ -228,16 +241,124 @@ function providerScope(provider) {
|
|
|
228
241
|
const conn = provider._getConnection?.();
|
|
229
242
|
if (conn?.url)
|
|
230
243
|
return `url:${conn.url}`;
|
|
231
|
-
return
|
|
244
|
+
return null;
|
|
232
245
|
}
|
|
233
|
-
/**
|
|
246
|
+
/**
|
|
247
|
+
* Scope string for cache keys, collapsing an unresolvable runner to the
|
|
248
|
+
* literal `'unknown'` bucket.
|
|
249
|
+
*
|
|
250
|
+
* This is the long-standing behaviour of every cached read in this module and
|
|
251
|
+
* is deliberately preserved: read callers keep keying exactly as they always
|
|
252
|
+
* have, `decimals-unknown-…` entries included. Only {@link getDecimalsOrThrow}
|
|
253
|
+
* treats an unresolvable scope specially — see the note there on why the write
|
|
254
|
+
* path holds itself to a higher bar than the read path.
|
|
255
|
+
*/
|
|
256
|
+
function providerScope(runner) {
|
|
257
|
+
return resolveProviderScope(runner) ?? 'unknown';
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Cache key for a token's `decimals()`, shared by **every** decimals reader in
|
|
261
|
+
* the SDK — the lenient {@link getDecimals} used by read paths and the strict
|
|
262
|
+
* {@link getDecimalsOrThrow} used by write paths. One namespace is the point:
|
|
263
|
+
* a page that reads a vault and then deposits into it must not pay the RPC
|
|
264
|
+
* twice (CLAUDE.md §4.1, "reuse results across functions").
|
|
265
|
+
*
|
|
266
|
+
* **What the entry holds is the *effective share-scale* decimals for `address`,
|
|
267
|
+
* not necessarily `address`'s own `decimals()`.** {@link fetchDecimals} with
|
|
268
|
+
* `isVault` redirects an `evm-2` vault to its receipt token, and the result is
|
|
269
|
+
* still stored under the vault's key — that redirect is the whole point of the
|
|
270
|
+
* flag, and every reader of a vault address wants the receipt token's scale.
|
|
271
|
+
* Consequence for future callers: `getDecimalsOrThrow(runner, evm2VaultAddr)`
|
|
272
|
+
* reads directly (`isVault: false`) but can be served the receipt token's value
|
|
273
|
+
* from this shared entry. Write paths therefore pass the receipt-token address
|
|
274
|
+
* explicitly for `evm-2` vaults; do not add a caller that expects an `evm-2`
|
|
275
|
+
* vault's own `decimals()` back out of this cache.
|
|
276
|
+
*
|
|
277
|
+
* @param scope - Result of {@link providerScope} / {@link resolveProviderScope}.
|
|
278
|
+
* @param address - Token address, or a vault address whose entry holds the
|
|
279
|
+
* share-scale token's decimals (see above).
|
|
280
|
+
*/
|
|
281
|
+
function decimalsCacheKey(scope, address) {
|
|
282
|
+
return `decimals-${scope}-${address}`;
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* In-flight `decimals()` reads, keyed exactly as the cache. Collapses a
|
|
286
|
+
* stampede of concurrent callers for the same uncached token into one RPC.
|
|
287
|
+
*
|
|
288
|
+
* The stored promise **may reject** — strict callers need the original error.
|
|
289
|
+
* Lenient callers therefore must not return it raw; {@link getDecimals} awaits
|
|
290
|
+
* it inside a `try`.
|
|
291
|
+
* @internal
|
|
292
|
+
*/
|
|
234
293
|
const DECIMALS_REQUESTS = new Map();
|
|
294
|
+
/**
|
|
295
|
+
* Run (or join) the single in-flight `decimals()` read for `key`, caching the
|
|
296
|
+
* result on success. Failures are never cached, so a transient outage does not
|
|
297
|
+
* poison the entry.
|
|
298
|
+
*
|
|
299
|
+
* @param key - Cache key from {@link decimalsCacheKey}.
|
|
300
|
+
* @param fetch - Performs the actual read. Only invoked on a miss with nothing
|
|
301
|
+
* already in flight. Whichever caller registers the in-flight promise first
|
|
302
|
+
* supplies this closure — a concurrent {@link getDecimalsOrThrow} joining a
|
|
303
|
+
* {@link getDecimals}-initiated request inherits the lenient, no-retry read
|
|
304
|
+
* instead of its own `retryOnTransientRpc` wrap. Not a correctness bug (the
|
|
305
|
+
* strict caller still throws the original error), just a narrow window
|
|
306
|
+
* where the strict path's transient-retry guarantee is momentarily lost.
|
|
307
|
+
* @returns The decimals value.
|
|
308
|
+
* @throws Whatever `fetch` throws — callers that must not throw wrap this.
|
|
309
|
+
*/
|
|
310
|
+
function sharedDecimalsRequest(key, fetch) {
|
|
311
|
+
const inflight = DECIMALS_REQUESTS.get(key);
|
|
312
|
+
if (inflight)
|
|
313
|
+
return inflight;
|
|
314
|
+
const request = (async () => {
|
|
315
|
+
const decimals = await fetch();
|
|
316
|
+
cache_1.CACHE.set(key, decimals);
|
|
317
|
+
return decimals;
|
|
318
|
+
})().finally(() => {
|
|
319
|
+
DECIMALS_REQUESTS.delete(key);
|
|
320
|
+
});
|
|
321
|
+
DECIMALS_REQUESTS.set(key, request);
|
|
322
|
+
return request;
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Read `decimals()` off an ERC-20 at `address`, or — when `isVault` and the
|
|
326
|
+
* vault is `evm-2` — off its receipt token, which is where the share scale
|
|
327
|
+
* actually lives.
|
|
328
|
+
*
|
|
329
|
+
* @param runner - Provider or signer to read through.
|
|
330
|
+
* @param address - Token or vault address.
|
|
331
|
+
* @param isVault - Resolve `address` as a vault (may cost a backend metadata
|
|
332
|
+
* fetch plus a `lpTokenAddress()` read) rather than reading it directly.
|
|
333
|
+
* @returns The decimals value.
|
|
334
|
+
* @throws Whatever the underlying read throws.
|
|
335
|
+
*/
|
|
336
|
+
async function fetchDecimals(runner, address, isVault) {
|
|
337
|
+
let realAddress = address;
|
|
338
|
+
if (isVault) {
|
|
339
|
+
const tokenizedVault = (await (0, fetcher_1.fetchTokenizedVault)(address))?.[0];
|
|
340
|
+
const version = (0, vault_version_1.getVaultVersionV2)(tokenizedVault);
|
|
341
|
+
if (version === 'evm-2') {
|
|
342
|
+
realAddress = await (0, exports.getReceiptTokenAddress)(runner, address);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
const contract = new ethers_1.Contract(realAddress, [web3_1.MIN_ABIS.decimals], runner);
|
|
346
|
+
return Number(await contract.decimals());
|
|
347
|
+
}
|
|
235
348
|
/**
|
|
236
349
|
* Fetch token decimals from contract or Solana mint.
|
|
237
350
|
* Results are cached to minimize RPC calls.
|
|
351
|
+
*
|
|
352
|
+
* **Never throws** — a failed read logs at error level and resolves
|
|
353
|
+
* `undefined`. Callers that must not silently proceed on an unknown scale (any
|
|
354
|
+
* path that encodes an amount) should use {@link getDecimalsOrThrow} instead:
|
|
355
|
+
* feeding `undefined` into `toNormalizedBn` silently defaults to 18 decimals.
|
|
356
|
+
*
|
|
238
357
|
* @param provider Web3 provider
|
|
239
358
|
* @param address Token contract address or Solana mint
|
|
240
|
-
* @
|
|
359
|
+
* @param isVault Resolve `address` as a vault (evm-2 vaults redirect to the
|
|
360
|
+
* receipt token) rather than reading it as a plain ERC-20. Defaults to `true`.
|
|
361
|
+
* @returns Number of decimals for the token, or `undefined` when the read failed.
|
|
241
362
|
*/
|
|
242
363
|
const getDecimals = async (provider, address, isVault = true) => {
|
|
243
364
|
if (address === ethers_1.ZeroAddress) {
|
|
@@ -248,45 +369,105 @@ const getDecimals = async (provider, address, isVault = true) => {
|
|
|
248
369
|
return constants_1.fallbackDecimals;
|
|
249
370
|
if (!(address && provider))
|
|
250
371
|
return;
|
|
251
|
-
const key =
|
|
372
|
+
const key = decimalsCacheKey(providerScope(provider), address);
|
|
252
373
|
if (cache_1.CACHE.has(key))
|
|
253
374
|
return cache_1.CACHE.get(key);
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
const version = (0, vault_version_1.getVaultVersionV2)(tokenizedVault);
|
|
262
|
-
let realAddress = address;
|
|
263
|
-
if (version === 'evm-2') {
|
|
264
|
-
realAddress = await (0, exports.getReceiptTokenAddress)(provider, address);
|
|
265
|
-
}
|
|
266
|
-
const contract = new ethers_1.Contract(realAddress, [web3_1.MIN_ABIS.decimals], provider);
|
|
267
|
-
const decimals = Number(await contract.decimals());
|
|
268
|
-
cache_1.CACHE.set(key, decimals);
|
|
269
|
-
return decimals;
|
|
270
|
-
}
|
|
271
|
-
else {
|
|
272
|
-
const contract = new ethers_1.Contract(address, [web3_1.MIN_ABIS.decimals], provider);
|
|
273
|
-
const decimals = Number(await contract.decimals());
|
|
274
|
-
cache_1.CACHE.set(key, decimals);
|
|
275
|
-
return decimals;
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
catch (e) {
|
|
279
|
-
logger_1.Logger.log.error('getDecimals', `${address}::${e}`);
|
|
280
|
-
return undefined;
|
|
281
|
-
}
|
|
282
|
-
finally {
|
|
283
|
-
DECIMALS_REQUESTS.delete(key);
|
|
284
|
-
}
|
|
285
|
-
})();
|
|
286
|
-
DECIMALS_REQUESTS.set(key, fetchPromise);
|
|
287
|
-
return fetchPromise;
|
|
375
|
+
try {
|
|
376
|
+
return await sharedDecimalsRequest(key, () => fetchDecimals(provider, address, isVault));
|
|
377
|
+
}
|
|
378
|
+
catch (e) {
|
|
379
|
+
logger_1.Logger.log.error('getDecimals', `${address}::${e}`);
|
|
380
|
+
return undefined;
|
|
381
|
+
}
|
|
288
382
|
};
|
|
289
383
|
exports.getDecimals = getDecimals;
|
|
384
|
+
/**
|
|
385
|
+
* `decimals()` — `keccak256("decimals()")[0..4]`. Scopes the empty-view-response
|
|
386
|
+
* retry to exactly the call this reader makes.
|
|
387
|
+
*/
|
|
388
|
+
const DECIMALS_SELECTOR = '0x313ce567';
|
|
389
|
+
/**
|
|
390
|
+
* Read a token's `decimals()` with the **same cache and stampede protection as
|
|
391
|
+
* {@link getDecimals}**, but surfacing failures instead of swallowing them, and
|
|
392
|
+
* retrying the transient ones.
|
|
393
|
+
*
|
|
394
|
+
* Two reasons this exists rather than a flag on `getDecimals`:
|
|
395
|
+
*
|
|
396
|
+
* 1. **`getDecimals` must keep returning `undefined` on failure** — a dozen
|
|
397
|
+
* read paths depend on that. Amount-encoding paths need the opposite: an
|
|
398
|
+
* `undefined` reaching `toNormalizedBn` silently means 18 decimals, which
|
|
399
|
+
* misencodes the transaction. Here the original error propagates, so the
|
|
400
|
+
* caller's `AugustSDKError` keeps its cause and its Sentry grouping.
|
|
401
|
+
* 2. **Retry.** Providers intermittently return an empty response to
|
|
402
|
+
* `decimals()`, which ethers reports as
|
|
403
|
+
* `missing revert data (action="call", data="0x313ce567", …)` — a shape a
|
|
404
|
+
* deployed ERC-20 cannot legitimately produce, since `decimals()` takes no
|
|
405
|
+
* arguments. That is retried here via {@link isEmptyViewResponse} scoped to
|
|
406
|
+
* this exact selector, alongside ordinary transport faults
|
|
407
|
+
* ({@link isRetryableRpcError}). The empty-view widening applies **only**
|
|
408
|
+
* here; everywhere else the strict transport definition stands. A genuine
|
|
409
|
+
* revert (`CALL_EXCEPTION` carrying revert data) is never retried.
|
|
410
|
+
*
|
|
411
|
+
* Cache is shared with `getDecimals`, so a read-then-write flow against the
|
|
412
|
+
* same token costs one `decimals()` RPC in total, and N concurrent callers for
|
|
413
|
+
* an uncached token collapse to one.
|
|
414
|
+
*
|
|
415
|
+
* **Exception — an unresolvable chain scope bypasses the cache entirely.** When
|
|
416
|
+
* {@link resolveProviderScope} cannot determine the chain (a runner with no
|
|
417
|
+
* resolved network and no connection URL, e.g. a browser provider before its
|
|
418
|
+
* first request), the only available key is the shared `unknown` bucket. Read
|
|
419
|
+
* paths happily use that bucket, and this function deliberately does not: the
|
|
420
|
+
* two paths have different blast radii. A wrong cached `decimals` on a read
|
|
421
|
+
* renders a wrong number on screen; on a write it misencodes an amount the user
|
|
422
|
+
* then *signs*. The collision needed — the same address being a different token
|
|
423
|
+
* with different decimals on two chains, within one process, across a
|
|
424
|
+
* mid-session chain switch — is narrow, but "no worse than the read path" is
|
|
425
|
+
* not the bar for a value that ends up in a transaction. In that case the read
|
|
426
|
+
* is neither served from nor written to the cache, and it also skips the
|
|
427
|
+
* in-flight dedup map, since joining another caller under an untrusted key
|
|
428
|
+
* would reintroduce exactly the collision being avoided. Retries still apply;
|
|
429
|
+
* the only thing forgone is the RPC saving.
|
|
430
|
+
*
|
|
431
|
+
* RPC cost: 1 on a cache miss, 0 on a hit, up to 3 on a miss that keeps
|
|
432
|
+
* faulting (~750ms of added latency in the worst case before it gives up).
|
|
433
|
+
* Always ≥1 when the chain scope is unresolvable.
|
|
434
|
+
*
|
|
435
|
+
* @param runner - Provider or signer to read through. A signer is unwrapped to
|
|
436
|
+
* its provider for cache scoping, so it shares entries with reads on the
|
|
437
|
+
* same chain.
|
|
438
|
+
* @param address - Token address. Read directly as an ERC-20 — pass the
|
|
439
|
+
* receipt-token address yourself for an `evm-2` vault's share scale.
|
|
440
|
+
* @param tag - Low-cardinality label for retry breadcrumbs (e.g.
|
|
441
|
+
* `'vaultDeposit:poolDecimals'`).
|
|
442
|
+
* @returns The token's decimals.
|
|
443
|
+
* @throws The underlying read error once retries are exhausted, or immediately
|
|
444
|
+
* when the failure is neither a transport fault nor an empty view response.
|
|
445
|
+
*
|
|
446
|
+
* @example
|
|
447
|
+
* ```ts
|
|
448
|
+
* // Amount encoding must not proceed on a guessed scale.
|
|
449
|
+
* const decimals = await getDecimalsOrThrow(signer, token, 'deposit:decimals');
|
|
450
|
+
* const amount = toNormalizedBn(userInput, decimals);
|
|
451
|
+
* ```
|
|
452
|
+
*/
|
|
453
|
+
const getDecimalsOrThrow = async (runner, address, tag) => {
|
|
454
|
+
const read = () => (0, chain_error_1.retryOnTransientRpc)(tag, () => fetchDecimals(runner, address, false), { address }, (error) => (0, chain_error_1.isRetryableRpcError)(error) ||
|
|
455
|
+
(0, chain_error_1.isEmptyViewResponse)(error, DECIMALS_SELECTOR));
|
|
456
|
+
const scope = resolveProviderScope(runner);
|
|
457
|
+
if (scope === null) {
|
|
458
|
+
// Untrusted key: no cache read, no cache write, no dedup. See the note
|
|
459
|
+
// above — a mis-scoped decimals value here gets signed into a transaction.
|
|
460
|
+
logger_1.Logger.log.warn(tag, 'decimals cache bypassed: chain scope unresolved', {
|
|
461
|
+
address,
|
|
462
|
+
});
|
|
463
|
+
return read();
|
|
464
|
+
}
|
|
465
|
+
const key = decimalsCacheKey(scope, address);
|
|
466
|
+
if (cache_1.CACHE.has(key))
|
|
467
|
+
return cache_1.CACHE.get(key);
|
|
468
|
+
return sharedDecimalsRequest(key, read);
|
|
469
|
+
};
|
|
470
|
+
exports.getDecimalsOrThrow = getDecimalsOrThrow;
|
|
290
471
|
/** @internal */
|
|
291
472
|
const WHITELISTED_ASSETS_REQUESTS = new Map();
|
|
292
473
|
/**
|
|
@@ -320,14 +501,96 @@ const getWhitelistedAssets = async (provider, whitelistAddress) => {
|
|
|
320
501
|
return fetchPromise;
|
|
321
502
|
};
|
|
322
503
|
exports.getWhitelistedAssets = getWhitelistedAssets;
|
|
504
|
+
/**
|
|
505
|
+
* Read an `evm-2` vault's receipt (LP) token address via `lpTokenAddress()`,
|
|
506
|
+
* retrying only the transient transport faults and surfacing everything else
|
|
507
|
+
* unchanged.
|
|
508
|
+
*
|
|
509
|
+
* Why this exists: `lpTokenAddress()` sits immediately before the receipt-token
|
|
510
|
+
* `decimals()` read on every `evm-2` write path (approve / deposit / request
|
|
511
|
+
* redeem / swap-router deposit). Those `decimals()` reads were hardened against
|
|
512
|
+
* provider blips ({@link getDecimalsOrThrow}); the `lpTokenAddress()` read one
|
|
513
|
+
* line above them was not, so a single truncated `eth_call` response still
|
|
514
|
+
* failed the whole write with
|
|
515
|
+
* `missing revert data (action="call", data="0xf5ae497a", …)` — observed in
|
|
516
|
+
* production against a mainnet vault whose `lpTokenAddress()` demonstrably
|
|
517
|
+
* returns a real address when the provider is healthy.
|
|
518
|
+
*
|
|
519
|
+
* **This must never hide a misrouted vault, and does not.** `lpTokenAddress()`
|
|
520
|
+
* exists *only* on `evm-2` vaults. A vault wrongly classified as `evm-2` returns
|
|
521
|
+
* empty returndata for it, deterministically and forever, and that empty
|
|
522
|
+
* response is byte-identical to the transient one — it is the *only* signal the
|
|
523
|
+
* SDK gets that its version routing was wrong. So the retry here is deliberately
|
|
524
|
+
* shaped to preserve that signal:
|
|
525
|
+
*
|
|
526
|
+
* - it is **bounded** (3 attempts, ~750ms of backoff in total — see
|
|
527
|
+
* `retryOnTransientRpc`), never open-ended;
|
|
528
|
+
* - on exhaustion it **rethrows the original error object**, with its identity,
|
|
529
|
+
* message, `code` and `transaction.data` intact, so the caller's
|
|
530
|
+
* `AugustSDKError` cause and Sentry grouping are exactly what they are today;
|
|
531
|
+
* - it has **no fallback**: it never substitutes another address, never resolves
|
|
532
|
+
* the vault address itself, and never resolves `null`/`undefined`. It either
|
|
533
|
+
* returns a real receipt-token address or throws.
|
|
534
|
+
*
|
|
535
|
+
* Net effect: a blip costs a retry, a misroute costs three `eth_call`s and then
|
|
536
|
+
* fails exactly as loudly as before.
|
|
537
|
+
*
|
|
538
|
+
* **Deliberately not cached.** Unlike `decimals()`, the vault→receipt-token
|
|
539
|
+
* mapping is not memoized here. Caching it would make a misroute's first
|
|
540
|
+
* (failed) probe and every subsequent one diverge, and would put a
|
|
541
|
+
* vault-identity mapping in the cache on the money path. The lenient, cached
|
|
542
|
+
* reader is {@link getReceiptTokenAddress} — use that on read paths that can
|
|
543
|
+
* tolerate `undefined`.
|
|
544
|
+
*
|
|
545
|
+
* RPC cost: exactly 1 `eth_call` on success; at most 3 when the provider keeps
|
|
546
|
+
* faulting.
|
|
547
|
+
*
|
|
548
|
+
* @param runner - Provider or signer to read through.
|
|
549
|
+
* @param vault - Address of the `evm-2` tokenized vault.
|
|
550
|
+
* @param tag - Low-cardinality label for retry breadcrumbs (e.g.
|
|
551
|
+
* `'vaultRequestRedeem:receiptToken'`).
|
|
552
|
+
* @returns The vault's receipt (LP) token address. Never `null`/`undefined`.
|
|
553
|
+
* @throws The underlying read error, unmodified, once the bounded retries are
|
|
554
|
+
* exhausted — or immediately when the failure is neither a transport fault nor
|
|
555
|
+
* an empty response to this exact selector (e.g. a genuine revert carrying
|
|
556
|
+
* revert data).
|
|
557
|
+
*
|
|
558
|
+
* @example
|
|
559
|
+
* ```ts
|
|
560
|
+
* const receiptToken = await getReceiptTokenAddressOrThrow(
|
|
561
|
+
* signer,
|
|
562
|
+
* vault,
|
|
563
|
+
* 'vaultRequestRedeem:receiptToken',
|
|
564
|
+
* );
|
|
565
|
+
* const decimals = await getDecimalsOrThrow(signer, receiptToken, 'tag');
|
|
566
|
+
* ```
|
|
567
|
+
*/
|
|
568
|
+
const getReceiptTokenAddressOrThrow = async (runner, vault, tag) => {
|
|
569
|
+
const contract = new ethers_1.Contract(vault, TokenizedVaultV2_1.ABI_TOKENIZED_VAULT_V2, runner);
|
|
570
|
+
return (0, chain_error_1.retryOnTransientRpc)(tag, () => contract.lpTokenAddress(), { vault }, (error) => (0, chain_error_1.isRetryableRpcError)(error) ||
|
|
571
|
+
(0, chain_error_1.isEmptyViewResponse)(error, chain_error_1.LP_TOKEN_ADDRESS_SELECTOR));
|
|
572
|
+
};
|
|
573
|
+
exports.getReceiptTokenAddressOrThrow = getReceiptTokenAddressOrThrow;
|
|
323
574
|
/** @internal */
|
|
324
575
|
const RECEIPT_TOKEN_REQUESTS = new Map();
|
|
325
576
|
/**
|
|
326
577
|
* Fetch receipt token address from tokenized vault contract.
|
|
327
578
|
* Results are cached to minimize RPC calls.
|
|
579
|
+
*
|
|
580
|
+
* **Never throws** — a failed read logs at error level and resolves
|
|
581
|
+
* `undefined`, which several read paths depend on. The underlying
|
|
582
|
+
* `lpTokenAddress()` call is made through
|
|
583
|
+
* {@link getReceiptTokenAddressOrThrow}, so a transient provider blip is
|
|
584
|
+
* absorbed by a bounded retry before that happens; a deterministic failure (a
|
|
585
|
+
* vault that has no `lpTokenAddress()` because it is not `evm-2`) still resolves
|
|
586
|
+
* `undefined` after the attempts are spent, exactly as before. Callers on a
|
|
587
|
+
* money path must use {@link getReceiptTokenAddressOrThrow} directly instead —
|
|
588
|
+
* an `undefined` receipt-token address there would silently mis-address a
|
|
589
|
+
* `decimals()` read.
|
|
590
|
+
*
|
|
328
591
|
* @param provider Web3 provider
|
|
329
592
|
* @param address Tokenized vault contract address
|
|
330
|
-
* @returns Receipt token address
|
|
593
|
+
* @returns Receipt token address, or `undefined` when the read failed.
|
|
331
594
|
*/
|
|
332
595
|
const getReceiptTokenAddress = async (provider, address) => {
|
|
333
596
|
if (!(address && provider))
|
|
@@ -340,8 +603,7 @@ const getReceiptTokenAddress = async (provider, address) => {
|
|
|
340
603
|
return inflight;
|
|
341
604
|
const fetchPromise = (async () => {
|
|
342
605
|
try {
|
|
343
|
-
const
|
|
344
|
-
const receiptToken = (await vaultContract.lpTokenAddress());
|
|
606
|
+
const receiptToken = await (0, exports.getReceiptTokenAddressOrThrow)(provider, address, 'getReceiptTokenAddress');
|
|
345
607
|
cache_1.CACHE.set(key, receiptToken);
|
|
346
608
|
return receiptToken;
|
|
347
609
|
}
|
package/lib/core/index.d.ts
CHANGED
package/lib/core/index.js
CHANGED
|
@@ -16,6 +16,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
17
|
__exportStar(require("./fetcher"), exports);
|
|
18
18
|
__exportStar(require("./base.class"), exports);
|
|
19
|
+
__exportStar(require("./attribution"), exports);
|
|
19
20
|
__exportStar(require("./auth"), exports);
|
|
20
21
|
__exportStar(require("./logger"), exports);
|
|
21
22
|
__exportStar(require("./analytics"), exports);
|
|
@@ -58,6 +58,7 @@ exports.isCrossChainOperation = isCrossChainOperation;
|
|
|
58
58
|
exports.getAvailableChains = getAvailableChains;
|
|
59
59
|
exports.formatCrossChainError = formatCrossChainError;
|
|
60
60
|
exports.getLayerZeroScanUrl = getLayerZeroScanUrl;
|
|
61
|
+
const attribution_1 = require("../../core/attribution");
|
|
61
62
|
const core_1 = require("../../core");
|
|
62
63
|
const crossChain_1 = require("../types/crossChain");
|
|
63
64
|
const OFT_1 = require("../../abis/OFT");
|
|
@@ -537,6 +538,7 @@ async function approveCrossChain(tokenAddress, spenderAddress, amount, walletCli
|
|
|
537
538
|
abi: OFT_1.ABI_CROSS_CHAIN_ERC20,
|
|
538
539
|
functionName: 'approve',
|
|
539
540
|
args: [spenderAddress, amount],
|
|
541
|
+
dataSuffix: (0, attribution_1.getAttributionSuffix)(),
|
|
540
542
|
});
|
|
541
543
|
const receipt = await publicClient.waitForTransactionReceipt({
|
|
542
544
|
hash: hash,
|
|
@@ -625,6 +627,7 @@ async function crossChainVaultDeposit(props) {
|
|
|
625
627
|
args: txInputs.txArgs,
|
|
626
628
|
value: txInputs.messageValue,
|
|
627
629
|
gas: gasLimit,
|
|
630
|
+
dataSuffix: (0, attribution_1.getAttributionSuffix)(props.userChainId),
|
|
628
631
|
});
|
|
629
632
|
// 5. Wait for confirmation
|
|
630
633
|
const receipt = await publicClient.waitForTransactionReceipt({
|
|
@@ -709,6 +712,7 @@ async function crossChainVaultRedeem(props) {
|
|
|
709
712
|
args: txInputs.txArgs,
|
|
710
713
|
value: txInputs.messageValue,
|
|
711
714
|
gas: gasLimit,
|
|
715
|
+
dataSuffix: (0, attribution_1.getAttributionSuffix)(props.config.hubChainId),
|
|
712
716
|
});
|
|
713
717
|
// 6. Wait for confirmation
|
|
714
718
|
const receipt = await publicClient.waitForTransactionReceipt({
|