@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
@@ -97,25 +97,233 @@ function isNonceParsing(error) {
97
97
  return ((code === 'INVALID_ARGUMENT' || code === 'BAD_DATA') &&
98
98
  msg.includes('nonce'));
99
99
  }
100
+ /**
101
+ * Own-properties stamped onto an error describing a transaction that already
102
+ * reached the network. Prefixed to avoid colliding with the `hash` /
103
+ * `transaction` / `receipt` fields ethers puts on its own errors.
104
+ *
105
+ * These are an internal wire format between the tx-wait helpers and the write
106
+ * paths' catch blocks — consumers read the *sanitized* form
107
+ * (`txHash` / `broadcast` / `confirmationUnknown`) off `AugustSDKError.context`,
108
+ * never these.
109
+ * @internal
110
+ */
111
+ const BROADCAST_HASH_KEY = 'augustBroadcastTxHash';
100
112
  /** @internal */
113
+ const BROADCAST_UNKNOWN_KEY = 'augustConfirmationUnknown';
114
+ /**
115
+ * Stamp "this transaction was broadcast" onto an error, in place.
116
+ *
117
+ * Mutating rather than wrapping is deliberate: callers up the stack compare
118
+ * error identity (and ethers' own typed fields matter for downstream
119
+ * classification), so replacing the error object would lose information.
120
+ *
121
+ * Annotating is strictly best-effort. A frozen or sealed error, or one with a
122
+ * getter-only property of the same name, must never turn a *recoverable
123
+ * transport failure* into a `TypeError` thrown from inside the error handler —
124
+ * that would be a strictly worse outcome than losing the hash. Each write is
125
+ * therefore attempted independently and failures are swallowed; the caller then
126
+ * simply reports the transaction as un-annotated (no `txHash` in the error
127
+ * context), which is the same conservative state as "never broadcast".
128
+ *
129
+ * @param error - The error about to be thrown. Non-objects pass through
130
+ * untouched — there is nowhere to put the marker.
131
+ * @param txHash - Hash of the transaction that reached the network.
132
+ * @param confirmationUnknown - `true` when the transaction is on the wire but
133
+ * its outcome could not be determined (transport retries exhausted, or the
134
+ * wait timed out). `false` when the chain gave a definitive answer, e.g. a
135
+ * receipt with `status === 0`.
136
+ * @returns The same error instance (identity preserved), for
137
+ * `throw markBroadcast(...)`.
138
+ * @internal
139
+ */
140
+ function markBroadcast(error, txHash, confirmationUnknown) {
141
+ if (!error || typeof error !== 'object')
142
+ return error;
143
+ // `defineProperty` with configurable/writable keeps the marker invisible to
144
+ // JSON.stringify and to spreads of the error, so it can't leak into a
145
+ // serialized crash report as a mystery field.
146
+ const stamp = (key, value) => {
147
+ try {
148
+ Object.defineProperty(error, key, {
149
+ value,
150
+ enumerable: false,
151
+ writable: true,
152
+ configurable: true,
153
+ });
154
+ }
155
+ catch {
156
+ // Frozen/sealed error, or a non-configurable same-named property. The
157
+ // hash is a nice-to-have; never let annotation failure mask the real
158
+ // transport error we are in the middle of reporting.
159
+ }
160
+ };
161
+ stamp(BROADCAST_HASH_KEY, txHash);
162
+ stamp(BROADCAST_UNKNOWN_KEY, confirmationUnknown);
163
+ return error;
164
+ }
165
+ /**
166
+ * Read back the markers {@link markBroadcast} stamped, if any.
167
+ *
168
+ * @param error - The caught value, of unknown type.
169
+ * @returns The recovered hash and confirmation state; both `undefined` when the
170
+ * error never passed through a tx-wait helper.
171
+ * @internal
172
+ */
173
+ function readBroadcastMarkers(error) {
174
+ if (!error || typeof error !== 'object')
175
+ return {};
176
+ const e = error;
177
+ const txHash = e[BROADCAST_HASH_KEY];
178
+ const confirmationUnknown = e[BROADCAST_UNKNOWN_KEY];
179
+ return {
180
+ txHash: typeof txHash === 'string' ? txHash : undefined,
181
+ confirmationUnknown: typeof confirmationUnknown === 'boolean'
182
+ ? confirmationUnknown
183
+ : undefined,
184
+ };
185
+ }
186
+ /**
187
+ * Build the broadcast half of a failed write's structured error context, for a
188
+ * path that holds its main transaction in a local variable.
189
+ *
190
+ * The distinction this encodes, and which consumers branch on:
191
+ *
192
+ * - **no `txHash` key at all** — the write never reached the network. Nothing
193
+ * is pending; it is safe to tell the user it failed and safe to retry.
194
+ * - **`txHash` + `broadcast: true` + `confirmationUnknown: true`** — the
195
+ * transaction *is* on-chain (or in the mempool) and only the confirmation was
196
+ * lost. Render a pending state against `txHash`; **do not** prompt a retry,
197
+ * that is exactly how users double-spend a redeem.
198
+ * - **`txHash` + `broadcast: true` + `confirmationUnknown: false`** — the
199
+ * transaction reached the chain and definitively failed (reverted). A real
200
+ * error, with a hash the user can inspect on an explorer.
201
+ *
202
+ * `tx` is the gate on purpose. Several write paths broadcast an inner approval
203
+ * before the main call; if that approval's wait fails, the caught error carries
204
+ * *its* marker. Falling back to the error's marker here would make
205
+ * "Request redeem failed" report the approve hash and claim the redeem is
206
+ * pending when it was never sent.
207
+ *
208
+ * @param tx - The path's main transaction, or `undefined`/`null` when it was
209
+ * never sent.
210
+ * @param error - The caught value, used only for the confirmation state.
211
+ * @returns A context fragment to spread into the thrown error's `context`.
212
+ * Empty when nothing was broadcast.
213
+ * @internal
214
+ */
215
+ function localTxBroadcastContext(tx, error) {
216
+ const txHash = tx?.hash;
217
+ if (!txHash)
218
+ return {};
219
+ const markers = readBroadcastMarkers(error);
220
+ return {
221
+ txHash,
222
+ broadcast: true,
223
+ // Only trust the marker when it describes *this* transaction.
224
+ confirmationUnknown: markers.txHash === txHash && markers.confirmationUnknown === true,
225
+ };
226
+ }
227
+ /**
228
+ * Same contract as {@link localTxBroadcastContext}, for write paths whose only
229
+ * transaction is sent through {@link safeSendTx} — there the hash never comes
230
+ * back to the caller on the failure path, so the error's own marker is the only
231
+ * source. Safe precisely because those paths send exactly one transaction, so
232
+ * the marker cannot belong to a different one.
233
+ *
234
+ * @param error - The caught value, of unknown type.
235
+ * @returns A context fragment to spread into the thrown error's `context`.
236
+ * Empty when nothing was broadcast.
237
+ * @internal
238
+ */
239
+ function errorTxBroadcastContext(error) {
240
+ const { txHash, confirmationUnknown } = readBroadcastMarkers(error);
241
+ if (!txHash)
242
+ return {};
243
+ return {
244
+ txHash,
245
+ broadcast: true,
246
+ confirmationUnknown: confirmationUnknown === true,
247
+ };
248
+ }
249
+ /**
250
+ * Await a broadcast transaction's receipt, surviving both malformed-nonce RPCs
251
+ * and transient transport faults while still reporting genuine on-chain
252
+ * failures.
253
+ *
254
+ * `tx.wait()` is tried first because it preserves ethers' replacement-tx
255
+ * detection and revert checks. Two recoverable failure modes are handled:
256
+ *
257
+ * 1. **Malformed nonce** ({@link isNonceParsing}) — Monad-style RPCs return
258
+ * `nonce: "undefined"` on the pending tx and ethers throws while parsing.
259
+ * One direct `provider.waitForTransaction()` recovers it (unchanged
260
+ * behaviour).
261
+ * 2. **Transient transport fault** ({@link isRetryableRpcError}) — the provider
262
+ * hiccups while polling `eth_getTransactionReceipt` and ethers surfaces
263
+ * `could not coalesce error (… "code": -32603 …)`. This used to propagate
264
+ * and make every write path report a **successfully mined** transaction as
265
+ * failed, driving users to retry into `ERC20InsufficientBalance`. The hash
266
+ * is already on the wire and receipt polling is idempotent, so we re-poll
267
+ * with bounded exponential backoff instead of rethrowing.
268
+ *
269
+ * Everything else — notably a real revert — is rethrown untouched.
270
+ *
271
+ * Every error this function throws is stamped by {@link markBroadcast}: by the
272
+ * time `tx.wait()` can fail, the transaction is already on the wire, so the
273
+ * caller must never report it as "never sent". See
274
+ * {@link localTxBroadcastContext} for how that reaches
275
+ * `AugustSDKError.context`.
276
+ *
277
+ * @param tx - The broadcast transaction response to await.
278
+ * @returns The mined receipt (never `null` on the fallback paths; `tx.wait()`
279
+ * itself may return `null` when the caller configured 0 confirmations).
280
+ * @throws {Error} `Transaction <hash> reverted on-chain` when the receipt
281
+ * reports `status === 0`. Marked `confirmationUnknown: false` — the chain
282
+ * answered.
283
+ * @throws {Error} `Transaction <hash> was not confirmed within <n>s` when the
284
+ * overall wait times out with no receipt. Marked
285
+ * `confirmationUnknown: true`.
286
+ * @throws The underlying transport error once receipt-poll retries are
287
+ * exhausted, marked `confirmationUnknown: true`.
288
+ * @internal
289
+ */
101
290
  async function safeWaitForTx(tx) {
102
291
  try {
103
292
  return await tx.wait();
104
293
  }
105
294
  catch (error) {
106
- if (isNonceParsing(error)) {
107
- const msg = error instanceof Error ? error.message : '';
108
- core_1.Logger.log.warn('safeWaitForTx:fallback', msg, { hash: tx.hash });
109
- const receipt = await tx.provider.waitForTransaction(tx.hash, 1, SAFE_WAIT_TIMEOUT_MS);
110
- if (!receipt) {
111
- throw new Error(`Transaction ${tx.hash} was not confirmed within ${SAFE_WAIT_TIMEOUT_MS / 1000}s`);
112
- }
113
- if (receipt.status === 0) {
114
- throw new Error(`Transaction ${tx.hash} reverted on-chain`);
115
- }
116
- return receipt;
295
+ const nonceParse = isNonceParsing(error);
296
+ // Not a shape we know how to recover from — surface it unchanged, but the
297
+ // tx is still on-chain (ethers reports a revert here), so keep the hash.
298
+ if (!nonceParse && !(0, core_1.isRetryableRpcError)(error)) {
299
+ throw markBroadcast(error, tx.hash, false);
300
+ }
301
+ const msg = error instanceof Error ? error.message : '';
302
+ core_1.Logger.log.warn(nonceParse
303
+ ? 'safeWaitForTx:fallback'
304
+ : 'safeWaitForTx:transport-fallback', msg, { hash: tx.hash });
305
+ // The nonce path keeps its original single-shot behaviour: the RPC is
306
+ // responsive, it just mis-serialized the pending tx. The transport path
307
+ // re-polls, because the provider itself is the thing that failed.
308
+ let receipt;
309
+ try {
310
+ receipt = nonceParse
311
+ ? await tx.provider.waitForTransaction(tx.hash, 1, SAFE_WAIT_TIMEOUT_MS)
312
+ : await (0, core_1.retryOnTransientRpc)('safeWaitForTx:transport-retry', () => tx.provider.waitForTransaction(tx.hash, 1, SAFE_WAIT_TIMEOUT_MS), { hash: tx.hash });
313
+ }
314
+ catch (pollError) {
315
+ // Retries exhausted. The transaction is on the wire and very likely
316
+ // mined; we simply could not read its receipt.
317
+ throw markBroadcast(pollError, tx.hash, true);
318
+ }
319
+ if (!receipt) {
320
+ throw markBroadcast(new Error(`Transaction ${tx.hash} was not confirmed within ${SAFE_WAIT_TIMEOUT_MS / 1000}s`), tx.hash, true);
321
+ }
322
+ if (receipt.status === 0) {
323
+ // Definitive: the chain executed it and it failed. Not pending.
324
+ throw markBroadcast(new Error(`Transaction ${tx.hash} reverted on-chain`), tx.hash, false);
117
325
  }
118
- throw error;
326
+ return receipt;
119
327
  }
120
328
  }
121
329
  /**
@@ -148,10 +356,10 @@ async function safeSendTx(contractCall, provider, shouldWait) {
148
356
  const signerProvider = 'provider' in provider ? provider.provider : provider;
149
357
  const receipt = await signerProvider.waitForTransaction(txHash, 1, SAFE_WAIT_TIMEOUT_MS);
150
358
  if (!receipt) {
151
- throw new Error(`Transaction ${txHash} was not confirmed within ${SAFE_WAIT_TIMEOUT_MS / 1000}s`);
359
+ throw markBroadcast(new Error(`Transaction ${txHash} was not confirmed within ${SAFE_WAIT_TIMEOUT_MS / 1000}s`), txHash, true);
152
360
  }
153
361
  if (receipt.status === 0) {
154
- throw new Error(`Transaction ${txHash} reverted on-chain`);
362
+ throw markBroadcast(new Error(`Transaction ${txHash} reverted on-chain`), txHash, false);
155
363
  }
156
364
  }
157
365
  return { tx: null, hash: txHash };
@@ -177,10 +385,10 @@ async function tryRecoverTxHash(error, signer, wait) {
177
385
  const provider = 'provider' in signer ? signer.provider : signer;
178
386
  const receipt = await provider.waitForTransaction(txHash, 1, SAFE_WAIT_TIMEOUT_MS);
179
387
  if (!receipt) {
180
- throw new Error(`Transaction ${txHash} was not confirmed within ${SAFE_WAIT_TIMEOUT_MS / 1000}s`);
388
+ throw markBroadcast(new Error(`Transaction ${txHash} was not confirmed within ${SAFE_WAIT_TIMEOUT_MS / 1000}s`), txHash, true);
181
389
  }
182
390
  if (receipt.status === 0) {
183
- throw new Error(`Transaction ${txHash} reverted on-chain`);
391
+ throw markBroadcast(new Error(`Transaction ${txHash} reverted on-chain`), txHash, false);
184
392
  }
185
393
  }
186
394
  return txHash;
@@ -252,23 +460,20 @@ async function approveCore(signer, options) {
252
460
  if (isMultiAssetVault) {
253
461
  const [asset, receiptTokenAddr] = await Promise.all([
254
462
  poolContract.asset(),
255
- poolContract.lpTokenAddress(),
463
+ // Retried on transport blips only; a misroute still throws. See
464
+ // getReceiptTokenAddressOrThrow.
465
+ (0, core_1.getReceiptTokenAddressOrThrow)(signer, target, 'vaultApprove:receiptToken'),
256
466
  ]);
257
467
  underlyingAsset = asset;
258
- const receiptContract = (0, core_1.createContract)({
259
- address: receiptTokenAddr,
260
- provider: signer,
261
- abi: abis_1.ABI_ERC20,
262
- });
263
- vaultDecimals = Number(await receiptContract.decimals());
468
+ vaultDecimals = await (0, core_1.getDecimalsOrThrow)(signer, receiptTokenAddr, 'vaultApprove:receiptDecimals');
264
469
  }
265
470
  else {
266
471
  const [asset, rawDecimals] = await Promise.all([
267
472
  poolContract.asset(),
268
- poolContract.decimals(),
473
+ (0, core_1.getDecimalsOrThrow)(signer, target, 'vaultApprove:poolDecimals'),
269
474
  ]);
270
475
  underlyingAsset = asset;
271
- vaultDecimals = Number(rawDecimals);
476
+ vaultDecimals = rawDecimals;
272
477
  }
273
478
  const actualDepositAsset = (depositAsset || underlyingAsset);
274
479
  const isNativeToken = actualDepositAsset === ethers_1.ZeroAddress ||
@@ -289,14 +494,7 @@ async function approveCore(signer, options) {
289
494
  isNativeToken: false,
290
495
  isMultiAssetVault,
291
496
  vaultDecimals,
292
- readErc20Decimals: async (addr) => {
293
- const erc20 = (0, core_1.createContract)({
294
- address: addr,
295
- provider: signer,
296
- abi: abis_1.ABI_ERC20,
297
- });
298
- return Number(await erc20.decimals());
299
- },
497
+ readErc20Decimals: async (addr) => (0, core_1.getDecimalsOrThrow)(signer, addr, 'vaultApprove:depositTokenDecimals'),
300
498
  });
301
499
  const normalizedAmt = (0, core_1.toNormalizedBn)(amount, depositTokenDecimals);
302
500
  const needed = BigInt(normalizedAmt.raw);
@@ -324,18 +522,23 @@ async function approveCore(signer, options) {
324
522
  catch (e) {
325
523
  if (e instanceof core_1.AugustSDKError)
326
524
  throw e;
525
+ // Only one tx is sent on this path, so any broadcast marker on the error
526
+ // necessarily describes the approval itself.
527
+ const broadcast = errorTxBroadcastContext(e);
327
528
  // A user declining the approval in their wallet is product behaviour, not
328
529
  // an SDK fault — demote it to a breadcrumb so it doesn't bill as an issue.
329
530
  const insufficientFunds = (0, core_1.isInsufficientFundsError)(e);
330
531
  (0, core_1.logChainError)('vaultApprove', e, (0, core_1.isUserRejectionError)(e) || insufficientFunds, {
331
532
  target,
332
533
  amount,
534
+ ...broadcast,
333
535
  });
334
536
  throwIfInsufficientFunds(insufficientFunds, 'Approval', e, {
335
537
  target,
336
538
  amount,
539
+ ...broadcast,
337
540
  });
338
- throw new core_1.AugustSDKError('UNKNOWN', `Approval failed: ${e instanceof Error ? e.message : 'Unknown error'}`, { cause: e, context: { target, amount } });
541
+ throw new core_1.AugustSDKError('UNKNOWN', `Approval failed: ${e instanceof Error ? e.message : 'Unknown error'}`, { cause: e, context: { target, amount, ...broadcast } });
339
542
  }
340
543
  }
341
544
  /**
@@ -457,6 +660,10 @@ async function vaultDeposit(signer, options) {
457
660
  throw new core_1.AugustValidationError('INVALID_INPUT', 'vaultDeposit: amount is required');
458
661
  }
459
662
  validateAmountPrecision(amount);
663
+ // Hoisted out of the try so the catch can tell "the deposit was broadcast and
664
+ // we lost the confirmation" from "the deposit never left". Any inner approval
665
+ // tx is deliberately NOT tracked here — see localTxBroadcastContext.
666
+ let depositTx;
460
667
  try {
461
668
  const tokenizedVault = (await (0, core_1.fetchTokenizedVault)(target))?.[0];
462
669
  const vaultVersion = (0, core_1.getVaultVersionV2)(tokenizedVault);
@@ -474,15 +681,12 @@ async function vaultDeposit(signer, options) {
474
681
  });
475
682
  const [asset, receiptTokenAddr] = await Promise.all([
476
683
  poolContract.asset(),
477
- poolContract.lpTokenAddress(),
684
+ // Retried on transport blips only; a misroute still throws. See
685
+ // getReceiptTokenAddressOrThrow.
686
+ (0, core_1.getReceiptTokenAddressOrThrow)(signer, target, 'vaultDeposit:receiptToken'),
478
687
  ]);
479
688
  underlyingAsset = asset;
480
- const receiptContract = (0, core_1.createContract)({
481
- address: receiptTokenAddr,
482
- provider: signer,
483
- abi: abis_1.ABI_ERC20,
484
- });
485
- vaultDecimals = Number(await receiptContract.decimals());
689
+ vaultDecimals = await (0, core_1.getDecimalsOrThrow)(signer, receiptTokenAddr, 'vaultDeposit:receiptDecimals');
486
690
  }
487
691
  else {
488
692
  poolContract = (0, core_1.createContract)({
@@ -492,9 +696,9 @@ async function vaultDeposit(signer, options) {
492
696
  });
493
697
  const [fetchedUnderlyingAsset, rawDecimals] = await Promise.all([
494
698
  poolContract.asset(),
495
- poolContract.decimals(),
699
+ (0, core_1.getDecimalsOrThrow)(signer, target, 'vaultDeposit:poolDecimals'),
496
700
  ]);
497
- vaultDecimals = Number(rawDecimals);
701
+ vaultDecimals = rawDecimals;
498
702
  underlyingAsset = fetchedUnderlyingAsset;
499
703
  }
500
704
  const actualDepositAsset = depositAsset || underlyingAsset;
@@ -515,14 +719,7 @@ async function vaultDeposit(signer, options) {
515
719
  isNativeToken,
516
720
  isMultiAssetVault,
517
721
  vaultDecimals,
518
- readErc20Decimals: async (addr) => {
519
- const erc20 = (0, core_1.createContract)({
520
- address: addr,
521
- provider: signer,
522
- abi: abis_1.ABI_ERC20,
523
- });
524
- return Number(await erc20.decimals());
525
- },
722
+ readErc20Decimals: async (addr) => (0, core_1.getDecimalsOrThrow)(signer, addr, 'vaultDeposit:depositTokenDecimals'),
526
723
  });
527
724
  const normalizedAmt = (0, core_1.toNormalizedBn)(amount, depositTokenDecimals);
528
725
  if (!isNativeToken) {
@@ -546,7 +743,6 @@ async function vaultDeposit(signer, options) {
546
743
  core_1.Logger.log.info('approve:tx_hash', approveHash);
547
744
  }
548
745
  }
549
- let depositTx;
550
746
  // Route to appropriate deposit method
551
747
  if (isMultiAssetVault) {
552
748
  if (options?.isDepositWithPermit) {
@@ -638,19 +834,29 @@ async function vaultDeposit(signer, options) {
638
834
  }
639
835
  if (e instanceof core_1.AugustSDKError)
640
836
  throw e;
837
+ // depositTx is only ever unset here when the depositWithPermit sub-path
838
+ // threw — that branch sends its single main tx through safeSendTx and
839
+ // never assigns depositTx, so the error's own markBroadcast marker is the
840
+ // only place the broadcast hash survives. Every other sub-path assigns
841
+ // depositTx before this catch can run.
842
+ const broadcast = depositTx
843
+ ? localTxBroadcastContext(depositTx, e)
844
+ : errorTxBroadcastContext(e);
641
845
  // User-cancelled deposits are normal; only genuine failures stay at error.
642
846
  const insufficientFunds = (0, core_1.isInsufficientFundsError)(e);
643
847
  (0, core_1.logChainError)('deposit', e, (0, core_1.isUserRejectionError)(e) || insufficientFunds, {
644
848
  target,
645
849
  amount,
646
850
  depositAsset,
851
+ ...broadcast,
647
852
  });
648
853
  throwIfInsufficientFunds(insufficientFunds, 'Deposit', e, {
649
854
  target,
650
855
  amount,
651
856
  depositAsset,
857
+ ...broadcast,
652
858
  });
653
- throw new core_1.AugustSDKError('UNKNOWN', `Deposit failed: ${e instanceof Error ? e.message : 'Unknown error'}`, { cause: e, context: { target, amount, depositAsset } });
859
+ throw new core_1.AugustSDKError('UNKNOWN', `Deposit failed: ${e instanceof Error ? e.message : 'Unknown error'}`, { cause: e, context: { target, amount, depositAsset, ...broadcast } });
654
860
  }
655
861
  }
656
862
  /**
@@ -702,6 +908,11 @@ async function vaultRequestRedeem(signer, options) {
702
908
  throw new core_1.AugustValidationError('INVALID_INPUT', 'vaultRequestRedeem: amount is required');
703
909
  }
704
910
  validateAmountPrecision(amount);
911
+ // Hoisted out of the try so the catch can tell "the redeem was broadcast and
912
+ // we lost the confirmation" from "the redeem never left". The EVM-2 receipt
913
+ // token approval sent earlier in the try is deliberately NOT tracked here:
914
+ // a failed approval means the redeem itself was never sent.
915
+ let requestRedeemTx;
705
916
  try {
706
917
  // Get vault version to determine correct ABI
707
918
  const tokenizedVault = (await (0, core_1.fetchTokenizedVault)(target))?.[0];
@@ -717,14 +928,16 @@ async function vaultRequestRedeem(signer, options) {
717
928
  provider: signer,
718
929
  abi: abis_1.ABI_TOKENIZED_VAULT_V2,
719
930
  });
720
- // Get receipt token for decimals
721
- const receiptTokenAddr = (await vaultContract.lpTokenAddress());
931
+ // Get receipt token for decimals. Retried on transport blips only; a
932
+ // misroute (a non-evm-2 vault reaching this branch) still throws the
933
+ // original error. See getReceiptTokenAddressOrThrow.
934
+ const receiptTokenAddr = await (0, core_1.getReceiptTokenAddressOrThrow)(signer, target, 'vaultRequestRedeem:receiptToken');
722
935
  const receiptContract = (0, core_1.createContract)({
723
936
  address: receiptTokenAddr,
724
937
  provider: signer,
725
938
  abi: abis_1.ABI_ERC20,
726
939
  });
727
- decimals = Number(await receiptContract.decimals());
940
+ decimals = await (0, core_1.getDecimalsOrThrow)(signer, receiptTokenAddr, 'vaultRequestRedeem:receiptDecimals');
728
941
  // Convert amount to bignumber
729
942
  const normalizedAmt = (0, core_1.toNormalizedBn)(amount, decimals);
730
943
  // EVM-2 lp/receipt token is a separate ERC-20: vault.transferFrom(lp)
@@ -752,12 +965,11 @@ async function vaultRequestRedeem(signer, options) {
752
965
  provider: signer,
753
966
  abi: abis_1.ABI_LENDING_POOLS,
754
967
  });
755
- decimals = await poolContract.decimals();
968
+ decimals = await (0, core_1.getDecimalsOrThrow)(signer, target, 'vaultRequestRedeem:poolDecimals');
756
969
  }
757
970
  // Convert amount to bignumber (for non-evm-2 vaults)
758
971
  const normalizedAmt = (0, core_1.toNormalizedBn)(amount, decimals);
759
972
  // Withdraw from vault - EVM-2 uses 2 params, others use 3 params
760
- let requestRedeemTx;
761
973
  if (options.isInstantRedeem) {
762
974
  if (vaultVersion === 'evm-2') {
763
975
  // EVM-2: instantRedeem(uint256 shares, address receiverAddr) - 2 params
@@ -797,17 +1009,20 @@ async function vaultRequestRedeem(signer, options) {
797
1009
  }
798
1010
  if (e instanceof core_1.AugustSDKError)
799
1011
  throw e;
1012
+ const broadcast = localTxBroadcastContext(requestRedeemTx, e);
800
1013
  // User-cancelled redeem requests are normal; only genuine failures stay at error.
801
1014
  const insufficientFunds = (0, core_1.isInsufficientFundsError)(e);
802
1015
  (0, core_1.logChainError)('requestRedeem', e, (0, core_1.isUserRejectionError)(e) || insufficientFunds, {
803
1016
  target,
804
1017
  amount,
1018
+ ...broadcast,
805
1019
  });
806
1020
  throwIfInsufficientFunds(insufficientFunds, 'Request redeem', e, {
807
1021
  target,
808
1022
  amount,
1023
+ ...broadcast,
809
1024
  });
810
- throw new core_1.AugustSDKError('UNKNOWN', `Request redeem failed: ${e instanceof Error ? e.message : 'Unknown error'}`, { cause: e, context: { target, amount } });
1025
+ throw new core_1.AugustSDKError('UNKNOWN', `Request redeem failed: ${e instanceof Error ? e.message : 'Unknown error'}`, { cause: e, context: { target, amount, ...broadcast } });
811
1026
  }
812
1027
  }
813
1028
  /**
@@ -849,6 +1064,8 @@ async function vaultRedeem(signer, options) {
849
1064
  }
850
1065
  if (e instanceof core_1.AugustSDKError)
851
1066
  throw e;
1067
+ // Single tx on this path, so the error's own marker can only be the redeem.
1068
+ const broadcast = errorTxBroadcastContext(e);
852
1069
  // User-cancelled redeems are normal; only genuine failures stay at error.
853
1070
  const insufficientFunds = (0, core_1.isInsufficientFundsError)(e);
854
1071
  (0, core_1.logChainError)('redeem', e, (0, core_1.isUserRejectionError)(e) || insufficientFunds, {
@@ -857,6 +1074,7 @@ async function vaultRedeem(signer, options) {
857
1074
  month,
858
1075
  day,
859
1076
  receiverIndex,
1077
+ ...broadcast,
860
1078
  });
861
1079
  throwIfInsufficientFunds(insufficientFunds, 'Redeem', e, {
862
1080
  target,
@@ -864,8 +1082,12 @@ async function vaultRedeem(signer, options) {
864
1082
  month,
865
1083
  day,
866
1084
  receiverIndex,
1085
+ ...broadcast,
1086
+ });
1087
+ throw new core_1.AugustSDKError('UNKNOWN', `Redeem failed: ${e instanceof Error ? e.message : 'Unknown error'}`, {
1088
+ cause: e,
1089
+ context: { target, year, month, day, receiverIndex, ...broadcast },
867
1090
  });
868
- throw new core_1.AugustSDKError('UNKNOWN', `Redeem failed: ${e instanceof Error ? e.message : 'Unknown error'}`, { cause: e, context: { target, year, month, day, receiverIndex } });
869
1091
  }
870
1092
  }
871
1093
  /**
@@ -906,6 +1128,9 @@ async function depositNative(signer, options) {
906
1128
  throw new core_1.AugustValidationError('INVALID_INPUT', 'depositNative: amount is required');
907
1129
  }
908
1130
  validateAmountPrecision(amount);
1131
+ // Hoisted so the catch can distinguish "broadcast, confirmation lost" from
1132
+ // "never sent" — see localTxBroadcastContext.
1133
+ let depositTx;
909
1134
  try {
910
1135
  // Create wrapper contract instance
911
1136
  const wrapperContract = (0, core_1.createContract)({
@@ -920,7 +1145,6 @@ async function depositNative(signer, options) {
920
1145
  : typeof amount === 'string'
921
1146
  ? BigInt(amount)
922
1147
  : BigInt(amount);
923
- let depositTx;
924
1148
  // Connect the contract first, then get the function to disambiguate between overloads
925
1149
  const connectedContract = wrapperContract.connect(signer);
926
1150
  // Call depositNative with or without receiver parameter
@@ -955,19 +1179,22 @@ async function depositNative(signer, options) {
955
1179
  }
956
1180
  if (e instanceof core_1.AugustSDKError)
957
1181
  throw e;
1182
+ const broadcast = localTxBroadcastContext(depositTx, e);
958
1183
  // User-cancelled native deposits are normal; only genuine failures stay at error.
959
1184
  const insufficientFunds = (0, core_1.isInsufficientFundsError)(e);
960
1185
  (0, core_1.logChainError)('depositNative', e, (0, core_1.isUserRejectionError)(e) || insufficientFunds, {
961
1186
  wrapperAddress,
962
1187
  receiver,
963
1188
  amount,
1189
+ ...broadcast,
964
1190
  });
965
1191
  throwIfInsufficientFunds(insufficientFunds, 'Deposit native', e, {
966
1192
  wrapperAddress,
967
1193
  receiver,
968
1194
  amount,
1195
+ ...broadcast,
969
1196
  });
970
- throw new core_1.AugustSDKError('UNKNOWN', `Deposit native failed: ${e instanceof Error ? e.message : 'Unknown error'}`, { cause: e, context: { wrapperAddress, receiver, amount } });
1197
+ throw new core_1.AugustSDKError('UNKNOWN', `Deposit native failed: ${e instanceof Error ? e.message : 'Unknown error'}`, { cause: e, context: { wrapperAddress, receiver, amount, ...broadcast } });
971
1198
  }
972
1199
  }
973
1200
  /**
@@ -1040,6 +1267,8 @@ async function rwaRedeemAsset(signer, options) {
1040
1267
  }
1041
1268
  if (e instanceof core_1.AugustSDKError)
1042
1269
  throw e;
1270
+ // Single tx on this path, so the error's own marker can only be the redeem.
1271
+ const broadcast = errorTxBroadcastContext(e);
1043
1272
  // User-cancelled RWA redeems are normal; only genuine failures stay at error.
1044
1273
  const insufficientFunds = (0, core_1.isInsufficientFundsError)(e);
1045
1274
  (0, core_1.logChainError)('rwaRedeemAsset', e, (0, core_1.isUserRejectionError)(e) || insufficientFunds, {
@@ -1047,14 +1276,16 @@ async function rwaRedeemAsset(signer, options) {
1047
1276
  asset,
1048
1277
  amount,
1049
1278
  minOut,
1279
+ ...broadcast,
1050
1280
  });
1051
1281
  throwIfInsufficientFunds(insufficientFunds, 'RWA redeem', e, {
1052
1282
  target,
1053
1283
  asset,
1054
1284
  amount,
1055
1285
  minOut,
1286
+ ...broadcast,
1056
1287
  });
1057
- throw new core_1.AugustSDKError('UNKNOWN', `RWA redeem failed: ${e instanceof Error ? e.message : 'Unknown error'}`, { cause: e, context: { target, asset, amount, minOut } });
1288
+ throw new core_1.AugustSDKError('UNKNOWN', `RWA redeem failed: ${e instanceof Error ? e.message : 'Unknown error'}`, { cause: e, context: { target, asset, amount, minOut, ...broadcast } });
1058
1289
  }
1059
1290
  }
1060
1291
  /** @internal */
@@ -1099,12 +1330,7 @@ async function dispatchViaSwapRouter(args) {
1099
1330
  // (e.g. an 18-decimal share token over an 8-decimal WBTC underlying on
1100
1331
  // a multi-asset v2 vault). The swap quote must use the underlying's
1101
1332
  // on-chain decimals so Paraswap's /prices interprets the route correctly.
1102
- const underlyingErc20 = (0, core_1.createContract)({
1103
- address: args.underlyingAsset,
1104
- provider: args.signer,
1105
- abi: abis_1.ABI_ERC20,
1106
- });
1107
- const underlyingDecimals = Number(await underlyingErc20.decimals());
1333
+ const underlyingDecimals = await (0, core_1.getDecimalsOrThrow)(args.signer, args.underlyingAsset, 'dispatchViaSwapRouter:underlyingDecimals');
1108
1334
  // The on-chain SwapRouter only authorizes a specific (router, selector) pair
1109
1335
  // per chain (see SWAP_ROUTER_DEX_AGGREGATOR). Pin the aggregator to that
1110
1336
  // router's single generic method so the calldata selector is deterministic,
@@ -1470,15 +1696,12 @@ async function swapRouterDeposit(signer, options) {
1470
1696
  });
1471
1697
  const [asset, receiptTokenAddr] = await Promise.all([
1472
1698
  poolContract.asset(),
1473
- poolContract.lpTokenAddress(),
1699
+ // Retried on transport blips only; a misroute still throws. See
1700
+ // getReceiptTokenAddressOrThrow.
1701
+ (0, core_1.getReceiptTokenAddressOrThrow)(signer, vault, 'swapRouterDeposit:receiptToken'),
1474
1702
  ]);
1475
1703
  underlyingAsset = asset;
1476
- const receiptContract = (0, core_1.createContract)({
1477
- address: receiptTokenAddr,
1478
- provider: signer,
1479
- abi: abis_1.ABI_ERC20,
1480
- });
1481
- vaultDecimals = Number(await receiptContract.decimals());
1704
+ vaultDecimals = await (0, core_1.getDecimalsOrThrow)(signer, receiptTokenAddr, 'swapRouterDeposit:receiptDecimals');
1482
1705
  }
