@augustdigital/sdk 8.17.0 → 8.20.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/lib/adapters/evm/index.js +4 -2
  2. package/lib/adapters/stellar/soroban.d.ts +8 -3
  3. package/lib/adapters/stellar/soroban.js +9 -4
  4. package/lib/adapters/sui/constants.d.ts +1 -1
  5. package/lib/adapters/sui/constants.js +6 -1
  6. package/lib/core/analytics/constants.d.ts +1 -1
  7. package/lib/core/analytics/constants.js +1 -1
  8. package/lib/core/analytics/sentry.d.ts +7 -0
  9. package/lib/core/analytics/sentry.js +182 -1
  10. package/lib/core/analytics/version.d.ts +1 -1
  11. package/lib/core/analytics/version.js +1 -1
  12. package/lib/core/attribution.d.ts +111 -0
  13. package/lib/core/attribution.js +142 -0
  14. package/lib/core/base.class.d.ts +17 -1
  15. package/lib/core/base.class.js +6 -1
  16. package/lib/core/constants/core.js +42 -15
  17. package/lib/core/constants/web3.d.ts +17 -0
  18. package/lib/core/constants/web3.js +22 -1
  19. package/lib/core/fetcher.js +10 -1
  20. package/lib/core/helpers/chain-error.d.ts +140 -0
  21. package/lib/core/helpers/chain-error.js +412 -0
  22. package/lib/core/helpers/chain-support.d.ts +80 -0
  23. package/lib/core/helpers/chain-support.js +115 -0
  24. package/lib/core/helpers/signer.d.ts +21 -0
  25. package/lib/core/helpers/signer.js +52 -0
  26. package/lib/core/helpers/web3.d.ts +172 -3
  27. package/lib/core/helpers/web3.js +357 -49
  28. package/lib/core/index.d.ts +2 -0
  29. package/lib/core/index.js +2 -0
  30. package/lib/evm/methods/crossChainVault.js +4 -0
  31. package/lib/modules/vaults/getters.js +115 -23
  32. package/lib/modules/vaults/main.d.ts +24 -3
  33. package/lib/modules/vaults/main.js +32 -31
  34. package/lib/modules/vaults/write.actions.d.ts +41 -1
  35. package/lib/modules/vaults/write.actions.js +302 -86
  36. package/lib/sdk.d.ts +11315 -10736
  37. package/lib/services/subgraph/vaults.js +85 -14
  38. package/package.json +1 -1
@@ -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.determineRpcBatchMaxCount = 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");
@@ -86,10 +87,54 @@ const determineBlockSkipInternal = (chain) => {
86
87
  case 999: // HyperEVM — rpc.hypurrscan.io limits eth_getLogs to 1000 blocks
87
88
  return 1_000;
88
89
  default:
89
- return 50_000;
90
+ // 10k blocks is the eth_getLogs range cap enforced by every mainstream
91
+ // provider (Alchemy, Infura, dRPC, QuickNode). The previous 50k default
92
+ // was rejected outright with JSON-RPC -32600 ("You can make eth_getLogs
93
+ // requests with up to a 10000 block range") on Ethereum mainnet, which
94
+ // failed `getVaultRedemptionHistory` for every vault on an unlisted
95
+ // chain. RPC-count impact: a 150k-block cutoff now costs 15 getLogs
96
+ // calls instead of 3, batched 20-concurrent, so still one round trip.
97
+ return 10_000;
90
98
  }
91
99
  };
92
100
  exports.determineBlockSkipInternal = determineBlockSkipInternal;
