@augustdigital/sdk 8.21.1 → 8.24.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.
@@ -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.1";
6
+ export declare const SDK_VERSION = "8.24.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.21.1';
9
+ exports.SDK_VERSION = '8.24.0';
10
10
  //# sourceMappingURL=version.js.map
@@ -42,10 +42,15 @@ export interface IAttributionConfig {
42
42
  /**
43
43
  * EVM chain IDs to attribute. Omit to attribute writes on every EVM chain
44
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.
45
+ * ~16 gas per non-zero byte). When set, gating is fail-closed: writes on
46
+ * other chains and writes whose chain ID cannot be determined are sent
47
+ * without the suffix.
48
+ *
49
+ * Over-attribution is not harmless. The suffix makes calldata longer than
50
+ * the ABI encoding of the call, which breaks clear-signing on hardware
51
+ * wallets: a Ledger rejects an over-long ERC-20 `approve` with
52
+ * `EthAppCommandError: Invalid data 6a80`, so an unattributed chain that
53
+ * receives the suffix anyway cannot be transacted on from a Ledger at all.
49
54
  */
50
55
  chains?: number[];
51
56
  }
@@ -83,13 +88,23 @@ export declare function buildAttributionSuffix(codes: string[]): string;
83
88
  * init, not on the first write.
84
89
  */
85
90
  export declare function setAttribution(config: IAttributionConfig | null): void;
