@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,7 @@
1
1
  import type { ISolanaConfig, IStellarConfig, IAddress, IChainId, IEnv, IProvidersConfig, IWSMonitorHeaders } from '../types';
2
2
  import { type IAnalyticsConfig } from './analytics';
3
3
  import { type IVersionCheckConfig } from './version-check';
4
+ import { type IAttributionConfig } from './attribution';
4
5
  interface IKeys {
5
6
  august?: string;
6
7
  graph?: string;
@@ -83,6 +84,21 @@ export interface IAugustBase {
83
84
  * a stale cross-environment base is higher blast-radius than a stale timeout.)
84
85
  */
85
86
  publicApiBaseUrl?: string;
87
+ /**
88
+ * ERC-8021 calldata-suffix attribution (Base Builder Codes). When set,
89
+ * every EVM write sent through the SDK — ethers vault writes and the
90
+ * cross-chain (OVault) viem writes — carries the attribution suffix so
91
+ * offchain indexers (base.dev) can credit the transaction to your app.
92
+ * Omit to send unattributed transactions (the default).
93
+ *
94
+ * Note: this is a process-global override applied on EVERY construction —
95
+ * the last `AugustSDK` instantiated is authoritative, and one that omits
96
+ * `attribution` RESETS it (same semantics as `publicApiBaseUrl`).
97
+ *
98
+ * @throws Throws synchronously from the constructor when the builder codes
99
+ * are malformed — see {@link IAttributionConfig}.
100
+ */
101
+ attribution?: IAttributionConfig;
86
102
  }