101
+ /**
102
+ * Default batch cap. Chosen to stay under the smallest limit among the
103
+ * providers we route to by default (e.g. rpc.hypurrscan.io rejects > 20).
104
+ */
105
+ const DEFAULT_RPC_BATCH_MAX_COUNT = 10;
106
+ /**
107
+ * Maximum JSON-RPC calls ethers may coalesce into a single batched HTTP
108
+ * request for a given endpoint.
109
+ *
110
+ * Providers advertise wildly different batch limits and reject the whole batch
111
+ * — not the excess — when exceeded, so one oversized batch fails every call
112
+ * inside it. dRPC's free tier caps batches at 3, which made every Mezo read
113
+ * fail with `server response 500 … "Batch of more than 3 requests are not
114
+ * allowed"` (the single highest-volume SDK error in production).
115
+ *
116
+ * Detection is host-based rather than chain-based because the limit is a
117
+ * property of the endpoint, not the chain: the same chain served by a
118
+ * self-hosted node has no such cap.
119
+ *
120
+ * @param rpcUrl - The RPC endpoint URL. Unparseable values fall back to the
121
+ * conservative default rather than throwing.
122
+ * @returns Batch size cap to pass to ethers as `batchMaxCount`.
123
+ */
124
+ const determineRpcBatchMaxCount = (rpcUrl) => {
125
+ let host = '';
126
+ try {
127
+ host = new URL(rpcUrl).hostname.toLowerCase();
128
+ }
129
+ catch {
130
+ return DEFAULT_RPC_BATCH_MAX_COUNT;
131
+ }
132
+ // dRPC free tier: "Batch of more than 3 requests are not allowed".
133
+ if (host === 'drpc.org' || host.endsWith('.drpc.org'))
134
+ return 3;
135
+ return DEFAULT_RPC_BATCH_MAX_COUNT;
136
+ };
137
+ exports.determineRpcBatchMaxCount = determineRpcBatchMaxCount;
93
138
  /**
94
139
  * Retrieve chain ID from web3 provider.
95
140
  * Handles both initialized and uninitialized provider states.
@@ -151,14 +196,16 @@ const createProvider = (rpcUrl, chainId) => {
151
196
  const cacheKey = chainId ? `${rpcUrl}|${chainId}` : rpcUrl;
152
197
  if (cache_1.CACHE.has(cacheKey))
153
198
  return cache_1.CACHE.get(cacheKey);
154
- // batchMaxCount caps server-side batch limits (e.g. rpc.hypurrscan.io > 20).
199
+ // batchMaxCount respects server-side batch limits providers reject the
200
+ // entire batch when it is exceeded (see determineRpcBatchMaxCount).
201
+ const batchMaxCount = (0, exports.determineRpcBatchMaxCount)(rpcUrl);
155
202
  const provider = chainId
156
203
  ? new ethers_1.JsonRpcProvider(rpcUrl, ethers_1.Network.from(chainId), {
157
204
  staticNetwork: ethers_1.Network.from(chainId),
158
- batchMaxCount: 10,
205
+ batchMaxCount,
159
206
  })
160
207
  : new ethers_1.JsonRpcProvider(rpcUrl, undefined, {
161
- batchMaxCount: 10,
208
+ batchMaxCount,
162
209
  });
163
210
  cache_1.CACHE.set(cacheKey, provider);
164
211
  return provider;
@@ -199,8 +246,9 @@ exports.getInfuraProvider = getInfuraProvider;
199
246
  var explorer_link_1 = require("./explorer-link");
200
247
  Object.defineProperty(exports, "explorerLink", { enumerable: true, get: function () { return explorer_link_1.explorerLink; } });
201
248
  /**
202
- * Stable per-provider identity for cache keys prevents cross-chain collisions
203
- * when the same address represents different tokens on different networks.
249
+ * Stable per-provider identity for cache keys, or `null` when the chain cannot
250
+ * be determined at all — the distinction callers need to decide whether a cache
251
+ * key is trustworthy.
204
252
  *
205
253
  * `provider._network` is a *getter* in ethers v6 that throws
206
254
  * `network is not available yet (NETWORK_ERROR)` until the provider has
@@ -210,8 +258,19 @@ Object.defineProperty(exports, "explorerLink", { enumerable: true, get: function
210
258
  * `getReceiptTokenAddress`, …) and surface as a confusing TVL failure on
211
259
  * fresh provider instances. We swallow the throw and fall back to the
212
260
  * connection URL — slightly less precise as a cache key but always safe.
261
+ *
262
+ * The runner is unwrapped to its provider first. An `IContractRunner` is either
263
+ * a `Provider` or a `Signer`, and a `Signer` carries the network on
264
+ * `signer.provider`, not on itself — without the unwrap every signer-backed
265
+ * read scoped to `'unknown'` and could never share a cache entry with the
266
+ * provider-backed read of the same token. The unwrap is a **no-op for
267
+ * providers**: ethers' `AbstractProvider` defines `get provider() { return
268
+ * this; }`, so read callers resolve to exactly the scope they did before.
269
+ *
270
+ * @returns `chain:<id>`, `url:<endpoint>`, or `null` when neither is available.
213
271
  */