91
+ /**
92
+ * Whether attribution is configured at all, independent of any chain gate.
93
+ *
94
+ * Call sites that need to know whether to resolve a chain ID before asking
95
+ * for the suffix use this; {@link getAttributionSuffix} applies the gate.
96
+ *
97
+ * @returns `true` when builder codes are configured.
98
+ */
99
+ export declare function isAttributionEnabled(): boolean;
86
100
  /**
87
101
  * 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.
102
+ * `undefined` when attribution is off or the chain is not attributed.
89
103
  *
90
104
  * @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
105
+ * it. When a `chains` restriction is configured, gating is fail-closed: an
106
+ * omitted chain ID yields no suffix, since a suffix on an unattributed
107
+ * chain buys nothing and breaks hardware-wallet clear-signing (see
93
108
  * {@link IAttributionConfig.chains}).
94
109
  * @returns `0x`-prefixed hex suffix, or `undefined` when nothing should be
95
110
  * appended.
@@ -99,7 +114,8 @@ export declare function getAttributionSuffix(chainId?: number): string | undefin
99
114
  * Append the active attribution suffix to calldata.
100
115
  *
101
116
  * No-ops (returns `data` unchanged) when attribution is off, the chain is
102
- * excluded, `data` is empty/absent (plain value transfers are never
117
+ * not attributed (including an unknown chain under a `chains` restriction),
118
+ * `data` is empty/absent (plain value transfers are never
103
119
  * attributed), or `data` already ends with the ERC-8021 marker (guards
104
120
  * against double-appending when an upstream layer — e.g. a wagmi config
105
121
  * `dataSuffix` — already attributed the transaction).
@@ -18,6 +18,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
18
18
  exports.ERC8021_MARKER = void 0;
19
19
  exports.buildAttributionSuffix = buildAttributionSuffix;
20
20
  exports.setAttribution = setAttribution;
21
+ exports.isAttributionEnabled = isAttributionEnabled;
21
22
  exports.getAttributionSuffix = getAttributionSuffix;
22
23
  exports.appendAttributionSuffix = appendAttributionSuffix;
23
24
  /**
@@ -96,13 +97,25 @@ function setAttribution(config) {
96
97
  ? config.chains
97
98
  : null;
98
99
  }
100
+ /**
101
+ * Whether attribution is configured at all, independent of any chain gate.
102
+ *
103
+ * Call sites that need to know whether to resolve a chain ID before asking
104
+ * for the suffix use this; {@link getAttributionSuffix} applies the gate.
105
+ *
106
+ * @returns `true` when builder codes are configured.
107
+ */
108
+ function isAttributionEnabled() {
109
+ return activeSuffix !== null;
110
+ }
99
111
  /**
100
112
  * 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.
113
+ * `undefined` when attribution is off or the chain is not attributed.
102
114
  *
103
115
  * @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
116
+ * it. When a `chains` restriction is configured, gating is fail-closed: an
117
+ * omitted chain ID yields no suffix, since a suffix on an unattributed
118
+ * chain buys nothing and breaks hardware-wallet clear-signing (see
106
119
  * {@link IAttributionConfig.chains}).
107
120
  * @returns `0x`-prefixed hex suffix, or `undefined` when nothing should be
108
121
  * appended.
@@ -110,7 +123,9 @@ function setAttribution(config) {
110
123
  function getAttributionSuffix(chainId) {
111
124
  if (!activeSuffix)
112
125
  return undefined;
113
- if (activeChains && typeof chainId === 'number') {
126
+ if (activeChains) {
127
+ if (typeof chainId !== 'number')
128
+ return undefined;
114
129
  if (!activeChains.includes(chainId))
115
130
  return undefined;
116
131
  }
@@ -120,7 +135,8 @@ function getAttributionSuffix(chainId) {
120
135
  * Append the active attribution suffix to calldata.
121
136
  *
122
137
  * No-ops (returns `data` unchanged) when attribution is off, the chain is
123
- * excluded, `data` is empty/absent (plain value transfers are never
138
+ * not attributed (including an unknown chain under a `chains` restriction),
139
+ * `data` is empty/absent (plain value transfers are never
124
140
  * attributed), or `data` already ends with the ERC-8021 marker (guards
125
141
  * against double-appending when an upstream layer — e.g. a wagmi config
126
142
  * `dataSuffix` — already attributed the transaction).
@@ -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
@@ -44,7 +44,8 @@ export type CompatibleSigner = Signer | Wallet | any;
44
44
  * already ending in the ERC-8021 marker is left untouched. When the
45
45
  * configured `chains` list requires a chain check and the transaction does
46
46
  * not carry a `chainId`, the signer's provider network is consulted (one
47
- * cached RPC call).
47
+ * cached RPC call); if that lookup fails the transaction is sent
48
+ * unattributed.
48
49
  *
49
50
  * @param signer Normalized ethers Signer or Wallet.
50
51
  * @returns A proxied signer with an attribution-aware `sendTransaction`.
@@ -107,7 +107,8 @@ async function normalizeSigner(signer) {
107
107
  * already ending in the ERC-8021 marker is left untouched. When the
108
108
  * configured `chains` list requires a chain check and the transaction does
109
109
  * not carry a `chainId`, the signer's provider network is consulted (one
110
- * cached RPC call).
110
+ * cached RPC call); if that lookup fails the transaction is sent
111
+ * unattributed.
111
112
  *
112
113
  * @param signer Normalized ethers Signer or Wallet.
113
114
  * @returns A proxied signer with an attribution-aware `sendTransaction`.
@@ -117,7 +118,7 @@ function wrapSignerWithAttribution(signer) {
117
118
  get(target, prop) {
118
119
  if (prop === 'sendTransaction') {
119
120
  return async (tx) => {
120
- if (!(0, attribution_1.getAttributionSuffix)() || typeof tx?.data !== 'string') {
121
+ if (!(0, attribution_1.isAttributionEnabled)() || typeof tx?.data !== 'string') {
121
122
  return target.sendTransaction(tx);
122
123
  }
123
124
  let chainId = tx.chainId != null ? Number(tx.chainId) : undefined;
@@ -126,9 +127,10 @@ function wrapSignerWithAttribution(signer) {
126
127
  chainId = Number((await target.provider.getNetwork()).chainId);
127
128
  }
128
129
  catch {
129
- // Unknown chain: fall through with chainId undefined, which
130
- // appends regardless of a `chains` restriction —
131
- // over-attribution is harmless, under-attribution loses data.
130
+ // Unknown chain: fall through with chainId undefined. Under a
131
+ // `chains` restriction that means no suffix attributing a
132
+ // chain we cannot identify risks breaking hardware-wallet
133
+ // clear-signing for a gain we cannot confirm.
132
134
  }
133
135
  }
134
136
  const data = (0, attribution_1.appendAttributionSuffix)(tx.data, chainId);
@@ -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 = {
@@ -43,6 +43,10 @@ export declare function needsCrossChainApproval(tokenAddress: IAddress, spenderA
43
43
  /**
44
44
  * Execute a token approval for a cross-chain operation.
45
45
  *
46
+ * @param chainId Chain the approval is sent on, used to gate ERC-8021
47
+ * attribution. Omitted means unattributed under a `chains` restriction —
48
+ * the suffix would lengthen `approve` calldata past its ABI encoding and
49
+ * break Ledger clear-signing.
46
50
  * @returns Transaction hash of the approval
47
51
  */
