@augustdigital/sdk 8.17.0 → 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/write.actions.d.ts +41 -1
- package/lib/modules/vaults/write.actions.js +302 -86
- package/lib/sdk.d.ts +438 -3
- package/lib/services/subgraph/vaults.js +85 -14
- package/package.json +1 -1
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.LP_TOKEN_ADDRESS_SELECTOR = void 0;
|
|
3
4
|
exports.isUserRejectionError = isUserRejectionError;
|
|
4
5
|
exports.isExpectedRevertError = isExpectedRevertError;
|
|
5
6
|
exports.isInsufficientFundsError = isInsufficientFundsError;
|
|
7
|
+
exports.isRetryableRpcError = isRetryableRpcError;
|
|
8
|
+
exports.isEmptyViewResponse = isEmptyViewResponse;
|
|
9
|
+
exports.retryOnTransientRpc = retryOnTransientRpc;
|
|
6
10
|
exports.logChainError = logChainError;
|
|
7
11
|
const logger_1 = require("../logger");
|
|
8
12
|
/**
|
|
@@ -239,6 +243,414 @@ function isInsufficientFundsError(error) {
|
|
|
239
243
|
return INSUFFICIENT_FUNDS_PHRASES.some((phrase) => msg.includes(phrase));
|
|
240
244
|
});
|
|
241
245
|
}
|
|
246
|
+
/**
|
|
247
|
+
* Collect the HTTP status codes an error-like value may carry. Transport
|
|
248
|
+
* failures that reach us through ethers' `FetchRequest` keep the upstream
|
|
249
|
+
* status on the error (`status`), on the wrapped fetch response
|
|
250
|
+
* (`info.status` / `response.status`), or on a `statusCode` alias depending on
|
|
251
|
+
* which layer produced it. We scan all of them so a caller needn't know.
|
|
252
|
+
*
|
|
253
|
+
* @param error - The caught value, of unknown type.
|
|
254
|
+
* @returns Every numeric status found (possibly empty).
|
|
255
|
+
*/
|
|
256
|
+
function errorStatuses(error) {
|
|
257
|
+
const out = [];
|
|
258
|
+
const push = (value) => {
|
|
259
|
+
if (typeof value === 'number' && Number.isFinite(value))
|
|
260
|
+
out.push(value);
|
|
261
|
+
};
|
|
262
|
+
if (error && typeof error === 'object') {
|
|
263
|
+
const e = error;
|
|
264
|
+
push(e.status);
|
|
265
|
+
push(e.statusCode);
|
|
266
|
+
push(e.info?.status);
|
|
267
|
+
push(e.info?.statusCode);
|
|
268
|
+
push(e.response?.status);
|
|
269
|
+
push(e.response?.statusCode);
|
|
270
|
+
}
|
|
271
|
+
return out;
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Does this error carry evidence that the EVM actually executed and reverted?
|
|
275
|
+
*
|
|
276
|
+
* Used as a **veto** by {@link isRetryableRpcError}: nodes reuse the generic
|
|
277
|
+
* JSON-RPC codes (`-32603`, `-32000`) for genuine execution reverts as well as
|
|
278
|
+
* for internal/transport faults, so a positive transport match must never win
|
|
279
|
+
* over a real revert. Evidence of real execution is:
|
|
280
|
+
*
|
|
281
|
+
* - a `CALL_EXCEPTION` that carries non-empty revert `data` (the ABI-encoded
|
|
282
|
+
* custom error / `Error(string)` payload),
|
|
283
|
+
* - an `execution reverted` / `call revert exception` message anywhere in the
|
|
284
|
+
* nested error chain,
|
|
285
|
+
* - an attached receipt with `status === 0` (the tx mined and failed).
|
|
286
|
+
*
|
|
287
|
+
* Note that a bare `CALL_EXCEPTION` with **no** revert data (ethers' `missing
|
|
288
|
+
* revert data`) is deliberately *not* evidence — see {@link isRetryableRpcError}.
|
|
289
|
+
*
|
|
290
|
+
* @param error - The caught value, of unknown type.
|
|
291
|
+
* @returns `true` when the error proves on-chain execution reverted.
|
|
292
|
+
*/
|
|
293
|
+
function hasRevertEvidence(error) {
|
|
294
|
+
if (error && typeof error === 'object') {
|
|
295
|
+
const e = error;
|
|
296
|
+
const codes = errorCodes(error);
|
|
297
|
+
if (codes.includes('CALL_EXCEPTION') &&
|
|
298
|
+
typeof e.data === 'string' &&
|
|
299
|
+
e.data.length > 2) {
|
|
300
|
+
return true;
|
|
301
|
+
}
|
|
302
|
+
if (e.receipt?.status === 0)
|
|
303
|
+
return true;
|
|
304
|
+
}
|
|
305
|
+
return nestedMessages(error).some((raw) => {
|
|
306
|
+
const msg = raw.toLowerCase();
|
|
307
|
+
return (msg.includes('execution reverted') ||
|
|
308
|
+
msg.includes('call revert exception'));
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* JSON-RPC error codes that indicate the *node or its transport* failed, not
|
|
313
|
+
* that the EVM rejected the call. `-32603` is the spec's "Internal error" and
|
|
314
|
+
* `-32000` the de-facto "Server error" both Alchemy and Infura emit for
|
|
315
|
+
* transient upstream faults (including while serving
|
|
316
|
+
* `eth_getTransactionReceipt` for a freshly broadcast tx).
|
|
317
|
+
*/
|
|
318
|
+
const RETRYABLE_RPC_CODES = [
|
|
319
|
+
-32603,
|
|
320
|
+
-32000,
|
|
321
|
+
'NETWORK_ERROR',
|
|
322
|
+
'SERVER_ERROR',
|
|
323
|
+
'TIMEOUT',
|
|
324
|
+
'ETIMEDOUT',
|
|
325
|
+
'ECONNRESET',
|
|
326
|
+
'ECONNREFUSED',
|
|
327
|
+
'ENOTFOUND',
|
|
328
|
+
'EAI_AGAIN',
|
|
329
|
+
];
|
|
330
|
+
/**
|
|
331
|
+
* Message fragments (lower-cased) that identify a transient transport failure.
|
|
332
|
+
*
|
|
333
|
+
* `could not coalesce error` is ethers v6's catch-all when it cannot map a
|
|
334
|
+
* node's JSON-RPC payload onto a typed error — in practice this is what a
|
|
335
|
+
* flaky provider looks like from inside `tx.wait()`. `eth_gettransactionreceipt`
|
|
336
|
+
* is included because a failure *naming that method* is by definition a receipt
|
|
337
|
+
* poll, which is safe to repeat: the transaction is already broadcast and
|
|
338
|
+
* polling is idempotent.
|
|
339
|
+
*
|
|
340
|
+
* `missing revert data` is deliberately **absent**. It is a `CALL_EXCEPTION`,
|
|
341
|
+
* not a transport frame, and {@link isExpectedRevertError} already treats it as
|
|
342
|
+
* a revert — having this predicate disagree would make the two classifiers
|
|
343
|
+
* contradict each other. The narrow subset of `missing revert data` that really
|
|
344
|
+
* is a transport artefact (an empty response to an argument-free view call) has
|
|
345
|
+
* its own predicate: {@link isEmptyViewResponse}.
|
|
346
|
+
*/
|
|
347
|
+
const RETRYABLE_RPC_PHRASES = [
|
|
348
|
+
'could not coalesce error',
|
|
349
|
+
'eth_gettransactionreceipt',
|
|
350
|
+
'timeout',
|
|
351
|
+
'timed out',
|
|
352
|
+
'etimedout',
|
|
353
|
+
'econnreset',
|
|
354
|
+
'econnrefused',
|
|
355
|
+
'enotfound',
|
|
356
|
+
'socket hang up',
|
|
357
|
+
'network error',
|
|
358
|
+
'network request failed',
|
|
359
|
+
'failed to fetch',
|
|
360
|
+
'fetch failed',
|
|
361
|
+
'load failed',
|
|
362
|
+
'connection closed',
|
|
363
|
+
'connection reset',
|
|
364
|
+
'too many requests',
|
|
365
|
+
'rate limit',
|
|
366
|
+
'service unavailable',
|
|
367
|
+
'bad gateway',
|
|
368
|
+
'gateway timeout',
|
|
369
|
+
'internal server error',
|
|
370
|
+
'internal error',
|
|
371
|
+
'server error',
|
|
372
|
+
];
|
|
373
|
+
/**
|
|
374
|
+
* Is this error a transient RPC **transport** failure that is safe to retry,
|
|
375
|
+
* rather than a decision the chain made?
|
|
376
|
+
*
|
|
377
|
+
* Why this exists: the SDK's write paths poll `eth_getTransactionReceipt` to
|
|
378
|
+
* confirm a broadcast transaction. When the provider hiccups mid-poll, ethers
|
|
379
|
+
* surfaces `could not coalesce error (error={ "code": -32603, … "method":
|
|
380
|
+
* "eth_getTransactionReceipt" … })`. Historically that propagated out of
|
|
381
|
+
* `safeWaitForTx` and the SDK reported the write as **failed** — even though
|
|
382
|
+
* the transaction was broadcast, its hash was known, and it mined fine. Users
|
|
383
|
+
* then retried and hit `ERC20InsufficientBalance` because the first attempt had
|
|
384
|
+
* in fact succeeded. Classifying the failure as transport-level lets callers
|
|
385
|
+
* re-poll instead of lying to the user.
|
|
386
|
+
*
|
|
387
|
+
* Matches, in order of precedence:
|
|
388
|
+
* 1. **Veto** — anything with revert evidence ({@link hasRevertEvidence}:
|
|
389
|
+
* `CALL_EXCEPTION` carrying revert `data`, an `execution reverted` message,
|
|
390
|
+
* or an attached `receipt.status === 0`) returns `false`. Nodes reuse
|
|
391
|
+
* `-32603`/`-32000` for real reverts, so the veto must come first.
|
|
392
|
+
* 2. JSON-RPC / ethers transport codes — see `RETRYABLE_RPC_CODES`.
|
|
393
|
+
* 3. HTTP `429` and any `5xx` carried on the error.
|
|
394
|
+
* 4. Transport message fragments — see `RETRYABLE_RPC_PHRASES`.
|
|
395
|
+
*
|
|
396
|
+
* Retrying is only safe for **idempotent** work: re-reading an immutable value
|
|
397
|
+
* (`decimals()`) or re-polling a receipt for a hash that is already on the
|
|
398
|
+
* wire. Never use this to re-send a transaction.
|
|
399
|
+
*
|
|
400
|
+
* @param error - The caught value, of unknown type.
|
|
401
|
+
* @returns `true` when the failure is a transient transport fault worth
|
|
402
|
+
* retrying with backoff; `false` for chain-level decisions (reverts) and for
|
|
403
|
+
* anything unrecognised — the safe default is to surface the error.
|
|
404
|
+
*
|
|
405
|
+
* @example
|
|
406
|
+
* ```ts
|
|
407
|
+
* try {
|
|
408
|
+
* return await provider.waitForTransaction(hash, 1, 120_000);
|
|
409
|
+
* } catch (e) {
|
|
410
|
+
* if (!isRetryableRpcError(e)) throw e; // real revert — surface it
|
|
411
|
+
* await sleep(250);
|
|
412
|
+
* return await provider.waitForTransaction(hash, 1, 120_000);
|
|
413
|
+
* }
|
|
414
|
+
* ```
|
|
415
|
+
*/
|
|
416
|
+
function isRetryableRpcError(error) {
|
|
417
|
+
if (error === null || error === undefined)
|
|
418
|
+
return false;
|
|
419
|
+
// A chain-level decision is never a transport fault. Check first: nodes
|
|
420
|
+
// reuse -32603/-32000 for genuine execution reverts.
|
|
421
|
+
if (hasRevertEvidence(error))
|
|
422
|
+
return false;
|
|
423
|
+
const codes = errorCodes(error);
|
|
424
|
+
if (codes.some((code) => RETRYABLE_RPC_CODES.includes(code)))
|
|
425
|
+
return true;
|
|
426
|
+
// 429 (rate limited) and any 5xx are upstream faults, not our request being
|
|
427
|
+
// wrong — 4xx other than 429 means retrying would fail identically.
|
|
428
|
+
if (errorStatuses(error).some((status) => status === 429 || (status >= 500 && status < 600))) {
|
|
429
|
+
return true;
|
|
430
|
+
}
|
|
431
|
+
return nestedMessages(error).some((raw) => {
|
|
432
|
+
const msg = raw.toLowerCase();
|
|
433
|
+
return RETRYABLE_RPC_PHRASES.some((phrase) => msg.includes(phrase));
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* `lpTokenAddress()` — `keccak256("lpTokenAddress()")[0..4]`. The August `evm-2`
|
|
438
|
+
* tokenized vault's receipt-token getter, exported so the readers that invoke it
|
|
439
|
+
* can scope {@link isEmptyViewResponse} to exactly this call instead of
|
|
440
|
+
* duplicating the literal.
|
|
441
|
+
*/
|
|
442
|
+
exports.LP_TOKEN_ADDRESS_SELECTOR = '0xf5ae497a';
|
|
443
|
+
/**
|
|
444
|
+
* Four-byte selectors for the argument-free view functions that a correctly
|
|
445
|
+
* addressed contract **cannot** legitimately revert on. Each is
|
|
446
|
+
* `keccak256(signature)[0..4]`.
|
|
447
|
+
*
|
|
448
|
+
* These are the only calls for which an empty RPC response is unambiguously a
|
|
449
|
+
* provider artefact rather than a contract decision — the metadata they return
|
|
450
|
+
* is fixed at deployment and takes no arguments, so there is no input that
|
|
451
|
+
* could make them fail.
|
|
452
|
+
*
|
|
453
|
+
* **Caveat for `lpTokenAddress()`.** The four ERC-20 entries hold that property
|
|
454
|
+
* absolutely: any deployed, conforming token implements them. `lpTokenAddress()`
|
|
455
|
+
* holds it only *given correct routing* — the function exists on `evm-2` vaults
|
|
456
|
+
* and nowhere else, so a vault wrongly routed into the `evm-2` branch returns
|
|
457
|
+
* empty returndata **deterministically**, producing a byte-identical error to a
|
|
458
|
+
* provider blip. That ambiguity is resolved not by this predicate but by the
|
|
459
|
+
* caller: every reader that retries on this selector is bounded (3 attempts) and
|
|
460
|
+
* rethrows the original error once they are spent, so a deterministic misroute
|
|
461
|
+
* still surfaces unchanged — only a transient blip is absorbed. Never pair this
|
|
462
|
+
* selector with an unbounded retry or a fallback value.
|
|
463
|
+
*/
|
|
464
|
+
const ARGUMENT_FREE_VIEW_SELECTORS = new Set([
|
|
465
|
+
'0x313ce567', // decimals()
|
|
466
|
+
'0x95d89b41', // symbol()
|
|
467
|
+
'0x06fdde03', // name()
|
|
468
|
+
'0x18160ddd', // totalSupply()
|
|
469
|
+
exports.LP_TOKEN_ADDRESS_SELECTOR, // lpTokenAddress()
|
|
470
|
+
]);
|
|
471
|
+
/**
|
|
472
|
+
* Extract the 4-byte selector of the call an ethers `CALL_EXCEPTION` describes.
|
|
473
|
+
*
|
|
474
|
+
* ethers v6 attaches the attempted call as `error.transaction.data`, and also
|
|
475
|
+
* embeds it in the human-readable message as `data="0x…"`. We read the
|
|
476
|
+
* structured field first and fall back to the message so the predicate still
|
|
477
|
+
* works on an error that has been serialized and rehydrated (which is how these
|
|
478
|
+
* arrive from a logging pipeline).
|
|
479
|
+
*
|
|
480
|
+
* @param error - The caught value, of unknown type.
|
|
481
|
+
* @returns The lower-cased `0x`-prefixed 4-byte selector, or `null` when the
|
|
482
|
+
* error does not name a call.
|
|
483
|
+
*/
|
|
484
|
+
function callSelector(error) {
|
|
485
|
+
if (error && typeof error === 'object') {
|
|
486
|
+
const e = error;
|
|
487
|
+
const data = e.transaction?.data;
|
|
488
|
+
if (typeof data === 'string' && /^0x[0-9a-fA-F]{8}/.test(data)) {
|
|
489
|
+
return data.slice(0, 10).toLowerCase();
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
// ethers renders the same field two ways depending on nesting depth:
|
|
493
|
+
// `data="0x313ce567"` at the top level and `"data": "0x313ce567"` inside the
|
|
494
|
+
// serialized `transaction={…}` blob. Accept both.
|
|
495
|
+
for (const raw of nestedMessages(error)) {
|
|
496
|
+
const match = raw.match(/data"?\s*[:=]\s*"(0x[0-9a-fA-F]{8})/);
|
|
497
|
+
if (match?.[1])
|
|
498
|
+
return match[1].toLowerCase();
|
|
499
|
+
}
|
|
500
|
+
return null;
|
|
501
|
+
}
|
|
502
|
+
/**
|
|
503
|
+
* Is this error an **empty RPC response to an argument-free view call** —
|
|
504
|
+
* i.e. a transport artefact wearing a revert's clothes?
|
|
505
|
+
*
|
|
506
|
+
* Why this is separate from {@link isRetryableRpcError}: when a provider
|
|
507
|
+
* truncates or 500s a response to `eth_call`, ethers reports
|
|
508
|
+
* `missing revert data (action="call", data="0x313ce567", …)` with a `null`
|
|
509
|
+
* `data` field. That is byte-for-byte the shape of a genuine revert with no
|
|
510
|
+
* reason string, so a general "transport" predicate cannot safely claim it —
|
|
511
|
+
* doing so would retry every data-less `CALL_EXCEPTION` in the SDK. This
|
|
512
|
+
* predicate narrows the claim to the one case where the ambiguity resolves:
|
|
513
|
+
* a **deployed ERC-20's `decimals()`/`symbol()`/`name()`/`totalSupply()` cannot
|
|
514
|
+
* legitimately revert**, because it takes no arguments and returns state fixed
|
|
515
|
+
* at deployment. An empty response there is the provider's fault, full stop.
|
|
516
|
+
*
|
|
517
|
+
* A match requires all of:
|
|
518
|
+
* 1. no revert evidence ({@link hasRevertEvidence}) — anything carrying real
|
|
519
|
+
* revert `data`, an `execution reverted` message, or a failed receipt is out;
|
|
520
|
+
* 2. a `missing revert data` message;
|
|
521
|
+
* 3. an `action` of `call` or `staticCall` — a read, never a state change;
|
|
522
|
+
* 4. **when a selector is derivable** from the error, that it is
|
|
523
|
+
* `expectedSelector` (if given) or one of
|
|
524
|
+
* {@link ARGUMENT_FREE_VIEW_SELECTORS}. When no selector can be recovered,
|
|
525
|
+
* conditions 1–3 stand on their own.
|
|
526
|
+
*
|
|
527
|
+
* Note the cost of a false positive is bounded and small: the caller retries an
|
|
528
|
+
* idempotent read a couple of times before surfacing the same error. The cost
|
|
529
|
+
* of a false negative is the production flood this predicate exists to stop.
|
|
530
|
+
*
|
|
531
|
+
* @param error - The caught value, of unknown type.
|
|
532
|
+
* @param expectedSelector - Optional `0x`-prefixed 4-byte selector the caller
|
|
533
|
+
* knows it invoked (e.g. `'0x313ce567'` for `decimals()`). When supplied, the
|
|
534
|
+
* error's own selector must match it — this stops a `decimals()` retry from
|
|
535
|
+
* firing on an unrelated view call that happened to fail the same way.
|
|
536
|
+
* @returns `true` when the failure is an empty provider response to a view call
|
|
537
|
+
* that cannot revert, and is therefore safe to retry.
|
|
538
|
+
*
|
|
539
|
+
* @example
|
|
540
|
+
* ```ts
|
|
541
|
+
* try { return Number(await erc20.decimals()); }
|
|
542
|
+
* catch (e) {
|
|
543
|
+
* if (!isEmptyViewResponse(e, '0x313ce567')) throw e; // real problem
|
|
544
|
+
* return Number(await erc20.decimals()); // provider blip
|
|
545
|
+
* }
|
|
546
|
+
* ```
|
|
547
|
+
*/
|
|
548
|
+
function isEmptyViewResponse(error, expectedSelector) {
|
|
549
|
+
if (error === null || error === undefined)
|
|
550
|
+
return false;
|
|
551
|
+
if (hasRevertEvidence(error))
|
|
552
|
+
return false;
|
|
553
|
+
const messages = nestedMessages(error).map((raw) => raw.toLowerCase());
|
|
554
|
+
if (!messages.some((msg) => msg.includes('missing revert data')))
|
|
555
|
+
return false;
|
|
556
|
+
// The call must be a read. ethers exposes this as a structured `action`
|
|
557
|
+
// ('call' | 'estimateGas' | 'sendTransaction' | …) and mirrors it in the
|
|
558
|
+
// message as action="call".
|
|
559
|
+
const action = error && typeof error === 'object'
|
|
560
|
+
? error.action
|
|
561
|
+
: undefined;
|
|
562
|
+
const isRead = action === 'call' ||
|
|
563
|
+
action === 'staticCall' ||
|
|
564
|
+
messages.some((msg) => msg.includes('action="call"') || msg.includes('action="staticcall"'));
|
|
565
|
+
if (!isRead)
|
|
566
|
+
return false;
|
|
567
|
+
const selector = callSelector(error);
|
|
568
|
+
// Nothing to check against — conditions 1-3 already establish "empty response
|
|
569
|
+
// to a read", which is the signal we act on.
|
|
570
|
+
if (!selector)
|
|
571
|
+
return true;
|
|
572
|
+
if (expectedSelector)
|
|
573
|
+
return selector === expectedSelector.toLowerCase();
|
|
574
|
+
return ARGUMENT_FREE_VIEW_SELECTORS.has(selector);
|
|
575
|
+
}
|
|
576
|
+
/**
|
|
577
|
+
* How many times an idempotent RPC read is attempted in total (1 initial call +
|
|
578
|
+
* 2 retries) before the transport error is surfaced. Deliberately small:
|
|
579
|
+
* CLAUDE.md §4.2 — retrying harder during a provider outage amplifies load
|
|
580
|
+
* rather than recovering from it.
|
|
581
|
+
*/
|
|
582
|
+
const RPC_RETRY_ATTEMPTS = 3;
|
|
583
|
+
/**
|
|
584
|
+
* Base backoff between retry attempts, in milliseconds. Doubles per attempt
|
|
585
|
+
* (250ms, then 500ms), so a fully-failed read costs ~750ms of added latency.
|
|
586
|
+
*/
|
|
587
|
+
const RPC_RETRY_BASE_DELAY_MS = 250;
|
|
588
|
+
/**
|
|
589
|
+
* Run an **idempotent** RPC read, retrying with exponential backoff while the
|
|
590
|
+
* failure classifies as a transient transport fault
|
|
591
|
+
* ({@link isRetryableRpcError}).
|
|
592
|
+
*
|
|
593
|
+
* Lives next to the classifiers it consumes so there is exactly one retry
|
|
594
|
+
* implementation in the SDK: both the receipt-poll fallback in the vault write
|
|
595
|
+
* paths and the cached `decimals()` reader in `core/helpers/web3.ts` call this.
|
|
596
|
+
*
|
|
597
|
+
* Only safe for operations that can be repeated without side effects: polling
|
|
598
|
+
* `eth_getTransactionReceipt` for an already-broadcast hash, or re-reading an
|
|
599
|
+
* immutable value such as `decimals()`. **Never wrap a transaction send in
|
|
600
|
+
* this.**
|
|
601
|
+
*
|
|
602
|
+
* Anything that is not a transport fault (a genuine revert, a user rejection,
|
|
603
|
+
* an insufficient-funds rejection) is rethrown on the first attempt with no
|
|
604
|
+
* delay, so real failures still fail fast.
|
|
605
|
+
*
|
|
606
|
+
* @param tag - Low-cardinality log label for the retry breadcrumb.
|
|
607
|
+
* @param operation - The idempotent async read to run.
|
|
608
|
+
* @param context - Extra structured context for the retry breadcrumb (e.g.
|
|
609
|
+
* `{ hash }`). Sanitized by the logger before transport.
|
|
610
|
+
* @param isRetryable - Predicate deciding whether a caught error warrants
|
|
611
|
+
* another attempt. Defaults to the strict transport definition
|
|
612
|
+
* ({@link isRetryableRpcError}); pass a wider one only where the call site
|
|
613
|
+
* can prove the extra shape is also a provider artefact — the only such case
|
|
614
|
+
* today is the selector-scoped {@link isEmptyViewResponse} used by
|
|
615
|
+
* `getDecimalsOrThrow`.
|
|
616
|
+
* @returns Whatever `operation` resolves to on the first successful attempt.
|
|
617
|
+
* @throws The last error thrown by `operation` once retries are exhausted, or
|
|
618
|
+
* immediately when the error is not retryable.
|
|
619
|
+
*
|
|
620
|
+
* @example
|
|
621
|
+
* ```ts
|
|
622
|
+
* const receipt = await retryOnTransientRpc(
|
|
623
|
+
* 'safeWaitForTx:transport-retry',
|
|
624
|
+
* () => provider.waitForTransaction(hash, 1, 120_000),
|
|
625
|
+
* { hash },
|
|
626
|
+
* );
|
|
627
|
+
* ```
|
|
628
|
+
*/
|
|
629
|
+
async function retryOnTransientRpc(tag, operation, context = {}, isRetryable = isRetryableRpcError) {
|
|
630
|
+
let lastError;
|
|
631
|
+
for (let attempt = 1; attempt <= RPC_RETRY_ATTEMPTS; attempt += 1) {
|
|
632
|
+
try {
|
|
633
|
+
return await operation();
|
|
634
|
+
}
|
|
635
|
+
catch (error) {
|
|
636
|
+
lastError = error;
|
|
637
|
+
if (!isRetryable(error) || attempt === RPC_RETRY_ATTEMPTS) {
|
|
638
|
+
throw error;
|
|
639
|
+
}
|
|
640
|
+
const delayMs = RPC_RETRY_BASE_DELAY_MS * 2 ** (attempt - 1);
|
|
641
|
+
logger_1.Logger.log.warn(tag, 'transient RPC error; retrying', {
|
|
642
|
+
...context,
|
|
643
|
+
attempt,
|
|
644
|
+
attempts: RPC_RETRY_ATTEMPTS,
|
|
645
|
+
delayMs,
|
|
646
|
+
message: error instanceof Error ? error.message : String(error),
|
|
647
|
+
});
|
|
648
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
// Unreachable: the loop either returns or throws on its final attempt.
|
|
652
|
+
throw lastError;
|
|
653
|
+
}
|
|
242
654
|
/**
|
|
243
655
|
* Log a caught chain error at the severity its category warrants, without
|
|
244
656
|
* swallowing it. When `isBenign` is `true` the failure is recorded as a `warn`
|
|
@@ -29,3 +29,24 @@ export declare function normalizeSigner(signer: Signer | Wallet | any): Promise<
|
|
|
29
29
|
* Type that represents either an ethers signer or a viem wallet client
|
|
30
30
|
*/
|
|
31
31
|
export type CompatibleSigner = Signer | Wallet | any;
|
|
32
|
+
/**
|
|
33
|
+
* Wrap an ethers signer so every transaction it sends carries the active
|
|
34
|
+
* ERC-8021 attribution suffix (Base Builder Codes).
|
|
35
|
+
*
|
|
36
|
+
* Every ethers write in the SDK funnels through the wrapped signer's
|
|
37
|
+
* `sendTransaction`, so this single wrap attributes all vault writes. The
|
|
38
|
+
* wrapper is a Proxy — the caller's signer object is never mutated, so a
|
|
39
|
+
* signer reused outside the SDK sends unattributed transactions. Attribution
|
|
40
|
+
* state is read at send time, not wrap time; when no attribution is
|
|
41
|
+
* configured (see `IAugustBase.attribution`) the wrapper is a pass-through.
|
|
42
|
+
*
|
|
43
|
+
* Plain value transfers (no calldata) are never attributed, and calldata
|
|
44
|
+
* already ending in the ERC-8021 marker is left untouched. When the
|
|
45
|
+
* configured `chains` list requires a chain check and the transaction does
|
|
46
|
+
* not carry a `chainId`, the signer's provider network is consulted (one
|
|
47
|
+
* cached RPC call).
|
|
48
|
+
*
|
|
49
|
+
* @param signer Normalized ethers Signer or Wallet.
|
|
50
|
+
* @returns A proxied signer with an attribution-aware `sendTransaction`.
|
|
51
|
+
*/
|
|
52
|
+
export declare function wrapSignerWithAttribution<T extends Signer | Wallet>(signer: T): T;
|
|
@@ -3,7 +3,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.isViemWalletClient = isViemWalletClient;
|
|
4
4
|
exports.viemToEthersSigner = viemToEthersSigner;
|
|
5
5
|
exports.normalizeSigner = normalizeSigner;
|
|
6
|
+
exports.wrapSignerWithAttribution = wrapSignerWithAttribution;
|
|
6
7
|
const ethers_1 = require("ethers");
|
|
8
|
+
const attribution_1 = require("../attribution");
|
|
7
9
|
/**
|
|
8
10
|
* Signer Compatibility Helpers
|
|
9
11
|
*
|
|
@@ -90,4 +92,54 @@ async function normalizeSigner(signer) {
|
|
|
90
92
|
// If we can't determine the type, throw an error
|
|
91
93
|
throw new Error('Invalid signer type. Expected ethers Signer, ethers Wallet, or viem WalletClient');
|
|
92
94
|
}
|
|
95
|
+
/**
|
|
96
|
+
* Wrap an ethers signer so every transaction it sends carries the active
|
|
97
|
+
* ERC-8021 attribution suffix (Base Builder Codes).
|
|
98
|
+
*
|
|
99
|
+
* Every ethers write in the SDK funnels through the wrapped signer's
|
|
100
|
+
* `sendTransaction`, so this single wrap attributes all vault writes. The
|
|
101
|
+
* wrapper is a Proxy — the caller's signer object is never mutated, so a
|
|
102
|
+
* signer reused outside the SDK sends unattributed transactions. Attribution
|
|
103
|
+
* state is read at send time, not wrap time; when no attribution is
|
|
104
|
+
* configured (see `IAugustBase.attribution`) the wrapper is a pass-through.
|
|
105
|
+
*
|
|
106
|
+
* Plain value transfers (no calldata) are never attributed, and calldata
|
|
107
|
+
* already ending in the ERC-8021 marker is left untouched. When the
|
|
108
|
+
* configured `chains` list requires a chain check and the transaction does
|
|
109
|
+
* not carry a `chainId`, the signer's provider network is consulted (one
|
|
110
|
+
* cached RPC call).
|
|
111
|
+
*
|
|
112
|
+
* @param signer Normalized ethers Signer or Wallet.
|
|
113
|
+
* @returns A proxied signer with an attribution-aware `sendTransaction`.
|
|
114
|
+
*/
|
|
115
|
+
function wrapSignerWithAttribution(signer) {
|
|
116
|
+
return new Proxy(signer, {
|
|
117
|
+
get(target, prop) {
|
|
118
|
+
if (prop === 'sendTransaction') {
|
|
119
|
+
return async (tx) => {
|
|
120
|
+
if (!(0, attribution_1.getAttributionSuffix)() || typeof tx?.data !== 'string') {
|
|
121
|
+
return target.sendTransaction(tx);
|
|
122
|
+
}
|
|
123
|
+
let chainId = tx.chainId != null ? Number(tx.chainId) : undefined;
|
|
124
|
+
if (chainId === undefined && target.provider) {
|
|
125
|
+
try {
|
|
126
|
+
chainId = Number((await target.provider.getNetwork()).chainId);
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
// Unknown chain: fall through with chainId undefined, which
|
|
130
|
+
// appends regardless of a `chains` restriction —
|
|
131
|
+
// over-attribution is harmless, under-attribution loses data.
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
const data = (0, attribution_1.appendAttributionSuffix)(tx.data, chainId);
|
|
135
|
+
return target.sendTransaction(data === tx.data ? tx : { ...tx, data });
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
// `this`-bind reads to the target, not the proxy — ethers classes use
|
|
139
|
+
// private fields, which throw when methods run with a Proxy receiver.
|
|
140
|
+
const value = Reflect.get(target, prop, target);
|
|
141
|
+
return typeof value === 'function' ? value.bind(target) : value;
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
}
|
|
93
145
|
//# sourceMappingURL=signer.js.map
|