@augustdigital/sdk 8.10.0 → 8.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/adapters/evm/index.d.ts +17 -1
- package/lib/adapters/evm/index.js +20 -0
- package/lib/adapters/stellar/actions.js +4 -4
- package/lib/adapters/stellar/getters.d.ts +9 -3
- package/lib/adapters/stellar/getters.js +18 -6
- package/lib/adapters/stellar/index.d.ts +17 -2
- package/lib/adapters/stellar/index.js +24 -5
- package/lib/adapters/stellar/soroban.d.ts +89 -2
- package/lib/adapters/stellar/soroban.js +487 -64
- package/lib/adapters/stellar/submit.d.ts +4 -1
- package/lib/adapters/stellar/submit.js +7 -3
- package/lib/adapters/stellar/types.d.ts +12 -0
- package/lib/core/analytics/method-taxonomy.js +1 -0
- package/lib/core/analytics/sanitize.js +19 -3
- package/lib/core/analytics/version.d.ts +1 -1
- package/lib/core/analytics/version.js +1 -1
- package/lib/core/base.class.d.ts +2 -1
- package/lib/core/constants/swap-router.d.ts +2 -0
- package/lib/core/constants/swap-router.js +1 -0
- package/lib/core/constants/web3.d.ts +19 -0
- package/lib/core/constants/web3.js +37 -1
- package/lib/core/helpers/multicall.d.ts +68 -0
- package/lib/core/helpers/multicall.js +103 -0
- package/lib/core/helpers/swap-router.d.ts +36 -1
- package/lib/core/helpers/swap-router.js +80 -0
- package/lib/core/helpers/vault-version.d.ts +4 -1
- package/lib/core/helpers/vaults.d.ts +1 -1
- package/lib/core/index.d.ts +1 -0
- package/lib/core/index.js +1 -0
- package/lib/main.js +6 -1
- package/lib/modules/vaults/getters.d.ts +25 -2
- package/lib/modules/vaults/getters.js +46 -12
- package/lib/modules/vaults/main.d.ts +28 -0
- package/lib/modules/vaults/main.js +89 -0
- package/lib/modules/vaults/prefetch.d.ts +65 -0
- package/lib/modules/vaults/prefetch.js +120 -0
- package/lib/modules/vaults/types.d.ts +11 -1
- package/lib/sdk.d.ts +6725 -6507
- package/lib/types/subgraph.d.ts +9 -0
- package/lib/types/vaults.d.ts +44 -0
- package/lib/types/web3.d.ts +15 -0
- package/package.json +1 -1
|
@@ -8,6 +8,12 @@
|
|
|
8
8
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
9
|
exports.createServer = createServer;
|
|
10
10
|
exports.resolveNetworkConfig = resolveNetworkConfig;
|
|
11
|
+
exports.resetHealthyServerCache = resetHealthyServerCache;
|
|
12
|
+
exports.redactRpcUrl = redactRpcUrl;
|
|
13
|
+
exports.getHealthyServer = getHealthyServer;
|
|
14
|
+
exports.isRetryableRpcError = isRetryableRpcError;
|
|
15
|
+
exports.isRetryableSimulationError = isRetryableSimulationError;
|
|
16
|
+
exports.withEndpointFailover = withEndpointFailover;
|
|
11
17
|
exports.toBigIntAmount = toBigIntAmount;
|
|
12
18
|
exports.queryContract = queryContract;
|
|
13
19
|
exports.buildSorobanTx = buildSorobanTx;
|
|
@@ -20,14 +26,410 @@ function createServer(rpcUrl) {
|
|
|
20
26
|
});
|
|
21
27
|
}
|
|
22
28
|
/**
|
|
23
|
-
*
|
|
29
|
+
* Ordered public Soroban RPC endpoints per network, for health-gated failover.
|
|
30
|
+
*
|
|
31
|
+
* The first entry is the default primary ({@link SOROBAN_RPC_URLS}); the rest
|
|
32
|
+
* are keyless public fallbacks. A provider node can stall — most visibly around
|
|
33
|
+
* a protocol upgrade, when it freezes behind the network head and every
|
|
34
|
+
* simulation fails with a snapshot error — so a single endpoint means one bad
|
|
35
|
+
* node takes down all reads, deposits, and submits. Module-private: an internal
|
|
36
|
+
* resilience detail, not part of the adapter's public surface.
|
|
37
|
+
*/
|
|
38
|
+
const SOROBAN_FALLBACK_RPC_URLS = {
|
|
39
|
+
mainnet: [constants_1.SOROBAN_RPC_URLS.mainnet, 'https://mainnet.sorobanrpc.com'],
|
|
40
|
+
testnet: [constants_1.SOROBAN_RPC_URLS.testnet],
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* Resolve the ordered RPC endpoints and network passphrase for a Stellar network.
|
|
44
|
+
*
|
|
45
|
+
* When `overrideRpcUrl` is supplied (e.g. a consumer's keyed Alchemy endpoint),
|
|
46
|
+
* it becomes the primary with the built-in public endpoints kept behind it as
|
|
47
|
+
* failover — so opting into a premium provider still degrades to the public
|
|
48
|
+
* network rather than becoming a new single point of failure. The endpoint is
|
|
49
|
+
* injected rather than hardcoded, so the adapter depends on configuration, not a
|
|
50
|
+
* constant.
|
|
24
51
|
*/
|
|
25
|
-
function resolveNetworkConfig(network) {
|
|
52
|
+
function resolveNetworkConfig(network, overrideRpcUrl) {
|
|
53
|
+
const defaults = SOROBAN_FALLBACK_RPC_URLS[network];
|
|
54
|
+
const rpcUrls = overrideRpcUrl
|
|
55
|
+
? [overrideRpcUrl, ...defaults]
|
|
56
|
+
: [...defaults];
|
|
26
57
|
return {
|
|
27
|
-
rpcUrl:
|
|
58
|
+
rpcUrl: rpcUrls[0],
|
|
59
|
+
rpcUrls,
|
|
28
60
|
passphrase: constants_1.NETWORK_PASSPHRASES[network],
|
|
29
61
|
};
|
|
30
62
|
}
|
|
63
|
+
/** How long a health-checked endpoint choice is reused before re-probing. */
|
|
64
|
+
const HEALTHY_SERVER_TTL_MS = 30_000;
|
|
65
|
+
/**
|
|
66
|
+
* Shorter reuse window after a fully-failed probe (no endpoint healthy). Caps
|
|
67
|
+
* probe traffic during an outage — failing fast on the primary beats re-probing
|
|
68
|
+
* every endpoint on every call — while keeping recovery within a few seconds.
|
|
69
|
+
*/
|
|
70
|
+
const NEGATIVE_CACHE_TTL_MS = 5_000;
|
|
71
|
+
/**
|
|
72
|
+
* Bound on a single `getHealth` probe so a *hung* (not erroring) node can't
|
|
73
|
+
* block the failover loop; a timeout is treated like an unhealthy endpoint.
|
|
74
|
+
*/
|
|
75
|
+
const HEALTH_PROBE_TIMEOUT_MS = 3_000;
|
|
76
|
+
/**
|
|
77
|
+
* Bound on a single real RPC operation (simulate / getAccount / build) so an
|
|
78
|
+
* endpoint that passed `getHealth()` but then *hangs* on the actual call fails
|
|
79
|
+
* over instead of blocking indefinitely; a timeout is treated as retryable.
|
|
80
|
+
*/
|
|
81
|
+
const RPC_OPERATION_TIMEOUT_MS = 15_000;
|
|
82
|
+
const healthyServerCache = new Map();
|
|
83
|
+
// De-dupes concurrent probes for the same endpoint set (e.g. the parallel reads
|
|
84
|
+
// a vault listing issues) so a cold cache costs one probe, not one per call.
|
|
85
|
+
const inFlightProbes = new Map();
|
|
86
|
+
/**
|
|
87
|
+
* Clear the health-check cache and any in-flight probes. Test-only seam.
|
|
88
|
+
* @internal
|
|
89
|
+
*/
|
|
90
|
+
function resetHealthyServerCache() {
|
|
91
|
+
healthyServerCache.clear();
|
|
92
|
+
inFlightProbes.clear();
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Host-only view of an RPC URL, safe for logs. Keyed provider endpoints put the
|
|
96
|
+
* API key in the path (e.g. Alchemy `/v2/<key>`), so logging the raw URL would
|
|
97
|
+
* leak the secret into SDK logs / crash reporters. Returns just protocol+host.
|
|
98
|
+
* @internal
|
|
99
|
+
*/
|
|
100
|
+
function redactRpcUrl(url) {
|
|
101
|
+
try {
|
|
102
|
+
const { protocol, host } = new URL(url);
|
|
103
|
+
return `${protocol}//${host}`;
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
return '<invalid-url>';
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Return a copy of `value` with every configured RPC URL reduced to protocol+host
|
|
111
|
+
* in any string it carries, so a keyed endpoint (e.g. Alchemy `/v2/<key>`) can't
|
|
112
|
+
* leak its API key through a thrown error's chained `cause`.
|
|
113
|
+
*
|
|
114
|
+
* Handles Errors, plain objects, and arrays — not just Errors: Soroban's
|
|
115
|
+
* JSON-RPC helper throws the raw `{ code, message, data }` object, which
|
|
116
|
+
* `AugustSDKError.toJSON()` serializes wholesale, so an unscrubbed object cause
|
|
117
|
+
* would leak the key. Cloned recursively (depth- and cycle-bounded); the
|
|
118
|
+
* original is never mutated.
|
|
119
|
+
*/
|
|
120
|
+
function scrubErrorUrls(value, scrub, depth = 0, seen = new WeakSet()) {
|
|
121
|
+
if (typeof value === 'string')
|
|
122
|
+
return scrub(value);
|
|
123
|
+
if (value === null || typeof value !== 'object')
|
|
124
|
+
return value;
|
|
125
|
+
if (depth >= 4)
|
|
126
|
+
return '[TRUNCATED]';
|
|
127
|
+
if (seen.has(value))
|
|
128
|
+
return '[Circular]';
|
|
129
|
+
seen.add(value);
|
|
130
|
+
if (value instanceof Error) {
|
|
131
|
+
const clone = new Error(scrub(value.message));
|
|
132
|
+
clone.name = value.name;
|
|
133
|
+
if (value.stack)
|
|
134
|
+
clone.stack = scrub(value.stack);
|
|
135
|
+
// Carry over any extra own props (e.g. a JSON-RPC `code` / `data`), scrubbed.
|
|
136
|
+
for (const key of Object.getOwnPropertyNames(value)) {
|
|
137
|
+
if (key === 'message' || key === 'stack' || key === 'name')
|
|
138
|
+
continue;
|
|
139
|
+
clone[key] = scrubErrorUrls(value[key], scrub, depth + 1, seen);
|
|
140
|
+
}
|
|
141
|
+
return clone;
|
|
142
|
+
}
|
|
143
|
+
if (Array.isArray(value)) {
|
|
144
|
+
return value.map((v) => scrubErrorUrls(v, scrub, depth + 1, seen));
|
|
145
|
+
}
|
|
146
|
+
const out = {};
|
|
147
|
+
for (const [key, v] of Object.entries(value)) {
|
|
148
|
+
out[key] = scrubErrorUrls(v, scrub, depth + 1, seen);
|
|
149
|
+
}
|
|
150
|
+
return out;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Race a promise against a timeout so a hung call can't block indefinitely. The
|
|
154
|
+
* timer is always cleared, so a fast-resolving promise leaves nothing pending.
|
|
155
|
+
*/
|
|
156
|
+
function withTimeout(promise, ms, label) {
|
|
157
|
+
let timer;
|
|
158
|
+
const timeout = new Promise((_resolve, reject) => {
|
|
159
|
+
timer = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms);
|
|
160
|
+
});
|
|
161
|
+
return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Return a Soroban RPC server backed by a healthy endpoint for `config`. Thin
|
|
165
|
+
* wrapper over {@link resolveHealthyServer} for callers (e.g. submission) that
|
|
166
|
+
* only need the server, not the URL it resolved to.
|
|
167
|
+
*
|
|
168
|
+
* @internal
|
|
169
|
+
*/
|
|
170
|
+
function getHealthyServer(config) {
|
|
171
|
+
return resolveHealthyServer(config).then((r) => r.server);
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Resolve a healthy endpoint for `config`, returning both the server and the URL
|
|
175
|
+
* it was built from.
|
|
176
|
+
*
|
|
177
|
+
* Probes `config.rpcUrls` in priority order via `getHealth()` and uses the first
|
|
178
|
+
* that reports healthy, so a stalled or lagging provider node is skipped instead
|
|
179
|
+
* of failing every call. The choice is cached for {@link HEALTHY_SERVER_TTL_MS},
|
|
180
|
+
* and concurrent callers on a cold cache share a single in-flight probe — so a
|
|
181
|
+
* burst of parallel reads costs one health probe, not one per call. When no
|
|
182
|
+
* endpoint reports healthy, falls back to the first reachable endpoint so the
|
|
183
|
+
* caller still attempts the request and surfaces the node's real error.
|
|
184
|
+
*
|
|
185
|
+
* The URL is returned (not just the server) so {@link withEndpointFailover} can
|
|
186
|
+
* skip the just-attempted endpoint and promote a working fallback into the cache.
|
|
187
|
+
*/
|
|
188
|
+
function resolveHealthyServer(config) {
|
|
189
|
+
// Prefer the ordered failover list; fall back to the single primary for a
|
|
190
|
+
// hand-built config that predates `rpcUrls`.
|
|
191
|
+
const rpcUrls = config.rpcUrls && config.rpcUrls.length > 0
|
|
192
|
+
? config.rpcUrls
|
|
193
|
+
: [config.rpcUrl];
|
|
194
|
+
// Key by the full ordered endpoint set, not just the primary: the same
|
|
195
|
+
// primary paired with different fallback lists selects a different server, so
|
|
196
|
+
// keying on `rpcUrls[0]` alone could return one endpoint set's choice for
|
|
197
|
+
// another during the TTL.
|
|
198
|
+
const cacheKey = rpcUrls.join('|');
|
|
199
|
+
const cached = healthyServerCache.get(cacheKey);
|
|
200
|
+
if (cached) {
|
|
201
|
+
// A fully-failed probe is cached only briefly (NEGATIVE_CACHE_TTL_MS) so an
|
|
202
|
+
// outage doesn't re-probe every endpoint on every call, yet recovers fast.
|
|
203
|
+
const ttl = cached.healthy ? HEALTHY_SERVER_TTL_MS : NEGATIVE_CACHE_TTL_MS;
|
|
204
|
+
if (Date.now() - cached.checkedAt < ttl) {
|
|
205
|
+
return Promise.resolve({
|
|
206
|
+
server: cached.server,
|
|
207
|
+
url: cached.url,
|
|
208
|
+
healthy: cached.healthy,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
const existing = inFlightProbes.get(cacheKey);
|
|
213
|
+
if (existing)
|
|
214
|
+
return existing;
|
|
215
|
+
const probe = probeHealthyServer(rpcUrls, cacheKey).finally(() => {
|
|
216
|
+
inFlightProbes.delete(cacheKey);
|
|
217
|
+
});
|
|
218
|
+
inFlightProbes.set(cacheKey, probe);
|
|
219
|
+
return probe;
|
|
220
|
+
}
|
|
221
|
+
async function probeHealthyServer(rpcUrls, cacheKey) {
|
|
222
|
+
// The first endpoint we can even construct — the fallback when none report
|
|
223
|
+
// healthy. Tracked separately from `rpcUrls[0]` so a malformed primary (e.g. a
|
|
224
|
+
// typo'd consumer override) is skipped rather than disabling every call.
|
|
225
|
+
let firstConstructable;
|
|
226
|
+
let firstConstructableUrl;
|
|
227
|
+
for (const url of rpcUrls) {
|
|
228
|
+
let server;
|
|
229
|
+
try {
|
|
230
|
+
// Construct inside the guard: a malformed URL throws from the rpc.Server
|
|
231
|
+
// ctor, and must skip to the next endpoint rather than abort the loop and
|
|
232
|
+
// disable every fallback.
|
|
233
|
+
server = createServer(url);
|
|
234
|
+
}
|
|
235
|
+
catch (err) {
|
|
236
|
+
core_1.Logger.log.warn('getHealthyServer', 'Skipping invalid Soroban RPC endpoint; trying next endpoint', { url: redactRpcUrl(url), error: String(err) });
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
if (firstConstructable === undefined) {
|
|
240
|
+
firstConstructable = server;
|
|
241
|
+
firstConstructableUrl = url;
|
|
242
|
+
}
|
|
243
|
+
try {
|
|
244
|
+
const health = await withTimeout(server.getHealth(), HEALTH_PROBE_TIMEOUT_MS, 'getHealth');
|
|
245
|
+
if (health.status === 'healthy') {
|
|
246
|
+
healthyServerCache.set(cacheKey, {
|
|
247
|
+
server,
|
|
248
|
+
url,
|
|
249
|
+
checkedAt: Date.now(),
|
|
250
|
+
healthy: true,
|
|
251
|
+
});
|
|
252
|
+
return { server, url, healthy: true };
|
|
253
|
+
}
|
|
254
|
+
core_1.Logger.log.warn('getHealthyServer', `Soroban RPC reported status "${health.status}"; trying next endpoint`, { url: redactRpcUrl(url) });
|
|
255
|
+
}
|
|
256
|
+
catch (err) {
|
|
257
|
+
core_1.Logger.log.warn('getHealthyServer', 'Soroban RPC health check failed; trying next endpoint',
|
|
258
|
+
// Scrub the endpoint from the error message too — SDK errors often
|
|
259
|
+
// embed the request URL (key and all).
|
|
260
|
+
{
|
|
261
|
+
url: redactRpcUrl(url),
|
|
262
|
+
error: String(err).split(url).join(redactRpcUrl(url)),
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
if (firstConstructable === undefined) {
|
|
267
|
+
// Every configured URL was malformed — there is nothing to call.
|
|
268
|
+
throw new core_1.AugustSDKError('UNKNOWN', 'No valid Soroban RPC endpoint could be constructed from the configured URLs');
|
|
269
|
+
}
|
|
270
|
+
const fallbackUrl = firstConstructableUrl;
|
|
271
|
+
core_1.Logger.log.warn('getHealthyServer', 'No healthy Soroban RPC endpoint; falling back to first reachable endpoint', { primary: redactRpcUrl(fallbackUrl) });
|
|
272
|
+
// Negative-cache the fallback (short TTL) so a sustained outage fails fast
|
|
273
|
+
// instead of re-probing every endpoint on every call.
|
|
274
|
+
healthyServerCache.set(cacheKey, {
|
|
275
|
+
server: firstConstructable,
|
|
276
|
+
url: fallbackUrl,
|
|
277
|
+
checkedAt: Date.now(),
|
|
278
|
+
healthy: false,
|
|
279
|
+
});
|
|
280
|
+
return { server: firstConstructable, url: fallbackUrl, healthy: false };
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* A thrown RPC error is retryable on another endpoint UNLESS it is one of our
|
|
284
|
+
* typed domain errors. Those mark a *deterministic* outcome — a contract revert,
|
|
285
|
+
* an unfunded account, archived state needing restore, a bad input, or a local
|
|
286
|
+
* assembly failure — which reproduces identically on every node, so retrying
|
|
287
|
+
* elsewhere only wastes calls. Any other throw (a transport hang/reset, or a
|
|
288
|
+
* method-specific node error from an endpoint that still answered `getHealth()`)
|
|
289
|
+
* is worth the next endpoint.
|
|
290
|
+
*
|
|
291
|
+
* Exported so an operation with different semantics (e.g. an idempotency-aware
|
|
292
|
+
* submission path) can compose or replace this policy via
|
|
293
|
+
* {@link FailoverOptions.isRetryable} — extension without editing the core.
|
|
294
|
+
* @internal
|
|
295
|
+
*/
|
|
296
|
+
function isRetryableRpcError(err) {
|
|
297
|
+
return !(err instanceof core_1.AugustValidationError || err instanceof core_1.AugustSDKError);
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Signatures of simulation errors that indicate a stalled/lagging *node* rather
|
|
301
|
+
* than a deterministic contract revert. The canonical case is the Protocol 27
|
|
302
|
+
* boundary, where a node frozen behind the network head answered `getHealth()`
|
|
303
|
+
* but reported reads with a "not present in the snapshot" error. Extend this
|
|
304
|
+
* list as new node-side signatures surface.
|
|
305
|
+
*/
|
|
306
|
+
const RETRYABLE_SIMULATION_ERROR_SIGNATURES = ['not present in the snapshot'];
|
|
307
|
+
/**
|
|
308
|
+
* Whether a simulation *error* string is a node/infrastructure failure (which
|
|
309
|
+
* clears on another endpoint) rather than a genuine contract revert (which
|
|
310
|
+
* reproduces on every node). `isSimulationError` alone can't tell these apart —
|
|
311
|
+
* both arrive as a populated `error` field — so callers use this to decide
|
|
312
|
+
* whether to fail over or treat the error as terminal.
|
|
313
|
+
* @internal
|
|
314
|
+
*/
|
|
315
|
+
function isRetryableSimulationError(errorText) {
|
|
316
|
+
const lower = errorText.toLowerCase();
|
|
317
|
+
return RETRYABLE_SIMULATION_ERROR_SIGNATURES.some((sig) => lower.includes(sig));
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Drop a cached endpoint choice so a server that just failed a *real* call is
|
|
321
|
+
* not re-used for the remainder of its {@link HEALTHY_SERVER_TTL_MS} window.
|
|
322
|
+
*/
|
|
323
|
+
function dropCachedServer(rpcUrls) {
|
|
324
|
+
healthyServerCache.delete(rpcUrls.join('|'));
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Run an idempotent, side-effect-free `operation` (a read or a tx build) against
|
|
328
|
+
* a Soroban endpoint, failing over to the remaining endpoints on a retryable
|
|
329
|
+
* failure.
|
|
330
|
+
*
|
|
331
|
+
* {@link getHealthyServer} only proves an endpoint answered `getHealth()`, and
|
|
332
|
+
* caches that choice for {@link HEALTHY_SERVER_TTL_MS} — but the node can still
|
|
333
|
+
* hang or return a method-specific error on the *real* call. So we run the op on
|
|
334
|
+
* the health-gated server first (the fast, cached path) and, on a retryable
|
|
335
|
+
* failure, drop that cached choice and retry the op against every endpoint in
|
|
336
|
+
* priority order. Deterministic contract/domain errors short-circuit (see
|
|
337
|
+
* {@link isRetryableRpcError}); each attempt is time-boxed so a hung node can't
|
|
338
|
+
* stall the loop.
|
|
339
|
+
*
|
|
340
|
+
* The `operation` is a strategy (dependency-injected), so new read/build calls
|
|
341
|
+
* reuse this failover without changing it; `options` keeps retry policy and
|
|
342
|
+
* timeout open for extension. Submission is deliberately NOT routed through here
|
|
343
|
+
* — re-sending a signed transaction across endpoints risks a double-submit, so
|
|
344
|
+
* it needs sequence/DUPLICATE-based idempotency rather than blind retry.
|
|
345
|
+
* @internal
|
|
346
|
+
*/
|
|
347
|
+
async function withEndpointFailover(config, method, operation, options = {}) {
|
|
348
|
+
const isRetryable = options.isRetryable ?? isRetryableRpcError;
|
|
349
|
+
const timeoutMs = options.timeoutMs ?? RPC_OPERATION_TIMEOUT_MS;
|
|
350
|
+
const rpcUrls = config.rpcUrls && config.rpcUrls.length > 0
|
|
351
|
+
? config.rpcUrls
|
|
352
|
+
: [config.rpcUrl];
|
|
353
|
+
const runOn = (server) => withTimeout(operation(server), timeoutMs, `Soroban ${method}`);
|
|
354
|
+
const cacheKey = rpcUrls.join('|');
|
|
355
|
+
// Reduce every configured endpoint URL to protocol+host in anything surfaced
|
|
356
|
+
// from here (logs, the thrown message, the chained cause). SDK errors embed the
|
|
357
|
+
// request URL — which for a keyed provider carries the API key in its path — so
|
|
358
|
+
// an unscrubbed `String(err)` would leak it. Belt-and-suspenders with the
|
|
359
|
+
// analytics sanitizer, and it also covers the thrown error that escapes to the
|
|
360
|
+
// caller (which our telemetry sanitizer never sees).
|
|
361
|
+
const scrub = (text) => rpcUrls.reduce((acc, u) => acc.split(u).join(redactRpcUrl(u)), text);
|
|
362
|
+
let lastError;
|
|
363
|
+
// A terminal "everything failed" error, scrubbed so a keyed endpoint can't
|
|
364
|
+
// leak through the message or chained cause.
|
|
365
|
+
const failAllEndpoints = () => new core_1.AugustSDKError('UNKNOWN', `Soroban ${method} failed on all endpoints: ${scrub(String(lastError))}`, { cause: scrubErrorUrls(lastError, scrub), context: { method } });
|
|
366
|
+
// Fast path: the health-gated, cross-call-cached endpoint.
|
|
367
|
+
const { server: primaryServer, url: primaryUrl, healthy, } = await resolveHealthyServer(config);
|
|
368
|
+
try {
|
|
369
|
+
return await runOn(primaryServer);
|
|
370
|
+
}
|
|
371
|
+
catch (err) {
|
|
372
|
+
if (!isRetryable(err))
|
|
373
|
+
throw err;
|
|
374
|
+
lastError = err;
|
|
375
|
+
if (!healthy) {
|
|
376
|
+
// The resolved endpoint is a negative-cached fallback: a prior probe found
|
|
377
|
+
// no healthy endpoint, so the whole set is down. Re-looping every endpoint
|
|
378
|
+
// (and re-probing on the next read) would only amplify load during the
|
|
379
|
+
// outage, so fail fast and leave the short-TTL negative entry in place —
|
|
380
|
+
// subsequent reads within NEGATIVE_CACHE_TTL_MS fast-fail on the primary too.
|
|
381
|
+
throw failAllEndpoints();
|
|
382
|
+
}
|
|
383
|
+
// A "healthy" endpoint failed the real call — drop it and fail over.
|
|
384
|
+
dropCachedServer(rpcUrls);
|
|
385
|
+
core_1.Logger.log.warn('withEndpointFailover', `Soroban ${method} failed on the health-gated endpoint; failing over`, { error: scrub(String(err)) });
|
|
386
|
+
}
|
|
387
|
+
// Failover: the remaining endpoints in priority order. Skip the one the fast
|
|
388
|
+
// path already tried (no point paying its timeout twice). On success, promote
|
|
389
|
+
// the working endpoint into the cache so the next call starts there instead of
|
|
390
|
+
// repeating the failed-primary-then-fallback sequence.
|
|
391
|
+
for (const url of rpcUrls) {
|
|
392
|
+
if (url === primaryUrl)
|
|
393
|
+
continue;
|
|
394
|
+
let server;
|
|
395
|
+
try {
|
|
396
|
+
server = createServer(url);
|
|
397
|
+
}
|
|
398
|
+
catch {
|
|
399
|
+
continue; // malformed endpoint URL — skip
|
|
400
|
+
}
|
|
401
|
+
try {
|
|
402
|
+
const result = await runOn(server);
|
|
403
|
+
healthyServerCache.set(cacheKey, {
|
|
404
|
+
server,
|
|
405
|
+
url,
|
|
406
|
+
checkedAt: Date.now(),
|
|
407
|
+
healthy: true,
|
|
408
|
+
});
|
|
409
|
+
return result;
|
|
410
|
+
}
|
|
411
|
+
catch (err) {
|
|
412
|
+
if (!isRetryable(err))
|
|
413
|
+
throw err;
|
|
414
|
+
lastError = err;
|
|
415
|
+
core_1.Logger.log.warn('withEndpointFailover', `Soroban ${method} failed; trying next endpoint`, {
|
|
416
|
+
url: redactRpcUrl(url),
|
|
417
|
+
error: scrub(String(err)),
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
// Every endpoint's real op failed even though one health-probed OK. Negative-
|
|
422
|
+
// cache the health-gated server (it constructed fine) so subsequent reads
|
|
423
|
+
// fast-fail within NEGATIVE_CACHE_TTL_MS instead of re-probing + re-looping the
|
|
424
|
+
// whole set during a sustained outage.
|
|
425
|
+
healthyServerCache.set(cacheKey, {
|
|
426
|
+
server: primaryServer,
|
|
427
|
+
url: primaryUrl,
|
|
428
|
+
checkedAt: Date.now(),
|
|
429
|
+
healthy: false,
|
|
430
|
+
});
|
|
431
|
+
throw failAllEndpoints();
|
|
432
|
+
}
|
|
31
433
|
/**
|
|
32
434
|
* Validate and convert a string amount to a non-negative BigInt.
|
|
33
435
|
* Throws a descriptive error if the value is not a valid non-negative integer string.
|
|
@@ -60,7 +462,8 @@ function extractSimulationError(simulated) {
|
|
|
60
462
|
}
|
|
61
463
|
async function queryContract(config, contractId, method, args = []) {
|
|
62
464
|
try {
|
|
63
|
-
|
|
465
|
+
// Build the simulation tx once — it is endpoint-independent, so only the
|
|
466
|
+
// simulate call is failed over across endpoints (not the tx assembly).
|
|
64
467
|
const contract = new stellar_sdk_1.Contract(contractId);
|
|
65
468
|
const sourceKeypair = stellar_sdk_1.Keypair.random();
|
|
66
469
|
const account = new stellar_sdk_1.Account(sourceKeypair.publicKey(), '0');
|
|
@@ -73,20 +476,27 @@ async function queryContract(config, contractId, method, args = []) {
|
|
|
73
476
|
.addOperation(contract.call(method, ...args))
|
|
74
477
|
.setTimeout(constants_1.QUERY_TIMEOUT_SECONDS)
|
|
75
478
|
.build();
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
479
|
+
return await withEndpointFailover(config, method, async (server) => {
|
|
480
|
+
const simulated = await server.simulateTransaction(tx);
|
|
481
|
+
if (stellar_sdk_1.rpc.Api.isSimulationError(simulated)) {
|
|
482
|
+
const errorText = extractSimulationError(simulated);
|
|
483
|
+
if (isRetryableSimulationError(errorText)) {
|
|
484
|
+
// A stalled/lagging node reports the read as a simulation error too
|
|
485
|
+
// (e.g. a post-upgrade snapshot gap) — not a real revert. Throw so the
|
|
486
|
+
// failover wrapper tries another endpoint instead of returning null.
|
|
487
|
+
throw new Error(`Soroban simulation node error for ${method}: ${errorText}`);
|
|
488
|
+
}
|
|
489
|
+
// A genuine contract revert is deterministic across nodes — surface null
|
|
490
|
+
// (best-effort read) rather than failing over to repeat it.
|
|
491
|
+
core_1.Logger.log.warn('queryContract', `Soroban simulation error for ${method}: ${errorText}`, { contractId });
|
|
492
|
+
return null;
|
|
493
|
+
}
|
|
494
|
+
if (stellar_sdk_1.rpc.Api.isSimulationSuccess(simulated) && simulated.result) {
|
|
495
|
+
return simulated.result.retval;
|
|
496
|
+
}
|
|
497
|
+
core_1.Logger.log.warn('queryContract', `Unexpected simulation state for ${method} — no result returned`, { contractId });
|
|
81
498
|
return null;
|
|
82
|
-
}
|
|
83
|
-
if (stellar_sdk_1.rpc.Api.isSimulationSuccess(simulated) && simulated.result) {
|
|
84
|
-
return simulated.result.retval;
|
|
85
|
-
}
|
|
86
|
-
core_1.Logger.log.warn('queryContract', `Unexpected simulation state for ${method} — no result returned`, {
|
|
87
|
-
contractId,
|
|
88
499
|
});
|
|
89
|
-
return null;
|
|
90
500
|
}
|
|
91
501
|
catch (err) {
|
|
92
502
|
core_1.Logger.log.warn('queryContract', `Soroban query failed: ${method}`, {
|
|
@@ -132,54 +542,67 @@ async function getNetworkCloseTime(server) {
|
|
|
132
542
|
* using MAX_FEE_STROOPS as the fee ceiling and incorporating the simulated resource fee.
|
|
133
543
|
*/
|
|
134
544
|
async function buildSorobanTx(config, sourceAddress, contractId, method, args) {
|
|
135
|
-
const server = createServer(config.rpcUrl);
|
|
136
|
-
// Read the network clock in parallel with the account fetch — it overlaps the
|
|
137
|
-
// account round-trip, so it adds no latency on the happy path.
|
|
138
|
-
const networkCloseTimePromise = getNetworkCloseTime(server);
|
|
139
|
-
let account;
|
|
140
|
-
try {
|
|
141
|
-
account = await server.getAccount(sourceAddress);
|
|
142
|
-
}
|
|
143
|
-
catch (err) {
|
|
144
|
-
// Soroban rpc currently throws plain `Error("Account not found: ...")`
|
|
145
|
-
// for unfunded accounts; check `NotFoundError` too so this survives an
|
|
146
|
-
// SDK refactor to typed errors.
|
|
147
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
148
|
-
const isAccountNotFound = err instanceof stellar_sdk_1.NotFoundError || /Account not found/.test(msg);
|
|
149
|
-
if (isAccountNotFound) {
|
|
150
|
-
throw new core_1.AugustValidationError('ACCOUNT_NOT_FUNDED', `Stellar account ${sourceAddress} is not funded. Send at least 1 XLM to this address to activate it before depositing or redeeming.`, { cause: err, context: { sourceAddress, method, contractId } });
|
|
151
|
-
}
|
|
152
|
-
throw err;
|
|
153
|
-
}
|
|
154
545
|
const contract = new stellar_sdk_1.Contract(contractId);
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
//
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
546
|
+
// Build is idempotent (no on-chain side effects), so the whole account-fetch →
|
|
547
|
+
// simulate → assemble sequence is failed over across endpoints. Deterministic
|
|
548
|
+
// outcomes (unfunded account, simulation/restore/assembly errors) throw typed
|
|
549
|
+
// errors and short-circuit the failover; transport errors get the next node.
|
|
550
|
+
return withEndpointFailover(config, method, async (server) => {
|
|
551
|
+
// Read the network clock in parallel with the account fetch — it overlaps
|
|
552
|
+
// the account round-trip, so it adds no latency on the happy path.
|
|
553
|
+
const networkCloseTimePromise = getNetworkCloseTime(server);
|
|
554
|
+
let account;
|
|
555
|
+
try {
|
|
556
|
+
account = await server.getAccount(sourceAddress);
|
|
557
|
+
}
|
|
558
|
+
catch (err) {
|
|
559
|
+
// Soroban rpc currently throws plain `Error("Account not found: ...")`
|
|
560
|
+
// for unfunded accounts; check `NotFoundError` too so this survives an
|
|
561
|
+
// SDK refactor to typed errors. An unfunded account is deterministic
|
|
562
|
+
// across nodes (typed error → no failover); any other getAccount error is
|
|
563
|
+
// transport-ish and falls over.
|
|
564
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
565
|
+
const isAccountNotFound = err instanceof stellar_sdk_1.NotFoundError || /Account not found/.test(msg);
|
|
566
|
+
if (isAccountNotFound) {
|
|
567
|
+
throw new core_1.AugustValidationError('ACCOUNT_NOT_FUNDED', `Stellar account ${sourceAddress} is not funded. Send at least 1 XLM to this address to activate it before depositing or redeeming.`, { cause: err, context: { sourceAddress, method, contractId } });
|
|
568
|
+
}
|
|
569
|
+
throw err;
|
|
570
|
+
}
|
|
571
|
+
const builder = new stellar_sdk_1.TransactionBuilder(account, {
|
|
572
|
+
fee: constants_1.MAX_FEE_STROOPS,
|
|
573
|
+
networkPassphrase: config.passphrase,
|
|
574
|
+
}).addOperation(contract.call(method, ...args));
|
|
575
|
+
const networkCloseTime = await networkCloseTimePromise;
|
|
576
|
+
if (networkCloseTime !== null) {
|
|
577
|
+
// Absolute deadline anchored to the network clock — survives signer skew.
|
|
578
|
+
builder.setTimebounds(0, networkCloseTime + constants_1.TX_TIMEOUT_SECONDS);
|
|
579
|
+
}
|
|
580
|
+
else {
|
|
581
|
+
// RPC couldn't report network time; fall back to the local-clock window.
|
|
582
|
+
builder.setTimeout(constants_1.TX_TIMEOUT_SECONDS);
|
|
583
|
+
}
|
|
584
|
+
const tx = builder.build();
|
|
585
|
+
const simulated = await server.simulateTransaction(tx);
|
|
586
|
+
if (stellar_sdk_1.rpc.Api.isSimulationError(simulated)) {
|
|
587
|
+
const errorText = extractSimulationError(simulated);
|
|
588
|
+
if (isRetryableSimulationError(errorText)) {
|
|
589
|
+
// Node/snapshot error (a stale node), not a contract revert — throw a
|
|
590
|
+
// retryable error so the failover wrapper tries another endpoint.
|
|
591
|
+
throw new Error(`Soroban simulation node error for ${method}: ${errorText}`);
|
|
592
|
+
}
|
|
593
|
+
throw new core_1.AugustSDKError('UNKNOWN', `Soroban simulation failed: ${errorText}`, { context: { method, contractId } });
|
|
594
|
+
}
|
|
595
|
+
if ('restorePreamble' in simulated && simulated.restorePreamble) {
|
|
596
|
+
throw new core_1.AugustSDKError('UNKNOWN', `Contract ledger state needs restoration before invoking ${method}. ` +
|
|
597
|
+
'Submit a restore footprint transaction first.', { context: { method, contractId } });
|
|
598
|
+
}
|
|
599
|
+
try {
|
|
600
|
+
const assembled = stellar_sdk_1.rpc.assembleTransaction(tx, simulated).build();
|
|
601
|
+
return assembled.toXDR();
|
|
602
|
+
}
|
|
603
|
+
catch (assemblyErr) {
|
|
604
|
+
throw new core_1.AugustSDKError('UNKNOWN', `Failed to assemble Soroban transaction for ${method}: ${String(assemblyErr)}`, { cause: assemblyErr, context: { method, contractId } });
|
|
605
|
+
}
|
|
606
|
+
});
|
|
184
607
|
}
|
|
185
608
|
//# sourceMappingURL=soroban.js.map
|
|
@@ -9,6 +9,9 @@ import type { IStellarNetwork } from './types';
|
|
|
9
9
|
*
|
|
10
10
|
* @param signedXdr - Base64-encoded XDR of the signed transaction
|
|
11
11
|
* @param network - Stellar network name
|
|
12
|
+
* @param sorobanRpcUrl - Optional override Soroban RPC endpoint (e.g. a keyed
|
|
13
|
+
* Alchemy URL); becomes the primary with the SDK's public endpoints kept as
|
|
14
|
+
* failover. Omit to use the public endpoints.
|
|
12
15
|
* @returns The transaction hash once the network confirms it as successful
|
|
13
16
|
* @throws {@link AugustTimeoutError} when the transaction is not confirmed
|
|
14
17
|
* within the poll budget (`MAX_POLL_ATTEMPTS`).
|
|
@@ -28,4 +31,4 @@ import type { IStellarNetwork } from './types';
|
|
|
28
31
|
* }
|
|
29
32
|
* ```
|
|
30
33
|
*/
|
|
31
|
-
export declare function submitStellarTransaction(signedXdr: string, network: IStellarNetwork): Promise<string>;
|
|
34
|
+
export declare function submitStellarTransaction(signedXdr: string, network: IStellarNetwork, sorobanRpcUrl?: string): Promise<string>;
|
|
@@ -53,6 +53,9 @@ function safeStringify(value) {
|
|
|
53
53
|
*
|
|
54
54
|
* @param signedXdr - Base64-encoded XDR of the signed transaction
|
|
55
55
|
* @param network - Stellar network name
|
|
56
|
+
* @param sorobanRpcUrl - Optional override Soroban RPC endpoint (e.g. a keyed
|
|
57
|
+
* Alchemy URL); becomes the primary with the SDK's public endpoints kept as
|
|
58
|
+
* failover. Omit to use the public endpoints.
|
|
56
59
|
* @returns The transaction hash once the network confirms it as successful
|
|
57
60
|
* @throws {@link AugustTimeoutError} when the transaction is not confirmed
|
|
58
61
|
* within the poll budget (`MAX_POLL_ATTEMPTS`).
|
|
@@ -72,9 +75,10 @@ function safeStringify(value) {
|
|
|
72
75
|
* }
|
|
73
76
|
* ```
|
|
74
77
|
*/
|
|
75
|
-
async function submitStellarTransaction(signedXdr, network) {
|
|
76
|
-
const
|
|
77
|
-
const
|
|
78
|
+
async function submitStellarTransaction(signedXdr, network, sorobanRpcUrl) {
|
|
79
|
+
const config = (0, soroban_1.resolveNetworkConfig)(network, sorobanRpcUrl);
|
|
80
|
+
const { passphrase } = config;
|
|
81
|
+
const server = await (0, soroban_1.getHealthyServer)(config);
|
|
78
82
|
const tx = stellar_sdk_1.TransactionBuilder.fromXDR(signedXdr, passphrase);
|
|
79
83
|
const sendResult = await server.sendTransaction(tx);
|
|
80
84
|
if (sendResult.status === 'ERROR') {
|
|
@@ -9,6 +9,12 @@ export interface IStellarDepositParams {
|
|
|
9
9
|
amount: string;
|
|
10
10
|
senderAddress: string;
|
|
11
11
|
network: IStellarNetwork;
|
|
12
|
+
/**
|
|
13
|
+
* Optional override Soroban RPC endpoint (e.g. a keyed Alchemy URL). When set
|
|
14
|
+
* it is the primary, with the SDK's public endpoints kept behind it as
|
|
15
|
+
* failover.
|
|
16
|
+
*/
|
|
17
|
+
sorobanRpcUrl?: string;
|
|
12
18
|
}
|
|
13
19
|
/** Parameters for building an unsigned Stellar vault redeem transaction. */
|
|
14
20
|
export interface IStellarRedeemParams {
|
|
@@ -16,6 +22,12 @@ export interface IStellarRedeemParams {
|
|
|
16
22
|
shares: string;
|
|
17
23
|
receiverAddress: string;
|
|
18
24
|
network: IStellarNetwork;
|
|
25
|
+
/**
|
|
26
|
+
* Optional override Soroban RPC endpoint (e.g. a keyed Alchemy URL). When set
|
|
27
|
+
* it is the primary, with the SDK's public endpoints kept behind it as
|
|
28
|
+
* failover.
|
|
29
|
+
*/
|
|
30
|
+
sorobanRpcUrl?: string;
|
|
19
31
|
}
|
|
20
32
|
/**
|
|
21
33
|
* User vault position returned by on-chain balance queries.
|
|
@@ -39,6 +39,7 @@ exports.METHOD_CATEGORIES = {
|
|
|
39
39
|
getVaultBorrowerHealthFactor: 'read.vault',
|
|
40
40
|
getYieldLastRealizedOn: 'read.vault',
|
|
41
41
|
getVaultActivity: 'read.vault',
|
|
42
|
+
getSwapRouterDepositResult: 'read.vault',
|
|
42
43
|
getVaultPositions: 'read.position',
|
|
43
44
|
getVaultUserHistory: 'read.position',
|
|
44
45
|
getUserHistory: 'read.position',
|