48
52
  export declare function approveCrossChain(tokenAddress: IAddress, spenderAddress: IAddress, amount: bigint, walletClient: {
@@ -56,7 +60,7 @@ export declare function approveCrossChain(tokenAddress: IAddress, spenderAddress
56
60
  }) => Promise<{
57
61
  status: string;
58
62
  }>;
59
- }): Promise<string>;
63
+ }, chainId?: number): Promise<string>;
60
64
  /**
61
65
  * Execute a cross-chain deposit via LayerZero OVault. Validates the source
62
66
  * chain, ensures approval, estimates gas with a buffer, and waits for the
@@ -526,9 +526,13 @@ async function needsCrossChainApproval(tokenAddress, spenderAddress, walletAddre
526
526
  /**
527
527
  * Execute a token approval for a cross-chain operation.
528
528
  *
529
+ * @param chainId Chain the approval is sent on, used to gate ERC-8021
530
+ * attribution. Omitted means unattributed under a `chains` restriction —
531
+ * the suffix would lengthen `approve` calldata past its ABI encoding and
532
+ * break Ledger clear-signing.
529
533
  * @returns Transaction hash of the approval
530
534
  */
531
- async function approveCrossChain(tokenAddress, spenderAddress, amount, walletClient, publicClient) {
535
+ async function approveCrossChain(tokenAddress, spenderAddress, amount, walletClient, publicClient, chainId) {
532
536
  if (!walletClient.account) {
533
537
  throw new Error('Wallet not ready — please reconnect your wallet');
534
538
  }
@@ -538,7 +542,7 @@ async function approveCrossChain(tokenAddress, spenderAddress, amount, walletCli
538
542
  abi: OFT_1.ABI_CROSS_CHAIN_ERC20,
539
543
  functionName: 'approve',
540
544
  args: [spenderAddress, amount],
541
- dataSuffix: (0, attribution_1.getAttributionSuffix)(),
545
+ dataSuffix: (0, attribution_1.getAttributionSuffix)(chainId),
542
546
  });
543
547
  const receipt = await publicClient.waitForTransactionReceipt({
544
548
  hash: hash,
@@ -595,7 +599,7 @@ async function crossChainVaultDeposit(props) {
595
599
  BigInt((0, core_1.toNormalizedBn)(props.amount, props.decimals).raw);
596
600
  const approvalNeeded = await needsCrossChainApproval(tokenAddr, spenderAddr, props.walletAddress, approvalAmount, publicClient);
597
601
  if (approvalNeeded) {
598
- await approveCrossChain(tokenAddr, spenderAddr, approvalAmount, walletClient, publicClient);
602
+ await approveCrossChain(tokenAddr, spenderAddr, approvalAmount, walletClient, publicClient, props.userChainId);
599
603
  }
600
604
  }
601
605
  // 3. Estimate gas with buffer
@@ -678,7 +682,7 @@ async function crossChainVaultRedeem(props) {
678
682
  if (!props.skipApprovalCheck && txInputs.approval) {
679
683
  const approvalNeeded = await needsCrossChainApproval(txInputs.approval.tokenAddress, txInputs.approval.spender, props.walletAddress, txInputs.approval.amount, publicClient);
680
684
  if (approvalNeeded) {
681
- await approveCrossChain(txInputs.approval.tokenAddress, txInputs.approval.spender, txInputs.approval.amount, walletClient, publicClient);
685
+ await approveCrossChain(txInputs.approval.tokenAddress, txInputs.approval.spender, txInputs.approval.amount, walletClient, publicClient, props.config.hubChainId);
682
686
  }
683
687
  }
684
688
  // 4. Estimate gas with buffer
@@ -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
@@ -15020,7 +15020,8 @@ export declare function allowance(signer: IContractRunner, options: IAllowanceOp
15020
15020
  * Append the active attribution suffix to calldata.
15021
15021
  *
15022
15022
  * No-ops (returns `data` unchanged) when attribution is off, the chain is
15023
- * excluded, `data` is empty/absent (plain value transfers are never
15023
+ * not attributed (including an unknown chain under a `chains` restriction),
15024
+ * `data` is empty/absent (plain value transfers are never
15024
15025
  * attributed), or `data` already ends with the ERC-8021 marker (guards
15025
15026
  * against double-appending when an upstream layer — e.g. a wagmi config
15026
15027
  * `dataSuffix` — already attributed the transaction).
@@ -15065,6 +15066,10 @@ export declare function approve(signer: Signer | Wallet, options: IContractWrite
15065
15066
  /**
15066
15067
  * Execute a token approval for a cross-chain operation.
15067
15068
  *
15069
+ * @param chainId Chain the approval is sent on, used to gate ERC-8021
15070
+ * attribution. Omitted means unattributed under a `chains` restriction —
15071
+ * the suffix would lengthen `approve` calldata past its ABI encoding and
15072
+ * break Ledger clear-signing.
15068
15073
  * @returns Transaction hash of the approval
15069
15074
  */
15070
15075
  export declare function approveCrossChain(tokenAddress: IAddress, spenderAddress: IAddress, amount: bigint, walletClient: {
@@ -15078,7 +15083,7 @@ export declare function approveCrossChain(tokenAddress: IAddress, spenderAddress
15078
15083
  }) => Promise<{
15079
15084
  status: string;
15080
15085
  }>;
15081
- }): Promise<string>;
15086
+ }, chainId?: number): Promise<string>;
15082
15087
 
15083
15088
  /**
15084
15089
  * Discriminated union returned by {@link approve}. Lets callers tell apart
@@ -17419,9 +17424,12 @@ declare type AsArray<T> = T extends readonly unknown[] ? T : never;
17419
17424
  };
17420
17425
 
17421
17426
  /**
17422
- * The default Slack webhook URL for logging errors.
17427
+ * @deprecated Was a hardcoded webhook credential baked into the published
17428
+ * bundle. It now resolves from {@link SLACK_WEBHOOK_ENV_VAR} and is `''` when
17429
+ * unset. Call {@link resolveSlackWebhookUrl} instead — this export exists only
17430
+ * so existing imports keep compiling, and is removed in the next major.
17423
17431
  */
17424
- declare const DEFAULT_SLACK_WEBHOOK_URL = "T04CM84GAV6/B0A2DS3ST8C/FLtOA3Jna3FN7UO4DoGxHfhG";
17432
+ declare const DEFAULT_SLACK_WEBHOOK_URL = "";
17425
17433
 
17426
17434
  /**
17427
17435
  * Deposit a native token (ETH / AVAX / etc.) into a vault via the
@@ -18274,11 +18282,12 @@ declare type AsArray<T> = T extends readonly unknown[] ? T : never;
18274
18282
 
18275
18283
  /**
18276
18284
  * Return the active ERC-8021 suffix for a write on the given chain, or
18277
- * `undefined` when attribution is off or the chain is excluded.
18285
+ * `undefined` when attribution is off or the chain is not attributed.
18278
18286
  *
18279
18287
  * @param chainId EVM chain ID of the transaction, when the call site knows
18280
- * it. When omitted and a `chains` restriction is configured, the suffix is
18281
- * returned anyway (over-attribution is harmless; see
18288
+ * it. When a `chains` restriction is configured, gating is fail-closed: an
18289
+ * omitted chain ID yields no suffix, since a suffix on an unattributed
18290
+ * chain buys nothing and breaks hardware-wallet clear-signing (see
18282
18291
  * {@link IAttributionConfig.chains}).
18283
18292
  * @returns `0x`-prefixed hex suffix, or `undefined` when nothing should be
18284
18293
  * appended.
@@ -18323,10 +18332,16 @@ declare type AsArray<T> = T extends readonly unknown[] ? T : never;
18323
18332
  * Fetch token decimals from contract or Solana mint.
18324
18333
  * Results are cached to minimize RPC calls.
18325
18334
  *
18326
- * **Never throws** — a failed read logs at error level and resolves
18327
- * `undefined`. Callers that must not silently proceed on an unknown scale (any
18328
- * path that encodes an amount) should use {@link getDecimalsOrThrow} instead:
18329
- * feeding `undefined` into `toNormalizedBn` silently defaults to 18 decimals.
18335
+ * **Never throws** — a failed read resolves `undefined`. Callers that must not
18336
+ * silently proceed on an unknown scale (any path that encodes an amount) should
18337
+ * use {@link getDecimalsOrThrow} instead: feeding `undefined` into
18338
+ * `toNormalizedBn` silently defaults to 18 decimals.
18339
+ *
18340
+ * Transient transport faults (provider rate limits, socket resets, an empty
18341
+ * response to `decimals()`) are logged at `warn` — a breadcrumb, not a
18342
+ * standalone Sentry issue — since the caller sees the same `undefined` either
18343
+ * way. Everything else is logged at `error`. The read itself is **not**
18344
+ * retried here; that is opt-in via {@link getDecimalsOrThrow}.
18330
18345
  *
18331
18346
  * @param provider Web3 provider
18332
18347
  * @param address Token contract address or Solana mint
@@ -19430,7 +19445,7 @@ declare type AsArray<T> = T extends readonly unknown[] ? T : never;
19430
19445
  * Subgraph base URL
19431
19446
  * @deprecated use getVaultMetadata to fetch subgraph URLs from the backend
19432
19447
  */
19433
- export declare const GOLDSKY_BASE_URL = "https://api.goldsky.com/api/private/project_cm9g0xy3o4j6v01vd34r3hvv9/subgraphs";
19448
+ export declare const GOLDSKY_BASE_URL = "https://api.goldsky.com/api/public/project_cm9g0xy3o4j6v01vd34r3hvv9/subgraphs";
19434
19449
 
19435
19450
  /**
19436
19451
  * Deposit funds into a Solana August vault and mint share tokens.
@@ -19640,10 +19655,15 @@ declare type AsArray<T> = T extends readonly unknown[] ? T : never;
19640
19655
  /**
19641
19656
  * EVM chain IDs to attribute. Omit to attribute writes on every EVM chain
19642
19657
  * (the suffix is inert on chains without an ERC-8021 indexer and costs
19643
- * ~16 gas per non-zero byte). When set, writes on other chains are sent
19644
- * without the suffix; call sites that cannot determine their chain ID
19645
- * append the suffix regardless, since over-attribution is harmless and
19646
- * under-attribution loses data.
19658
+ * ~16 gas per non-zero byte). When set, gating is fail-closed: writes on
19659
+ * other chains and writes whose chain ID cannot be determined are sent
19660
+ * without the suffix.
19661
+ *
19662
+ * Over-attribution is not harmless. The suffix makes calldata longer than
19663
+ * the ABI encoding of the call, which breaks clear-signing on hardware
19664
+ * wallets: a Ledger rejects an over-long ERC-20 `approve` with
19665
+ * `EthAppCommandError: Invalid data 6a80`, so an unattributed chain that
19666
+ * receives the suffix anyway cannot be transacted on from a Ledger at all.
19647
19667
  */
19648
19668
  chains?: number[];
19649
19669
  }
@@ -21057,6 +21077,16 @@ declare type AsArray<T> = T extends readonly unknown[] ? T : never;
21057
21077
  */
21058
21078
  export declare function isAnalyticsForcedOnViaEnv(): boolean;
21059
21079
 
21080
+ /**
21081
+ * Whether attribution is configured at all, independent of any chain gate.
21082
+ *
21083
+ * Call sites that need to know whether to resolve a chain ID before asking
21084
+ * for the suffix use this; {@link getAttributionSuffix} applies the gate.
21085
+ *
21086
+ * @returns `true` when builder codes are configured.
21087
+ */
21088
+ export declare function isAttributionEnabled(): boolean;
21089
+
21060
21090
  /** Type guard. Works across realms (e.g. Web Worker / VM contexts). */
21061
21091
  export declare function isAugustSDKError(err: unknown): err is AugustSDKError;
21062
21092
 
@@ -24059,6 +24089,31 @@ declare type AsArray<T> = T extends readonly unknown[] ? T : never;
24059
24089
  */
24060
24090
  export declare function resolveOriginCode(provided?: `0x${string}`): `0x${string}`;
24061
24091
 
24092
+ /**
24093
+ * Resolve the Slack webhook to alert through.
24094
+ *
24095
+ * Why this replaced a hardcoded constant: a webhook path is a **bearer
24096
+ * credential** — anyone holding it can post to the channel. The previous
24097
+ * default embedded August's own webhook as a string literal, which shipped in
24098
+ * every published tarball and in the generated `.d.ts`, handing every consumer
24099
+ * of the SDK write access to an internal Slack channel (CLAUDE.md §5: no
24100
+ * secrets in client-reachable code). It also meant a consumer's alerts went to
24101
+ * August's channel rather than their own, with no way to redirect them.
24102
+ *
24103
+ * Resolution order:
24104
+ * 1. `explicit` — passed by the caller, wins outright;
24105
+ * 2. `AUGUST_SDK_SLACK_WEBHOOK_URL` in the environment;
24106
+ * 3. none — alerting is disabled and the call becomes a no-op.
24107
+ *
24108
+ * Reading the env var per call (rather than once at module load) means a
24109
+ * consumer configuring it after import still gets alerts, and a test can set
24110
+ * and unset it without re-importing the module.
24111
+ *
24112
+ * @param explicit - Caller-supplied webhook, full URL or bare path suffix.
24113
+ * @returns The bare `T…/B…/x…` path suffix, or `''` when unconfigured.
24114
+ */
24115
+ declare function resolveSlackWebhookUrl(explicit?: string): string;
24116
+
24062
24117
  /* Excluded from this release type: resolveSpender */
24063
24118
 
24064
24119
  /**
@@ -25089,12 +25144,21 @@ declare type AsArray<T> = T extends readonly unknown[] ? T : never;
25089
25144
 
25090
25145
  declare namespace Slack {
25091
25146
  export {
25147
+ resolveSlackWebhookUrl,
25148
+ SLACK_WEBHOOK_ENV_VAR,
25092
25149
  DEFAULT_SLACK_WEBHOOK_URL,
25093
25150
  SLACK
25094
25151
  }
25095
25152
  }
25096
25153
  export { Slack }
25097
25154
 
25155
+ /**
25156
+ * Environment variable holding the Slack incoming-webhook to post SDK alerts
25157
+ * to. Accepts either the full `https://hooks.slack.com/services/T…/B…/x…` URL
25158
+ * or the bare `T…/B…/x…` path suffix.
25159
+ */
25160
+ declare const SLACK_WEBHOOK_ENV_VAR = "AUGUST_SDK_SLACK_WEBHOOK_URL";
25161
+
25098
25162
  export declare const Solana: {
25099
25163
  utils: {
25100
25164
  getExplorerLink: ({ signature, type, network, }: {
@@ -26791,7 +26855,8 @@ declare type AsArray<T> = T extends readonly unknown[] ? T : never;
26791
26855
  * already ending in the ERC-8021 marker is left untouched. When the
26792
26856
  * configured `chains` list requires a chain check and the transaction does
26793
26857
  * not carry a `chainId`, the signer's provider network is consulted (one
26794
- * cached RPC call).
26858
+ * cached RPC call); if that lookup fails the transaction is sent
26859
+ * unattributed.
26795
26860
  *
26796
26861
  * @param signer Normalized ethers Signer or Wallet.
26797
26862
  * @returns A proxied signer with an attribution-aware `sendTransaction`.
@@ -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.1",
3
+ "version": "8.24.0",
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 .",