87
103
  interface IActiveNetwork {
88
104
  chainId: IChainId;
@@ -107,7 +123,7 @@ export declare class AugustBase {
107
123
  * @throws If `appName` is missing, malformed, or out of the allowed
108
124
  * length range — see {@link IAugustBase.appName}.
109
125
  */
110
- constructor({ appName, providers, keys, monitoring, analytics, versionCheck, timeoutMs, publicApiBaseUrl, }: IAugustBase);
126
+ constructor({ appName, providers, keys, monitoring, analytics, versionCheck, timeoutMs, publicApiBaseUrl, attribution, }: IAugustBase);
111
127
  /**
112
128
  * Verify API keys and authorize SDK usage.
113
129
  * TODO: initialize class with appropriate keys and verify august key
@@ -6,6 +6,7 @@ const logger_1 = require("./logger");
6
6
  const analytics_1 = require("./analytics");
7
7
  const version_check_1 = require("./version-check");
8
8
  const fetcher_1 = require("./fetcher");
9
+ const attribution_1 = require("./attribution");
9
10
  /**
10
11
  * Validate an appName at SDK construction time.
11
12
  *
@@ -55,7 +56,7 @@ class AugustBase {
55
56
  * @throws If `appName` is missing, malformed, or out of the allowed
56
57
  * length range — see {@link IAugustBase.appName}.
57
58
  */
58
- constructor({ appName, providers = {}, keys, monitoring, analytics, versionCheck, timeoutMs, publicApiBaseUrl, }) {
59
+ constructor({ appName, providers = {}, keys, monitoring, analytics, versionCheck, timeoutMs, publicApiBaseUrl, attribution, }) {
59
60
  // Validate first so the failure mode is a clear, actionable error before
60
61
  // any provider / analytics side effects run.
61
62
  this.appName = validateAppName(appName);
@@ -95,6 +96,10 @@ class AugustBase {
95
96
  // prior staging instance would leak into a later prod instance in the same
96
97
  // process — the inverse of the isolation this option exists to provide.
97
98
  (0, fetcher_1.setPublicApiBaseUrl)(publicApiBaseUrl ?? null);
99
+ // ERC-8021 attribution follows the same reset-on-omit contract as
100
+ // `publicApiBaseUrl`: called unconditionally so a prior instance's
101
+ // builder codes never leak into a later instance in the same process.
102
+ (0, attribution_1.setAttribution)(attribution ?? null);
98
103
  }
99
104
  /**
100
105
  * Verify API keys and authorize SDK usage.
@@ -5,6 +5,33 @@
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.DEFAULT_FETCH_OPTIONS = exports.WEBSERVER_ENDPOINTS = exports.WEBSERVER_URL = exports.PRICE_SERVER_URL = exports.REQUEST_TIMEOUT_MS = void 0;
7
7
  const ethers_1 = require("ethers");
8
+ /**
9
+ * Build the path segment for a subaccount identifier.
10
+ *
11
+ * Why this exists: subaccounts are not EVM-only. Vault operators and loan
12
+ * borrowers can be Solana base58 keys or Stellar `G…`/`C…` addresses, and the
13
+ * backend keys those records by their native string form. Calling ethers'
14
+ * `getAddress()` on them throws `INVALID_ARGUMENT` before the request is ever
15
+ * made — which is how a Solana borrower turned every
16
+ * `getVaultAllocations` call for a mixed-chain vault into a
17
+ * `TypeError: invalid address` (see `getVaultAllocations:cefi` /
18
+ * `:otc` in Sentry).
19
+ *
20
+ * EVM addresses are still checksummed, because the backend expects the
21
+ * checksummed form for those and case-normalising avoids cache misses.
22
+ * Everything else is passed through URL-encoded so a malformed value can never
23
+ * inject extra path segments.
24
+ *
25
+ * @param subaccount - Subaccount identifier: EVM hex address, Solana base58
26
+ * public key, or Stellar strkey. Not validated beyond the EVM branch — the
27
+ * backend is the authority on whether a non-EVM identifier exists.
28
+ * @returns A single URL path segment safe to interpolate into an endpoint.
29
+ */
30
+ function subaccountSegment(subaccount) {
31
+ return (0, ethers_1.isAddress)(subaccount)
32
+ ? (0, ethers_1.getAddress)(subaccount)
33
+ : encodeURIComponent(subaccount);
34
+ }
8
35
  /**
9
36
  * Request timeout in milliseconds.
10
37
  * Set to 90 seconds to accommodate slow API responses.
@@ -34,22 +61,22 @@ exports.WEBSERVER_ENDPOINTS = {
34
61
  },
35
62
  subaccount: {
36
63
  list: (offset = 0, limit = 100) => `/subaccount?offset=${offset}&limit=${limit}`,
37
- rewards: (subaccount) => `/subaccount/${(0, ethers_1.getAddress)(subaccount)}/rewards`,
38
- tokens: (subaccount) => `/subaccount/${(0, ethers_1.getAddress)(subaccount)}/tokens`,
64
+ rewards: (subaccount) => `/subaccount/${subaccountSegment(subaccount)}/rewards`,
65
+ tokens: (subaccount) => `/subaccount/${subaccountSegment(subaccount)}/tokens`,
39
66
  twap: {
40
- create: (subaccount) => `/subaccount/${(0, ethers_1.getAddress)(subaccount)}/twap`,
41
- stop: (subaccount, id) => `/subaccount/${(0, ethers_1.getAddress)(subaccount)}/twap/${id}/stop`,
42
- fills: (subaccount, id) => `/subaccount/${(0, ethers_1.getAddress)(subaccount)}/twap/${id}/fills`,
67
+ create: (subaccount) => `/subaccount/${subaccountSegment(subaccount)}/twap`,
68
+ stop: (subaccount, id) => `/subaccount/${subaccountSegment(subaccount)}/twap/${id}/stop`,
69
+ fills: (subaccount, id) => `/subaccount/${subaccountSegment(subaccount)}/twap/${id}/fills`,
43
70
  },
44
- debank: (subaccount) => `/subaccount/${(0, ethers_1.getAddress)(subaccount)}/debank`,
45
- health_factor: (subaccount) => `/subaccount/${(0, ethers_1.getAddress)(subaccount)}/health_factor`,
46
- summary: (subaccount) => `/subaccount/${(0, ethers_1.getAddress)(subaccount)}/summary`,
47
- batch: (subaccount) => `/subaccount/${(0, ethers_1.getAddress)(subaccount)}/tx_batcher_integrations`,
48
- loans: (subaccount, side = 'BOTH', active = true) => `/subaccount/${(0, ethers_1.getAddress)(subaccount)}/loans?side=${side}&active=${active}`,
49
- cefi: (subaccount) => `/subaccount/${(0, ethers_1.getAddress)(subaccount)}/cefi_position`,
50
- otc_positions: (subaccount) => `/subaccount/${(0, ethers_1.getAddress)(subaccount)}/otc_positions`,
71
+ debank: (subaccount) => `/subaccount/${subaccountSegment(subaccount)}/debank`,
72
+ health_factor: (subaccount) => `/subaccount/${subaccountSegment(subaccount)}/health_factor`,
73
+ summary: (subaccount) => `/subaccount/${subaccountSegment(subaccount)}/summary`,
74
+ batch: (subaccount) => `/subaccount/${subaccountSegment(subaccount)}/tx_batcher_integrations`,
75
+ loans: (subaccount, side = 'BOTH', active = true) => `/subaccount/${subaccountSegment(subaccount)}/loans?side=${side}&active=${active}`,
76
+ cefi: (subaccount) => `/subaccount/${subaccountSegment(subaccount)}/cefi_position`,
77
+ otc_positions: (subaccount) => `/subaccount/${subaccountSegment(subaccount)}/otc_positions`,
51
78
  loanByAddress: (loanAddress, chainId) => `/subaccount/loans/${encodeURIComponent(loanAddress)}?chain_id=${chainId}`,
52
- _: (subaccount) => `/subaccount/${(0, ethers_1.getAddress)(subaccount)}`,
79
+ _: (subaccount) => `/subaccount/${subaccountSegment(subaccount)}`,
53
80
  },
54
81
  transactions: {
55
82
  v2: (subaccount, startTime, endTime) => {
@@ -108,7 +135,7 @@ exports.WEBSERVER_ENDPOINTS = {
108
135
  },
109
136
  prices: (symbol) => `/prices/${symbol}`,
110
137
  metrics: {
111
- pnl: (subaccount, startTime, endTime) => `/metrics/pnl?subaccount_address=${(0, ethers_1.getAddress)(subaccount)}${startTime && endTime ? `&start=${startTime}&end=${endTime}` : ''}`,
138
+ pnl: (subaccount, startTime, endTime) => `/metrics/pnl?subaccount_address=${subaccountSegment(subaccount)}${startTime && endTime ? `&start=${startTime}&end=${endTime}` : ''}`,
112
139
  vaultPerformanceFees: (params) => {
113
140
  const q = new URLSearchParams({
114
141
  vault_address: params.vaultAddress,
@@ -126,7 +153,7 @@ exports.WEBSERVER_ENDPOINTS = {
126
153
  public: {
127
154
  integrations: {
128
155
  morpho: {
129
- apy: (subaccount, vaultAddress) => `/integrations/morpho/apy?subaccount_address=${(0, ethers_1.getAddress)(subaccount)}&vault_address=${(0, ethers_1.getAddress)(vaultAddress)}`,
156
+ apy: (subaccount, vaultAddress) => `/integrations/morpho/apy?subaccount_address=${subaccountSegment(subaccount)}&vault_address=${(0, ethers_1.getAddress)(vaultAddress)}`,
130
157
  },
131
158
  },
132
159
  tokenizedVault: {
@@ -11,6 +11,23 @@ export declare const SPECIAL_CHAINS: {
11
11
  explorer: string;
12
12
  };
13
13
  };
14
+ /**
15
+ * Chain ID used to address Sui vaults.
16
+ *
17
+ * Unlike Solana (`-1`) and Stellar (`-3`), Sui does not use a synthetic
18
+ * negative ID — `101` is the SLIP-44-derived value the Sui adapter has always
19
+ * used. It lives here rather than in `adapters/sui/constants.ts` so that
20
+ * `core/` can recognise it without importing the adapter layer (which would
21
+ * create a `core → adapters` back-edge); the adapter re-exports it.
22
+ */
23
+ export declare const SUI_CHAIN_ID = 101;
24
+ /**
25
+ * Chain IDs the SDK understands that are **not** served by an EVM JSON-RPC
26
+ * provider. Callers may legitimately pass these to vault methods without a
27
+ * matching entry in the `providers` map — the request routes through the
28
+ * Solana / Stellar / Sui adapter instead.
29
+ */
30
+ export declare const NON_EVM_CHAIN_IDS: ReadonlySet<number>;
14
31
  export declare const NATIVE_ADDRESS = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE";
15
32
  /**
16
33
  * Decimal precision of the native gas token on every EVM chain this SDK
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.FALLBACK_RPC_URLS = exports.FALLBACK_CHAINID = exports.FALLBACK_DECIMALS = exports.MULTICALL3_VERIFIED_CHAINS = exports.MULTICALL3_ADDRESS = exports.AVAILABLE_CHAINS = exports.NETWORKS = exports.ORACLE_CONTRACTS = exports.MIN_ABIS = exports.EVM_NATIVE_DECIMALS = exports.NATIVE_ADDRESS = exports.SPECIAL_CHAINS = void 0;
3
+ exports.FALLBACK_RPC_URLS = exports.FALLBACK_CHAINID = exports.FALLBACK_DECIMALS = exports.MULTICALL3_VERIFIED_CHAINS = exports.MULTICALL3_ADDRESS = exports.AVAILABLE_CHAINS = exports.NETWORKS = exports.ORACLE_CONTRACTS = exports.MIN_ABIS = exports.EVM_NATIVE_DECIMALS = exports.NATIVE_ADDRESS = exports.NON_EVM_CHAIN_IDS = exports.SUI_CHAIN_ID = exports.SPECIAL_CHAINS = void 0;
4
4
  // Special Chains
5
5
  exports.SPECIAL_CHAINS = {
6
6
  solana: {
@@ -14,6 +14,27 @@ exports.SPECIAL_CHAINS = {
14
14
  explorer: 'https://stellar.expert',
15
15
  },
16
16
  };
17
+ /**
18
+ * Chain ID used to address Sui vaults.
19
+ *
20
+ * Unlike Solana (`-1`) and Stellar (`-3`), Sui does not use a synthetic
21
+ * negative ID — `101` is the SLIP-44-derived value the Sui adapter has always
22
+ * used. It lives here rather than in `adapters/sui/constants.ts` so that
23
+ * `core/` can recognise it without importing the adapter layer (which would
24
+ * create a `core → adapters` back-edge); the adapter re-exports it.
25
+ */
26
+ exports.SUI_CHAIN_ID = 101;
27
+ /**
28
+ * Chain IDs the SDK understands that are **not** served by an EVM JSON-RPC
29
+ * provider. Callers may legitimately pass these to vault methods without a
30
+ * matching entry in the `providers` map — the request routes through the
31
+ * Solana / Stellar / Sui adapter instead.
32
+ */
33
+ exports.NON_EVM_CHAIN_IDS = new Set([
34
+ exports.SPECIAL_CHAINS.solana.chainId,
35
+ exports.SPECIAL_CHAINS.stellar.chainId,
36
+ exports.SUI_CHAIN_ID,
37
+ ]);
17
38
  // General
18
39
  exports.NATIVE_ADDRESS = '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE';
19
40
  /**
@@ -23,6 +23,7 @@ const ethers_1 = require("ethers");
23
23
  const abis_1 = require("../abis");
24
24
  const lru_cache_1 = require("lru-cache");
25
25
  const chain_address_1 = require("./helpers/chain-address");
26
+ const chain_error_1 = require("./helpers/chain-error");
26
27
  const fetcher_1 = require("../services/coingecko/fetcher");
27
28
  const logger_1 = require("./logger");
28
29
  const ethers_2 = require("ethers");
@@ -705,7 +706,15 @@ async function _fetchTokenPriceInternal(symbol, provider, coinGeckoKey, headers)
705
706
  if (foundVaultLpAsset?.vault && provider && version === 'evm-2') {
706
707
  try {
707
708
  const vaultContract = new ethers_2.Contract((0, ethers_1.getAddress)(foundVaultLpAsset?.vault), abis_1.ABI_TOKENIZED_VAULT_V2, provider);
708
- const receiptAddress = (await vaultContract.lpTokenAddress());
709
+ // Same bounded, no-fallback retry the write paths use for this selector
710
+ // (`getReceiptTokenAddressOrThrow` in `core/helpers/web3`). It cannot be
711
+ // reused here: `helpers/web3` imports `fetchTokenizedVault` from this
712
+ // module, so importing it back would be a circular dependency (CLAUDE.md
713
+ // §9). A transient empty `eth_call` response is absorbed; a vault that
714
+ // genuinely lacks `lpTokenAddress()` still falls into the catch below
715
+ // after the attempts are spent.
716
+ const receiptAddress = await (0, chain_error_1.retryOnTransientRpc)('fetchTokenPrice:receiptToken', () => vaultContract.lpTokenAddress(), { vault: foundVaultLpAsset.vault }, (error) => (0, chain_error_1.isRetryableRpcError)(error) ||
717
+ (0, chain_error_1.isEmptyViewResponse)(error, chain_error_1.LP_TOKEN_ADDRESS_SELECTOR));
709
718
  const receiptContract = new ethers_2.Contract(receiptAddress, [web3_1.MIN_ABIS.decimals], provider);
710
719
  const decimals = Number(await receiptContract.decimals());
711
720
  const sharePrice = await vaultContract.getSharePrice();
@@ -46,6 +46,146 @@ export declare function isUserRejectionError(error: unknown): boolean;
46
46
  */
47
47
  export declare function isExpectedRevertError(error: unknown): boolean;
48
48
  export declare function isInsufficientFundsError(error: unknown): boolean;
49
+ /**
50
+ * Is this error a transient RPC **transport** failure that is safe to retry,
51
+ * rather than a decision the chain made?
52
+ *
53
+ * Why this exists: the SDK's write paths poll `eth_getTransactionReceipt` to
54
+ * confirm a broadcast transaction. When the provider hiccups mid-poll, ethers
55
+ * surfaces `could not coalesce error (error={ "code": -32603, … "method":
56
+ * "eth_getTransactionReceipt" … })`. Historically that propagated out of
57
+ * `safeWaitForTx` and the SDK reported the write as **failed** — even though
58
+ * the transaction was broadcast, its hash was known, and it mined fine. Users
59
+ * then retried and hit `ERC20InsufficientBalance` because the first attempt had
60
+ * in fact succeeded. Classifying the failure as transport-level lets callers
61
+ * re-poll instead of lying to the user.
62
+ *
63
+ * Matches, in order of precedence:
64
+ * 1. **Veto** — anything with revert evidence ({@link hasRevertEvidence}:
65
+ * `CALL_EXCEPTION` carrying revert `data`, an `execution reverted` message,
66
+ * or an attached `receipt.status === 0`) returns `false`. Nodes reuse
67
+ * `-32603`/`-32000` for real reverts, so the veto must come first.
68
+ * 2. JSON-RPC / ethers transport codes — see `RETRYABLE_RPC_CODES`.
69
+ * 3. HTTP `429` and any `5xx` carried on the error.
70
+ * 4. Transport message fragments — see `RETRYABLE_RPC_PHRASES`.
71
+ *
72
+ * Retrying is only safe for **idempotent** work: re-reading an immutable value
73
+ * (`decimals()`) or re-polling a receipt for a hash that is already on the
74
+ * wire. Never use this to re-send a transaction.
75
+ *
76
+ * @param error - The caught value, of unknown type.
77
+ * @returns `true` when the failure is a transient transport fault worth
78
+ * retrying with backoff; `false` for chain-level decisions (reverts) and for
79
+ * anything unrecognised — the safe default is to surface the error.
80
+ *
81
+ * @example
82
+ * ```ts
83
+ * try {
84
+ * return await provider.waitForTransaction(hash, 1, 120_000);
85
+ * } catch (e) {
86
+ * if (!isRetryableRpcError(e)) throw e; // real revert — surface it
87
+ * await sleep(250);
88
+ * return await provider.waitForTransaction(hash, 1, 120_000);
89
+ * }
90
+ * ```
91
+ */
92
+ export declare function isRetryableRpcError(error: unknown): boolean;
93
+ /**
94
+ * `lpTokenAddress()` — `keccak256("lpTokenAddress()")[0..4]`. The August `evm-2`
95
+ * tokenized vault's receipt-token getter, exported so the readers that invoke it
96
+ * can scope {@link isEmptyViewResponse} to exactly this call instead of
97
+ * duplicating the literal.
98
+ */
99
+ export declare const LP_TOKEN_ADDRESS_SELECTOR = "0xf5ae497a";
100
+ /**
101
+ * Is this error an **empty RPC response to an argument-free view call** —
102
+ * i.e. a transport artefact wearing a revert's clothes?
103
+ *
104
+ * Why this is separate from {@link isRetryableRpcError}: when a provider
105
+ * truncates or 500s a response to `eth_call`, ethers reports
106
+ * `missing revert data (action="call", data="0x313ce567", …)` with a `null`
107
+ * `data` field. That is byte-for-byte the shape of a genuine revert with no
108
+ * reason string, so a general "transport" predicate cannot safely claim it —
109
+ * doing so would retry every data-less `CALL_EXCEPTION` in the SDK. This
110
+ * predicate narrows the claim to the one case where the ambiguity resolves:
111
+ * a **deployed ERC-20's `decimals()`/`symbol()`/`name()`/`totalSupply()` cannot
112
+ * legitimately revert**, because it takes no arguments and returns state fixed
113
+ * at deployment. An empty response there is the provider's fault, full stop.
114
+ *
115
+ * A match requires all of:
116
+ * 1. no revert evidence ({@link hasRevertEvidence}) — anything carrying real
117
+ * revert `data`, an `execution reverted` message, or a failed receipt is out;
118
+ * 2. a `missing revert data` message;
119
+ * 3. an `action` of `call` or `staticCall` — a read, never a state change;
120
+ * 4. **when a selector is derivable** from the error, that it is
121
+ * `expectedSelector` (if given) or one of
122
+ * {@link ARGUMENT_FREE_VIEW_SELECTORS}. When no selector can be recovered,
123
+ * conditions 1–3 stand on their own.
124
+ *
125
+ * Note the cost of a false positive is bounded and small: the caller retries an
126
+ * idempotent read a couple of times before surfacing the same error. The cost
127
+ * of a false negative is the production flood this predicate exists to stop.
128
+ *
129
+ * @param error - The caught value, of unknown type.
130
+ * @param expectedSelector - Optional `0x`-prefixed 4-byte selector the caller
131
+ * knows it invoked (e.g. `'0x313ce567'` for `decimals()`). When supplied, the
132
+ * error's own selector must match it — this stops a `decimals()` retry from
133
+ * firing on an unrelated view call that happened to fail the same way.
134
+ * @returns `true` when the failure is an empty provider response to a view call
135
+ * that cannot revert, and is therefore safe to retry.
136
+ *
137
+ * @example
138
+ * ```ts
139
+ * try { return Number(await erc20.decimals()); }
140
+ * catch (e) {
141
+ * if (!isEmptyViewResponse(e, '0x313ce567')) throw e; // real problem
142
+ * return Number(await erc20.decimals()); // provider blip
143
+ * }
144
+ * ```
145
+ */
146
+ export declare function isEmptyViewResponse(error: unknown, expectedSelector?: string): boolean;
147
+ /**
148
+ * Run an **idempotent** RPC read, retrying with exponential backoff while the
149
+ * failure classifies as a transient transport fault
150
+ * ({@link isRetryableRpcError}).
151
+ *
152
+ * Lives next to the classifiers it consumes so there is exactly one retry
153
+ * implementation in the SDK: both the receipt-poll fallback in the vault write
154
+ * paths and the cached `decimals()` reader in `core/helpers/web3.ts` call this.
155
+ *
156
+ * Only safe for operations that can be repeated without side effects: polling
157
+ * `eth_getTransactionReceipt` for an already-broadcast hash, or re-reading an
158
+ * immutable value such as `decimals()`. **Never wrap a transaction send in
159
+ * this.**
160
+ *
161
+ * Anything that is not a transport fault (a genuine revert, a user rejection,
162
+ * an insufficient-funds rejection) is rethrown on the first attempt with no
163
+ * delay, so real failures still fail fast.
164
+ *
165
+ * @param tag - Low-cardinality log label for the retry breadcrumb.
166
+ * @param operation - The idempotent async read to run.
167
+ * @param context - Extra structured context for the retry breadcrumb (e.g.
168
+ * `{ hash }`). Sanitized by the logger before transport.
169
+ * @param isRetryable - Predicate deciding whether a caught error warrants
170
+ * another attempt. Defaults to the strict transport definition
171
+ * ({@link isRetryableRpcError}); pass a wider one only where the call site
172
+ * can prove the extra shape is also a provider artefact — the only such case
173
+ * today is the selector-scoped {@link isEmptyViewResponse} used by
174
+ * `getDecimalsOrThrow`.
175
+ * @returns Whatever `operation` resolves to on the first successful attempt.
176
+ * @throws The last error thrown by `operation` once retries are exhausted, or
177
+ * immediately when the error is not retryable.
178
+ *
179
+ * @example
180
+ * ```ts
181
+ * const receipt = await retryOnTransientRpc(
182
+ * 'safeWaitForTx:transport-retry',
183
+ * () => provider.waitForTransaction(hash, 1, 120_000),
184
+ * { hash },
185
+ * );
186
+ * ```
187
+ */
188
+ export declare function retryOnTransientRpc<T>(tag: string, operation: () => Promise<T>, context?: Record<string, unknown>, isRetryable?: (error: unknown) => boolean): Promise<T>;
49
189
  /**
50
190
  * Log a caught chain error at the severity its category warrants, without
51
191
  * swallowing it. When `isBenign` is `true` the failure is recorded as a `warn`