1483
1706
  else {
1484
1707
  const poolContract = (0, core_1.createContract)({
@@ -1488,10 +1711,10 @@ async function swapRouterDeposit(signer, options) {
1488
1711
  });
1489
1712
  const [asset, rawDecimals] = await Promise.all([
1490
1713
  poolContract.asset(),
1491
- poolContract.decimals(),
1714
+ (0, core_1.getDecimalsOrThrow)(signer, vault, 'swapRouterDeposit:poolDecimals'),
1492
1715
  ]);
1493
1716
  underlyingAsset = asset;
1494
- vaultDecimals = Number(rawDecimals);
1717
+ vaultDecimals = rawDecimals;
1495
1718
  }
1496
1719
  const isNativeToken = depositAsset === ethers_1.ZeroAddress ||
1497
1720
  depositAsset === '0x0000000000000000000000000000000000000000';
@@ -1501,14 +1724,7 @@ async function swapRouterDeposit(signer, options) {
1501
1724
  isNativeToken,
1502
1725
  isMultiAssetVault,
1503
1726
  vaultDecimals,
1504
- readErc20Decimals: async (addr) => {
1505
- const erc20 = (0, core_1.createContract)({
1506
- address: addr,
1507
- provider: signer,
1508
- abi: abis_1.ABI_ERC20,
1509
- });
1510
- return Number(await erc20.decimals());
1511
- },
1727
+ readErc20Decimals: async (addr) => (0, core_1.getDecimalsOrThrow)(signer, addr, 'swapRouterDeposit:depositTokenDecimals'),
1512
1728
  });
1513
1729
  const normalizedAmt = (0, core_1.toNormalizedBn)(amount, depositTokenDecimals);
1514
1730
  if (BigInt(normalizedAmt.raw) === 0n) {