@augustdigital/sdk 8.16.1 → 8.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -57,8 +57,10 @@ class EVMAdapter {
57
57
  if (this.signer) {
58
58
  return this.signer;
59
59
  }
60
- // Normalize and cache the signer
61
- this.signer = await (0, signer_1.normalizeSigner)(this.rawSigner);
60
+ // Normalize and cache the signer. The attribution wrap is unconditional
61
+ // and reads the process-global config at send time, so a cached signer
62
+ // honors attribution enabled or reset after caching.
63
+ this.signer = (0, signer_1.wrapSignerWithAttribution)(await (0, signer_1.normalizeSigner)(this.rawSigner));
62
64
  return this.signer;
63
65
  }
64
66
  // Core EVM operations
@@ -58,9 +58,14 @@ export declare function getHealthyServer(config: ISorobanNetworkConfig): Promise
58
58
  * Exported so an operation with different semantics (e.g. an idempotency-aware
59
59
  * submission path) can compose or replace this policy via
60
60
  * {@link FailoverOptions.isRetryable} — extension without editing the core.
61
+ *
62
+ * Named distinctly from `core/helpers/chain-error`'s `isRetryableRpcError`
63
+ * (the EVM transport classifier, deliberately public) — the internal-dts
64
+ * leak check matches `@internal` tags by name across the whole `src.ts` tree,
65
+ * so a same-named pair would make it flag the public EVM one as a leak.
61
66
  * @internal
62
67
  */
63
- export declare function isRetryableRpcError(err: unknown): boolean;
68
+ export declare function isRetryableSorobanRpcError(err: unknown): boolean;
64
69
  /**
65
70
  * Whether a simulation *error* string is a node/infrastructure failure (which
66
71
  * clears on another endpoint) rather than a genuine contract revert (which
@@ -76,7 +81,7 @@ export declare function isRetryableSimulationError(errorText: string): boolean;
76
81
  * failover core (Open/Closed at the call site).
77
82
  */
