@augustdigital/sdk 8.21.0 → 8.22.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.
@@ -384,12 +384,16 @@ function initializeSentry(config, environment, walletAddress, apiKey, appName) {
384
384
  ? config.tracesSampleRate
385
385
  : 0.1; // Phase 2 default — verified partners can opt into 1.0 explicitly
386
386
  const tracesSampleRate = Math.max(0, Math.min(1, rawSampleRate));
387
+ // Hoisted so `release`, `august.sdk_version`, and the deprecated
388
+ // `sdk.version` are provably the same value rather than three separate
389
+ // reads of `./version`.
390
+ const sdkVersion = getSDKVersion();
387
391
  sdk.init({
388
392
  dsn: constants_1.SENTRY_DSN,
389
393
  tracesSampleRate,
390
394
  enableTracing: true,
391
395
  environment: environment.toLowerCase(),
392
- release: `august-sdk@${getSDKVersion()}`,
396
+ release: `august-sdk@${sdkVersion}`,
393
397
  sendDefaultPii: true,
394
398
  // SDK errors route through Logger.setSentrySink → captureException directly,
395
399
  // so captureConsoleIntegration is redundant here. Omitting it also prevents
@@ -445,7 +449,27 @@ function initializeSentry(config, environment, walletAddress, apiKey, appName) {
445
449
  updateUser(walletAddress, environment);
446
450
  // Set global SDK tags
447
451
  safeSetTag('sdk', 'august-digital');
448
- safeSetTag('sdk.version', getSDKVersion());
452
+ // `august.sdk_version` is the queryable dimension for "which SDK build is
453
+ // this partner on" — the question that decides whether an error is a live
454
+ // defect or an already-shipped fix the consumer hasn't picked up.
455
+ //
456
+ // It exists because `sdk.version` (set below, kept for back-compat) is a
457
+ // RESERVED Sentry field: Sentry populates it with the version of its own
458
+ // client (`@sentry/core`). The custom tag is still stored, but a bare
459
+ // `sdk.version` in Discover resolves to Sentry's value, so the natural
460
+ // query silently answers the wrong question — uniform across every
461
+ // consumer, which makes it look like a working answer. Reaching the real
462
+ // value requires `tags[sdk.version]`, which nobody remembers to type.
463
+ //
464
+ // Both are low-cardinality version strings, so carrying the pair costs
465
+ // nothing against the tag budget (CLAUDE.md §8.4).
466
+ safeSetTag('august.sdk_version', sdkVersion);
467
+ /**
468
+ * @deprecated Shadowed by Sentry's reserved `sdk.version` field. Query
469
+ * `august.sdk_version` instead. Retained so existing saved queries,
470
+ * dashboards, and alert rules keep resolving; remove in the next major.
471
+ */
472
+ safeSetTag('sdk.version', sdkVersion);
449
473
  safeSetTag('sdk.runtime', (0, sentry_runtime_1.getSentryRuntime)());
450
474
  // `app.name` is the dimension we filter on in Sentry to attribute
451
475
  // events to a specific consuming application — see SDK quickstart docs.
@@ -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.21.0";
6
+ export declare const SDK_VERSION = "8.22.1";
@@ -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.21.0';
9
+ exports.SDK_VERSION = '8.22.1';
10
10
  //# sourceMappingURL=version.js.map
@@ -69,7 +69,7 @@ export declare const NEMO_VAULT_ADDRESS = "0xa422c3018c46ba90a14acd14f96cb60616f
69
69
  * Subgraph base URL
70
70
  * @deprecated use getVaultMetadata to fetch subgraph URLs from the backend
71
71
  */
72
- export declare const GOLDSKY_BASE_URL = "https://api.goldsky.com/api/private/project_cm9g0xy3o4j6v01vd34r3hvv9/subgraphs";
72
+ export declare const GOLDSKY_BASE_URL = "https://api.goldsky.com/api/public/project_cm9g0xy3o4j6v01vd34r3hvv9/subgraphs";
73
73
  /**
74
74
  * Subgraph URLs
75
75
  * @deprecated use getVaultMetadata to fetch subgraph URLs from the backend
@@ -131,7 +131,7 @@ exports.NEMO_VAULT_ADDRESS = '0xa422c3018c46ba90a14acd14f96cb60616f5c91b';
131
131
  * Subgraph base URL
132
132
  * @deprecated use getVaultMetadata to fetch subgraph URLs from the backend
133
133
  */
134
- exports.GOLDSKY_BASE_URL = 'https://api.goldsky.com/api/private/project_cm9g0xy3o4j6v01vd34r3hvv9/subgraphs';
134
+ exports.GOLDSKY_BASE_URL = 'https://api.goldsky.com/api/public/project_cm9g0xy3o4j6v01vd34r3hvv9/subgraphs';
135
135
  /**
136
136
  * Subgraph URLs
137
137
  * @deprecated use getVaultMetadata to fetch subgraph URLs from the backend
@@ -32,7 +32,8 @@ export declare const NATIVE_ADDRESS = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEe
32
32
  /**
33
33
  * Decimal precision of the native gas token on every EVM chain this SDK
34
34
  * supports (ETH, Base, Arbitrum, Avalanche, Polygon, BNB, HyperEVM, Unichain,
35
- * Mezo, Monad, Plasma, Ink, Flare, Katana, Citrea, Fluent, Tempo).
35
+ * Mezo, Monad, Plasma, Ink, Flare, Katana, Citrea, Fluent, Tempo, X Layer —
36
+ * whose native OKB is also 18 decimals).
36
37
  *
37
38
  * All of them use 18-decimal native tokens, so this is a single constant rather
38
39
  * than a per-chain map. It exists because the native token has no ERC-20
@@ -73,7 +74,8 @@ export declare const MULTICALL3_ADDRESS = "0xcA11bde05977b3631167028862bE2a17397
73
74
  /**
74
75
  * Chains where the canonical Multicall3 deployment was **verified on-chain**
75
76
  * (`eth_getCode` returned the 3808-byte runtime at {@link MULTICALL3_ADDRESS};
76
- * checked per chain on 2026-07-14, Tempo checked separately on 2026-07-15) —
77
+ * checked per chain on 2026-07-14, Tempo checked separately on 2026-07-15,
78
+ * X Layer on 2026-08-11) —
77
79
  * deterministic-deployer presence is NOT assumed. Chains outside this set
78
80
  * keep the per-call read path.
79
81
  *
@@ -40,7 +40,8 @@ exports.NATIVE_ADDRESS = '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE';
40
40
  /**
41
41
  * Decimal precision of the native gas token on every EVM chain this SDK
42
42
  * supports (ETH, Base, Arbitrum, Avalanche, Polygon, BNB, HyperEVM, Unichain,
43
- * Mezo, Monad, Plasma, Ink, Flare, Katana, Citrea, Fluent, Tempo).
43
+ * Mezo, Monad, Plasma, Ink, Flare, Katana, Citrea, Fluent, Tempo, X Layer —
44
+ * whose native OKB is also 18 decimals).
44
45
  *
45
46
  * All of them use 18-decimal native tokens, so this is a single constant rather
46
47
  * than a per-chain map. It exists because the native token has no ERC-20
@@ -160,6 +161,11 @@ exports.NETWORKS = {
160
161
  chainId: 4217,
161
162
  explorer: 'https://explore.tempo.xyz',
162
163
  },
164
+ 196: {
165
+ name: 'X Layer',
166
+ chainId: 196,
167
+ explorer: 'https://xlayerscan.com',
168
+ },
163
169
  };
164
170
  exports.AVAILABLE_CHAINS = Object.keys(exports.NETWORKS).map((c) => Number(c));
165
171
  /**
@@ -171,7 +177,8 @@ exports.MULTICALL3_ADDRESS = '0xcA11bde05977b3631167028862bE2a173976CA11';
171
177
  /**
172
178
  * Chains where the canonical Multicall3 deployment was **verified on-chain**
173
179
  * (`eth_getCode` returned the 3808-byte runtime at {@link MULTICALL3_ADDRESS};
174
- * checked per chain on 2026-07-14, Tempo checked separately on 2026-07-15) —
180
+ * checked per chain on 2026-07-14, Tempo checked separately on 2026-07-15,
181
+ * X Layer on 2026-08-11) —
175
182
  * deterministic-deployer presence is NOT assumed. Chains outside this set
176
183
  * keep the per-call read path.
177
184
  *
@@ -197,6 +204,7 @@ exports.MULTICALL3_VERIFIED_CHAINS = new Set([
197
204
  747474, // Katana
198
205
  25363, // Fluent
199
206
  4217, // Tempo
207
+ 196, // X Layer
200
208
  ]);
201
209
  /**
202
210
  * Fallbacks
@@ -225,5 +233,6 @@ exports.FALLBACK_RPC_URLS = {
225
233
  4114: ['https://rpc.mainnet.citrea.xyz'],
226
234
  25363: ['https://rpc.fluent.xyz'],
227
235
  4217: ['https://rpc.mainnet.tempo.xyz'],
236
+ 196: ['https://rpc.xlayer.tech'],
228
237
  };
229
238
  //# sourceMappingURL=web3.js.map
@@ -95,10 +95,16 @@ export { explorerLink } from './explorer-link';
95
95
  * Fetch token decimals from contract or Solana mint.
96
96
  * Results are cached to minimize RPC calls.
97
97
  *
98
- * **Never throws** — a failed read logs at error level and resolves
99
- * `undefined`. Callers that must not silently proceed on an unknown scale (any
100
- * path that encodes an amount) should use {@link getDecimalsOrThrow} instead:
101
- * feeding `undefined` into `toNormalizedBn` silently defaults to 18 decimals.
98
+ * **Never throws** — a failed read resolves `undefined`. Callers that must not
99
+ * silently proceed on an unknown scale (any path that encodes an amount) should
100
+ * use {@link getDecimalsOrThrow} instead: feeding `undefined` into
101
+ * `toNormalizedBn` silently defaults to 18 decimals.
102
+ *
103
+ * Transient transport faults (provider rate limits, socket resets, an empty
104
+ * response to `decimals()`) are logged at `warn` — a breadcrumb, not a
105
+ * standalone Sentry issue — since the caller sees the same `undefined` either
106
+ * way. Everything else is logged at `error`. The read itself is **not**
107
+ * retried here; that is opt-in via {@link getDecimalsOrThrow}.
102
108
  *
103
109
  * @param provider Web3 provider
104
110
  * @param address Token contract address or Solana mint
@@ -395,10 +395,16 @@ async function fetchDecimals(runner, address, isVault) {
395
395
  * Fetch token decimals from contract or Solana mint.
396
396
  * Results are cached to minimize RPC calls.
397
397
  *
398
- * **Never throws** — a failed read logs at error level and resolves
399
- * `undefined`. Callers that must not silently proceed on an unknown scale (any
400
- * path that encodes an amount) should use {@link getDecimalsOrThrow} instead:
401
- * feeding `undefined` into `toNormalizedBn` silently defaults to 18 decimals.
398
+ * **Never throws** — a failed read resolves `undefined`. Callers that must not
399
+ * silently proceed on an unknown scale (any path that encodes an amount) should
400
+ * use {@link getDecimalsOrThrow} instead: feeding `undefined` into
401
+ * `toNormalizedBn` silently defaults to 18 decimals.
402
+ *
403
+ * Transient transport faults (provider rate limits, socket resets, an empty
404
+ * response to `decimals()`) are logged at `warn` — a breadcrumb, not a
405
+ * standalone Sentry issue — since the caller sees the same `undefined` either
406
+ * way. Everything else is logged at `error`. The read itself is **not**
407
+ * retried here; that is opt-in via {@link getDecimalsOrThrow}.
402
408
  *
403
409
  * @param provider Web3 provider
404
410
  * @param address Token contract address or Solana mint
@@ -422,7 +428,27 @@ const getDecimals = async (provider, address, isVault = true) => {
422
428
  return await sharedDecimalsRequest(key, () => fetchDecimals(provider, address, isVault));
423
429
  }
424
430
  catch (e) {
425
- logger_1.Logger.log.error('getDecimals', `${address}::${e}`);
431
+ // Severity split. A provider rate-limit or socket fault is not a defect in
432
+ // the SDK or the caller's input — the read resolves `undefined` either way,
433
+ // exactly as documented, and logging it at error level made a QuickNode
434
+ // `50/second request limit reached` burst the fourth highest-volume issue
435
+ // in Sentry with nothing actionable in it. Genuine failures (a non-ERC-20
436
+ // at this address, a real revert) stay at error.
437
+ //
438
+ // Note this only changes the log level. The no-retry contract of this
439
+ // lenient reader is deliberate and unchanged — callers that must not
440
+ // proceed on an unknown scale use `getDecimalsOrThrow`, which retries.
441
+ const isTransient = (0, chain_error_1.isRetryableRpcError)(e) || (0, chain_error_1.isEmptyViewResponse)(e, DECIMALS_SELECTOR);
442
+ if (isTransient) {
443
+ logger_1.Logger.log.warn('getDecimals', {
444
+ address,
445
+ reason: 'transient RPC failure',
446
+ message: e instanceof Error ? e.message : String(e),
447
+ });
448
+ }
449
+ else {
450
+ logger_1.Logger.log.error('getDecimals', `${address}::${e}`);
451
+ }
426
452
  return undefined;
427
453
  }
428
454
  };
@@ -1,7 +1,40 @@
1
1
  /**
2
- * The default Slack webhook URL for logging errors.
2
+ * Environment variable holding the Slack incoming-webhook to post SDK alerts
3
+ * to. Accepts either the full `https://hooks.slack.com/services/T…/B…/x…` URL
4
+ * or the bare `T…/B…/x…` path suffix.
3
5
  */
4
- export declare const DEFAULT_SLACK_WEBHOOK_URL = "T04CM84GAV6/B0A2DS3ST8C/FLtOA3Jna3FN7UO4DoGxHfhG";
6
+ export declare const SLACK_WEBHOOK_ENV_VAR = "AUGUST_SDK_SLACK_WEBHOOK_URL";
7
+ /**
8
+ * Resolve the Slack webhook to alert through.
9
+ *
10
+ * Why this replaced a hardcoded constant: a webhook path is a **bearer
11
+ * credential** — anyone holding it can post to the channel. The previous
12
+ * default embedded August's own webhook as a string literal, which shipped in
13
+ * every published tarball and in the generated `.d.ts`, handing every consumer
14
+ * of the SDK write access to an internal Slack channel (CLAUDE.md §5: no
15
+ * secrets in client-reachable code). It also meant a consumer's alerts went to
16
+ * August's channel rather than their own, with no way to redirect them.
17
+ *
18
+ * Resolution order:
19
+ * 1. `explicit` — passed by the caller, wins outright;
20
+ * 2. `AUGUST_SDK_SLACK_WEBHOOK_URL` in the environment;
21
+ * 3. none — alerting is disabled and the call becomes a no-op.
22
+ *
23
+ * Reading the env var per call (rather than once at module load) means a
24
+ * consumer configuring it after import still gets alerts, and a test can set
25
+ * and unset it without re-importing the module.
26
+ *
27
+ * @param explicit - Caller-supplied webhook, full URL or bare path suffix.
28
+ * @returns The bare `T…/B…/x…` path suffix, or `''` when unconfigured.
29
+ */
30
+ export declare function resolveSlackWebhookUrl(explicit?: string): string;
31
+ /**
32
+ * @deprecated Was a hardcoded webhook credential baked into the published
33
+ * bundle. It now resolves from {@link SLACK_WEBHOOK_ENV_VAR} and is `''` when
34
+ * unset. Call {@link resolveSlackWebhookUrl} instead — this export exists only
35
+ * so existing imports keep compiling, and is removed in the next major.
36
+ */
37
+ export declare const DEFAULT_SLACK_WEBHOOK_URL = "";
5
38
  declare function error(options: {
6
39
  title: string;
7
40
  error: string;
@@ -1,16 +1,70 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.SLACK = exports.DEFAULT_SLACK_WEBHOOK_URL = void 0;
3
+ exports.SLACK = exports.DEFAULT_SLACK_WEBHOOK_URL = exports.SLACK_WEBHOOK_ENV_VAR = void 0;
4
+ exports.resolveSlackWebhookUrl = resolveSlackWebhookUrl;
4
5
  const core_1 = require("../helpers/core");
5
6
  const explorer_link_1 = require("../helpers/explorer-link");
6
7
  const sanitize_1 = require("../analytics/sanitize");
8
+ const env_1 = require("../analytics/env");
7
9
  /**
8
- * The default Slack webhook URL for logging errors.
10
+ * Environment variable holding the Slack incoming-webhook to post SDK alerts
11
+ * to. Accepts either the full `https://hooks.slack.com/services/T…/B…/x…` URL
12
+ * or the bare `T…/B…/x…` path suffix.
9
13
  */
10
- exports.DEFAULT_SLACK_WEBHOOK_URL = 'T04CM84GAV6/B0A2DS3ST8C/FLtOA3Jna3FN7UO4DoGxHfhG';
14
+ exports.SLACK_WEBHOOK_ENV_VAR = 'AUGUST_SDK_SLACK_WEBHOOK_URL';
15
+ /** `https://hooks.slack.com/services/` — stripped so both forms are accepted. */
16
+ const SLACK_WEBHOOK_PREFIX = 'https://hooks.slack.com/services/';
17
+ /**
18
+ * Resolve the Slack webhook to alert through.
19
+ *
20
+ * Why this replaced a hardcoded constant: a webhook path is a **bearer
21
+ * credential** — anyone holding it can post to the channel. The previous
22
+ * default embedded August's own webhook as a string literal, which shipped in
23
+ * every published tarball and in the generated `.d.ts`, handing every consumer
24
+ * of the SDK write access to an internal Slack channel (CLAUDE.md §5: no
25
+ * secrets in client-reachable code). It also meant a consumer's alerts went to
26
+ * August's channel rather than their own, with no way to redirect them.
27
+ *
28
+ * Resolution order:
29
+ * 1. `explicit` — passed by the caller, wins outright;
30
+ * 2. `AUGUST_SDK_SLACK_WEBHOOK_URL` in the environment;
31
+ * 3. none — alerting is disabled and the call becomes a no-op.
32
+ *
33
+ * Reading the env var per call (rather than once at module load) means a
34
+ * consumer configuring it after import still gets alerts, and a test can set
35
+ * and unset it without re-importing the module.
36
+ *
37
+ * @param explicit - Caller-supplied webhook, full URL or bare path suffix.
38
+ * @returns The bare `T…/B…/x…` path suffix, or `''` when unconfigured.
39
+ */
40
+ function resolveSlackWebhookUrl(explicit) {
41
+ const raw = explicit?.trim() || (0, env_1.readEnv)(exports.SLACK_WEBHOOK_ENV_VAR)?.trim() || '';
42
+ return raw.startsWith(SLACK_WEBHOOK_PREFIX)
43
+ ? raw.slice(SLACK_WEBHOOK_PREFIX.length)
44
+ : raw;
45
+ }
46
+ /**
47
+ * @deprecated Was a hardcoded webhook credential baked into the published
48
+ * bundle. It now resolves from {@link SLACK_WEBHOOK_ENV_VAR} and is `''` when
49
+ * unset. Call {@link resolveSlackWebhookUrl} instead — this export exists only
50
+ * so existing imports keep compiling, and is removed in the next major.
51
+ */
52
+ exports.DEFAULT_SLACK_WEBHOOK_URL = '';
53
+ /** One-shot guard so an unconfigured webhook warns once, not per alert. */
54
+ let warnedUnconfigured = false;
11
55
  function error(options) {
12
56
  const { title, error, poolAddress, chainId, slackWebookUrl, address } = options;
13
- if (!slackWebookUrl) {
57
+ // Falls back to the environment so a caller that passes nothing still
58
+ // alerts, provided the deployment configured a webhook of its own.
59
+ const webhookPath = resolveSlackWebhookUrl(slackWebookUrl);
60
+ if (!webhookPath) {
61
+ // Silence here used to be the hardcoded-default's job. Say it once, so a
62
+ // deployment that expected alerts learns they are off instead of watching
63
+ // an empty channel. Never log the webhook value itself.
64
+ if (!warnedUnconfigured) {
65
+ warnedUnconfigured = true;
66
+ console.warn(`#Slack.error: no webhook configured — set ${exports.SLACK_WEBHOOK_ENV_VAR} to enable Slack alerts. Alerts are disabled.`);
67
+ }
14
68
  return;
15
69
  }
16
70
  if (!error) {
@@ -34,7 +88,7 @@ function error(options) {
34
88
  return;
35
89
  }
36
90
  (async () => {
37
- const webhookUrl = `https://hooks.slack.com/services/${slackWebookUrl}`;
91
+ const webhookUrl = `${SLACK_WEBHOOK_PREFIX}${webhookPath}`;
38
92
  const safeError = (0, sanitize_1.sanitizeString)(String(error));
39
93
  const safeTitle = (0, sanitize_1.sanitizeString)(String(title));
40
94
  const data = {
@@ -68,6 +68,7 @@ const abis_1 = require("../../abis");
68
68
  const types_1 = require("../../types");
69
69
  const core_1 = require("../../core");
70
70
  const ethers_1 = require("ethers");
71
+ const cache_1 = require("../../core/cache");
71
72
  const utils_1 = require("./utils");
72
73
  const subgraph_1 = require("../../services/subgraph");
73
74
  const vaults_1 = require("../../services/subgraph/vaults");
@@ -356,8 +357,30 @@ async function getVaultSubaccountLoans(vault, options) {
356
357
  * HTTP statuses that mean "no such record for this subaccount", not "something
357
358
  * broke". The backend returns 404 for a subaccount with no CeFi/OTC position,
358
359
  * which is the normal case for most vault borrowers.
360
+ *
361
+ * `400` is here for the same reason. The CeFi and OTC endpoints answer `400`
362
+ * for a borrower they do not track — a well-formed, checksummed EVM address
363
+ * that simply has no record on that side — and every `getVaultAllocations`
364
+ * call re-asks for the same untracked borrowers on every render. In Sentry
365
+ * that produced ~2.7k `AugustServerError: Request failed: 400` events across
366
+ * two issues in two days, all from the same handful of borrowers, none of them
367
+ * actionable. Both fetches are strictly best-effort enrichment: the vault's
368
+ * allocations are returned either way, so a `400` here can never mean the
369
+ * caller's request was malformed.
370
+ *
371
+ * This demotes the *log severity* only — nothing about the response handling
372
+ * changes, and any other status (401, 5xx, transport failures) is still an
373
+ * error-level Sentry issue.
374
+ *
375
+ * **Maintainer note on the `400` entry specifically.** Unlike `204`/`404`, a
376
+ * `400` is semantically "the client sent something wrong", so this demotion is
377
+ * safe only for as long as these two endpoints use `400` to mean "unknown
378
+ * borrower". If either ever starts returning `400` for a genuinely malformed
379
+ * request — a real defect in how the SDK builds the URL or body — this set
380
+ * would silence it. Should that contract change, drop `400` from here and let
381
+ * the backend distinguish the two cases with a status of its own.
359
382
  */
360
- const EXPECTED_SUBACCOUNT_FETCH_STATUSES = new Set([204, 404]);
383
+ const EXPECTED_SUBACCOUNT_FETCH_STATUSES = new Set([204, 400, 404]);
361
384
  /**
362
385
  * Log a per-subaccount enrichment fetch failure at the right severity.
363
386
  *
@@ -628,6 +651,87 @@ function subgraphAmountToBigInt(value) {
628
651
  return BigInt(0);
629
652
  }
630
653
  }
654
+ /**
655
+ * Read `lagDuration()` off a vault, tolerating vaults that do not implement it.
656
+ *
657
+ * Why this exists: `getVaultAvailableRedemptions` binds every vault — v1 and
658
+ * v2 — to `ABI_LENDING_POOL_V2` because that ABI carries the superset of
659
+ * methods the scan needs. `lagDuration()` is not in that superset for every
660
+ * deployed pool. Calling it on a pool without the function reaches the
661
+ * fallback-less contract, which returns empty calldata, and ethers reports it
662
+ * as `missing revert data (action="call", data=null, …, code=CALL_EXCEPTION)`.
663
+ * That threw out of the whole redemption scan and became the single
664
+ * highest-volume error in production Sentry (~5.5k events over two days from
665
+ * three server deployments), while also returning an empty redemption list to
666
+ * every caller for the affected vaults.
667
+ *
668
+ * A missing `lagDuration()` means "this pool has no claim lag", which is what
669
+ * `0` encodes — the same value the v1 branch of the caller assumes.
670
+ *
671
+ * The empty response is **retried before it is believed**, because a provider
672
+ * that truncates an `eth_call` produces the identical shape: falling straight
673
+ * to `0` for a vault that really does have a lag would shift every computed
674
+ * claimable date. Only after the retries agree is the function treated as
675
+ * absent. A genuine revert carrying revert data still propagates, so real
676
+ * breakage stays loud.
677
+ *
678
+ * **The absence is memoized, the value is not.** Paying the retry budget (3
679
+ * `eth_call`s plus ~750ms of backoff) on every call for a vault that will never
680
+ * implement `lagDuration()` would trade an error flood for a latency and
681
+ * RPC-volume regression on exactly the vaults this path targets — a dashboard
682
+ * re-rendering redemptions pays it per render (CLAUDE.md §4.1, §4.2). Whether a
683
+ * deployed pool implements the function is fixed by its bytecode, so a
684
+ * confirmed absence is cacheable forever; the lag *value* is operator-settable
685
+ * and is deliberately never cached.
686
+ *
687
+ * Caveat on "fixed by its bytecode": an **upgradeable proxy** could gain a
688
+ * `lagDuration()` in a later implementation, and a process holding the memo
689
+ * would keep answering `0` for it. Bounded, not unbounded — `CACHE` carries a
690
+ * 24h TTL and `allowStale: true`, which serves an expired entry exactly once
691
+ * before evicting it, so the vault is re-read on the following call. Vaults
692
+ * behind an upgradeable proxy that add the function mid-process therefore
693
+ * misreport a zero lag for at most that window. Accepted deliberately: no
694
+ * deployed pool does this today, and the alternative is re-paying the retry
695
+ * budget forever for the vaults this path exists to serve.
696
+ *
697
+ * @param vaultContract - Anything exposing the vault's `lagDuration()` view;
698
+ * structurally typed so both `ethers.Contract` and the ABI-typed contract
699
+ * returned by `createContract` satisfy it.
700
+ * @param vault - Vault address, for the breadcrumb only.
701
+ * @param scope - Cache scope for the absence memo — the RPC endpoint the read
702
+ * went through. Scoping by endpoint (rather than globally by address) keeps
703
+ * the same address on two chains in separate entries.
704
+ * @returns The lag in seconds, or `0` when the vault has no `lagDuration()`.
705
+ * @throws The original error for any failure that is not an empty response.
706
+ */
707
+ async function readLagDuration(vaultContract, vault, scope) {
708
+ const absenceKey = `lag-absent-${scope}-${vault.toLowerCase()}`;
709
+ // A vault proven not to implement the function cannot start implementing it.
710
+ if (cache_1.CACHE.get(absenceKey) === true)
711
+ return 0;
712
+ try {
713
+ return Number(await (0, core_1.retryOnTransientRpc)('getVaultAvailableRedemptions:lagDuration', () => vaultContract.lagDuration(), { vault }, (error) => (0, core_1.isRetryableRpcError)(error) ||
714
+ (0, core_1.isEmptyViewResponse)(error, LAG_DURATION_SELECTOR)));
715
+ }
716
+ catch (error) {
717
+ if (!(0, core_1.isEmptyViewResponse)(error, LAG_DURATION_SELECTOR))
718
+ throw error;
719
+ // Retries agreed the response is empty — record the absence so the next
720
+ // call short-circuits instead of re-paying the full retry budget.
721
+ cache_1.CACHE.set(absenceKey, true);
722
+ core_1.Logger.log.warn('getVaultAvailableRedemptions', {
723
+ vault,
724
+ reason: 'vault does not implement lagDuration(); assuming no claim lag',
725
+ });
726
+ return 0;
727
+ }
728
+ }
729
+ /**
730
+ * `lagDuration()` — `keccak256("lagDuration()")[0..4]`. Scopes the
731
+ * empty-response tolerance in {@link readLagDuration} to exactly that call, so
732
+ * an empty response to any *other* view still surfaces as an error.
733
+ */
734
+ const LAG_DURATION_SELECTOR = '0x24e86d67';
631
735
  async function getVaultAvailableRedemptions({ vault, wallet, options, prefetchedReads, }) {
632
736
  try {
633
737
  // Stellar vaults don't support on-chain redemptions yet
@@ -674,7 +778,7 @@ async function getVaultAvailableRedemptions({ vault, wallet, options, prefetched
674
778
  }
675
779
  const lagDuration = typeof prefetchedReads?.lagDuration === 'number'
676
780
  ? prefetchedReads.lagDuration
677
- : Number(await vaultContract.lagDuration());
781
+ : await readLagDuration(vaultContract, vault, options.rpcUrl);
678
782
  const { withdrawalRequesteds, withdrawalProcesseds } = await (0, subgraph_1.getSubgraphAllWithdrawals)(vault, provider);
679
783
  // format
680
784
  const availableRedemptions = [];
@@ -954,7 +1058,13 @@ async function getVaultRedemptionHistory({ vault, wallet, lookbackBlocks, option
954
1058
  const currentBlock = await provider.getBlockNumber();
955
1059
  const blockSkip = (0, core_1.determineBlockSkipInternal)(chainId);
956
1060
  const cutoffBlock = currentBlock - (lookbackBlocks ?? (0, core_1.determineBlockCutoff)(chainId));
957
- const BATCH_SIZE = 20; // Max concurrent RPC requests per batch
1061
+ // Max concurrent `eth_getLogs` requests per batch. Kept well under the
1062
+ // per-second cap of the smallest provider plan we run against (QuickNode:
1063
+ // 50/s) because this scan is not the only caller on the connection — a
1064
+ // page rendering several vaults runs several of these concurrently. At 20
1065
+ // the batches reliably tripped `-32007 50/second request limit reached`,
1066
+ // which the retry below now absorbs and this ceiling mostly avoids.
1067
+ const BATCH_SIZE = 8;
958
1068
  core_1.Logger.log.info('getVaultRedemptionHistory', {
959
1069
  vault,
960
1070
  version,
@@ -987,7 +1097,13 @@ async function getVaultRedemptionHistory({ vault, wallet, lookbackBlocks, option
987
1097
  const logs = [];
988
1098
  for (let i = 0; i < ranges.length; i += BATCH_SIZE) {
989
1099
  const batch = ranges.slice(i, i + BATCH_SIZE);
990
- const batchResults = await Promise.allSettled(batch.map((r) => poolContract.queryFilter('WithdrawalProcessed', BigInt(r.from), BigInt(r.to))));
1100
+ const batchResults = await Promise.allSettled(batch.map((r) =>
1101
+ // `eth_getLogs` over a fixed block range is idempotent, so a chunk
1102
+ // rejected by a rate limiter or a flaky socket is safe to repeat.
1103
+ // Without this a single `-32007` burst aborted the whole scan and
1104
+ // the caller lost the redemption history entirely — the same failure
1105
+ // mode the throw below was added to make visible.
1106
+ (0, core_1.retryOnTransientRpc)('getVaultRedemptionHistory:chunk', () => poolContract.queryFilter('WithdrawalProcessed', BigInt(r.from), BigInt(r.to)), { vault, fromBlock: r.from, toBlock: r.to })));
991
1107
  const failed = batchResults.filter((r) => r.status === 'rejected');
992
1108
  if (failed.length > 0) {
993
1109
  const firstReason = failed[0].reason;
package/lib/sdk.d.ts CHANGED
@@ -17419,9 +17419,12 @@ declare type AsArray<T> = T extends readonly unknown[] ? T : never;
17419
17419
  };
17420
17420
 
17421
17421
  /**
17422
- * The default Slack webhook URL for logging errors.
17422
+ * @deprecated Was a hardcoded webhook credential baked into the published
17423
+ * bundle. It now resolves from {@link SLACK_WEBHOOK_ENV_VAR} and is `''` when
17424
+ * unset. Call {@link resolveSlackWebhookUrl} instead — this export exists only
17425
+ * so existing imports keep compiling, and is removed in the next major.
17423
17426
  */
17424
- declare const DEFAULT_SLACK_WEBHOOK_URL = "T04CM84GAV6/B0A2DS3ST8C/FLtOA3Jna3FN7UO4DoGxHfhG";
17427
+ declare const DEFAULT_SLACK_WEBHOOK_URL = "";
17425
17428
 
17426
17429
  /**
17427
17430
  * Deposit a native token (ETH / AVAX / etc.) into a vault via the
@@ -17568,7 +17571,8 @@ declare type AsArray<T> = T extends readonly unknown[] ? T : never;
17568
17571
  /**
17569
17572
  * Decimal precision of the native gas token on every EVM chain this SDK
17570
17573
  * supports (ETH, Base, Arbitrum, Avalanche, Polygon, BNB, HyperEVM, Unichain,
17571
- * Mezo, Monad, Plasma, Ink, Flare, Katana, Citrea, Fluent, Tempo).
17574
+ * Mezo, Monad, Plasma, Ink, Flare, Katana, Citrea, Fluent, Tempo, X Layer —
17575
+ * whose native OKB is also 18 decimals).
17572
17576
  *
17573
17577
  * All of them use 18-decimal native tokens, so this is a single constant rather
17574
17578
  * than a per-chain map. It exists because the native token has no ERC-20
@@ -18322,10 +18326,16 @@ declare type AsArray<T> = T extends readonly unknown[] ? T : never;
18322
18326
  * Fetch token decimals from contract or Solana mint.
18323
18327
  * Results are cached to minimize RPC calls.
18324
18328
  *
18325
- * **Never throws** — a failed read logs at error level and resolves
18326
- * `undefined`. Callers that must not silently proceed on an unknown scale (any
18327
- * path that encodes an amount) should use {@link getDecimalsOrThrow} instead:
18328
- * feeding `undefined` into `toNormalizedBn` silently defaults to 18 decimals.
18329
+ * **Never throws** — a failed read resolves `undefined`. Callers that must not
18330
+ * silently proceed on an unknown scale (any path that encodes an amount) should
18331
+ * use {@link getDecimalsOrThrow} instead: feeding `undefined` into
18332
+ * `toNormalizedBn` silently defaults to 18 decimals.
18333
+ *
18334
+ * Transient transport faults (provider rate limits, socket resets, an empty
18335
+ * response to `decimals()`) are logged at `warn` — a breadcrumb, not a
18336
+ * standalone Sentry issue — since the caller sees the same `undefined` either
18337
+ * way. Everything else is logged at `error`. The read itself is **not**
18338
+ * retried here; that is opt-in via {@link getDecimalsOrThrow}.
18329
18339
  *
18330
18340
  * @param provider Web3 provider
18331
18341
  * @param address Token contract address or Solana mint
@@ -19429,7 +19439,7 @@ declare type AsArray<T> = T extends readonly unknown[] ? T : never;
19429
19439
  * Subgraph base URL
19430
19440
  * @deprecated use getVaultMetadata to fetch subgraph URLs from the backend
19431
19441
  */
19432
- export declare const GOLDSKY_BASE_URL = "https://api.goldsky.com/api/private/project_cm9g0xy3o4j6v01vd34r3hvv9/subgraphs";
19442
+ export declare const GOLDSKY_BASE_URL = "https://api.goldsky.com/api/public/project_cm9g0xy3o4j6v01vd34r3hvv9/subgraphs";
19433
19443
 
19434
19444
  /**
19435
19445
  * Deposit funds into a Solana August vault and mint share tokens.
@@ -24058,6 +24068,31 @@ declare type AsArray<T> = T extends readonly unknown[] ? T : never;
24058
24068
  */
24059
24069
  export declare function resolveOriginCode(provided?: `0x${string}`): `0x${string}`;
24060
24070
 
24071
+ /**
24072
+ * Resolve the Slack webhook to alert through.
24073
+ *
24074
+ * Why this replaced a hardcoded constant: a webhook path is a **bearer
24075
+ * credential** — anyone holding it can post to the channel. The previous
24076
+ * default embedded August's own webhook as a string literal, which shipped in
24077
+ * every published tarball and in the generated `.d.ts`, handing every consumer
24078
+ * of the SDK write access to an internal Slack channel (CLAUDE.md §5: no
24079
+ * secrets in client-reachable code). It also meant a consumer's alerts went to
24080
+ * August's channel rather than their own, with no way to redirect them.
24081
+ *
24082
+ * Resolution order:
24083
+ * 1. `explicit` — passed by the caller, wins outright;
24084
+ * 2. `AUGUST_SDK_SLACK_WEBHOOK_URL` in the environment;
24085
+ * 3. none — alerting is disabled and the call becomes a no-op.
24086
+ *
24087
+ * Reading the env var per call (rather than once at module load) means a
24088
+ * consumer configuring it after import still gets alerts, and a test can set
24089
+ * and unset it without re-importing the module.
24090
+ *
24091
+ * @param explicit - Caller-supplied webhook, full URL or bare path suffix.
24092
+ * @returns The bare `T…/B…/x…` path suffix, or `''` when unconfigured.
24093
+ */
24094
+ declare function resolveSlackWebhookUrl(explicit?: string): string;
24095
+
24061
24096
  /* Excluded from this release type: resolveSpender */
24062
24097
 
24063
24098
  /**
@@ -25088,12 +25123,21 @@ declare type AsArray<T> = T extends readonly unknown[] ? T : never;
25088
25123
 
25089
25124
  declare namespace Slack {
25090
25125
  export {
25126
+ resolveSlackWebhookUrl,
25127
+ SLACK_WEBHOOK_ENV_VAR,
25091
25128
  DEFAULT_SLACK_WEBHOOK_URL,
25092
25129
  SLACK
25093
25130
  }
25094
25131
  }
25095
25132
  export { Slack }
25096
25133
 
25134
+ /**
25135
+ * Environment variable holding the Slack incoming-webhook to post SDK alerts
25136
+ * to. Accepts either the full `https://hooks.slack.com/services/T…/B…/x…` URL
25137
+ * or the bare `T…/B…/x…` path suffix.
25138
+ */
25139
+ declare const SLACK_WEBHOOK_ENV_VAR = "AUGUST_SDK_SLACK_WEBHOOK_URL";
25140
+
25097
25141
  export declare const Solana: {
25098
25142
  utils: {
25099
25143
  getExplorerLink: ({ signature, type, network, }: {
@@ -16,7 +16,7 @@ const fetcher_2 = require("../../modules/vaults/fetcher");
16
16
  /**
17
17
  * Utils
18
18
  */
19
- const GOLDSKY_API_KEY = 'cmd0lz6qf35lg01ty7u20aijy';
19
+ const GOLDSKY_API_KEY = '';
20
20
  // Dedupe window for "Missing Subgraph" alerts (per pool).
21
21
  const MISSING_SUBGRAPH_ALERT_TTL_MS = 60 * 60 * 1000; // 1 hour
22
22
  /**
@@ -371,7 +371,7 @@ const TRANSFER_QUERY_PROPS = `
371
371
  * @param provider
372
372
  * @returns
373
373
  */
374
- async function getSubgraphWithdrawRequests(pool, provider, slackWebookUrl = slack_1.DEFAULT_SLACK_WEBHOOK_URL) {
374
+ async function getSubgraphWithdrawRequests(pool, provider, slackWebookUrl = (0, slack_1.resolveSlackWebhookUrl)()) {
375
375
  // setup
376
376
  let vaultSymbol = await (0, core_1.getVaultSymbol)(pool, provider);
377
377
  if (vaultSymbol === undefined) {
@@ -443,7 +443,7 @@ async function getSubgraphWithdrawRequests(pool, provider, slackWebookUrl = slac
443
443
  requests.push(...(json?.data?.withdrawalRequesteds || []));
444
444
  return requests;
445
445
  }
446
- async function getSubgraphWithdrawProccessed(pool, provider, slackWebookUrl = slack_1.DEFAULT_SLACK_WEBHOOK_URL) {
446
+ async function getSubgraphWithdrawProccessed(pool, provider, slackWebookUrl = (0, slack_1.resolveSlackWebhookUrl)()) {
447
447
  try {
448
448
  // setup
449
449
  let vaultSymbol = await (0, core_1.getVaultSymbol)(pool, provider);
@@ -526,7 +526,7 @@ async function getSubgraphWithdrawProccessed(pool, provider, slackWebookUrl = sl
526
526
  * @param provider
527
527
  * @returns
528
528
  */
529
- async function getSubgraphAllWithdrawals(pool, provider, slackWebookUrl = slack_1.DEFAULT_SLACK_WEBHOOK_URL) {
529
+ async function getSubgraphAllWithdrawals(pool, provider, slackWebookUrl = (0, slack_1.resolveSlackWebhookUrl)()) {
530
530
  let query = '';
531
531
  try {
532
532
  // setup
@@ -695,7 +695,7 @@ async function getArchivedVaultUserHistory(user, provider, pool) {
695
695
  // chronology. The backend returns rows in arbitrary order, so re-sort here.
696
696
  .sort((a, b) => Number(a.timestamp_) - Number(b.timestamp_)));
697
697
  }
698
- async function getSubgraphUserHistory(user, provider, pool, slackWebookUrl = slack_1.DEFAULT_SLACK_WEBHOOK_URL) {
698
+ async function getSubgraphUserHistory(user, provider, pool, slackWebookUrl = (0, slack_1.resolveSlackWebhookUrl)()) {
699
699
  try {
700
700
  // This path is EVM-only: it reads chain id and network from an EVM
701
701
  // JsonRpcProvider (eth_chainId / getNetwork) and queries an EVM subgraph.
@@ -847,7 +847,7 @@ async function getSubgraphUserHistory(user, provider, pool, slackWebookUrl = sla
847
847
  return [];
848
848
  }
849
849
  }
850
- async function getSubgraphVaultHistory(provider, pool, slackWebookUrl = slack_1.DEFAULT_SLACK_WEBHOOK_URL, opts = {}) {
850
+ async function getSubgraphVaultHistory(provider, pool, slackWebookUrl = (0, slack_1.resolveSlackWebhookUrl)(), opts = {}) {
851
851
  try {
852
852
  // setup
853
853
  const requests = [];
@@ -1008,7 +1008,7 @@ async function getSubgraphVaultHistory(provider, pool, slackWebookUrl = slack_1.
1008
1008
  return [];
1009
1009
  }
1010
1010
  }
1011
- async function getSubgraphUserTransfers(user, provider, pool, slackWebookUrl = slack_1.DEFAULT_SLACK_WEBHOOK_URL) {
1011
+ async function getSubgraphUserTransfers(user, provider, pool, slackWebookUrl = (0, slack_1.resolveSlackWebhookUrl)()) {
1012
1012
  try {
1013
1013
  // setup
1014
1014
  let amountOfTransfers = 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@augustdigital/sdk",
3
- "version": "8.21.0",
3
+ "version": "8.22.1",
4
4
  "main": "lib/index.js",
5
5
  "types": "lib/sdk.d.ts",
6
6
  "keywords": [
@@ -66,6 +66,7 @@
66
66
  "test:jest:watch": "jest --config jest.config.unit.js --watch",
67
67
  "test:jest:coverage": "jest --config jest.config.unit.js --coverage",
68
68
  "test:forknet": "node tests/forknet/run.mjs",
69
+ "test:solana-localnet": "node tests/solana-localnet/run.mjs",
69
70
  "test:solana-idl": "jest --config jest.config.idl-drift.js",
70
71
  "clean": "rm -rf ./lib",
71
72
  "format": "biome check --write .",