214
- function providerScope(provider) {
272
+ function resolveProviderScope(runner) {
273
+ const provider = runner?.provider ?? runner;
215
274
  let chainId;
216
275
  try {
217
276
  const net = provider
@@ -228,16 +287,124 @@ function providerScope(provider) {
228
287
  const conn = provider._getConnection?.();
229
288
  if (conn?.url)
230
289
  return `url:${conn.url}`;
231
- return 'unknown';
290
+ return null;
232
291
  }
233
- /** @internal */
292
+ /**
293
+ * Scope string for cache keys, collapsing an unresolvable runner to the
294
+ * literal `'unknown'` bucket.
295
+ *
296
+ * This is the long-standing behaviour of every cached read in this module and
297
+ * is deliberately preserved: read callers keep keying exactly as they always
298
+ * have, `decimals-unknown-…` entries included. Only {@link getDecimalsOrThrow}
299
+ * treats an unresolvable scope specially — see the note there on why the write
300
+ * path holds itself to a higher bar than the read path.
301
+ */
302
+ function providerScope(runner) {
303
+ return resolveProviderScope(runner) ?? 'unknown';
304
+ }
305
+ /**
306
+ * Cache key for a token's `decimals()`, shared by **every** decimals reader in
307
+ * the SDK — the lenient {@link getDecimals} used by read paths and the strict
308
+ * {@link getDecimalsOrThrow} used by write paths. One namespace is the point:
309
+ * a page that reads a vault and then deposits into it must not pay the RPC
310
+ * twice (CLAUDE.md §4.1, "reuse results across functions").
311
+ *
312
+ * **What the entry holds is the *effective share-scale* decimals for `address`,
313
+ * not necessarily `address`'s own `decimals()`.** {@link fetchDecimals} with
314
+ * `isVault` redirects an `evm-2` vault to its receipt token, and the result is
315
+ * still stored under the vault's key — that redirect is the whole point of the
316
+ * flag, and every reader of a vault address wants the receipt token's scale.
317
+ * Consequence for future callers: `getDecimalsOrThrow(runner, evm2VaultAddr)`
318
+ * reads directly (`isVault: false`) but can be served the receipt token's value
319
+ * from this shared entry. Write paths therefore pass the receipt-token address
320
+ * explicitly for `evm-2` vaults; do not add a caller that expects an `evm-2`
321
+ * vault's own `decimals()` back out of this cache.
322
+ *
323
+ * @param scope - Result of {@link providerScope} / {@link resolveProviderScope}.
324
+ * @param address - Token address, or a vault address whose entry holds the
325
+ * share-scale token's decimals (see above).
326
+ */
327
+ function decimalsCacheKey(scope, address) {
328
+ return `decimals-${scope}-${address}`;
329
+ }
330
+ /**
331
+ * In-flight `decimals()` reads, keyed exactly as the cache. Collapses a
332
+ * stampede of concurrent callers for the same uncached token into one RPC.
333
+ *
334
+ * The stored promise **may reject** — strict callers need the original error.
335
+ * Lenient callers therefore must not return it raw; {@link getDecimals} awaits
336
+ * it inside a `try`.
337
+ * @internal
338
+ */
234
339
  const DECIMALS_REQUESTS = new Map();
340
+ /**
341
+ * Run (or join) the single in-flight `decimals()` read for `key`, caching the
342
+ * result on success. Failures are never cached, so a transient outage does not
343
+ * poison the entry.
344
+ *
345
+ * @param key - Cache key from {@link decimalsCacheKey}.
346
+ * @param fetch - Performs the actual read. Only invoked on a miss with nothing
347
+ * already in flight. Whichever caller registers the in-flight promise first
348
+ * supplies this closure — a concurrent {@link getDecimalsOrThrow} joining a
349
+ * {@link getDecimals}-initiated request inherits the lenient, no-retry read
350
+ * instead of its own `retryOnTransientRpc` wrap. Not a correctness bug (the
351
+ * strict caller still throws the original error), just a narrow window
352
+ * where the strict path's transient-retry guarantee is momentarily lost.
353
+ * @returns The decimals value.
354
+ * @throws Whatever `fetch` throws — callers that must not throw wrap this.
355
+ */
356
+ function sharedDecimalsRequest(key, fetch) {
357
+ const inflight = DECIMALS_REQUESTS.get(key);
358
+ if (inflight)
359
+ return inflight;
360
+ const request = (async () => {
361
+ const decimals = await fetch();
362
+ cache_1.CACHE.set(key, decimals);
363
+ return decimals;
364
+ })().finally(() => {
365
+ DECIMALS_REQUESTS.delete(key);
366
+ });
367
+ DECIMALS_REQUESTS.set(key, request);
368
+ return request;
369
+ }
370
+ /**
371
+ * Read `decimals()` off an ERC-20 at `address`, or — when `isVault` and the
372
+ * vault is `evm-2` — off its receipt token, which is where the share scale
373
+ * actually lives.
374
+ *
375
+ * @param runner - Provider or signer to read through.
376
+ * @param address - Token or vault address.
377
+ * @param isVault - Resolve `address` as a vault (may cost a backend metadata
378
+ * fetch plus a `lpTokenAddress()` read) rather than reading it directly.
379
+ * @returns The decimals value.
380
+ * @throws Whatever the underlying read throws.
381
+ */
382
+ async function fetchDecimals(runner, address, isVault) {
383
+ let realAddress = address;
384
+ if (isVault) {
385
+ const tokenizedVault = (await (0, fetcher_1.fetchTokenizedVault)(address))?.[0];
386
+ const version = (0, vault_version_1.getVaultVersionV2)(tokenizedVault);
387
+ if (version === 'evm-2') {
388
+ realAddress = await (0, exports.getReceiptTokenAddress)(runner, address);
389
+ }
390
+ }
391
+ const contract = new ethers_1.Contract(realAddress, [web3_1.MIN_ABIS.decimals], runner);
392
+ return Number(await contract.decimals());
393
+ }
235
394
  /**
236
395
  * Fetch token decimals from contract or Solana mint.
237
396
  * Results are cached to minimize RPC calls.
397
+ *
398
+ * **Never throws** — a failed read logs at error level and resolves
399
+ * `undefined`. Callers that must not silently proceed on an unknown scale (any
400
+ * path that encodes an amount) should use {@link getDecimalsOrThrow} instead:
401
+ * feeding `undefined` into `toNormalizedBn` silently defaults to 18 decimals.
402
+ *
238
403
  * @param provider Web3 provider
239
404
  * @param address Token contract address or Solana mint
240
- * @returns Number of decimals for the token
405
+ * @param isVault Resolve `address` as a vault (evm-2 vaults redirect to the
406
+ * receipt token) rather than reading it as a plain ERC-20. Defaults to `true`.
407
+ * @returns Number of decimals for the token, or `undefined` when the read failed.
241
408
  */
242
409
  const getDecimals = async (provider, address, isVault = true) => {
243
410
  if (address === ethers_1.ZeroAddress) {
@@ -248,45 +415,105 @@ const getDecimals = async (provider, address, isVault = true) => {
248
415
  return constants_1.fallbackDecimals;
249
416
  if (!(address && provider))
250
417
  return;
251
- const key = `decimals-${providerScope(provider)}-${address}`;
418
+ const key = decimalsCacheKey(providerScope(provider), address);
252
419
  if (cache_1.CACHE.has(key))
253
420
  return cache_1.CACHE.get(key);
254
- const inflight = DECIMALS_REQUESTS.get(key);
255
- if (inflight)
256
- return inflight;
257
- const fetchPromise = (async () => {
258
- try {
259
- if (isVault) {
260
- const tokenizedVault = (await (0, fetcher_1.fetchTokenizedVault)(address))?.[0];
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;
421
+ try {
422
+ return await sharedDecimalsRequest(key, () => fetchDecimals(provider, address, isVault));
423
+ }
424
+ catch (e) {
425
+ logger_1.Logger.log.error('getDecimals', `${address}::${e}`);
426
+ return undefined;
427
+ }
288
428
  };
289
429
  exports.getDecimals = getDecimals;
430
+ /**
431
+ * `decimals()` — `keccak256("decimals()")[0..4]`. Scopes the empty-view-response
432
+ * retry to exactly the call this reader makes.
433
+ */
434
+ const DECIMALS_SELECTOR = '0x313ce567';
435
+ /**
436
+ * Read a token's `decimals()` with the **same cache and stampede protection as
437
+ * {@link getDecimals}**, but surfacing failures instead of swallowing them, and
438
+ * retrying the transient ones.
439
+ *
440
+ * Two reasons this exists rather than a flag on `getDecimals`:
441
+ *
442
+ * 1. **`getDecimals` must keep returning `undefined` on failure** — a dozen
443
+ * read paths depend on that. Amount-encoding paths need the opposite: an
444
+ * `undefined` reaching `toNormalizedBn` silently means 18 decimals, which
445
+ * misencodes the transaction. Here the original error propagates, so the
446
+ * caller's `AugustSDKError` keeps its cause and its Sentry grouping.
447
+ * 2. **Retry.** Providers intermittently return an empty response to
448
+ * `decimals()`, which ethers reports as
449
+ * `missing revert data (action="call", data="0x313ce567", …)` — a shape a
450
+ * deployed ERC-20 cannot legitimately produce, since `decimals()` takes no
451
+ * arguments. That is retried here via {@link isEmptyViewResponse} scoped to
452
+ * this exact selector, alongside ordinary transport faults
453
+ * ({@link isRetryableRpcError}). The empty-view widening applies **only**
454
+ * here; everywhere else the strict transport definition stands. A genuine
455
+ * revert (`CALL_EXCEPTION` carrying revert data) is never retried.
456
+ *
457
+ * Cache is shared with `getDecimals`, so a read-then-write flow against the
458
+ * same token costs one `decimals()` RPC in total, and N concurrent callers for
459
+ * an uncached token collapse to one.
460
+ *
461
+ * **Exception — an unresolvable chain scope bypasses the cache entirely.** When
462
+ * {@link resolveProviderScope} cannot determine the chain (a runner with no
463
+ * resolved network and no connection URL, e.g. a browser provider before its
464
+ * first request), the only available key is the shared `unknown` bucket. Read
465
+ * paths happily use that bucket, and this function deliberately does not: the
466
+ * two paths have different blast radii. A wrong cached `decimals` on a read
467
+ * renders a wrong number on screen; on a write it misencodes an amount the user
468
+ * then *signs*. The collision needed — the same address being a different token
469
+ * with different decimals on two chains, within one process, across a
470
+ * mid-session chain switch — is narrow, but "no worse than the read path" is
471
+ * not the bar for a value that ends up in a transaction. In that case the read
472
+ * is neither served from nor written to the cache, and it also skips the
473
+ * in-flight dedup map, since joining another caller under an untrusted key
474
+ * would reintroduce exactly the collision being avoided. Retries still apply;
475
+ * the only thing forgone is the RPC saving.
476
+ *
477
+ * RPC cost: 1 on a cache miss, 0 on a hit, up to 3 on a miss that keeps
478
+ * faulting (~750ms of added latency in the worst case before it gives up).
479
+ * Always ≥1 when the chain scope is unresolvable.
480
+ *
481
+ * @param runner - Provider or signer to read through. A signer is unwrapped to
482
+ * its provider for cache scoping, so it shares entries with reads on the
483
+ * same chain.
484
+ * @param address - Token address. Read directly as an ERC-20 — pass the
485
+ * receipt-token address yourself for an `evm-2` vault's share scale.
486
+ * @param tag - Low-cardinality label for retry breadcrumbs (e.g.
487
+ * `'vaultDeposit:poolDecimals'`).
488
+ * @returns The token's decimals.
489
+ * @throws The underlying read error once retries are exhausted, or immediately
490
+ * when the failure is neither a transport fault nor an empty view response.
491
+ *
492
+ * @example
493
+ * ```ts
494
+ * // Amount encoding must not proceed on a guessed scale.
495
+ * const decimals = await getDecimalsOrThrow(signer, token, 'deposit:decimals');
496
+ * const amount = toNormalizedBn(userInput, decimals);
497
+ * ```
498
+ */
499
+ const getDecimalsOrThrow = async (runner, address, tag) => {
500
+ const read = () => (0, chain_error_1.retryOnTransientRpc)(tag, () => fetchDecimals(runner, address, false), { address }, (error) => (0, chain_error_1.isRetryableRpcError)(error) ||
501
+ (0, chain_error_1.isEmptyViewResponse)(error, DECIMALS_SELECTOR));
502
+ const scope = resolveProviderScope(runner);
503
+ if (scope === null) {
504
+ // Untrusted key: no cache read, no cache write, no dedup. See the note
505
+ // above — a mis-scoped decimals value here gets signed into a transaction.
506
+ logger_1.Logger.log.warn(tag, 'decimals cache bypassed: chain scope unresolved', {
507
+ address,
508
+ });
509
+ return read();
510
+ }
511
+ const key = decimalsCacheKey(scope, address);
512
+ if (cache_1.CACHE.has(key))
513
+ return cache_1.CACHE.get(key);
514
+ return sharedDecimalsRequest(key, read);
515
+ };
516
+ exports.getDecimalsOrThrow = getDecimalsOrThrow;
290
517
  /** @internal */
291
518
  const WHITELISTED_ASSETS_REQUESTS = new Map();
292
519
  /**
@@ -320,14 +547,96 @@ const getWhitelistedAssets = async (provider, whitelistAddress) => {
320
547
  return fetchPromise;
321
548
  };
322
549
  exports.getWhitelistedAssets = getWhitelistedAssets;
550
+ /**
551
+ * Read an `evm-2` vault's receipt (LP) token address via `lpTokenAddress()`,
552
+ * retrying only the transient transport faults and surfacing everything else
553
+ * unchanged.
554
+ *
555
+ * Why this exists: `lpTokenAddress()` sits immediately before the receipt-token
556
+ * `decimals()` read on every `evm-2` write path (approve / deposit / request
557
+ * redeem / swap-router deposit). Those `decimals()` reads were hardened against
558
+ * provider blips ({@link getDecimalsOrThrow}); the `lpTokenAddress()` read one
559
+ * line above them was not, so a single truncated `eth_call` response still
560
+ * failed the whole write with
561
+ * `missing revert data (action="call", data="0xf5ae497a", …)` — observed in
562
+ * production against a mainnet vault whose `lpTokenAddress()` demonstrably
563
+ * returns a real address when the provider is healthy.
564
+ *
565
+ * **This must never hide a misrouted vault, and does not.** `lpTokenAddress()`
566
+ * exists *only* on `evm-2` vaults. A vault wrongly classified as `evm-2` returns
567
+ * empty returndata for it, deterministically and forever, and that empty
568
+ * response is byte-identical to the transient one — it is the *only* signal the
569
+ * SDK gets that its version routing was wrong. So the retry here is deliberately
570
+ * shaped to preserve that signal:
571
+ *
572
+ * - it is **bounded** (3 attempts, ~750ms of backoff in total — see
573
+ * `retryOnTransientRpc`), never open-ended;
574
+ * - on exhaustion it **rethrows the original error object**, with its identity,
575
+ * message, `code` and `transaction.data` intact, so the caller's
576
+ * `AugustSDKError` cause and Sentry grouping are exactly what they are today;
577
+ * - it has **no fallback**: it never substitutes another address, never resolves
578
+ * the vault address itself, and never resolves `null`/`undefined`. It either
579
+ * returns a real receipt-token address or throws.
580
+ *
581
+ * Net effect: a blip costs a retry, a misroute costs three `eth_call`s and then
582
+ * fails exactly as loudly as before.
583
+ *
584
+ * **Deliberately not cached.** Unlike `decimals()`, the vault→receipt-token
585
+ * mapping is not memoized here. Caching it would make a misroute's first
586
+ * (failed) probe and every subsequent one diverge, and would put a
587
+ * vault-identity mapping in the cache on the money path. The lenient, cached
588
+ * reader is {@link getReceiptTokenAddress} — use that on read paths that can
589
+ * tolerate `undefined`.
590
+ *
591
+ * RPC cost: exactly 1 `eth_call` on success; at most 3 when the provider keeps
592
+ * faulting.
593
+ *
594
+ * @param runner - Provider or signer to read through.
595
+ * @param vault - Address of the `evm-2` tokenized vault.
596
+ * @param tag - Low-cardinality label for retry breadcrumbs (e.g.
597
+ * `'vaultRequestRedeem:receiptToken'`).
598
+ * @returns The vault's receipt (LP) token address. Never `null`/`undefined`.
599
+ * @throws The underlying read error, unmodified, once the bounded retries are
600
+ * exhausted — or immediately when the failure is neither a transport fault nor
601
+ * an empty response to this exact selector (e.g. a genuine revert carrying
602
+ * revert data).
603
+ *
604
+ * @example
605
+ * ```ts
606
+ * const receiptToken = await getReceiptTokenAddressOrThrow(
607
+ * signer,
608
+ * vault,
609
+ * 'vaultRequestRedeem:receiptToken',
610
+ * );
611
+ * const decimals = await getDecimalsOrThrow(signer, receiptToken, 'tag');
612
+ * ```
613
+ */
614
+ const getReceiptTokenAddressOrThrow = async (runner, vault, tag) => {
615
+ const contract = new ethers_1.Contract(vault, TokenizedVaultV2_1.ABI_TOKENIZED_VAULT_V2, runner);
616
+ return (0, chain_error_1.retryOnTransientRpc)(tag, () => contract.lpTokenAddress(), { vault }, (error) => (0, chain_error_1.isRetryableRpcError)(error) ||
617
+ (0, chain_error_1.isEmptyViewResponse)(error, chain_error_1.LP_TOKEN_ADDRESS_SELECTOR));
618
+ };
619
+ exports.getReceiptTokenAddressOrThrow = getReceiptTokenAddressOrThrow;
323
620
  /** @internal */
324
621
  const RECEIPT_TOKEN_REQUESTS = new Map();
325
622
  /**
326
623
  * Fetch receipt token address from tokenized vault contract.
327
624
  * Results are cached to minimize RPC calls.
625
+ *
626
+ * **Never throws** — a failed read logs at error level and resolves
627
+ * `undefined`, which several read paths depend on. The underlying
628
+ * `lpTokenAddress()` call is made through
629
+ * {@link getReceiptTokenAddressOrThrow}, so a transient provider blip is
630
+ * absorbed by a bounded retry before that happens; a deterministic failure (a
631
+ * vault that has no `lpTokenAddress()` because it is not `evm-2`) still resolves
632
+ * `undefined` after the attempts are spent, exactly as before. Callers on a
633
+ * money path must use {@link getReceiptTokenAddressOrThrow} directly instead —
634
+ * an `undefined` receipt-token address there would silently mis-address a
635
+ * `decimals()` read.
636
+ *
328
637
  * @param provider Web3 provider
329
638
  * @param address Tokenized vault contract address
330
- * @returns Receipt token address
639
+ * @returns Receipt token address, or `undefined` when the read failed.
331
640
  */
332
641
  const getReceiptTokenAddress = async (provider, address) => {
333
642
  if (!(address && provider))
@@ -340,8 +649,7 @@ const getReceiptTokenAddress = async (provider, address) => {
340
649
  return inflight;
341
650
  const fetchPromise = (async () => {
342
651
  try {
343
- const vaultContract = new ethers_1.Contract(address, TokenizedVaultV2_1.ABI_TOKENIZED_VAULT_V2, provider);
344
- const receiptToken = (await vaultContract.lpTokenAddress());
652
+ const receiptToken = await (0, exports.getReceiptTokenAddressOrThrow)(provider, address, 'getReceiptTokenAddress');
345
653
  cache_1.CACHE.set(key, receiptToken);
346
654
  return receiptToken;
347
655
  }
@@ -1,5 +1,6 @@
1
1
  export * from './fetcher';
2
2
  export * from './base.class';
3
+ export * from './attribution';
3
4
  export * from './auth';
4
5
  export * from './logger';
5
6
  export * from './analytics';
@@ -14,6 +15,7 @@ export * from './helpers/web3';
14
15
  export * from './helpers/multicall';
15
16
  export * from './helpers/vaults';
16
17
  export * from './helpers/chain-error';
18
+ export * from './helpers/chain-support';
17
19
  export * from './helpers/core';
18
20
  export * from './helpers/adapters';
19
21
  export * from './helpers/signer';
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);
@@ -30,6 +31,7 @@ __exportStar(require("./helpers/web3"), exports);
30
31
  __exportStar(require("./helpers/multicall"), exports);
31
32
  __exportStar(require("./helpers/vaults"), exports);
32
33
  __exportStar(require("./helpers/chain-error"), exports);
34
+ __exportStar(require("./helpers/chain-support"), exports);
33
35
  __exportStar(require("./helpers/core"), exports);
34
36
  __exportStar(require("./helpers/adapters"), exports);
35
37
  __exportStar(require("./helpers/signer"), 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({