78
83
  export interface FailoverOptions {
79
- /** Which thrown errors get the next endpoint. Defaults to {@link isRetryableRpcError}. */
84
+ /** Which thrown errors get the next endpoint. Defaults to {@link isRetryableSorobanRpcError}. */
80
85
  isRetryable?: (err: unknown) => boolean;
81
86
  /** Per-attempt timeout in ms. Defaults to {@link RPC_OPERATION_TIMEOUT_MS}. */
82
87
  timeoutMs?: number;
@@ -92,7 +97,7 @@ export interface FailoverOptions {
92
97
  * the health-gated server first (the fast, cached path) and, on a retryable
93
98
  * failure, drop that cached choice and retry the op against every endpoint in
94
99
  * priority order. Deterministic contract/domain errors short-circuit (see
95
- * {@link isRetryableRpcError}); each attempt is time-boxed so a hung node can't
100
+ * {@link isRetryableSorobanRpcError}); each attempt is time-boxed so a hung node can't
96
101
  * stall the loop.
97
102
  *
98
103
  * The `operation` is a strategy (dependency-injected), so new read/build calls
@@ -11,7 +11,7 @@ exports.resolveNetworkConfig = resolveNetworkConfig;
11
11
  exports.resetHealthyServerCache = resetHealthyServerCache;
12
12
  exports.redactRpcUrl = redactRpcUrl;
13
13
  exports.getHealthyServer = getHealthyServer;
14
- exports.isRetryableRpcError = isRetryableRpcError;
14
+ exports.isRetryableSorobanRpcError = isRetryableSorobanRpcError;
15
15
  exports.isRetryableSimulationError = isRetryableSimulationError;
16
16
  exports.withEndpointFailover = withEndpointFailover;
17
17
  exports.toBigIntAmount = toBigIntAmount;
@@ -291,9 +291,14 @@ async function probeHealthyServer(rpcUrls, cacheKey) {
291
291
  * Exported so an operation with different semantics (e.g. an idempotency-aware
292
292
  * submission path) can compose or replace this policy via
293
293
  * {@link FailoverOptions.isRetryable} — extension without editing the core.
294
+ *
295
+ * Named distinctly from `core/helpers/chain-error`'s `isRetryableRpcError`
296
+ * (the EVM transport classifier, deliberately public) — the internal-dts
297
+ * leak check matches `@internal` tags by name across the whole `src.ts` tree,
298
+ * so a same-named pair would make it flag the public EVM one as a leak.
294
299
  * @internal
295
300
  */
296
- function isRetryableRpcError(err) {
301
+ function isRetryableSorobanRpcError(err) {
297
302
  return !(err instanceof core_1.AugustValidationError || err instanceof core_1.AugustSDKError);
298
303
  }
299
304
  /**
@@ -334,7 +339,7 @@ function dropCachedServer(rpcUrls) {
334
339
  * the health-gated server first (the fast, cached path) and, on a retryable
335
340
  * failure, drop that cached choice and retry the op against every endpoint in
336
341
  * priority order. Deterministic contract/domain errors short-circuit (see
337
- * {@link isRetryableRpcError}); each attempt is time-boxed so a hung node can't
342
+ * {@link isRetryableSorobanRpcError}); each attempt is time-boxed so a hung node can't
338
343
  * stall the loop.
339
344
  *
340
345
  * The `operation` is a strategy (dependency-injected), so new read/build calls
@@ -345,7 +350,7 @@ function dropCachedServer(rpcUrls) {
345
350
  * @internal
346
351
  */
347
352
  async function withEndpointFailover(config, method, operation, options = {}) {
348
- const isRetryable = options.isRetryable ?? isRetryableRpcError;
353
+ const isRetryable = options.isRetryable ?? isRetryableSorobanRpcError;
349
354
  const timeoutMs = options.timeoutMs ?? RPC_OPERATION_TIMEOUT_MS;
350
355
  const rpcUrls = config.rpcUrls && config.rpcUrls.length > 0
351
356
  ? config.rpcUrls
@@ -2,4 +2,4 @@
2
2
  * Sentry DSN for SDK analytics.
3
3
  * This is an internal constant - not exposed to SDK users.
4
4
  */
5
- export declare const SENTRY_DSN = "https://d73b6a85fe14960b8dac8ac61a743a94@o4507215496609792.ingest.de.sentry.io/4510699327389776";
5
+ export declare const SENTRY_DSN = "https://ca39246016d0b43154f0640314cbd3b5@o4507215496609792.ingest.de.sentry.io/4511864614813776";
@@ -5,5 +5,5 @@ exports.SENTRY_DSN = void 0;
5
5
  * Sentry DSN for SDK analytics.
6
6
  * This is an internal constant - not exposed to SDK users.
7
7
  */
8
- exports.SENTRY_DSN = 'https://d73b6a85fe14960b8dac8ac61a743a94@o4507215496609792.ingest.de.sentry.io/4510699327389776';
8
+ exports.SENTRY_DSN = 'https://ca39246016d0b43154f0640314cbd3b5@o4507215496609792.ingest.de.sentry.io/4511864614813776';
9
9
  //# sourceMappingURL=constants.js.map
@@ -3,4 +3,4 @@
3
3
  * Generated during publish from package.json version
4
4
  * This file is gitignored and created at publish time
5
5
  */
6
- export declare const SDK_VERSION = "8.16.1";
6
+ export declare const SDK_VERSION = "8.19.0";
@@ -6,5 +6,5 @@ exports.SDK_VERSION = void 0;
6
6
  * Generated during publish from package.json version
7
7
  * This file is gitignored and created at publish time
8
8
  */
9
- exports.SDK_VERSION = '8.16.1';
9
+ exports.SDK_VERSION = '8.19.0';
10
10
  //# sourceMappingURL=version.js.map
@@ -0,0 +1,111 @@
1
+ /**
2
+ * ERC-8021 transaction attribution (Base Builder Codes).
3
+ *
4
+ * ERC-8021 appends an attribution suffix to transaction calldata. Contracts
5
+ * decode their ABI-encoded arguments normally and ignore the trailing bytes,
6
+ * while offchain indexers (e.g. base.dev analytics for Base Builder Codes)
7
+ * read the suffix to attribute the transaction to the originating app.
8
+ *
9
+ * This is a leaf module: process-global state configured once via
10
+ * {@link IAugustBase.attribution} in the SDK constructor and read by the
11
+ * EVM write paths (the normalized-signer wrap in the EVM adapter and the
12
+ * cross-chain vault `writeContract` calls).
13
+ *
14
+ * @module attribution
15
+ */
16
+ /**
17
+ * The 16-byte ERC-8021 suffix terminator. The last 16 bytes of an attributed
18
+ * transaction's calldata are always this marker; parsers read backwards from
19
+ * it to recover the schema ID and builder codes.
20
+ */
21
+ export declare const ERC8021_MARKER = "80218021802180218021802180218021";
22
+ /**
23
+ * Configuration for ERC-8021 calldata-suffix attribution (Base Builder Codes).
24
+ *
25
+ * @example
26
+ * ```typescript
27
+ * const sdk = new AugustSDK({
28
+ * appName: 'my-app',
29
+ * providers: { 8453: 'https://...' },
30
+ * keys: { august: '...' },
31
+ * attribution: { builderCodes: ['bc_abc123'] },
32
+ * });
33
+ * ```
34
+ */
35
+ export interface IAttributionConfig {
36
+ /**
37
+ * Builder codes to embed in the suffix, e.g. from base.dev registration
38
+ * (`bc_…`). ASCII strings of 1–64 characters each; the comma-joined list
39
+ * must fit in 255 bytes (schema 0 length prefix is a single byte).
40
+ */
41
+ builderCodes: string[];
42
+ /**
43
+ * EVM chain IDs to attribute. Omit to attribute writes on every EVM chain
44
+ * (the suffix is inert on chains without an ERC-8021 indexer and costs
45
+ * ~16 gas per non-zero byte). When set, writes on other chains are sent
46
+ * without the suffix; call sites that cannot determine their chain ID
47
+ * append the suffix regardless, since over-attribution is harmless and
48
+ * under-attribution loses data.
49
+ */
50
+ chains?: number[];
51
+ }
52
+ /**
53
+ * Build an ERC-8021 schema-0 attribution suffix from builder codes.
54
+ *
55
+ * Layout (appended to calldata, read back-to-front by parsers):
56
+ * `ascii(codes.join(','))` ∥ codesLength (1 byte) ∥ schemaId `0x00` ∥
57
+ * 16-byte marker `0x80218021802180218021802180218021`.
58
+ *
59
+ * @param codes Builder codes as printable-ASCII strings (e.g. `bc_abc123`),
60
+ * each 1–64 chars, no commas; the comma-joined list must be ≤ 255 bytes.
61
+ * @returns `0x`-prefixed hex suffix ready to concatenate onto calldata.
62
+ * @throws Error when `codes` is empty, a code contains a comma or
63
+ * non-printable/non-ASCII characters, or the joined list exceeds 255 bytes.
64
+ * @example
65
+ * ```typescript
66
+ * buildAttributionSuffix(['baseapp', 'morpho']);
67
+ * // '0x626173656170702c6d6f7270686f0e0080218021802180218021802180218021'
68
+ * ```
69
+ */
70
+ export declare function buildAttributionSuffix(codes: string[]): string;
71
+ /**
72
+ * Set (or clear) the process-global attribution config.
73
+ *
74
+ * Called unconditionally on every `AugustSDK` construction — an instance
75
+ * that omits `attribution` passes `null` and RESETS the state, so a prior
76
+ * instance's builder codes never leak into a later instance in the same
77
+ * process (same semantics as `setPublicApiBaseUrl`).
78
+ *
79
+ * @param config Attribution config from the SDK constructor, or `null` to
80
+ * disable attribution.
81
+ * @throws Error when the config's builder codes fail validation — thrown
82
+ * synchronously from the constructor so misconfiguration is caught at
83
+ * init, not on the first write.
84
+ */
85
+ export declare function setAttribution(config: IAttributionConfig | null): void;
86
+ /**
87
+ * Return the active ERC-8021 suffix for a write on the given chain, or
88
+ * `undefined` when attribution is off or the chain is excluded.
89
+ *
90
+ * @param chainId EVM chain ID of the transaction, when the call site knows
91
+ * it. When omitted and a `chains` restriction is configured, the suffix is
92
+ * returned anyway (over-attribution is harmless; see
93
+ * {@link IAttributionConfig.chains}).
94
+ * @returns `0x`-prefixed hex suffix, or `undefined` when nothing should be
95
+ * appended.
96
+ */
97
+ export declare function getAttributionSuffix(chainId?: number): string | undefined;
98
+ /**
99
+ * Append the active attribution suffix to calldata.
100
+ *
101
+ * No-ops (returns `data` unchanged) when attribution is off, the chain is
102
+ * excluded, `data` is empty/absent (plain value transfers are never
103
+ * attributed), or `data` already ends with the ERC-8021 marker (guards
104
+ * against double-appending when an upstream layer — e.g. a wagmi config
105
+ * `dataSuffix` — already attributed the transaction).
106
+ *
107
+ * @param data `0x`-prefixed calldata of the outgoing transaction.
108
+ * @param chainId EVM chain ID of the transaction, when known.
109
+ * @returns Calldata with the suffix appended, or the input unchanged.
110
+ */
111
+ export declare function appendAttributionSuffix(data: string | undefined | null, chainId?: number): string | undefined | null;
@@ -0,0 +1,142 @@
1
+ "use strict";
2
+ /**
3
+ * ERC-8021 transaction attribution (Base Builder Codes).
4
+ *
5
+ * ERC-8021 appends an attribution suffix to transaction calldata. Contracts
6
+ * decode their ABI-encoded arguments normally and ignore the trailing bytes,
7
+ * while offchain indexers (e.g. base.dev analytics for Base Builder Codes)
8
+ * read the suffix to attribute the transaction to the originating app.
9
+ *
10
+ * This is a leaf module: process-global state configured once via
11
+ * {@link IAugustBase.attribution} in the SDK constructor and read by the
12
+ * EVM write paths (the normalized-signer wrap in the EVM adapter and the
13
+ * cross-chain vault `writeContract` calls).
14
+ *
15
+ * @module attribution
16
+ */
17
+ Object.defineProperty(exports, "__esModule", { value: true });
18
+ exports.ERC8021_MARKER = void 0;
19
+ exports.buildAttributionSuffix = buildAttributionSuffix;
20
+ exports.setAttribution = setAttribution;
21
+ exports.getAttributionSuffix = getAttributionSuffix;
22
+ exports.appendAttributionSuffix = appendAttributionSuffix;
23
+ /**
24
+ * The 16-byte ERC-8021 suffix terminator. The last 16 bytes of an attributed
25
+ * transaction's calldata are always this marker; parsers read backwards from
26
+ * it to recover the schema ID and builder codes.
27
+ */
28
+ exports.ERC8021_MARKER = '80218021802180218021802180218021';
29
+ /**
30
+ * Build an ERC-8021 schema-0 attribution suffix from builder codes.
31
+ *
32
+ * Layout (appended to calldata, read back-to-front by parsers):
33
+ * `ascii(codes.join(','))` ∥ codesLength (1 byte) ∥ schemaId `0x00` ∥
34
+ * 16-byte marker `0x80218021802180218021802180218021`.
35
+ *
36
+ * @param codes Builder codes as printable-ASCII strings (e.g. `bc_abc123`),
37
+ * each 1–64 chars, no commas; the comma-joined list must be ≤ 255 bytes.
38
+ * @returns `0x`-prefixed hex suffix ready to concatenate onto calldata.
39
+ * @throws Error when `codes` is empty, a code contains a comma or
40
+ * non-printable/non-ASCII characters, or the joined list exceeds 255 bytes.
41
+ * @example
42
+ * ```typescript
43
+ * buildAttributionSuffix(['baseapp', 'morpho']);
44
+ * // '0x626173656170702c6d6f7270686f0e0080218021802180218021802180218021'
45
+ * ```
46
+ */
47
+ function buildAttributionSuffix(codes) {
48
+ if (!Array.isArray(codes) || codes.length === 0) {
49
+ throw new Error('August SDK: attribution.builderCodes must be a non-empty array of builder-code strings (e.g. ["bc_abc123"]). Register at https://base.dev to obtain one.');
50
+ }
51
+ for (const code of codes) {
52
+ if (typeof code !== 'string' ||
53
+ code.length === 0 ||
54
+ code.length > 64 ||
55
+ !/^[\x21-\x7e]+$/.test(code) ||
56
+ code.includes(',')) {
57
+ throw new Error(`August SDK: invalid builder code "${code}". Codes must be 1-64 printable ASCII characters with no commas or whitespace.`);
58
+ }
59
+ }
60
+ const joined = codes.join(',');
61
+ if (joined.length > 255) {
62
+ throw new Error(`August SDK: attribution builder codes exceed 255 bytes when comma-joined (got ${joined.length}). Use fewer or shorter codes.`);
63
+ }
64
+ let hex = '';
65
+ for (let i = 0; i < joined.length; i++) {
66
+ hex += joined.charCodeAt(i).toString(16).padStart(2, '0');
67
+ }
68
+ const length = joined.length.toString(16).padStart(2, '0');
69
+ return `0x${hex}${length}00${exports.ERC8021_MARKER}`;
70
+ }
71
+ let activeSuffix = null;
72
+ let activeChains = null;
73
+ /**
74
+ * Set (or clear) the process-global attribution config.
75
+ *
76
+ * Called unconditionally on every `AugustSDK` construction — an instance
77
+ * that omits `attribution` passes `null` and RESETS the state, so a prior
78
+ * instance's builder codes never leak into a later instance in the same
79
+ * process (same semantics as `setPublicApiBaseUrl`).
80
+ *
81
+ * @param config Attribution config from the SDK constructor, or `null` to
82
+ * disable attribution.
83
+ * @throws Error when the config's builder codes fail validation — thrown
84
+ * synchronously from the constructor so misconfiguration is caught at
85
+ * init, not on the first write.
86
+ */
87
+ function setAttribution(config) {
88
+ if (!config) {
89
+ activeSuffix = null;
90
+ activeChains = null;
91
+ return;
92
+ }
93
+ activeSuffix = buildAttributionSuffix(config.builderCodes);
94
+ activeChains =
95
+ Array.isArray(config.chains) && config.chains.length > 0
96
+ ? config.chains
97
+ : null;
98
+ }
99
+ /**
100
+ * Return the active ERC-8021 suffix for a write on the given chain, or
101
+ * `undefined` when attribution is off or the chain is excluded.
102
+ *
103
+ * @param chainId EVM chain ID of the transaction, when the call site knows
104
+ * it. When omitted and a `chains` restriction is configured, the suffix is
105
+ * returned anyway (over-attribution is harmless; see
106
+ * {@link IAttributionConfig.chains}).
107
+ * @returns `0x`-prefixed hex suffix, or `undefined` when nothing should be
108
+ * appended.
109
+ */
110
+ function getAttributionSuffix(chainId) {
111
+ if (!activeSuffix)
112
+ return undefined;
113
+ if (activeChains && typeof chainId === 'number') {
114
+ if (!activeChains.includes(chainId))
115
+ return undefined;
116
+ }
117
+ return activeSuffix;
118
+ }
119
+ /**
120
+ * Append the active attribution suffix to calldata.
121
+ *
122
+ * No-ops (returns `data` unchanged) when attribution is off, the chain is
123
+ * excluded, `data` is empty/absent (plain value transfers are never
124
+ * attributed), or `data` already ends with the ERC-8021 marker (guards
125
+ * against double-appending when an upstream layer — e.g. a wagmi config
126
+ * `dataSuffix` — already attributed the transaction).
127
+ *
128
+ * @param data `0x`-prefixed calldata of the outgoing transaction.
129
+ * @param chainId EVM chain ID of the transaction, when known.
130
+ * @returns Calldata with the suffix appended, or the input unchanged.
131
+ */
132
+ function appendAttributionSuffix(data, chainId) {
133
+ const suffix = getAttributionSuffix(chainId);
134
+ if (!suffix)
135
+ return data;
136
+ if (typeof data !== 'string' || data === '' || data === '0x')
137
+ return data;
138
+ if (data.toLowerCase().endsWith(exports.ERC8021_MARKER))
139
+ return data;
140
+ return data + suffix.slice(2);
141
+ }
142
+ //# sourceMappingURL=attribution.js.map
@@ -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.
@@ -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`