stellar-shade 0.0.1 → 0.1.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/dist/index.d.cts CHANGED
@@ -1,6 +1,686 @@
1
+ import * as StellarSdk from '@stellar/stellar-sdk';
2
+ import { Networks, Account, Transaction, rpc } from '@stellar/stellar-sdk';
3
+
4
+ /** Static endpoints and passphrase for one supported Stellar network. */
5
+ interface NetworkDefinition {
6
+ networkPassphrase: string;
7
+ rpcUrl: string;
8
+ horizonUrl: string;
9
+ allowHttp: boolean;
10
+ }
11
+ /**
12
+ * The single source of truth for the networks this SDK can talk to. Adding a
13
+ * network is ONE new entry here: {@link NetworkName} (and with it every
14
+ * `ClientConfig.network` union across the SDK and its consumers) widens
15
+ * automatically.
16
+ */
17
+ declare const NETWORKS: {
18
+ readonly testnet: {
19
+ readonly networkPassphrase: Networks.TESTNET;
20
+ readonly rpcUrl: "https://soroban-testnet.stellar.org";
21
+ readonly horizonUrl: "https://horizon-testnet.stellar.org";
22
+ readonly allowHttp: false;
23
+ };
24
+ };
25
+ /** The names of the currently supported networks (today: `'testnet'`). */
26
+ type NetworkName = keyof typeof NETWORKS;
27
+ /** Network configuration resolved from a network name. */
28
+ interface NetworkConfig extends NetworkDefinition {
29
+ server: StellarSdk.rpc.Server;
30
+ }
31
+ /**
32
+ * Reverse lookup: the {@link NETWORKS} name whose passphrase matches, or
33
+ * `undefined` for a passphrase outside the table. Lets components that only
34
+ * hold a passphrase (the delivery adapters) derive the network name a relayer
35
+ * `/health` should report; an unknown passphrase yields `undefined`, which
36
+ * callers treat as "skip the check" rather than a failure (mainnet-open).
37
+ */
38
+ declare function networkNameForPassphrase(passphrase: string): NetworkName | undefined;
39
+ /**
40
+ * Build network config (endpoints + a connected RPC server) from a network
41
+ * name. Unknown names throw {@link UnsupportedNetworkError} — the type system
42
+ * already prevents them, but plain-JS callers and stale configs need the
43
+ * runtime guard too.
44
+ */
45
+ declare function getNetworkConfig(network: NetworkName): NetworkConfig;
46
+ /** Create a dummy transaction for read-only contract simulation. */
47
+ declare function createSimulationTx(operation: StellarSdk.xdr.Operation, networkPassphrase: string): StellarSdk.Transaction;
48
+ /** Execute a read-only contract call via simulation. Returns the decoded native result. */
49
+ declare function simulateReadOnly(contractId: string, method: string, args: StellarSdk.xdr.ScVal[], server: StellarSdk.rpc.Server, networkPassphrase: string): Promise<unknown | null>;
50
+ /** Resolve an asset string ("native", "XLM", or "CODE:ISSUER") to a SAC contract address. */
51
+ declare function resolveTokenAddress(assetArg: string | undefined, networkPassphrase: string): string;
52
+ /**
53
+ * Give a human-readable label for a SAC token contract address (the opaque
54
+ * `C...` string stored in pool announcements). The native XLM SAC has a
55
+ * deterministic contract id — `Asset.native().contractId(passphrase)` — so we
56
+ * detect it and return `'XLM'`; anything else cannot be reversed to CODE:ISSUER
57
+ * from the address alone, so the original `C...` address is returned unchanged.
58
+ *
59
+ * This is what turns a balance/scan row from an unreadable C-address into a
60
+ * recognizable token name for the common (native) case.
61
+ *
62
+ * @param tokenAddress - The token's SAC contract address, or 'native'/'unknown'.
63
+ * @param networkPassphrase - Passphrase used to derive the native SAC id.
64
+ * @returns 'XLM' for the native SAC (or the literal 'native'); otherwise the input.
65
+ */
66
+ declare function labelForToken(tokenAddress: string, networkPassphrase: string): string;
67
+ /**
68
+ * The minimal transaction-status surface of `rpc.Server` needed to poll a tx
69
+ * hash to a terminal status. Structural, so confirm-polling callers (e.g. the
70
+ * `RelayerClient` confirm option) can inject a real server or a test stub.
71
+ */
72
+ interface TransactionStatusSource {
73
+ getTransaction(hash: string): Promise<{
74
+ status: string;
75
+ }>;
76
+ }
77
+ /**
78
+ * Poll for transaction confirmation. Throws on failure or timeout.
79
+ *
80
+ * The timeout is a typed {@link TransactionTimeoutError} carrying the tx hash:
81
+ * a still-PENDING transaction MAY land after we stop polling, so callers must
82
+ * be able to distinguish "gave up waiting" (poll the hash, do NOT resubmit)
83
+ * from a hard failure — a generic error here invites retry loops that
84
+ * double-send funds.
85
+ */
86
+ declare function waitForTransaction(server: TransactionStatusSource, hash: string): Promise<void>;
87
+ /**
88
+ * Build the withdraw message hash. Must be byte-identical to the contract's
89
+ * `build_withdraw_message`.
90
+ *
91
+ * Layout (concatenated, then SHA-256) — 278-byte preimage:
92
+ * domain_tag[22] ("SHADE-POOL-WITHDRAW-V1" ASCII) || stealth_pk[32]
93
+ * || token_strkey_ascii[56] || amount_be_i128[16]
94
+ * || dest_strkey_ascii[56] || nonce_be_u64[8]
95
+ * || contract_strkey_ascii[56] || network_id[32]
96
+ *
97
+ * The leading static domain tag (SH-3) separates withdraw preimages from any
98
+ * other message a stealth key might sign; the trailing contract address and
99
+ * network id provide domain separation so a signature cannot be replayed on a
100
+ * different deployment or network.
101
+ * `networkId` equals `SHA-256(utf8(networkPassphrase))`, which matches the
102
+ * on-chain `env.ledger().network_id()`.
103
+ */
104
+ declare function buildWithdrawMessage(stealthPk: Uint8Array, tokenAddress: string, amount: bigint, destination: string, nonce: bigint, contractId: string, networkPassphrase: string): Uint8Array;
105
+
106
+ /**
107
+ * Minimal Horizon REST helper with an injectable `fetch`.
108
+ *
109
+ * The account delivery method relies only on Horizon's classic REST API
110
+ * (transaction paging + operations + account probe + submit). Keeping this in a
111
+ * tiny, dependency-free wrapper with an injectable fetch makes the account
112
+ * scanner unit-testable fully offline: tests pass a stub fetch returning
113
+ * synthetic fixtures, no docker network required.
114
+ */
115
+ /** A subset of a Horizon transaction record relevant to the account method. */
116
+ interface HorizonTx {
117
+ id: string;
118
+ hash: string;
119
+ paging_token: string;
120
+ memo_type: string;
121
+ memo?: string;
122
+ successful?: boolean;
123
+ /** The account that submitted (and pays for) the transaction. */
124
+ source_account?: string;
125
+ }
126
+ /**
127
+ * A CAP-23 claim predicate as Horizon serializes it. A missing/`unconditional`
128
+ * predicate is always claimable; the time-bounded variants are evaluated
129
+ * against ledger time so an unsatisfiable CB is not reported as income.
130
+ */
131
+ interface HorizonPredicate {
132
+ unconditional?: boolean;
133
+ and?: HorizonPredicate[];
134
+ or?: HorizonPredicate[];
135
+ not?: HorizonPredicate;
136
+ /** RFC-3339 timestamp: claimable strictly BEFORE this absolute time. */
137
+ abs_before?: string;
138
+ /** Same bound as an epoch-seconds string (Horizon includes both). */
139
+ abs_before_epoch?: string;
140
+ /** Seconds since the CB was created after which it becomes claimable. */
141
+ rel_before?: string;
142
+ }
143
+ /** A single claimant on a create_claimable_balance operation. */
144
+ interface HorizonClaimant {
145
+ destination: string;
146
+ predicate?: HorizonPredicate;
147
+ }
148
+ /** A subset of a Horizon operation record relevant to the account method. */
149
+ interface HorizonOp {
150
+ id: string;
151
+ type: string;
152
+ transaction_hash: string;
153
+ /** create_account: the newly created account */
154
+ account?: string;
155
+ /** create_account: starting balance (string, whole units) */
156
+ starting_balance?: string;
157
+ /** payment: destination account */
158
+ to?: string;
159
+ /** payment: amount (string, whole units) */
160
+ amount?: string;
161
+ /** payment / create_claimable_balance: asset type ('native' for XLM) */
162
+ asset_type?: string;
163
+ /** create_claimable_balance: asset in "CODE:ISSUER" form (or 'native') */
164
+ asset?: string;
165
+ /** create_claimable_balance: the claimants list */
166
+ claimants?: HorizonClaimant[];
167
+ }
168
+ /** A Horizon claimable-balance record (used by the token account method). */
169
+ interface HorizonClaimableBalance {
170
+ id: string;
171
+ asset: string;
172
+ amount: string;
173
+ sponsor?: string;
174
+ claimants: HorizonClaimant[];
175
+ /** Horizon paging token — used to resume the next page of a listing. */
176
+ paging_token?: string;
177
+ }
178
+ /** Horizon account record subset (used to probe existence + sequence). */
179
+ interface HorizonAccount {
180
+ id: string;
181
+ sequence: string;
182
+ balances: Array<{
183
+ asset_type: string;
184
+ balance: string;
185
+ }>;
186
+ }
187
+ /** Injectable fetch signature (compatible with the global `fetch`). */
188
+ type FetchLike = (url: string, init?: {
189
+ method?: string;
190
+ headers?: Record<string, string>;
191
+ body?: string;
192
+ }) => Promise<{
193
+ ok: boolean;
194
+ status: number;
195
+ json(): Promise<unknown>;
196
+ }>;
197
+ /** A thin, injectable-fetch Horizon REST client. */
198
+ declare class HorizonClient {
199
+ private readonly baseUrl;
200
+ private readonly fetchFn;
201
+ /**
202
+ * @param baseUrl - Horizon root URL (no trailing slash required).
203
+ * @param fetchFn - Injectable fetch (defaults to the global `fetch`).
204
+ */
205
+ constructor(baseUrl: string, fetchFn?: FetchLike);
206
+ private getJson;
207
+ /**
208
+ * Page transactions in ascending order.
209
+ *
210
+ * @param cursor - Paging token to resume from (omit for the beginning).
211
+ * @param limit - Page size (default 200, Horizon's max).
212
+ * @returns The page's transaction records (may be empty).
213
+ */
214
+ getTransactions(cursor?: string, limit?: number): Promise<HorizonTx[]>;
215
+ /**
216
+ * The paging token of the newest transaction this Horizon serves (one
217
+ * `order=desc&limit=1` page), or undefined on an empty network. The scan
218
+ * uses it to sanity-clamp cursors adopted from indexer-supplied data: a
219
+ * persisted scan cursor must never exceed the chain position Horizon
220
+ * itself reports, or a malicious feed could blind every future scan.
221
+ */
222
+ getLatestTransactionToken(): Promise<string | undefined>;
223
+ /**
224
+ * Fetch the operations belonging to a single transaction.
225
+ *
226
+ * @param txHash - Transaction hash.
227
+ * @returns The transaction's operation records.
228
+ */
229
+ getTransactionOperations(txHash: string): Promise<HorizonOp[]>;
230
+ /**
231
+ * Probe an account. Returns null if the account does not exist (404).
232
+ *
233
+ * @param address - Stellar G-address.
234
+ */
235
+ getAccount(address: string): Promise<HorizonAccount | null>;
236
+ /**
237
+ * List ALL claimable balances for which the given address is a claimant,
238
+ * paging through every Horizon page (not just the first).
239
+ *
240
+ * Used by the token account method: a direct token send lands as a
241
+ * CreateClaimableBalance to the derived stealth address, which the recipient
242
+ * later claims. Returns an empty array when the account has none.
243
+ *
244
+ * Paging matters for correctness, not just completeness: anyone can create a
245
+ * claimable balance naming a (public, on-chain) stealth address as claimant,
246
+ * so an attacker could spam cheap balances to push the genuine payment past
247
+ * the first page and make the scanner silently miss it. Each page is resumed
248
+ * via the last record's `paging_token` (falling back to the `cursor` in
249
+ * `_links.next.href`), stopping on a short page, a non-advancing cursor, or
250
+ * the defensive {@link MAX_CLAIMABLE_BALANCE_PAGES} bound.
251
+ *
252
+ * @param claimant - Stellar G-address to filter claimable balances by.
253
+ */
254
+ getClaimableBalances(claimant: string): Promise<HorizonClaimableBalance[]>;
255
+ /**
256
+ * Extract the `cursor` query parameter from a Horizon `_links.next.href`.
257
+ * Fallback for full pages whose records carry no `paging_token`.
258
+ */
259
+ private cursorFromNextLink;
260
+ /**
261
+ * Submit a base64 transaction envelope XDR to Horizon.
262
+ *
263
+ * @param xdr - base64-encoded transaction envelope.
264
+ * @returns The submitted transaction hash.
265
+ */
266
+ submitTransaction(xdr: string): Promise<{
267
+ hash: string;
268
+ }>;
269
+ }
270
+
271
+ /**
272
+ * Signs a canonical challenge message with the funding account's ed25519 key,
273
+ * proving control of that account for a fee-spending relayer request. Returns
274
+ * the signature as base64 or hex (or raw bytes). Kept abstract so an app can
275
+ * back it by a wallet, an HSM, or a raw secret key without the SDK ever holding
276
+ * the funding key.
277
+ */
278
+ type FundingSigner = (message: string) => Promise<Uint8Array | string> | Uint8Array | string;
279
+ /** Response from `GET /credit/challenge`. */
280
+ interface CreditChallenge {
281
+ account: string;
282
+ nonce: string;
283
+ }
284
+ /**
285
+ * Canonical single-line message the funding account signs to authorize a spend.
286
+ * MUST match the relayer's `challengeMessage` byte-for-byte. When `bind` is
287
+ * supplied (e.g. the inner-tx hash on the `/relay` path) it is appended so the
288
+ * signature is pinned to that specific transaction and cannot be paired with a
289
+ * different attacker-signed inner XDR of the same amount.
290
+ */
291
+ declare function challengeMessage(endpoint: string, fundingAccount: string, nonce: string, amount: string, bind?: string): string;
292
+ /** Relayer `/health` response. */
293
+ interface RelayerHealth {
294
+ status: string;
295
+ network?: string;
296
+ relayerAddress?: string;
297
+ balance?: string;
298
+ /** Whether the relayer gates fee-spending endpoints on credit. */
299
+ requireCredit?: boolean;
300
+ /** Advertised ceiling (XLM) on a fee-bump — the amount a client authorizes. */
301
+ maxRelayFeeXlm?: number;
302
+ /**
303
+ * The reserve component (7-dp XLM string, e.g. `'1.0000000'`) a credit-gated
304
+ * relayer adds to a sponsor-claim tx's fee when verifying the
305
+ * proof-of-control signature. Clients signing the EXACT `fee + reserve`
306
+ * total should prefer this over mirroring the relayer-side constant.
307
+ */
308
+ sponsoredReserveEstimate?: string;
309
+ /** Credit-ledger backend: `'postgres'` (durable) or `'json'` (dev fallback). */
310
+ store?: string;
311
+ /** Nonce/rate-limit backend: `'redis'` (multi-instance) or `'memory'`. */
312
+ sharedState?: string;
313
+ }
314
+ /** Options for {@link RelayerClient.relay}. */
315
+ interface RelayOpts {
316
+ /** App account to debit the fee-bump fee against (credit-gated relayers). */
317
+ fundingAccount?: string;
318
+ /** Signer proving control of `fundingAccount` (credit-gated relayers). */
319
+ fundingSigner?: FundingSigner;
320
+ /**
321
+ * The fee/amount (7-dp XLM string) the proof-of-control signature authorizes.
322
+ * MUST equal the fee the relayer will charge, or the relayer rejects with 401.
323
+ */
324
+ authAmount?: string;
325
+ /**
326
+ * Network passphrase used to compute the inner-tx hash bound into the
327
+ * proof-of-control signature. MUST match the passphrase the relayer verifies
328
+ * under, or a credit-gated relayer rejects with 401. Required to bind the
329
+ * inner tx on a credit-gated `/relay`; omit for the free/default path.
330
+ */
331
+ networkPassphrase?: string;
332
+ /**
333
+ * Do not trust the relayer's returned txHash at face value: poll it to a
334
+ * terminal on-chain status before resolving (SDK-TXHASH-TRUST). Requires an
335
+ * `rpcServer` in the {@link RelayerClient} constructor opts. Throws
336
+ * `TransactionTimeoutError` (carrying the txHash) if the poll window closes
337
+ * while the tx is still unseen/pending, or an error if it FAILED on-chain.
338
+ * Default `false`: resolve as soon as the relayer responds, exactly as before.
339
+ */
340
+ confirm?: boolean;
341
+ }
342
+ /** Options for {@link RelayerClient.sponsor}. */
343
+ interface SponsorOpts {
344
+ /** Starting balance for the newly created account (whole XLM). */
345
+ startingBalance?: string;
346
+ /** App account to debit against (credit-gated relayers). */
347
+ fundingAccount?: string;
348
+ /** Signer proving control of `fundingAccount` (credit-gated relayers). */
349
+ fundingSigner?: FundingSigner;
350
+ /**
351
+ * The total (startingBalance + fee, 7-dp XLM string) the proof-of-control
352
+ * signature authorizes. MUST equal what the relayer will charge.
353
+ */
354
+ authAmount?: string;
355
+ }
356
+ /** Arguments for {@link RelayerClient.sponsorClaimPrepare}. */
357
+ interface SponsorClaimPrepareArgs {
358
+ /** The stealth address that will claim the balance. */
359
+ stealthAddress: string;
360
+ /** Asset to add a trustline for, in "CODE:ISSUER" form. */
361
+ asset: string;
362
+ /** Claimable balance id to claim. */
363
+ balanceId: string;
364
+ /** Destination that the claimed token is paid out to (must trust the asset). */
365
+ destination: string;
366
+ /** Amount of the claimed token to pay out to the destination (7-dp string). */
367
+ amount: string;
368
+ }
369
+ /** Response from {@link RelayerClient.sponsorClaimPrepare}. */
370
+ interface SponsorClaimPrepared {
371
+ /** Unsigned transaction XDR for the client to co-sign with the stealth key. */
372
+ xdr: string;
373
+ /** ISO timestamp after which the prepared tx's timebounds expire. */
374
+ expiresAt: string;
375
+ }
376
+ /** A read-only credit-ledger view for an app funding account. */
377
+ interface CreditView {
378
+ fundingAccount: string;
379
+ balance: string;
380
+ updatedAt?: string;
381
+ }
382
+ /**
383
+ * Thin HTTP client for the reserve relayer service. Every fee-spending endpoint
384
+ * is exposed as a typed method so the SDK (and apps) never hand-roll fetch calls
385
+ * or URL joining. Construct once and reuse.
386
+ *
387
+ * @example
388
+ * ```typescript
389
+ * const relayer = new RelayerClient('http://localhost:3000');
390
+ * const { status } = await relayer.health();
391
+ * ```
392
+ */
393
+ declare class RelayerClient {
394
+ private readonly baseUrl;
395
+ private readonly fetchFn;
396
+ private readonly fundingSigner?;
397
+ private readonly fundingAccount?;
398
+ private readonly rpcServer?;
399
+ /**
400
+ * @param baseUrl - Relayer root URL. A trailing `/relay` is stripped so a
401
+ * bare relay URL (back-compat) resolves to the service root.
402
+ * @param fetchFn - Injectable fetch (defaults to the global `fetch`).
403
+ * @param opts - Optional default `fundingAccount` + `fundingSigner` used to
404
+ * authenticate fee-spending requests against a credit-gated relayer, and
405
+ * an optional `rpcServer` (an `rpc.Server`, or any object with its
406
+ * `getTransaction`) used to confirm-poll returned tx hashes when a call
407
+ * sets `confirm: true`.
408
+ */
409
+ constructor(baseUrl: string, fetchFn?: FetchLike, opts?: {
410
+ fundingAccount?: string;
411
+ fundingSigner?: FundingSigner;
412
+ rpcServer?: TransactionStatusSource;
413
+ });
414
+ /**
415
+ * Poll a relayer-returned txHash until it lands on-chain (SDK-TXHASH-TRUST).
416
+ * A relayer could return a fabricated or failed hash with a 200; polling to a
417
+ * terminal status is what turns "the relayer said so" into "the chain says
418
+ * so". Deliberately loud when no poll handle exists — silently skipping the
419
+ * confirmation a caller asked for would defeat its purpose. On timeout the
420
+ * underlying `waitForTransaction` throws `TransactionTimeoutError` carrying
421
+ * the txHash, so the caller can keep polling instead of blindly resubmitting.
422
+ */
423
+ private confirmOnChain;
424
+ /**
425
+ * Fetch a fresh challenge nonce for `fundingAccount`, sign the canonical
426
+ * message binding `endpoint` + account + nonce + `amount`, and return the
427
+ * `{ fundingAccount, nonce, signature }` to attach to a fee-spending request.
428
+ * Returns `undefined` when no signer is available (non-gated relayer path).
429
+ */
430
+ private signedAuth;
431
+ private post;
432
+ private get;
433
+ /**
434
+ * Decode a relayer response. A non-JSON body on an error status (e.g. a
435
+ * proxy's HTML 502 page) must not mask the status, so decode failures fall
436
+ * back to an empty body there; a non-JSON body on a 2xx is a broken relayer
437
+ * and surfaces as a transport error.
438
+ */
439
+ private decode;
440
+ /** Probe the relayer's health, balance, and address. */
441
+ health(): Promise<RelayerHealth>;
442
+ /**
443
+ * Fee-bump and submit a signed transaction envelope.
444
+ *
445
+ * @param xdr - base64 signed transaction envelope.
446
+ * @param opts - Optional funding account to debit the fee against, and
447
+ * `confirm` to poll the returned hash to an on-chain terminal status.
448
+ * @returns The submitted transaction hash.
449
+ */
450
+ relay(xdr: string, opts?: RelayOpts): Promise<{
451
+ txHash: string;
452
+ }>;
453
+ /** Cached per-client relayer fee ceiling (XLM, 7-dp), fetched from /health. */
454
+ private cachedMaxRelayFee?;
455
+ /**
456
+ * The relayer's advertised max fee-bump fee (`maxRelayFeeXlm` from /health),
457
+ * as the fee ceiling to authorize. Falls back to the protocol default cap
458
+ * (0.1 XLM) when an older relayer does not advertise it. Cached per client.
459
+ */
460
+ private maxRelayFee;
461
+ /** Cached per-client sponsored-reserve estimate; `null` = fetched, absent. */
462
+ private cachedSponsoredReserve?;
463
+ /**
464
+ * The relayer's advertised sponsored-reserve estimate
465
+ * (`sponsoredReserveEstimate` from /health), parsed to exact stroops.
466
+ * Returns `undefined` when the relayer does not advertise one, the value is
467
+ * unparsable, or /health is unreachable — the caller falls back to its own
468
+ * mirrored constant, so a health fault never breaks a claim. Cached per
469
+ * client, mirroring {@link maxRelayFee}.
470
+ */
471
+ sponsoredReserveEstimateStroops(): Promise<bigint | undefined>;
472
+ /**
473
+ * Create a stealth account from the relayer's own balance (funded
474
+ * CreateAccount — no sponsorship sandwich).
475
+ *
476
+ * @param address - Stealth G-address to create.
477
+ * @param opts - Starting balance and optional funding account.
478
+ */
479
+ sponsor(address: string, opts?: SponsorOpts): Promise<{
480
+ txHash: string;
481
+ }>;
482
+ /**
483
+ * Ask the relayer to build the sponsored claimable-balance-claim transaction.
484
+ * Returns UNSIGNED XDR the client must co-sign with the stealth key before
485
+ * calling {@link sponsorClaimSubmit}.
486
+ */
487
+ sponsorClaimPrepare(args: SponsorClaimPrepareArgs): Promise<SponsorClaimPrepared>;
488
+ /**
489
+ * Submit a sponsor-claim transaction the client has co-signed. The relayer
490
+ * re-derives the expected operations from the trusted inputs
491
+ * (`stealthAddress`, `asset`, `balanceId`) and verifies the submitted ops
492
+ * match field-by-field before adding its signature and submitting.
493
+ *
494
+ * @param xdr - The stealth-co-signed transaction XDR.
495
+ * @param args - The trusted claim inputs (and optional funding account).
496
+ * `confirm: true` polls the returned hash to an on-chain terminal status
497
+ * (requires `rpcServer` in the constructor opts).
498
+ */
499
+ sponsorClaimSubmit(xdr: string, args: {
500
+ stealthAddress: string;
501
+ asset: string;
502
+ balanceId: string;
503
+ destination: string;
504
+ amount: string;
505
+ fundingAccount?: string;
506
+ fundingSigner?: FundingSigner;
507
+ /** Total (reserve + fee) the proof-of-control signature authorizes. */
508
+ authAmount?: string;
509
+ /** Poll the returned txHash until it lands on-chain (see {@link RelayOpts.confirm}). */
510
+ confirm?: boolean;
511
+ }): Promise<{
512
+ txHash: string;
513
+ }>;
514
+ /**
515
+ * Report a completed on-chain deposit to the relayer so it credits the app's
516
+ * funding account with the paid amount.
517
+ */
518
+ creditClaim(fundingAccount: string, txHash: string): Promise<CreditView>;
519
+ /** Read the current credit balance for an app funding account. */
520
+ creditBalance(fundingAccount: string): Promise<CreditView>;
521
+ }
522
+
523
+ /**
524
+ * How a healthy relayer is picked from the candidate list. `'random'` is the
525
+ * default deliberately: users spread across the relayer set instead of herding
526
+ * onto the first entry, preserving the anonymity-set benefit of shared
527
+ * relayers. `'first'` gives deterministic priority ordering.
528
+ */
529
+ type RelayerSelection = 'random' | 'first';
530
+ /** Tuning knobs for a {@link RelayerPool}. Every field has a safe default. */
531
+ interface RelayerPoolOpts {
532
+ /**
533
+ * Network name a candidate's `/health` must report. Omit (or an unknown
534
+ * passphrase upstream) to skip the check; a candidate that reports no
535
+ * network also passes — only an explicit contradiction rejects.
536
+ */
537
+ network?: NetworkName;
538
+ /** Selection strategy. Default `'random'`. */
539
+ selection?: RelayerSelection;
540
+ /** Injectable fetch (tests); defaults to the global `fetch`. */
541
+ fetchFn?: FetchLike;
542
+ /** Shared parallel `/health` probe budget in ms. Default 2500. */
543
+ probeTimeoutMs?: number;
544
+ /**
545
+ * Per-attempt budget in ms inside {@link RelayerPool.withRelayer}, applied
546
+ * ONLY while a failover alternative remains. Default 10000.
547
+ */
548
+ attemptTimeoutMs?: number;
549
+ /**
550
+ * Minimum XLM balance a candidate must report to count as healthy — a
551
+ * relayer one fee-bump from empty is operationally dead. Default 1 XLM
552
+ * (~10 fee-bumps at the default 0.1 cap).
553
+ */
554
+ minBalanceXlm?: number;
555
+ /** Probe cache TTL in ms (per pool instance). Default 30000. */
556
+ probeTtlMs?: number;
557
+ /** Injectable RNG for `'random'` selection (tests). Default `Math.random`. */
558
+ rng?: () => number;
559
+ }
560
+ /**
561
+ * Per-call context the health rule and the built clients need: a credit-gated
562
+ * relayer only counts as healthy when the caller can actually pass its gate
563
+ * (`fundingAccount` + `fundingSigner` present).
564
+ */
565
+ interface RelayerCallCtx {
566
+ fundingAccount?: string;
567
+ fundingSigner?: FundingSigner;
568
+ rpcServer?: TransactionStatusSource;
569
+ }
570
+ /** One candidate's probe result: a health document, or the rejection reason. */
571
+ interface ProbeOutcome {
572
+ url: string;
573
+ health?: RelayerHealth;
574
+ reason?: string;
575
+ }
576
+ /**
577
+ * Normalize a relayer config value into a clean URL list: trims entries, drops
578
+ * empties, and collapses an empty result to `undefined` so `[]` and `['']`
579
+ * mean "no relayer" everywhere a truthiness check gates relayed behavior
580
+ * (e.g. the account claim's merge-fee arithmetic).
581
+ */
582
+ declare function normalizeRelayList(relay?: string | string[]): string[] | undefined;
583
+ /**
584
+ * Health-probing selector and failover harness over a list of relayer URLs
585
+ * (A3 BYO-relayer discovery).
586
+ *
587
+ * A candidate is healthy iff its `/health` reports `status === 'ok'`, its
588
+ * network matches the expected one (when both sides are known), its balance
589
+ * meets the minimum, and its credit gate is passable — `requireCredit ===
590
+ * false`, or the caller brought funding auth. `requireCredit` missing is
591
+ * treated as gated (fail-closed, matching the relayer's gating-on default).
592
+ *
593
+ * A single-URL pool is a transparent pass-through: no probe, no timeout —
594
+ * byte-identical behavior to constructing a `RelayerClient` directly, so
595
+ * existing single-relayer callers see no new traffic or failure modes.
596
+ */
597
+ declare class RelayerPool {
598
+ readonly candidates: readonly string[];
599
+ private readonly network?;
600
+ private readonly selection;
601
+ private readonly fetchFn?;
602
+ private readonly probeTimeoutMs;
603
+ private readonly attemptTimeoutMs;
604
+ private readonly minBalanceXlm;
605
+ private readonly probeTtlMs;
606
+ private readonly rng;
607
+ private cachedProbe?;
608
+ private cachedProbeAt;
609
+ constructor(candidates: string[], opts?: RelayerPoolOpts);
610
+ /**
611
+ * Build a pool from any accepted relayer config shape, or `undefined` when
612
+ * the input normalizes to "no relayer" — the back-compat seam that lets
613
+ * `RelayerPool.from(opts.relay)` slot in wherever a single URL was used.
614
+ */
615
+ static from(relay: string | string[] | undefined, opts?: RelayerPoolOpts): RelayerPool | undefined;
616
+ /**
617
+ * Probe every candidate's `/health` in parallel under one shared time
618
+ * budget. Results are cached per pool instance for {@link
619
+ * RelayerPoolOpts.probeTtlMs}; `force` refreshes. Only transport-level
620
+ * outcomes are recorded here — health-rule classification happens per call
621
+ * in {@link select}/{@link withRelayer}, because it depends on the caller's
622
+ * funding context.
623
+ */
624
+ probe(force?: boolean): Promise<ProbeOutcome[]>;
625
+ /**
626
+ * Pick one healthy relayer URL for this call, or throw
627
+ * {@link NoHealthyRelayerError} naming every candidate's rejection reason.
628
+ * A single-candidate pool returns its URL without probing (pass-through).
629
+ */
630
+ select(ctx?: RelayerCallCtx): Promise<string>;
631
+ /**
632
+ * Run `fn` against a selected relayer, failing over to the next healthy
633
+ * candidate ONLY on transport faults — {@link RelayerNetworkError}, a 5xx
634
+ * {@link RelayerHttpError}, or an attempt timeout. A 4xx (bad request,
635
+ * insufficient credit) or any non-transport error would just repeat and
636
+ * rethrows immediately. At most 2 attempts; the attempt timeout applies
637
+ * only while another candidate remains.
638
+ *
639
+ * Failover after an ambiguous timeout cannot double-spend: both attempts
640
+ * fee-bump the SAME signed inner tx, so its sequence number lets at most
641
+ * one land (`txBAD_SEQ` for the loser).
642
+ */
643
+ withRelayer<T>(fn: (client: RelayerClient, url: string) => Promise<T>, ctx?: RelayerCallCtx): Promise<T>;
644
+ /** Per-call client carrying the caller's funding auth + confirm handle. */
645
+ private buildClient;
646
+ /**
647
+ * Classify every probed candidate for this call's context and order the
648
+ * healthy ones by the selection strategy.
649
+ */
650
+ private rankHealthy;
651
+ /** The health rule. Returns the rejection reason, or `undefined` = healthy. */
652
+ private healthReason;
653
+ }
654
+
655
+ /**
656
+ * A delivery method describes HOW a stealth payment reaches its recipient.
657
+ * - `'pool'` — deposit into the Soroban pool contract (default, private, any SAC token).
658
+ * - `'account'` — a direct classic Stellar payment that creates/pays a one-time stealth
659
+ * account, with the ephemeral key carried in a MemoHash. Native XLM only for now.
660
+ * - `'spp'` — reserved slot for a future Stellar Private Payments (ZK shielded pool) integration.
661
+ */
662
+ type DeliveryMethod = 'pool' | 'account' | 'spp';
663
+ /**
664
+ * An external transaction signer, shaped like Freighter's `signTransaction`.
665
+ *
666
+ * Takes an UNSIGNED transaction XDR and returns a SIGNED transaction XDR. This
667
+ * lets a browser wallet (e.g. Freighter) sign the SENDER and FEE-PAYER legs of
668
+ * an SDK transaction so a dapp never touches a raw Stellar secret. The recovered
669
+ * stealth-key legs still sign locally — a wallet cannot hold the derived stealth
670
+ * scalar — so a signer is only ever applied to the sender / fee-payer legs.
671
+ *
672
+ * @param xdr - The unsigned transaction XDR (base64).
673
+ * @param opts - Network passphrase and, optionally, the G-address expected to
674
+ * sign (the public key the caller passed where a secret normally goes).
675
+ * @returns The signed transaction XDR (base64).
676
+ */
677
+ type TransactionSigner = (xdr: string, opts: {
678
+ networkPassphrase: string;
679
+ address?: string;
680
+ }) => Promise<string>;
1
681
  /** Stealth key material. All keys are hex-encoded strings for easy serialization. */
2
682
  interface StealthKeys {
3
- /** Meta-address string (st:stellar:...) — share this publicly */
683
+ /** Meta-address string (shade:stellar:...) — share this publicly */
4
684
  metaAddress: string;
5
685
  /** Spend public key (hex) */
6
686
  spendPubKey: string;
@@ -22,12 +702,45 @@ interface SendReceipt {
22
702
  interface Payment {
23
703
  /** The stealth address holding the funds */
24
704
  stealthAddress: string;
25
- /** Ephemeral public key from the announcement (hex) */
705
+ /** Ephemeral public key from the announcement/memo (hex) */
26
706
  ephemeralPubKey: string;
27
- /** Token contract address */
707
+ /**
708
+ * Token identifier — tri-modal, depending on how the payment arrived:
709
+ * - pool method: the SAC token CONTRACT address (`C...`);
710
+ * - account method, native send: the literal string `'native'`;
711
+ * - account method, token send: Horizon's `"CODE:ISSUER"` form.
712
+ */
28
713
  token: string;
714
+ /**
715
+ * Human-oriented asset label. Set on EVERY pool payment (`'XLM'` for the
716
+ * native SAC, otherwise the `C...` address — see `labelForToken`) and on
717
+ * account-method token payments (`"CODE:ISSUER"`, from the claimable
718
+ * balance). Absent only on account-method NATIVE payments, where
719
+ * {@link token} is already `'native'`.
720
+ */
721
+ asset?: string;
722
+ /**
723
+ * Claimable balance id (account-method token payments only). Its presence is
724
+ * what marks a payment as a token claim rather than a plain XLM send.
725
+ */
726
+ claimableBalanceId?: string;
29
727
  /** Amount in whole units (e.g. 100.0 = 100 XLM) */
30
728
  amount: number;
729
+ /**
730
+ * Exact amount as a decimal `bigint` count of stroops (1e-7 units), serialized
731
+ * as a string. ALWAYS set alongside {@link amount}, so callers that need
732
+ * exactness above ~9.007e8 XLM (where a float can no longer represent every
733
+ * stroop) can avoid the lossy `number`. The SDK's scan adapters populate it on
734
+ * every payment; when building a `Payment` by hand (or rehydrating one from an
735
+ * older cache that predates this field), compute it from the exact on-chain
736
+ * amount — claims prefer it over the lossy `number`. The `number` field is
737
+ * retained for display and backwards compatibility.
738
+ */
739
+ amountStroops: string;
740
+ /** Which delivery method surfaced this payment */
741
+ method: DeliveryMethod;
742
+ /** Transaction hash that delivered the payment (when known) */
743
+ txHash?: string;
31
744
  }
32
745
  /** Balance entry for a stealth address. */
33
746
  interface Balance {
@@ -37,6 +750,12 @@ interface Balance {
37
750
  token: string;
38
751
  /** Amount in whole units */
39
752
  amount: number;
753
+ /**
754
+ * Exact amount as a decimal `bigint` count of stroops, serialized as a string.
755
+ * Always set, so callers can sum/display without a lossy float. See
756
+ * {@link Payment.amountStroops}.
757
+ */
758
+ amountStroops: string;
40
759
  }
41
760
  /** Result of a withdrawal. */
42
761
  interface WithdrawReceipt {
@@ -47,60 +766,332 @@ interface WithdrawReceipt {
47
766
  }
48
767
  /** Options for sending to a stealth address. */
49
768
  interface SendOpts {
769
+ /**
770
+ * Delivery method to use. REQUIRED — callers must pick a method on every send.
771
+ * Pass `'auto'` to let the client resolve one (native + amount > 1 + 'account'
772
+ * enabled -> 'account'; otherwise 'pool').
773
+ */
774
+ method: DeliveryMethod | 'auto';
50
775
  /** Asset to send. Default: native XLM. Format: "CODE:ISSUER" */
51
776
  asset?: string;
777
+ /**
778
+ * External signer for the SENDER leg (Freighter-style). When set, the caller
779
+ * passes a PUBLIC key (G...) where a secret is normally expected — `send()`'s
780
+ * `senderSecret` positional carries the sender's G-address — and the SDK
781
+ * delegates signing to this function instead of `Keypair.fromSecret`. The
782
+ * stealth-key legs (claim/withdraw signatures) are unaffected: a wallet cannot
783
+ * hold the derived stealth scalar, so those always sign locally.
784
+ */
785
+ signTransaction?: TransactionSigner;
786
+ /**
787
+ * The G-address of the fee payer when {@link signTransaction} is used on a
788
+ * flow that needs a distinct fee payer. Unused by `send()` (whose fee payer is
789
+ * the sender itself); present here for symmetry with {@link ClaimOpts}.
790
+ */
791
+ feePayerAddress?: string;
52
792
  }
53
- /** Options for withdrawing from a stealth address. */
793
+ /** Options for withdrawing from a stealth address (pool method). */
54
794
  interface WithdrawOpts {
55
795
  /** Stealth keys (need view + spend private keys) */
56
796
  keys: StealthKeys;
57
797
  /** Secret key of account paying the Soroban invocation fee */
58
798
  feePayer: string;
59
- /** Relay URL for fee-bumped submission (privacy-preserving) */
60
- relay?: string;
799
+ /**
800
+ * Relay URL(s) for fee-bumped submission (privacy-preserving). A list is
801
+ * health-probed and routed like {@link ClientConfig.relayer}.
802
+ */
803
+ relay?: string | string[];
61
804
  /** Asset to withdraw. Default: native XLM. Format: "CODE:ISSUER" */
62
805
  asset?: string;
63
806
  /** Amount to withdraw. Default: full balance */
64
807
  amount?: number;
808
+ /**
809
+ * App funding account (G-address) to debit the relayer fee against when the
810
+ * relayer is credit-gated. Threaded into the `/relay` call so a gated relayer
811
+ * does not respond 402 `insufficient_credit`. Ignored by a non-gated relayer.
812
+ * See {@link ClaimOpts.fundingAccount}.
813
+ */
814
+ fundingAccount?: string;
815
+ /**
816
+ * Signer proving control of {@link fundingAccount}: a credit-gated relayer
817
+ * requires a fresh challenge nonce signed by the funding account before it
818
+ * debits credit for the fee-bump (proof-of-control; without it every gated
819
+ * `/relay` call is rejected 402 `missing_auth`). Omit for non-gated relayers.
820
+ */
821
+ fundingSigner?: FundingSigner;
822
+ /**
823
+ * Relayed submissions only: poll the relayer-returned txHash until it is
824
+ * on-chain before returning (SDK-TXHASH-TRUST), surfacing a
825
+ * `TransactionTimeoutError` (with the hash) if it never lands.
826
+ */
827
+ confirm?: boolean;
828
+ }
829
+ /** Per-method scan cursors, so each adapter can resume where it left off. */
830
+ interface ScanCursor {
831
+ /** Pool announcement start index (next unread announcement) */
832
+ pool?: string;
833
+ /** Horizon paging token for the account method */
834
+ account?: string;
835
+ /** Reserved for the spp method */
836
+ spp?: string;
837
+ }
838
+ /**
839
+ * Options controlling a cursor-aware scan. Also accepted by the balance path
840
+ * (`StealthClient.balance`/`balanceWithCursor`), whose account phase otherwise
841
+ * re-walks the entire Horizon history on every call.
842
+ */
843
+ interface ScanOpts {
844
+ /** Restrict the scan to these methods (default: all enabled adapters) */
845
+ methods?: DeliveryMethod[];
846
+ /** Resume from a previously returned cursor */
847
+ cursor?: ScanCursor;
848
+ /**
849
+ * Account method with an indexer configured ({@link ClientConfig.indexerUrl})
850
+ * only: a COLD scan (no cursor) walks the full Horizon history from genesis
851
+ * instead of fast-starting at the indexer's first covered position. Set it
852
+ * to discover payments that predate the indexer's coverage. Without an
853
+ * indexer the Horizon walk is always exhaustive, so this has no effect.
854
+ */
855
+ exhaustive?: boolean;
856
+ }
857
+ /**
858
+ * Diagnostics for ONE slice of a segmented scan. The account method walks up
859
+ * to three segments per scan (Horizon pre-segment, indexer-covered span,
860
+ * Horizon tail) — or a single unbounded Horizon walk when no indexer applies —
861
+ * and reports each slice's throughput so operators can see where discovery
862
+ * work actually happened.
863
+ */
864
+ interface ScanSegmentMeta {
865
+ /** Which feed served this segment's transactions. */
866
+ source: 'indexer' | 'horizon';
867
+ /**
868
+ * The segment's position in the scan: the bounded Horizon walk before
869
+ * indexer coverage (`'pre'`), the indexer-covered span (`'indexer'`), the
870
+ * always-run Horizon tail (`'tail'`), or the single unbounded walk of a
871
+ * scan with no usable indexer (`'full'`).
872
+ */
873
+ role: 'pre' | 'indexer' | 'tail' | 'full';
874
+ /** Hash-memo txs that entered the matcher with a plausible 32-byte R. */
875
+ candidates: number;
876
+ /** Payments collected from this segment. */
877
+ matches: number;
878
+ }
879
+ /**
880
+ * Per-method scan diagnostics. Produced by the account method today, and
881
+ * strictly observability: the numbers describe how a scan discovered its
882
+ * payments, never which payments it finds — the Horizon tail bounds
883
+ * correctness regardless of what happens to the indexer segment.
884
+ */
885
+ interface MethodScanMeta {
886
+ /** True once at least one `/announcements` page was successfully consumed. */
887
+ indexerUsed: boolean;
888
+ /**
889
+ * Why a CONFIGURED indexer was skipped by the health guard (absent when no
890
+ * indexer is configured, or when it was used). `'stale'` means the indexer's
891
+ * self-reported lag exceeded `ClientConfig.indexerMaxLagSeconds`.
892
+ */
893
+ indexerSkipReason?: 'unhealthy' | 'network_mismatch' | 'no_coverage' | 'stale' | 'unreachable';
894
+ /**
895
+ * The indexer's self-reported lag when `/health` was read (`null` when the
896
+ * indexer did not report one). Absent when `/health` was never readable.
897
+ */
898
+ indexerLagSeconds?: number | null;
899
+ /**
900
+ * The post-segment `/health` re-check verdict (present whenever the scan
901
+ * entered the indexer branch). Anything but `'ok'` means the segment's
902
+ * results and adopted positions were DISCARDED and the Horizon tail
903
+ * re-walked the whole span — a gap recorded while the segment was being
904
+ * paged must not advance the client cursor past a hole.
905
+ */
906
+ postCheck?: 'ok' | 'unhealthy' | 'unreachable';
907
+ /**
908
+ * True when the final cursor came from indexer-supplied data, exceeded the
909
+ * chain head Horizon reports, and was clamped down to that head — a
910
+ * persisted cursor must never point past the chain, or one malicious feed
911
+ * response would blind every future scan.
912
+ */
913
+ cursorClamped?: boolean;
914
+ /** The scan's segments, in the order they ran. */
915
+ segments: ScanSegmentMeta[];
916
+ }
917
+ /**
918
+ * Result of a cursor-aware scan — and of `StealthClient.balanceWithCursor`,
919
+ * which returns the same shape with balance-view rows (claimed payments
920
+ * dropped, native rows reporting the live remaining balance).
921
+ */
922
+ interface ScanResult {
923
+ /** Detected payments across all scanned methods */
924
+ payments: Payment[];
925
+ /** Updated cursor to persist and pass to the next scan */
926
+ cursor: ScanCursor;
927
+ /**
928
+ * Per-method scan diagnostics; populated by the account method today.
929
+ * Observability only — never affects which payments are found.
930
+ */
931
+ meta?: Partial<Record<DeliveryMethod, MethodScanMeta>>;
932
+ }
933
+ /** Options for claiming a detected payment. */
934
+ interface ClaimOpts {
935
+ /** Stealth keys (need view + spend private keys) */
936
+ keys: StealthKeys;
937
+ /**
938
+ * Relay URL(s) for fee-bumped submission (privacy-preserving). A list is
939
+ * health-probed and routed like {@link ClientConfig.relayer}.
940
+ */
941
+ relay?: string | string[];
942
+ /**
943
+ * For the account method: sweep the whole account via AccountMerge (default true).
944
+ * Set false to leave the stealth account open (Payment, keeping the base reserve).
945
+ * For the pool method this flag is ignored.
946
+ */
947
+ merge?: boolean;
948
+ /** Secret key of an account paying the fee (pool method direct submission) */
949
+ feePayer?: string;
950
+ /** Asset to claim (pool method). Default: native XLM. Format: "CODE:ISSUER" */
951
+ asset?: string;
952
+ /**
953
+ * Amount to claim. Default: full balance. Honored by POOL claims (partial
954
+ * withdrawal) AND by account-method NATIVE claims with `merge: false`
955
+ * (partial payout that keeps the stealth account open). Passing it on an
956
+ * account-method native claim WITHOUT `merge: false`, or on an
957
+ * account-method token claim (always claimed in full), throws
958
+ * {@link ClaimAmountRequiresNoMergeError} instead of being silently ignored.
959
+ */
960
+ amount?: number;
961
+ /**
962
+ * For the account-method token claim: use the relayer's sponsor-claim pair
963
+ * (prepare -> stealth-sign -> submit) instead of a self-funded stealth tx.
964
+ * Needed when the stealth account stub is missing / cannot pay reserves.
965
+ */
966
+ sponsored?: boolean;
967
+ /**
968
+ * App funding account (G-address) to debit the relayer fee against when the
969
+ * relayer is credit-gated (`RELAYER_REQUIRE_CREDIT=1`). Threaded into the
970
+ * relay / sponsor-claim submit calls so a gated relayer does not respond 402
971
+ * `insufficient_credit`. Ignored by a non-gated relayer.
972
+ */
973
+ fundingAccount?: string;
974
+ /**
975
+ * Signer proving control of {@link fundingAccount}: a credit-gated relayer
976
+ * requires a fresh challenge nonce signed by the funding account before it
977
+ * debits credit (proof-of-control; without it every gated relay /
978
+ * sponsor-claim submit is rejected 401 `missing_auth`). Omit for non-gated
979
+ * relayers.
980
+ */
981
+ fundingSigner?: FundingSigner;
982
+ /**
983
+ * Relayed submissions only: poll the relayer-returned txHash until it is
984
+ * on-chain before returning (SDK-TXHASH-TRUST), surfacing a
985
+ * `TransactionTimeoutError` (with the hash) if it never lands.
986
+ */
987
+ confirm?: boolean;
988
+ /**
989
+ * External signer for the FEE-PAYER leg (Freighter-style). When set on a pool
990
+ * claim, the caller passes the fee payer's PUBLIC key via
991
+ * {@link ClaimOpts.feePayerAddress} where a secret is normally expected (in
992
+ * {@link ClaimOpts.feePayer}) and the SDK delegates signing of the fee-paying
993
+ * transaction to this function instead of `Keypair.fromSecret`. The
994
+ * stealth-key withdrawal signature is unaffected — a wallet cannot hold the
995
+ * derived stealth scalar, so it always signs locally.
996
+ */
997
+ signTransaction?: TransactionSigner;
998
+ /**
999
+ * The fee payer's G-address, required when {@link signTransaction} is set on a
1000
+ * pool claim (which needs a fee payer). The SDK never calls
1001
+ * `Keypair.fromSecret` on this value; it is handed to {@link signTransaction}
1002
+ * as the address to sign with. When {@link signTransaction} is set but this is
1003
+ * missing, a {@link FeePayerAddressRequiredError} is thrown.
1004
+ */
1005
+ feePayerAddress?: string;
1006
+ }
1007
+ /** Receipt returned after a successful claim. */
1008
+ interface ClaimReceipt {
1009
+ /** Transaction hash */
1010
+ txHash: string;
1011
+ /** Amount claimed in whole units */
1012
+ amount: number;
1013
+ /** Which delivery method the claim used */
1014
+ method: DeliveryMethod;
65
1015
  }
66
1016
  /** Client configuration. */
67
1017
  interface ClientConfig {
68
- /** Network to connect to */
69
- network: 'local' | 'testnet';
1018
+ /** Network to connect to (a key of the SDK's `NETWORKS` table) */
1019
+ network: NetworkName;
70
1020
  /** Override the default contract ID for the stealth pool */
71
1021
  contractId?: string;
1022
+ /** Override the Horizon REST endpoint (used by the account method) */
1023
+ horizonUrl?: string;
1024
+ /**
1025
+ * Optional announcement-indexer URL — an account-method DISCOVERY
1026
+ * accelerator. When set, `scan`/`balance` consume the indexer's
1027
+ * pre-extracted announcement feed (operations inlined, no per-tx Horizon
1028
+ * round-trip) instead of walking every Horizon transaction. Horizon remains
1029
+ * the source of truth: the scan verifies the indexer's /health coverage
1030
+ * first and falls back to the pure Horizon walk automatically when the
1031
+ * indexer is unreachable, unhealthy, or on the wrong network — and always
1032
+ * finishes with a Horizon tail so indexer lag cannot hide a payment.
1033
+ */
1034
+ indexerUrl?: string;
1035
+ /**
1036
+ * Cap (seconds) on the indexer's self-reported `/health` lag before the
1037
+ * account scan skips it entirely. Default 21600 (6 hours) — deliberately
1038
+ * lenient: the Horizon tail already bounds correctness (a lagging indexer
1039
+ * can never hide a payment), and skipping a merely-slow indexer would force
1040
+ * cold scans back toward the full-history walk — so the cap exists only to
1041
+ * disqualify operationally abandoned indexers. `Number.POSITIVE_INFINITY`
1042
+ * disables the cap.
1043
+ */
1044
+ indexerMaxLagSeconds?: number;
1045
+ /** Delivery methods to enable. Default: ['pool'] */
1046
+ methods?: DeliveryMethod[];
1047
+ /**
1048
+ * Default relayer(s) for fee-bumped submissions. A list enables BYO-relayer
1049
+ * discovery: candidates are health-probed in parallel and relayed calls
1050
+ * route to a healthy one (with failover on relayer faults) per
1051
+ * {@link relayerSelection}. A single string behaves exactly as before.
1052
+ */
1053
+ relayer?: string | string[];
1054
+ /**
1055
+ * How a healthy relayer is picked from a multi-URL {@link relayer} list.
1056
+ * Default `'random'` — spreads users across the relayer set instead of
1057
+ * herding onto the first entry (anonymity-set preserving). Only meaningful
1058
+ * with more than one URL.
1059
+ */
1060
+ relayerSelection?: RelayerSelection;
72
1061
  }
73
1062
 
74
1063
  /**
75
1064
  * High-level client for stealth payments on Stellar.
76
1065
  *
77
- * Wraps DKSAP cryptography and Soroban contract interaction into a simple API.
78
- * Developers don't need to understand the underlying protocol to use this.
1066
+ * Wraps DKSAP cryptography behind pluggable delivery methods. Each method
1067
+ * (`'pool'`, `'account'`, and the reserved `'spp'`) is an adapter; the client
1068
+ * builds a registry from `config.methods` and routes send/scan/claim through it.
79
1069
  *
80
1070
  * @example
81
1071
  * ```typescript
82
- * const client = new StealthClient({ network: 'local', contractId: 'CXXX...' });
1072
+ * const client = new StealthClient({
1073
+ * network: 'testnet',
1074
+ * contractId: 'C...', // your deployed pool contract (required for 'pool')
1075
+ * methods: ['pool', 'account'],
1076
+ * });
83
1077
  *
84
- * // Generate keys
85
1078
  * const keys = StealthClient.keygen();
86
1079
  *
87
- * // Send 100 XLM to a stealth address
88
- * const receipt = await client.send(keys.metaAddress, 100, 'SXXX...');
1080
+ * // A method is REQUIRED on every send — no implicit default.
1081
+ * await client.send(keys.metaAddress, 100, 'SXXX...', { method: 'auto' });
89
1082
  *
90
- * // Scan for received payments
91
- * const payments = await client.scan(keys);
92
- *
93
- * // Withdraw
94
- * await client.withdraw(payments[0].stealthAddress, 'GDEST...', {
95
- * keys,
96
- * feePayer: 'SXXX...',
97
- * });
1083
+ * const { payments } = await client.scanWithCursor(keys);
1084
+ * await client.claim(payments[0], 'GDEST...', { keys });
98
1085
  * ```
99
1086
  */
100
1087
  declare class StealthClient {
101
1088
  private readonly contractId;
102
1089
  private readonly networkPassphrase;
103
1090
  private readonly server;
1091
+ private readonly enabledMethods;
1092
+ private readonly adapters;
1093
+ private readonly relayer?;
1094
+ private readonly relayerSelection?;
104
1095
  constructor(config: ClientConfig);
105
1096
  /**
106
1097
  * Generate a new random stealth key pair.
@@ -116,44 +1107,1209 @@ declare class StealthClient {
116
1107
  mnemonic: string;
117
1108
  };
118
1109
  /**
119
- * Send tokens to a stealth address.
1110
+ * Resolve `'auto'` to a concrete method: native asset AND amount > 1 AND
1111
+ * 'account' enabled -> 'account'; otherwise 'pool'.
1112
+ */
1113
+ private resolveMethod;
1114
+ private getAdapter;
1115
+ /**
1116
+ * Send tokens to a stealth address via the chosen delivery method.
120
1117
  *
121
- * Derives a one-time stealth address from the recipient's meta-address,
122
- * deposits tokens into the pool contract, and records the announcement.
1118
+ * A method is REQUIRED on every call (`opts.method`). Pass `'auto'` to let the
1119
+ * client resolve one; there is deliberately no implicit default.
123
1120
  *
124
- * @param metaAddress - Recipient's meta-address (st:stellar:... format)
1121
+ * @param metaAddress - Recipient's meta-address (shade:stellar:... format)
125
1122
  * @param amount - Amount in whole units (e.g. 100 = 100 XLM)
126
1123
  * @param senderSecret - Sender's Stellar secret key
127
- * @param opts - Optional: asset to send
1124
+ * @param opts - Delivery method (required) and optional asset
1125
+ * @throws {MethodRequiredError} If no method is provided.
1126
+ * @throws {MethodNotEnabledError} If the resolved method is not enabled.
128
1127
  */
129
- send(metaAddress: string, amount: number, senderSecret: string, opts?: SendOpts): Promise<SendReceipt>;
1128
+ send(metaAddress: string, amount: number, senderSecret: string, opts: SendOpts): Promise<SendReceipt>;
130
1129
  /**
131
- * Scan for stealth payments you received.
132
- *
133
- * Uses the view key to detect which announcements are yours,
134
- * then queries the contract for current balances.
1130
+ * Scan for stealth payments across every enabled delivery method.
135
1131
  *
136
1132
  * @param keys - Your stealth keys (needs viewPrivKey + spendPubKey)
137
1133
  */
138
1134
  scan(keys: StealthKeys): Promise<Payment[]>;
139
1135
  /**
140
- * Get balances for all your stealth addresses in the pool.
1136
+ * Cursor-aware scan across enabled (or explicitly requested) methods. Returns
1137
+ * the merged payments plus an updated per-method cursor to persist and pass to
1138
+ * the next scan for incremental discovery.
141
1139
  *
142
1140
  * @param keys - Your stealth keys (needs viewPrivKey + spendPubKey)
1141
+ * @param opts - Optional method filter and resume cursor
143
1142
  */
144
- balance(keys: StealthKeys): Promise<Balance[]>;
1143
+ scanWithCursor(keys: StealthKeys, opts?: ScanOpts): Promise<ScanResult>;
145
1144
  /**
146
- * Withdraw tokens from the stealth pool.
1145
+ * Shared scan implementation. When `suppressClaimed` is set (the balance
1146
+ * path), the account adapter drops fully-swept/merged native accounts (live
1147
+ * balance 0) so a spent stealth account is not reported as spendable — while
1148
+ * the plain discovery scan keeps returning still-claimable rows.
1149
+ */
1150
+ private scanInternal;
1151
+ /**
1152
+ * Get balances for all your stealth payments across enabled methods.
147
1153
  *
148
- * Recovers the stealth private key, signs a withdraw message,
149
- * and submits the transaction. Use `opts.relay` for privacy-preserving
150
- * withdrawal via a relayer.
1154
+ * @param keys - Your stealth keys (needs viewPrivKey + spendPubKey)
1155
+ * @param opts - Optional method filter and resume cursor (a cursor returned
1156
+ * by a previous {@link scanWithCursor}/{@link balanceWithCursor}). With no
1157
+ * opts the whole history is scanned, exactly as before. Passing the
1158
+ * persisted cursor turns the account phase from a full Horizon re-walk
1159
+ * into O(new transactions); use {@link balanceWithCursor} when you also
1160
+ * need the advanced cursor back to persist it.
1161
+ */
1162
+ balance(keys: StealthKeys, opts?: ScanOpts): Promise<Balance[]>;
1163
+ /**
1164
+ * Cursor-aware balance check: like {@link balance}, but resumes the
1165
+ * underlying scan from `opts.cursor` and returns the full live payments
1166
+ * (ephemeral key, txHash, claimable-balance id) PLUS the advanced cursor so
1167
+ * callers can persist it for the next call — the exact counterpart of
1168
+ * {@link scanWithCursor} for the balance path.
151
1169
  *
152
- * @param stealthAddress - The stealth address to withdraw from
1170
+ * The rows are the balance view: claimed/fully-swept payments are dropped
1171
+ * and native rows report the LIVE remaining account balance (not the
1172
+ * original per-transaction amount). Callers that persist discovered
1173
+ * payments should merge these rows into their cache BEFORE persisting the
1174
+ * advanced cursor, since the next resumed scan will skip everything behind
1175
+ * it.
1176
+ *
1177
+ * @param keys - Your stealth keys (needs viewPrivKey + spendPubKey)
1178
+ * @param opts - Optional method filter and resume cursor
1179
+ */
1180
+ balanceWithCursor(keys: StealthKeys, opts?: ScanOpts): Promise<ScanResult>;
1181
+ /**
1182
+ * Claim a detected payment to a destination, branching on the payment's
1183
+ * delivery method: `'pool'` uses the withdraw path, `'account'` sweeps or
1184
+ * partially pays out the stealth account.
1185
+ *
1186
+ * @param payment - A payment returned from {@link scan}/{@link scanWithCursor}.
1187
+ * @param destination - Destination Stellar G-address.
1188
+ * @param opts - Claim options (keys required; relay/merge/feePayer optional).
1189
+ */
1190
+ claim(payment: Payment, destination: string, opts: ClaimOpts): Promise<ClaimReceipt>;
1191
+ /**
1192
+ * Withdraw tokens from the stealth pool.
1193
+ *
1194
+ * @deprecated Use {@link claim} with a pool payment instead. Retained for
1195
+ * backwards compatibility; behaves exactly like the original pool withdraw.
1196
+ *
1197
+ * @param stealthAddress - The stealth address to withdraw from
153
1198
  * @param destination - Destination Stellar address (G...)
154
1199
  * @param opts - Withdraw options (keys, feePayer, optional relay/asset/amount)
155
1200
  */
156
1201
  withdraw(stealthAddress: string, destination: string, opts: WithdrawOpts): Promise<WithdrawReceipt>;
157
1202
  }
158
1203
 
159
- export { type Balance, type ClientConfig, type Payment, type SendOpts, type SendReceipt, StealthClient, type StealthKeys, type WithdrawOpts, type WithdrawReceipt };
1204
+ /**
1205
+ * Crypto's raw (`Uint8Array`-based) meta-address, mirrored here.
1206
+ *
1207
+ * The SDK's own {@link StealthMetaAddress} is a `shade:stellar:` *string*; this
1208
+ * is the byte-level pair the crypto layer works with.
1209
+ */
1210
+ interface RawStealthMetaAddress {
1211
+ /** 32-byte ed25519 public key for spending */
1212
+ spendPubKey: Uint8Array;
1213
+ /** 32-byte ed25519 public key for viewing */
1214
+ viewPubKey: Uint8Array;
1215
+ }
1216
+ /**
1217
+ * Crypto's raw (`Uint8Array`-based) stealth keys, exposed under a distinct name
1218
+ * so code that mixes both layers has one import site: convert with
1219
+ * {@link stealthKeysFromRaw} to get the SDK's hex-string {@link StealthKeys}.
1220
+ */
1221
+ interface RawStealthKeys {
1222
+ /** 32-byte ed25519 private key for spending */
1223
+ spendPrivKey: Uint8Array;
1224
+ /** 32-byte ed25519 private key for viewing */
1225
+ viewPrivKey: Uint8Array;
1226
+ /** Derived meta-address containing public keys */
1227
+ metaAddress: RawStealthMetaAddress;
1228
+ }
1229
+
1230
+ /**
1231
+ * Convert crypto's raw stealth keys (Uint8Array scalars + meta-address object,
1232
+ * as returned by `generateMetaAddress`, `mnemonicToStealthKeys`, or
1233
+ * `deriveKeysFromSignature`) into the SDK's hex-string {@link StealthKeys}
1234
+ * shape used by every client/session API.
1235
+ *
1236
+ * The two packages deliberately reuse the name `StealthKeys` for different
1237
+ * shapes; import the raw one as `RawStealthKeys` (re-exported from the SDK
1238
+ * index) and convert at the boundary with this helper.
1239
+ */
1240
+ declare function stealthKeysFromRaw(raw: RawStealthKeys): StealthKeys;
1241
+ /**
1242
+ * A wallet signer. Given the derivation message it returns an ed25519 signature
1243
+ * in one of the shapes real wallets produce: raw bytes, a hex/base64 string, or
1244
+ * Freighter's `{ signedMessage }` envelope.
1245
+ */
1246
+ type WalletSigner = (message: string) => Promise<Uint8Array | string | {
1247
+ signedMessage: string | Uint8Array;
1248
+ }>;
1249
+ /**
1250
+ * The default key-derivation scope. This is DECOUPLED from any transport network
1251
+ * (e.g. testnet vs. local) so that a wallet re-derives the same stealth keys
1252
+ * regardless of which network a transaction is later submitted to. The CLI's
1253
+ * `--key-scope` flag defaults to this exact value so both tools line up.
1254
+ */
1255
+ declare const DEFAULT_KEY_SCOPE = "stealth";
1256
+ /** The default application id used to scope derived keys. */
1257
+ declare const DEFAULT_APP_ID = "default";
1258
+ /** Options for {@link keysFromWalletSignature}. */
1259
+ interface WalletKeysOpts {
1260
+ /**
1261
+ * Key-derivation scope folded into the signed message (crypto's `network`
1262
+ * field). This MUST be decoupled from the transport network: use the same
1263
+ * value everywhere you derive from this wallet, or you will get different
1264
+ * (unrecoverable) keys. Defaults to {@link DEFAULT_KEY_SCOPE} (`'stealth'`),
1265
+ * matching the CLI's `--key-scope` default so both tools derive identical keys.
1266
+ */
1267
+ keyScope?: string;
1268
+ /**
1269
+ * Application id to scope keys (folded into the signed message). Defaults to
1270
+ * {@link DEFAULT_APP_ID} (`'default'`), matching the CLI's `--app-id` default.
1271
+ */
1272
+ appId?: string;
1273
+ /**
1274
+ * Sign the message TWICE and throw when the two signatures differ. This guards
1275
+ * against randomized/hardware signers that would otherwise derive different
1276
+ * (unrecoverable) keys on every call. Defaults to `true`; pass `false` only
1277
+ * for a signer you KNOW is deterministic (RFC 8032) to skip the extra signature.
1278
+ */
1279
+ verifyDeterminism?: boolean;
1280
+ }
1281
+ /**
1282
+ * Derive stealth keys from a wallet's signature over the canonical derivation
1283
+ * message.
1284
+ *
1285
+ * Because RFC 8032 ed25519 signatures are deterministic, the same wallet
1286
+ * signing the same message always re-derives the same stealth keys — that is
1287
+ * what lets a user recover stealth keys from their wallet alone, with no extra
1288
+ * secret to store. Wallet compromise equals stealth-key compromise; this is the
1289
+ * accepted trade-off for keyless recovery.
1290
+ *
1291
+ * @param signer - Callback that signs the derivation message (see {@link WalletSigner}).
1292
+ * @param opts - Optional keyScope/appId scoping and determinism verification.
1293
+ * `keyScope` and `appId` MUST match across every tool that derives from this
1294
+ * wallet (they default to `'stealth'` / `'default'`, the same as the CLI).
1295
+ * @returns Hex-string {@link StealthKeys} in the same shape as `keygen()`.
1296
+ * @throws If the signature is not exactly 64 bytes, or (unless
1297
+ * `verifyDeterminism: false`) if two signatures over the same message differ.
1298
+ *
1299
+ * @example
1300
+ * ```typescript
1301
+ * // verifyDeterminism defaults to true; pass false only for a known-good signer.
1302
+ * const keys = await keysFromWalletSignature(
1303
+ * (msg) => freighter.signMessage(msg),
1304
+ * { appId: 'my-app' },
1305
+ * );
1306
+ * ```
1307
+ */
1308
+ declare function keysFromWalletSignature(signer: WalletSigner, opts?: WalletKeysOpts): Promise<StealthKeys>;
1309
+
1310
+ /**
1311
+ * Base class for every error the SDK throws deliberately.
1312
+ *
1313
+ * Carries a stable, machine-readable {@link code} so applications can branch on
1314
+ * the error KIND without string-matching messages (which may be reworded) or
1315
+ * relying on `instanceof` (which breaks across duplicated package instances).
1316
+ * Subclasses keep their own `name` and human-readable message.
1317
+ */
1318
+ declare class ShadeError extends Error {
1319
+ /**
1320
+ * Stable machine-readable identifier for this error kind (snake_case).
1321
+ * Guaranteed not to change across SDK versions, unlike the message text.
1322
+ */
1323
+ readonly code: string;
1324
+ constructor(code: string, message: string);
1325
+ }
1326
+ /**
1327
+ * Thrown when `send()` is called without an explicit delivery method.
1328
+ * The SDK deliberately has NO implicit default: the app must choose a method
1329
+ * (or `'auto'`) on every send so the privacy trade-off is always a conscious one.
1330
+ */
1331
+ declare class MethodRequiredError extends ShadeError {
1332
+ constructor();
1333
+ }
1334
+ /**
1335
+ * Thrown when a requested delivery method is not present in the client's
1336
+ * configured `methods` list.
1337
+ */
1338
+ declare class MethodNotEnabledError extends ShadeError {
1339
+ constructor(method: DeliveryMethod);
1340
+ }
1341
+ /**
1342
+ * Thrown when a delivery method exists but cannot service the request
1343
+ * (e.g. a non-native asset over the account method, or the reserved spp method).
1344
+ */
1345
+ declare class MethodNotAvailableError extends ShadeError {
1346
+ constructor(message: string);
1347
+ }
1348
+ /**
1349
+ * Thrown when an account-method XLM send is below the protocol minimum
1350
+ * (amount must be strictly greater than 1 XLM to fund a new stealth account).
1351
+ */
1352
+ declare class MinimumAmountError extends ShadeError {
1353
+ constructor(amount: number);
1354
+ }
1355
+ /**
1356
+ * Thrown when a partial account claim requests more than the stealth account can
1357
+ * pay out while keeping its base reserve and covering the fee. The message names
1358
+ * the maximum claimable amount so the caller can retry within bounds.
1359
+ */
1360
+ declare class ClaimAmountError extends ShadeError {
1361
+ /** The maximum amount that could be claimed for this account. */
1362
+ readonly max: number;
1363
+ constructor(requested: number, max: number);
1364
+ }
1365
+ /**
1366
+ * Thrown when `opts.amount` is passed to a claim that CANNOT honor it, instead
1367
+ * of silently moving a different amount (fund-safety):
1368
+ * - An account-method NATIVE claim with an effective merge (`merge` omitted or
1369
+ * `true`) sweeps the ENTIRE balance via AccountMerge — `amount` would be
1370
+ * ignored. Pass `merge: false` for a partial claim, or drop `amount` to sweep.
1371
+ * - An account-method TOKEN claim always claims the claimable balance in full;
1372
+ * `amount` is not supported there at all.
1373
+ *
1374
+ * Pool claims are unaffected: `amount` is honored as a partial withdrawal.
1375
+ */
1376
+ declare class ClaimAmountRequiresNoMergeError extends ShadeError {
1377
+ constructor(kind?: 'native-merge' | 'token');
1378
+ }
1379
+ /**
1380
+ * Thrown when a send/claim amount is not a positive finite number, caught before
1381
+ * building the transaction so the caller gets an actionable SDK error rather than
1382
+ * an on-chain failure after the fee is burned.
1383
+ */
1384
+ declare class InvalidAmountError extends ShadeError {
1385
+ constructor(amount: unknown);
1386
+ }
1387
+ /**
1388
+ * Thrown by the sponsored token-claim path when the relayer-prepared XDR does
1389
+ * NOT match the operation list the client re-derives from its own trusted inputs
1390
+ * (stealth address, asset, balanceId, destination, amount). A malicious relayer
1391
+ * could otherwise redirect the payout or append an AccountMerge to steal the
1392
+ * just-claimed token; the client refuses to sign such a transaction. The message
1393
+ * names the specific mismatch (e.g. a tampered payout destination or an extra
1394
+ * appended operation) so the failure is diagnosable.
1395
+ */
1396
+ declare class SponsoredClaimMismatchError extends ShadeError {
1397
+ constructor(detail: string);
1398
+ }
1399
+ /**
1400
+ * Thrown by {@link StealthSession.unlock} when the supplied password fails to
1401
+ * decrypt the stored envelope. Surfaces as an AES-GCM authentication failure
1402
+ * (the tag does not verify), which is indistinguishable from tampering — either
1403
+ * way the key material must not be trusted.
1404
+ */
1405
+ declare class WrongPasswordError extends ShadeError {
1406
+ constructor();
1407
+ }
1408
+ /**
1409
+ * Thrown by {@link StealthSession.unlock} when the public keys stored in the
1410
+ * clear on the envelope do NOT match the public keys re-derived from the
1411
+ * decrypted private scalars. The private material is protected by AES-GCM, but
1412
+ * the cleartext `spendPublicKey`/`viewPublicKey` are not — a storage-WRITE
1413
+ * attacker (no password) could otherwise substitute a wrong pubkey and silently
1414
+ * break scanning (denial of discovery). Re-deriving and asserting equality on
1415
+ * unlock turns that silent mis-scan into a loud, diagnosable failure.
1416
+ */
1417
+ declare class SessionIntegrityError extends ShadeError {
1418
+ constructor(which: 'spend' | 'view');
1419
+ }
1420
+ /**
1421
+ * Thrown by the pool withdraw path when the resolved stealth address holds no
1422
+ * balance in the pool contract for the requested asset — there is nothing to
1423
+ * withdraw. Recoverable: the caller may have already withdrawn, or is querying
1424
+ * the wrong asset.
1425
+ */
1426
+ declare class NoBalanceError extends ShadeError {
1427
+ constructor();
1428
+ }
1429
+ /**
1430
+ * Thrown by the pool withdraw path when no announcement matching the given
1431
+ * stealth address can be found for these keys — the payment is not ours or has
1432
+ * not yet been indexed.
1433
+ */
1434
+ declare class AnnouncementNotFoundError extends ShadeError {
1435
+ constructor();
1436
+ }
1437
+ /**
1438
+ * Thrown by the account claim path when the stealth account cannot be found on
1439
+ * Horizon — typically because the funding send has not yet confirmed.
1440
+ */
1441
+ declare class StealthAccountNotFoundError extends ShadeError {
1442
+ constructor();
1443
+ }
1444
+ /**
1445
+ * Thrown by a token claim when the destination account does not (or cannot yet)
1446
+ * trust the asset being claimed. The message names the actionable fix — add the
1447
+ * trustline on the destination before claiming.
1448
+ */
1449
+ declare class DestinationTrustlineError extends ShadeError {
1450
+ constructor(message: string);
1451
+ }
1452
+ /**
1453
+ * Thrown by the pool withdraw path when a fee-payer secret is required (to pay
1454
+ * the Soroban invocation fee) but none was supplied.
1455
+ */
1456
+ declare class FeePayerRequiredError extends ShadeError {
1457
+ constructor();
1458
+ }
1459
+ /**
1460
+ * Thrown by the pool claim path when an external {@link TransactionSigner} is
1461
+ * supplied (via `signTransaction`) but no `feePayerAddress` is given. A pool
1462
+ * claim needs a fee payer; with external signing the fee payer is identified by
1463
+ * its G-address, never a secret. Failing loudly here prevents the SDK from ever
1464
+ * calling `Keypair.fromSecret` on a public key.
1465
+ */
1466
+ declare class FeePayerAddressRequiredError extends ShadeError {
1467
+ constructor();
1468
+ }
1469
+ /**
1470
+ * Thrown by the pool withdraw path when the recipient's persistent Balance/Nonce
1471
+ * ledger entry has been archived (Soroban state expiration) and the automatic
1472
+ * RestoreFootprint transaction that must precede the withdraw could not be
1473
+ * completed. Withdrawing over an archived footprint fails on-chain even though
1474
+ * `get_balance`/`scan` still report the funds, so the SDK restores the entry
1475
+ * first; if that restore itself fails the funds are recoverable but the withdraw
1476
+ * cannot proceed until the entry is restored. The message carries the underlying
1477
+ * cause so the operator can diagnose (e.g. an unfunded fee payer or a relayer
1478
+ * rejection).
1479
+ */
1480
+ declare class EntryArchivedRestoringError extends ShadeError {
1481
+ constructor(cause: string);
1482
+ }
1483
+ /**
1484
+ * Thrown when the RPC `sendTransaction` returns a non-terminal status that means
1485
+ * the transaction did NOT land — `TRY_AGAIN_LATER` (the node dropped it without
1486
+ * queueing) or any other non-`PENDING`/`SUCCESS` status. Nothing was submitted,
1487
+ * so the caller may safely retry with a fresh submission. Treating this as a
1488
+ * (retryable) failure prevents returning a success receipt for a tx that never
1489
+ * entered the ledger (SDK-01).
1490
+ */
1491
+ declare class TransactionRetryableError extends ShadeError {
1492
+ /** Marks this error as safe to retry (the tx never entered the ledger). */
1493
+ readonly retryable: true;
1494
+ constructor(status: string);
1495
+ }
1496
+ /**
1497
+ * Thrown when confirmation polling gives up while the transaction is still
1498
+ * PENDING. The transaction was ACCEPTED by the RPC and MAY STILL LAND on-chain
1499
+ * after this error — blindly resubmitting could double-send the funds. The
1500
+ * error carries {@link txHash} so callers can keep polling that hash (e.g.
1501
+ * `getTransaction`) to a terminal status before deciding to retry;
1502
+ * `retryable` is `false` to distinguish it from
1503
+ * {@link TransactionRetryableError} in shared retry loops.
1504
+ */
1505
+ declare class TransactionTimeoutError extends ShadeError {
1506
+ /**
1507
+ * NOT safe to blind-retry: unlike {@link TransactionRetryableError}, the
1508
+ * transaction may still be applied after this error is thrown.
1509
+ */
1510
+ readonly retryable: false;
1511
+ /** Hash of the still-pending transaction — poll it before any resubmission. */
1512
+ readonly txHash: string;
1513
+ constructor(txHash: string);
1514
+ }
1515
+ /**
1516
+ * Thrown when a relayer HTTP endpoint responds non-2xx. Carries the HTTP
1517
+ * {@link status} and the relayer's own machine-readable {@link relayerCode}
1518
+ * (e.g. `insufficient_credit`, `missing_auth`) so callers — and the
1519
+ * `RelayerPool` failover — can branch on the KIND of failure: 5xx is a relayer
1520
+ * fault worth failing over, 4xx is this request's fault and would only repeat.
1521
+ */
1522
+ declare class RelayerHttpError extends ShadeError {
1523
+ /** HTTP status the relayer responded with. */
1524
+ readonly status: number;
1525
+ /** The relayer's own `code` field from the error body, when present. */
1526
+ readonly relayerCode?: string;
1527
+ constructor(path: string, status: number, relayerCode?: string, detail?: string);
1528
+ }
1529
+ /**
1530
+ * Thrown when a relayer cannot be reached at the transport level (DNS failure,
1531
+ * refused connection, aborted fetch, invalid response body). Distinct from
1532
+ * {@link RelayerHttpError} so the `RelayerPool` can treat it as a candidate
1533
+ * fault (fail over) rather than a request fault.
1534
+ */
1535
+ declare class RelayerNetworkError extends ShadeError {
1536
+ constructor(path: string, detail: string);
1537
+ }
1538
+ /**
1539
+ * Thrown when an indexer HTTP endpoint responds non-2xx. Carries the HTTP
1540
+ * {@link status} and the indexer's own machine-readable {@link indexerCode}
1541
+ * from the `{ error, code }` body so callers can branch on the failure KIND.
1542
+ * The account scan treats it as "abandon the indexer segment" and falls back
1543
+ * to the Horizon walk — Horizon remains the source of truth.
1544
+ */
1545
+ declare class IndexerHttpError extends ShadeError {
1546
+ /** HTTP status the indexer responded with. */
1547
+ readonly status: number;
1548
+ /** The indexer's own `code` field from the error body, when present. */
1549
+ readonly indexerCode?: string;
1550
+ constructor(path: string, status: number, indexerCode?: string, detail?: string);
1551
+ }
1552
+ /**
1553
+ * Thrown when an indexer cannot be reached at the transport level (DNS
1554
+ * failure, refused connection, request timeout, invalid response body).
1555
+ * Distinct from {@link IndexerHttpError} for parity with the relayer errors;
1556
+ * the account scan treats both the same way (fall back to Horizon).
1557
+ */
1558
+ declare class IndexerNetworkError extends ShadeError {
1559
+ constructor(path: string, detail: string);
1560
+ }
1561
+ /**
1562
+ * Thrown by the `RelayerPool` when no candidate relayer is usable for this
1563
+ * call. {@link candidates} maps every probed URL to the reason it was rejected
1564
+ * (`unreachable: ...`, `timeout`, `http_<status>`, `status_not_ok`,
1565
+ * `network_mismatch (...)`, `balance_below_min (...)`,
1566
+ * `credit_gated_no_funding_auth`), so the failure is diagnosable per relayer
1567
+ * instead of a bare "nothing worked".
1568
+ */
1569
+ declare class NoHealthyRelayerError extends ShadeError {
1570
+ /** Per-URL rejection reason for every probed candidate. */
1571
+ readonly candidates: Record<string, string>;
1572
+ constructor(candidates: Record<string, string>);
1573
+ }
1574
+ /**
1575
+ * Thrown by the {@link StealthClient} constructor when a pool-capable method is
1576
+ * enabled but no contract id was supplied (there are no built-in defaults —
1577
+ * pool deployments are per-operator). Failing here — rather than deep inside a
1578
+ * later Soroban call with an opaque error — makes the misconfiguration obvious:
1579
+ * pass `contractId` explicitly.
1580
+ */
1581
+ declare class ContractIdRequiredError extends ShadeError {
1582
+ constructor(network: string);
1583
+ }
1584
+ /**
1585
+ * Thrown by `getNetworkConfig` (and therefore the {@link StealthClient}
1586
+ * constructor) when the requested network name is not in the `NETWORKS` table.
1587
+ * The type system already restricts `network` to the supported names; this is
1588
+ * the runtime guard for plain-JS callers and stale configs. The message lists
1589
+ * the currently supported networks, so it stays accurate as new networks
1590
+ * (e.g. `'public'` after the external audit) are added to the table.
1591
+ */
1592
+ declare class UnsupportedNetworkError extends ShadeError {
1593
+ /** The unknown network name that was requested. */
1594
+ readonly network: string;
1595
+ /** The network names this SDK build supports. */
1596
+ readonly supported: string[];
1597
+ constructor(network: string, supported: string[]);
1598
+ }
1599
+
1600
+ /**
1601
+ * Exact decimal <-> stroop conversions.
1602
+ *
1603
+ * A stroop is 1e-7 of a Stellar asset unit. Converting whole-unit amounts with
1604
+ * `Math.round(amount * 1e7)` or `Number(stroops) / 1e7` silently loses precision
1605
+ * once the magnitude exceeds `Number.MAX_SAFE_INTEGER` stroops (~9.007e8 XLM) —
1606
+ * a float can no longer represent every integer stroop, so large sends drift.
1607
+ * These helpers keep the value as a `bigint` count of stroops end to end and
1608
+ * only render a string at the display edge, so no amount ever round-trips
1609
+ * through a lossy float.
1610
+ */
1611
+ /**
1612
+ * Parse a decimal amount string (whole units, e.g. `"100.5"`) into an exact
1613
+ * `bigint` count of stroops.
1614
+ *
1615
+ * @param amount - Decimal string with at most 7 fractional digits.
1616
+ * @throws {Error} If the input is not a non-negative decimal, or carries more
1617
+ * than 7 fractional digits (which cannot be represented in stroops).
1618
+ */
1619
+ declare function parseStroops(amount: string): bigint;
1620
+ /**
1621
+ * Convert a `number` amount (whole units) into exact stroops, rejecting values
1622
+ * that cannot be represented exactly (non-finite, negative, or with sub-stroop
1623
+ * fractional precision). Prefer {@link parseStroops} when the amount originates
1624
+ * as a string; this exists for the numeric SDK surface.
1625
+ *
1626
+ * @param amount - Non-negative finite whole-unit amount.
1627
+ */
1628
+ declare function numberToStroops(amount: number): bigint;
1629
+ /**
1630
+ * Render an exact `bigint` stroop count back to a fixed 7-decimal string
1631
+ * (e.g. `12345000000n` -> `"1234.5000000"`). Never goes through a float.
1632
+ *
1633
+ * @param stroops - Stroop count (non-negative).
1634
+ */
1635
+ declare function formatStroops(stroops: bigint): string;
1636
+
1637
+ /**
1638
+ * Indexer `/health` response. Coverage is the OPEN interval
1639
+ * `(startCursor, cursor]` over Horizon's ascending transaction feed: the tx
1640
+ * whose paging_token equals `startCursor` is NOT covered (the scan fetches it
1641
+ * from Horizon), while everything after it up to `cursor` is.
1642
+ */
1643
+ interface IndexerHealth {
1644
+ status: string;
1645
+ network?: string;
1646
+ /** Announcement store backend. */
1647
+ store?: string;
1648
+ /** Feed position fully covered by the indexer; `null` before first ingest. */
1649
+ cursor: string | null;
1650
+ /** First covered feed position (exclusive); `null` before first ingest. */
1651
+ startCursor: string | null;
1652
+ /** Close time of the last ingested ledger (ISO timestamp). */
1653
+ lastCloseTime?: string | null;
1654
+ /** Seconds the indexer lags the network head. */
1655
+ lagSeconds?: number | null;
1656
+ /** Total announcements held by the store. */
1657
+ announcements?: number;
1658
+ /**
1659
+ * Coverage holes (ledger ranges the indexer knows it skipped). A gapped
1660
+ * indexer reports `status: 'degraded'` and is therefore skipped by the
1661
+ * scan's health guard — listed here so operators can see WHAT is missing.
1662
+ */
1663
+ gaps?: Array<{
1664
+ fromLedger: number;
1665
+ toLedger: number;
1666
+ detectedAt: string;
1667
+ }>;
1668
+ /** Ingest-loop diagnostics (shape owned by the indexer service). */
1669
+ ingest?: unknown;
1670
+ }
1671
+ /**
1672
+ * One announcement served by the indexer: a successful hash-memo transaction
1673
+ * with its operation records inlined VERBATIM (the same `HorizonOp` shapes
1674
+ * Horizon's `/transactions/{hash}/operations` returns), so consumers need no
1675
+ * per-tx Horizon round-trip.
1676
+ */
1677
+ interface IndexerAnnouncement {
1678
+ hash: string;
1679
+ paging_token: string;
1680
+ /** base64 32-byte ephemeral key R (Horizon's MemoHash encoding). */
1681
+ memo: string;
1682
+ memo_type: string;
1683
+ successful: boolean;
1684
+ created_at?: string;
1685
+ /**
1686
+ * Transaction-level source account, when the indexer has it. Enables the
1687
+ * sponsor-precise claimable-balance binding without a per-tx Horizon
1688
+ * round-trip; older feeds omit it and the scan falls back to the
1689
+ * asset+amount binding.
1690
+ */
1691
+ source_account?: string;
1692
+ /** Verbatim Horizon operation records for this transaction. */
1693
+ operations: HorizonOp[];
1694
+ }
1695
+ /**
1696
+ * Response page from `GET /announcements`. `records` ascend by paging_token
1697
+ * and are strictly greater than the request cursor. `cursor` is the position
1698
+ * to adopt directly for the next call: the last record's token on a full
1699
+ * page, otherwise max(request cursor, indexer global cursor).
1700
+ */
1701
+ interface AnnouncementsPage {
1702
+ records: IndexerAnnouncement[];
1703
+ cursor: string;
1704
+ }
1705
+ /**
1706
+ * Thin HTTP client for the announcement indexer service — the account
1707
+ * method's discovery accelerator. Horizon remains the source of truth: every
1708
+ * failure here surfaces as a typed error the scan treats as "fall back to the
1709
+ * Horizon walk", never as a lost payment.
1710
+ *
1711
+ * Transport failures and timeouts throw {@link IndexerNetworkError}; non-2xx
1712
+ * responses throw {@link IndexerHttpError} carrying the HTTP status and the
1713
+ * indexer's `{ error, code }` body code.
1714
+ *
1715
+ * @example
1716
+ * ```typescript
1717
+ * const indexer = new IndexerClient('http://localhost:3100');
1718
+ * const { cursor } = await indexer.health();
1719
+ * ```
1720
+ */
1721
+ declare class IndexerClient {
1722
+ private readonly baseUrl;
1723
+ private readonly fetchFn;
1724
+ private readonly timeoutMs;
1725
+ /**
1726
+ * @param baseUrl - Indexer root URL (no trailing slash required).
1727
+ * @param fetchFn - Injectable fetch (defaults to the global `fetch`).
1728
+ * @param opts - Optional per-request timeout in ms (default 10000).
1729
+ */
1730
+ constructor(baseUrl: string, fetchFn?: FetchLike, opts?: {
1731
+ timeoutMs?: number;
1732
+ });
1733
+ private get;
1734
+ /**
1735
+ * Decode an indexer response. A non-JSON body on an error status (e.g. a
1736
+ * proxy's HTML 502 page) must not mask the status, so decode failures fall
1737
+ * back to an empty body there; a non-JSON body on a 2xx is a broken indexer
1738
+ * and surfaces as a transport error.
1739
+ */
1740
+ private decode;
1741
+ /** Probe the indexer's health and coverage window. */
1742
+ health(): Promise<IndexerHealth>;
1743
+ /**
1744
+ * Fetch one page of announcements strictly after `cursor` (omit for the
1745
+ * start of coverage).
1746
+ *
1747
+ * @param cursor - Feed position to resume from (records returned are > it).
1748
+ * @param limit - Page size (the service caps it at 200).
1749
+ * @returns The page's records plus the cursor to adopt for the next call.
1750
+ */
1751
+ getAnnouncements(cursor?: string, limit?: number): Promise<AnnouncementsPage>;
1752
+ }
1753
+
1754
+ /**
1755
+ * A minimal key/value storage interface. `window.localStorage` satisfies it
1756
+ * directly; cookie / IndexedDB / React-Native wrappers are the app's job. All
1757
+ * methods may be sync or async so async backends work transparently.
1758
+ */
1759
+ interface KVStorage {
1760
+ getItem(key: string): string | null | Promise<string | null>;
1761
+ setItem(key: string, value: string): void | Promise<void>;
1762
+ removeItem(key: string): void | Promise<void>;
1763
+ }
1764
+ /** Options for {@link StealthSession}. */
1765
+ interface StealthSessionOpts {
1766
+ /** The backing key/value store (e.g. `window.localStorage`). */
1767
+ storage: KVStorage;
1768
+ /** Storage-key namespace prefix. Default `'stealth'`. */
1769
+ namespace?: string;
1770
+ }
1771
+ /** Cached scan progress so returning users neither re-key nor rescan from zero. */
1772
+ interface ScanState {
1773
+ /** Per-method resume cursor. */
1774
+ cursor: ScanCursor;
1775
+ /** Discovered payments so far. */
1776
+ payments: Payment[];
1777
+ /** ISO timestamp of the last update. */
1778
+ updatedAt: string;
1779
+ }
1780
+ /**
1781
+ * Storage-agnostic, encrypted web session for stealth keys and scan state.
1782
+ *
1783
+ * Keys and scan history are encrypted with AES-256-GCM under a PBKDF2-SHA256
1784
+ * (600k iterations) key derived from the user's password, using only
1785
+ * `globalThis.crypto.subtle` — no new dependencies, works in browsers and Node
1786
+ * 18+. This is intentionally separate from the CLI keystore (which stays on
1787
+ * Node's scrypt).
1788
+ *
1789
+ * @example
1790
+ * ```typescript
1791
+ * const session = new StealthSession({ storage: window.localStorage });
1792
+ * await session.saveKeys(keys, password);
1793
+ * // ... later / new page load ...
1794
+ * await session.unlock(password);
1795
+ * const keys = session.keys;
1796
+ * ```
1797
+ */
1798
+ declare class StealthSession {
1799
+ private readonly storage;
1800
+ private readonly namespace;
1801
+ private unlockedKeys;
1802
+ private password;
1803
+ constructor(opts: StealthSessionOpts);
1804
+ private get keysKey();
1805
+ private get scanKey();
1806
+ /**
1807
+ * Encrypt and persist the stealth keys under the given password. The public
1808
+ * keys are stored in the clear (they are shareable); only the private key
1809
+ * material is encrypted.
1810
+ */
1811
+ saveKeys(keys: StealthKeys, password: string): Promise<void>;
1812
+ /**
1813
+ * Decrypt the stored keys into memory using the password.
1814
+ *
1815
+ * @throws {WrongPasswordError} If the password fails the AES-GCM auth check.
1816
+ * @throws If no keys are stored.
1817
+ */
1818
+ unlock(password: string): Promise<StealthKeys>;
1819
+ /** The in-memory keys after {@link unlock}/{@link saveKeys}. */
1820
+ get keys(): StealthKeys;
1821
+ /** Wipe in-memory key material (storage is untouched). */
1822
+ lock(): void;
1823
+ /** Whether keys are present in storage (does not require unlock). */
1824
+ hasKeys(): Promise<boolean>;
1825
+ /** Remove all session data (keys + scan state) and lock in memory. */
1826
+ clear(): Promise<void>;
1827
+ /**
1828
+ * Load the encrypted scan-state cache. Requires an unlocked session (the same
1829
+ * derived key protects it, since payment history is private). Returns null
1830
+ * when nothing is cached.
1831
+ */
1832
+ loadScanState(): Promise<ScanState | null>;
1833
+ /**
1834
+ * Encrypt and persist the scan-state cache under the session password.
1835
+ * Requires an unlocked session.
1836
+ */
1837
+ saveScanState(state: ScanState): Promise<void>;
1838
+ }
1839
+
1840
+ /**
1841
+ * How to sign a transaction leg for the restore flow — either the fee-payer
1842
+ * secret / external signer already used by the withdraw, expressed as a single
1843
+ * async callback. The helper stays agnostic to local-vs-Freighter signing.
1844
+ */
1845
+ type RestoreSigner = (tx: Transaction) => Promise<Transaction>;
1846
+ /**
1847
+ * How to submit a signed transaction — either directly to the RPC or fee-bumped
1848
+ * through the relayer, mirroring whatever the withdraw itself uses. Returns the
1849
+ * submitted transaction hash.
1850
+ */
1851
+ type RestoreSubmit = (tx: Transaction) => Promise<string>;
1852
+ /**
1853
+ * Optional progress reporter (the CLI wires this to a spinner/log line; the SDK
1854
+ * leaves it undefined).
1855
+ */
1856
+ type RestoreNotify = (message: string) => void;
1857
+ /**
1858
+ * Rebuild the invocation transaction (e.g. the withdraw) from a freshly-fetched
1859
+ * source account. Used ONLY on the restore branch: after the RestoreFootprint
1860
+ * consumes the source account's next sequence number, the invocation must be
1861
+ * rebuilt on the new sequence — reusing the original pre-restore transaction
1862
+ * would submit at an already-consumed sequence (`txBAD_SEQ`). The factory must
1863
+ * produce the exact same operation/fee/timeout as the original build so only the
1864
+ * sequence differs.
1865
+ */
1866
+ type RebuildInvocation = (account: Account) => Transaction;
1867
+ /**
1868
+ * Prepare a Soroban invocation transaction for submission, transparently
1869
+ * restoring an archived persistent footprint first when required.
1870
+ *
1871
+ * `server.prepareTransaction` only throws on a simulation ERROR — it never
1872
+ * consults `sim.restorePreamble`. When a recipient's persistent Balance/Nonce
1873
+ * entry has archived (Soroban state expiration), simulation returns
1874
+ * SUCCESS-with-restorePreamble; the bare prepare then assembles the withdraw
1875
+ * over the archived footprint, which submits but fails on-chain while
1876
+ * `get_balance`/`scan` still show the funds — an opaque brick.
1877
+ *
1878
+ * This helper reproduces `prepareTransaction`'s behavior for the normal path
1879
+ * (simulate → assemble the passed-in `tx`) but, when
1880
+ * {@link rpc.Api.isSimulationRestore} is true, first builds and submits a
1881
+ * RestoreFootprint transaction from `sim.restorePreamble`, waits for it to
1882
+ * succeed, then — because the restore consumes the fee-payer's next sequence
1883
+ * number — **re-fetches the source account and rebuilds the invocation on the
1884
+ * fresh sequence** via {@link RebuildInvocation} before re-simulating and
1885
+ * assembling. This avoids a `txBAD_SEQ` collision between the restore and the
1886
+ * withdraw. The non-archived path is behaviorally identical to the original
1887
+ * `prepareTransaction` call (the passed-in `tx` is assembled untouched).
1888
+ *
1889
+ * @param tx - The unsigned Soroban invocation transaction (e.g. the withdraw),
1890
+ * already built on the source account's current sequence. Used directly on the
1891
+ * non-archived path.
1892
+ * @param rebuildTx - Rebuilds the invocation from a freshly-fetched source
1893
+ * account; used only on the restore branch to obtain a non-colliding sequence.
1894
+ * @param server - The Soroban RPC server.
1895
+ * @param networkPassphrase - Network passphrase for building the restore tx.
1896
+ * @param signRestore - Signs the RestoreFootprint transaction (fee-payer leg).
1897
+ * @param submitRestore - Submits the signed RestoreFootprint transaction.
1898
+ * @param notify - Optional progress reporter.
1899
+ * @returns The assembled invocation transaction, ready to sign and submit.
1900
+ * @throws {EntryArchivedRestoringError} If the entry is archived and the
1901
+ * restore transaction could not be completed.
1902
+ */
1903
+ declare function prepareWithRestore(tx: Transaction, rebuildTx: RebuildInvocation, server: rpc.Server, networkPassphrase: string, signRestore: RestoreSigner, submitRestore: RestoreSubmit, notify?: RestoreNotify): Promise<Transaction>;
1904
+
1905
+ /** Parameters passed to a delivery adapter's `send`. */
1906
+ interface AdapterSendParams {
1907
+ /** Recipient meta-address (shade:stellar:... or spend:view hex) */
1908
+ metaAddress: string;
1909
+ /** Amount in whole units */
1910
+ amount: number;
1911
+ /**
1912
+ * Sender's Stellar secret key — OR, when {@link signTransaction} is set, the
1913
+ * sender's PUBLIC G-address (the SDK never calls `Keypair.fromSecret` on it).
1914
+ */
1915
+ senderSecret: string;
1916
+ /** Asset to send. Default: native XLM. Format: "CODE:ISSUER" */
1917
+ asset?: string;
1918
+ /**
1919
+ * Optional external signer for the sender leg (Freighter-style). When set,
1920
+ * {@link senderSecret} carries the sender's G-address and the adapter
1921
+ * delegates signing to this function instead of `Keypair.fromSecret`.
1922
+ */
1923
+ signTransaction?: TransactionSigner;
1924
+ }
1925
+ /**
1926
+ * A pluggable delivery method. Each adapter fully owns one way of getting funds
1927
+ * to a stealth recipient: how to send, how to discover incoming payments, and
1928
+ * how to claim them. The client just wires adapters together per config.
1929
+ */
1930
+ interface DeliveryAdapter {
1931
+ /** The delivery method this adapter implements. */
1932
+ readonly method: DeliveryMethod;
1933
+ /**
1934
+ * Send funds to a stealth recipient using this method.
1935
+ *
1936
+ * @param params - Recipient, amount, sender secret, and optional asset.
1937
+ * @returns The one-time stealth address and delivering transaction hash.
1938
+ */
1939
+ send(params: AdapterSendParams): Promise<SendReceipt>;
1940
+ /**
1941
+ * Discover incoming payments for the given keys.
1942
+ *
1943
+ * @param keys - Recipient stealth keys (needs viewPrivKey + spendPubKey).
1944
+ * @param cursor - Opaque per-method resume cursor.
1945
+ * @param opts - `suppressClaimedNative` drops swept accounts (balance view);
1946
+ * `exhaustive` disables the account adapter's indexer fast cold start.
1947
+ * Adapters ignore flags that do not apply to their method.
1948
+ * @returns Detected payments, the next cursor to persist, and optional
1949
+ * per-scan diagnostics (observability only — see {@link MethodScanMeta}).
1950
+ */
1951
+ scan(keys: StealthKeys, cursor?: string, opts?: {
1952
+ suppressClaimedNative?: boolean;
1953
+ exhaustive?: boolean;
1954
+ }): Promise<{
1955
+ payments: Payment[];
1956
+ cursor?: string;
1957
+ meta?: MethodScanMeta;
1958
+ }>;
1959
+ /**
1960
+ * Claim a previously discovered payment to a destination.
1961
+ *
1962
+ * @param payment - The payment to claim (from `scan`).
1963
+ * @param destination - Destination Stellar G-address.
1964
+ * @param opts - Claim options (keys, relay, merge, etc.).
1965
+ * @returns The claim transaction hash, amount, and method.
1966
+ */
1967
+ claim(payment: Payment, destination: string, opts: ClaimOpts): Promise<ClaimReceipt>;
1968
+ }
1969
+
1970
+ /**
1971
+ * The pool delivery adapter: deposit/scan/withdraw against the Soroban pool
1972
+ * contract. This is the original, private, multi-token path — the logic here is
1973
+ * the behavior previously inlined in `StealthClient`, extracted unchanged so the
1974
+ * client can treat it as one adapter among several.
1975
+ */
1976
+ declare class PoolAdapter implements DeliveryAdapter {
1977
+ private readonly contractId;
1978
+ private readonly networkPassphrase;
1979
+ private readonly server;
1980
+ readonly method: "pool";
1981
+ private readonly relayerSelection?;
1982
+ constructor(contractId: string, networkPassphrase: string, server: StellarSdk.rpc.Server, opts?: {
1983
+ relayerSelection?: RelayerSelection;
1984
+ });
1985
+ /**
1986
+ * Derive a one-time stealth address and deposit into the pool contract,
1987
+ * recording the announcement atomically with the deposit.
1988
+ */
1989
+ send(params: AdapterSendParams): Promise<SendReceipt>;
1990
+ /**
1991
+ * Scan pool announcements starting at the cursor (announcement index). The
1992
+ * cursor advances by the number of announcements returned; a cheap
1993
+ * `get_announcement_count` lets callers skip a full scan when nothing is new.
1994
+ */
1995
+ scan(keys: StealthKeys, cursor?: string): Promise<{
1996
+ payments: Payment[];
1997
+ cursor?: string;
1998
+ }>;
1999
+ private matchAndPrice;
2000
+ /**
2001
+ * Claim (withdraw) a pool payment: recover the stealth key, sign a withdraw
2002
+ * message, and submit — directly or via a relayer fee-bump.
2003
+ */
2004
+ claim(payment: Payment, destination: string, opts: ClaimOpts): Promise<ClaimReceipt>;
2005
+ /**
2006
+ * The original pool withdraw path, preserved for the client's `@deprecated`
2007
+ * `withdraw()` alias and reused by `claim()`.
2008
+ */
2009
+ withdraw(stealthAddress: string, destination: string, opts: {
2010
+ keys: StealthKeys;
2011
+ feePayer: string;
2012
+ /** Relay URL(s); a list is health-probed and routed with failover. */
2013
+ relay?: string | string[];
2014
+ asset?: string;
2015
+ amount?: number;
2016
+ signTransaction?: TransactionSigner;
2017
+ feePayerAddress?: string;
2018
+ /** App funding account to debit against (credit-gated relayers). */
2019
+ fundingAccount?: string;
2020
+ /**
2021
+ * Signer proving control of `fundingAccount` — a credit-gated relayer
2022
+ * rejects `/relay` without a signed challenge (proof-of-control), so
2023
+ * `fundingAccount` alone is not enough to spend credit.
2024
+ */
2025
+ fundingSigner?: FundingSigner;
2026
+ /**
2027
+ * Relayed submissions only: poll the relayer-returned txHash until it is
2028
+ * actually on-chain before returning (SDK-TXHASH-TRUST), surfacing a
2029
+ * `TransactionTimeoutError` (with the hash) if it never lands. Default
2030
+ * `false`: trust the relayer's response, exactly as before. The direct
2031
+ * (non-relay) path already confirms via `resolveSendResult` regardless.
2032
+ */
2033
+ confirm?: boolean;
2034
+ }): Promise<{
2035
+ txHash: string;
2036
+ amount: number;
2037
+ }>;
2038
+ }
2039
+
2040
+ /**
2041
+ * The account delivery adapter: a direct classic Stellar payment that
2042
+ * creates/pays a one-time stealth account, with the ephemeral key R carried in
2043
+ * a 32-byte MemoHash. No view tag is needed — the destination match against the
2044
+ * DKSAP-derived address IS the full verification.
2045
+ *
2046
+ * Native XLM lands via CreateAccount/Payment; tokens land as a
2047
+ * CreateClaimableBalance the recipient later claims (adding a trustline first).
2048
+ */
2049
+ declare class AccountAdapter implements DeliveryAdapter {
2050
+ private readonly networkPassphrase;
2051
+ private readonly horizon;
2052
+ private readonly relayer?;
2053
+ readonly method: "account";
2054
+ private readonly rpcServer?;
2055
+ private readonly relayerSelection?;
2056
+ private readonly indexer?;
2057
+ private readonly indexerMaxLagSeconds;
2058
+ constructor(networkPassphrase: string, horizon: HorizonClient, relayer?: string | string[] | undefined, opts?: {
2059
+ rpcServer?: TransactionStatusSource;
2060
+ relayerSelection?: RelayerSelection;
2061
+ /** Optional discovery accelerator consumed by {@link scan}. */
2062
+ indexer?: IndexerClient;
2063
+ /**
2064
+ * Cap on the indexer's self-reported lag before the health guard skips
2065
+ * it (default {@link DEFAULT_INDEXER_MAX_LAG_SECONDS}; see the rationale
2066
+ * there). `Number.POSITIVE_INFINITY` disables the cap.
2067
+ */
2068
+ indexerMaxLagSeconds?: number;
2069
+ });
2070
+ /**
2071
+ * Send funds directly to a one-time stealth account. Native XLM uses a plain
2072
+ * CreateAccount/Payment; a non-native asset uses CreateAccount (to open the
2073
+ * stealth account) plus a CreateClaimableBalance carrying the token. Amount
2074
+ * MUST be strictly greater than 1 XLM for native sends.
2075
+ */
2076
+ send(params: AdapterSendParams): Promise<SendReceipt>;
2077
+ /** Native XLM send: CreateAccount (falls back to Payment on retry). */
2078
+ private sendNative;
2079
+ /**
2080
+ * Token send: one classic tx carrying two operations —
2081
+ * CreateAccount(stealth, 1.5001 XLM) to open the account with trustline
2082
+ * headroom, then CreateClaimableBalance(asset, amount, claimant: stealth,
2083
+ * unconditional). The 0.5 XLM claimable-balance reserve returns to the sender
2084
+ * when the recipient claims. The ephemeral R rides in the MemoHash exactly as
2085
+ * for native sends.
2086
+ *
2087
+ * Not idempotent on retry: unlike {@link sendNative} (which falls back to a
2088
+ * Payment on `op_already_exists`), a token send has no such fallback. Each
2089
+ * token send uses a fresh ephemeral — hence a fresh, never-before-created
2090
+ * stealth address — so a resubmit targets a new account rather than colliding,
2091
+ * making the retry path unnecessary here.
2092
+ */
2093
+ private sendToken;
2094
+ /**
2095
+ * Discover incoming direct payments over Horizon's ascending transaction
2096
+ * feed. For each tx with a hash memo, the memo decodes to a candidate R;
2097
+ * deriving the stealth address from (viewPrivKey, spendPubKey, R) and
2098
+ * finding an operation whose destination equals that address confirms the
2099
+ * payment is ours. Three op shapes are matched:
2100
+ * - `create_account` / `payment` -> a native XLM send.
2101
+ * - `create_claimable_balance` with our address as a claimant -> a token send;
2102
+ * the claimable balance id is resolved via Horizon's claimable-balances API.
2103
+ *
2104
+ * With an indexer configured (and passing the health guard) the walk is
2105
+ * segmented: a bounded Horizon pre-segment covers anything before the
2106
+ * indexer's coverage window, the covered span consumes pre-extracted
2107
+ * announcements with inlined operations (no per-tx round-trip), and a
2108
+ * Horizon tail ALWAYS runs from the last adopted position so indexer lag —
2109
+ * or an indexer fault mid-segment — can never hide a payment. A cold scan
2110
+ * fast-starts at the indexer's first covered position; payments predating
2111
+ * that coverage need `exhaustive: true`. Without an indexer the behavior is
2112
+ * exactly the original unbounded Horizon walk.
2113
+ *
2114
+ * Every path also returns `meta` — per-segment candidate/match counts plus
2115
+ * the indexer guard's verdict. Strictly observability: the numbers describe
2116
+ * how the scan ran, never which payments it finds.
2117
+ */
2118
+ scan(keys: StealthKeys, cursor?: string, opts?: {
2119
+ suppressClaimedNative?: boolean;
2120
+ exhaustive?: boolean;
2121
+ }): Promise<{
2122
+ payments: Payment[];
2123
+ cursor?: string;
2124
+ meta: MethodScanMeta;
2125
+ }>;
2126
+ /**
2127
+ * Probe the configured indexer and classify whether it is usable for THIS
2128
+ * scan: `status === 'ok'`, same network as this adapter, a non-null
2129
+ * coverage interval, and self-reported lag within the configured cap. On
2130
+ * any guard failure the scan stays on the pure Horizon walk (Horizon is the
2131
+ * source of truth); the returned reason feeds {@link MethodScanMeta} so the
2132
+ * otherwise-silent fallback is observable. A null/absent `lagSeconds` is
2133
+ * NOT stale — lag cannot be measured then, and correctness is tail-bounded
2134
+ * anyway.
2135
+ */
2136
+ private indexerCoverage;
2137
+ /**
2138
+ * Ascending Horizon transaction walk shared by every scan segment,
2139
+ * processing candidates from `from` (exclusive, Horizon cursor semantics).
2140
+ * Unbounded when `stopAtToken` is undefined. When bounded, txs with
2141
+ * `BigInt(paging_token) <= BigInt(stopAtToken)` are processed — INCLUSIVE
2142
+ * of the boundary tx, matching the indexer's open coverage interval
2143
+ * (startCursor, cursor] — and the walk returns `stopAtToken` at the first
2144
+ * tx beyond it. Otherwise returns the last seen paging token (the scan
2145
+ * cursor), or `from` when nothing new was seen. `stats` accumulates the
2146
+ * segment's candidate/match counts (observability only).
2147
+ */
2148
+ private walkHorizon;
2149
+ /**
2150
+ * Run one transaction through the candidate pipeline: verify the memo
2151
+ * shape, decode the ephemeral R, derive the receiver-side stealth address,
2152
+ * and — on an ownership match — collect the native and/or token payment
2153
+ * rows into `payments`.
2154
+ *
2155
+ * `ops` is either the inline operation records (indexer announcements carry
2156
+ * them verbatim — no per-tx round-trip) or a lazy fetch invoked only after
2157
+ * the candidate checks pass (the Horizon walk — preserving the pre-indexer
2158
+ * behavior of never fetching operations for non-candidate txs). The memo
2159
+ * checks run here even for indexer-served records, so a corrupt feed entry
2160
+ * degrades to "not a candidate" rather than a bogus derivation.
2161
+ *
2162
+ * `stats.candidates` counts every tx that passes the memo-shape checks —
2163
+ * BEFORE derivation, so Horizon- and indexer-served sources count
2164
+ * identically — and `stats.matches` counts every payment pushed.
2165
+ */
2166
+ private collectCandidate;
2167
+ /**
2168
+ * Build the native income row for a matched create_account/payment leg, or
2169
+ * `null` to suppress it.
2170
+ *
2171
+ * Suppression is gated on whether the SAME tx also delivered a matching token
2172
+ * claimable balance (`hasTokenLeg`): the token path fronts a fixed reserve via
2173
+ * a create_account stub, which must NOT surface as a native payment — but this
2174
+ * is decided by the presence of the token leg, NOT by any magic starting
2175
+ * balance, so a genuine native send that merely happens to equal the reserve
2176
+ * constant is still discovered.
2177
+ *
2178
+ * On the balance path (`suppressClaimed`) the row reports the account's LIVE
2179
+ * native balance (what remains after any partial claim), and a fully-swept
2180
+ * (0) account is dropped. The discovery path reports the per-tx op amount so
2181
+ * two sends to the same address each surface independently.
2182
+ */
2183
+ private buildNativePayment;
2184
+ /**
2185
+ * Build the token income row for a matched create_claimable_balance leg, or
2186
+ * `null` when no genuine, currently-claimable CB binds to it.
2187
+ *
2188
+ * The candidate CBs (Horizon lists ALL CBs the derived address can claim) are
2189
+ * bound to THIS specific op — not resolved by first-claimant — so an attacker
2190
+ * who creates their own CreateClaimableBalance naming the public stealth
2191
+ * address cannot mask/misattribute the real payment. Binding matches the CB
2192
+ * whose sponsor is the tx source AND whose asset+amount equal this op's, and
2193
+ * whose claim predicate for the derived address is currently satisfiable.
2194
+ */
2195
+ private buildTokenPayment;
2196
+ /**
2197
+ * Select the live claimable balance that genuinely corresponds to this tx's
2198
+ * create_claimable_balance op. Filters candidates to those whose claimant is
2199
+ * the derived address with a currently-satisfiable predicate, then binds by
2200
+ * sponsor === tx source AND asset === op asset AND amount === op amount. Only
2201
+ * when that precise binding is ambiguous or the tx source is unknown does it
2202
+ * fall back to an asset+amount match — never a bare first-claimant pick, so an
2203
+ * attacker CB cannot shadow the real one.
2204
+ */
2205
+ private bindClaimableBalance;
2206
+ /**
2207
+ * Claim a direct-account payment. Branches on the payment shape: a claimable
2208
+ * balance (token send) runs the trustline + claim recipe; a plain XLM send
2209
+ * sweeps or partially pays out the stealth account.
2210
+ */
2211
+ claim(payment: Payment, destination: string, opts: ClaimOpts): Promise<ClaimReceipt>;
2212
+ /**
2213
+ * Claim a native XLM direct send. The stealth account is a real funded Stellar
2214
+ * account with a sequence number. Full sweep (default) uses AccountMerge; a
2215
+ * partial claim uses Payment. Signing uses the raw stealth scalar (verifies as
2216
+ * standard ed25519). Optionally fee-bumped via a relayer.
2217
+ *
2218
+ * Passing `opts.amount` without `merge: false` is rejected up front
2219
+ * ({@link ClaimAmountRequiresNoMergeError}): the default merge sweeps the
2220
+ * ENTIRE balance via AccountMerge, so silently ignoring `amount` would move
2221
+ * more funds than the caller asked for.
2222
+ */
2223
+ private claimNative;
2224
+ /**
2225
+ * Claim a token direct send delivered as a claimable balance. Requires the
2226
+ * destination to already trust the asset (probed first with an actionable
2227
+ * error otherwise).
2228
+ *
2229
+ * Self-funded path (stealth account exists): ChangeTrust(asset) ->
2230
+ * ClaimClaimableBalance(id) -> optional full exit
2231
+ * [Payment(asset, destination) -> ChangeTrust(limit '0') ->
2232
+ * AccountMerge(destination)].
2233
+ *
2234
+ * Sponsored path (`opts.sponsored`, or account stub missing): delegate to the
2235
+ * relayer's sponsor-claim pair — prepare returns XDR, we attach the stealth
2236
+ * signature and submit.
2237
+ */
2238
+ private claimToken;
2239
+ /**
2240
+ * Sponsored claim: relayer prepares the XDR (BeginSponsoring -> [CreateAccount]
2241
+ * -> ChangeTrust -> EndSponsoring -> ClaimClaimableBalance -> Payment to the
2242
+ * destination), we co-sign with the stealth key, relayer submits and pays the
2243
+ * fee. The claimed token is delivered to `destination` in the same tx — the
2244
+ * stealth account never needs its own fee balance. The receipt's amount is the
2245
+ * amount that reached the destination.
2246
+ */
2247
+ private claimTokenSponsored;
2248
+ /**
2249
+ * Parse and verify a relayer-prepared sponsored-claim XDR against the client's
2250
+ * OWN trusted inputs BEFORE signing. This is the client-side security control:
2251
+ * the relayer-side `opsMatch` protects the relayer, not us, so a malicious
2252
+ * relayer could otherwise redirect the payout Payment or append an
2253
+ * AccountMerge to steal the just-claimed token.
2254
+ *
2255
+ * Verifies, throwing {@link SponsoredClaimMismatchError} on any mismatch:
2256
+ * - the tx is sourced by the relayer (never the stealth account);
2257
+ * - the tx carries no memo;
2258
+ * - every operation is one of the allowed sponsor-claim shapes, in the exact
2259
+ * order the relayer builds — BeginSponsoring(stealth) -> optional
2260
+ * CreateAccount(stealth, '0') -> ChangeTrust(asset, source stealth) ->
2261
+ * EndSponsoring(source stealth) -> ClaimClaimableBalance(balanceId, source
2262
+ * stealth) -> Payment(destination, asset, amount, source stealth);
2263
+ * - every value-moving op is sourced by the stealth account;
2264
+ * - the payout Payment's destination/asset/amount equal the caller's intent;
2265
+ * - no extra, missing, or reordered operations.
2266
+ *
2267
+ * Returns the parsed {@link Transaction} (ready to co-sign) on success.
2268
+ */
2269
+ private verifySponsoredClaimXdr;
2270
+ /**
2271
+ * Resolve the EXACT 7-dp payout string for a token claim from the precise
2272
+ * stroop count, never the lossy `payment.amount` double. Prefers the exact
2273
+ * `payment.amountStroops` string; when it is absent, falls back to the 7-dp
2274
+ * `payment.amount` double with NO extra Horizon read (the amount is already in
2275
+ * hand from the scan). Above 2^53 stroops the double in `payment.amount`
2276
+ * cannot represent every stroop, so an exact stroop count should always be
2277
+ * threaded through when available (SDK-PREC-1).
2278
+ */
2279
+ private resolveExactPayout;
2280
+ /** Recover the stealth scalar for signing from the payment's ephemeral R. */
2281
+ private recoverKey;
2282
+ /** Fail with an actionable error unless the destination trusts the asset. */
2283
+ private assertDestinationTrusts;
2284
+ /** Submit an XDR directly to Horizon, or via a relayer fee-bump when set. */
2285
+ private submit;
2286
+ }
2287
+
2288
+ /**
2289
+ * Reserved adapter slot for Stellar Private Payments (SPP).
2290
+ *
2291
+ * SPP is the Nethermind ZK shielded-pool research prototype (testnet-only
2292
+ * today). This adapter intentionally implements the full {@link DeliveryAdapter}
2293
+ * surface but every method throws {@link MethodNotAvailableError}. The slot
2294
+ * exists so applications can opt into SPP later — by adding `'spp'` to
2295
+ * `ClientConfig.methods` — with ZERO API changes: the same `send`/`scan`/`claim`
2296
+ * calls simply start routing through a real SPP implementation once it lands.
2297
+ */
2298
+ declare class SppAdapter implements DeliveryAdapter {
2299
+ readonly method: "spp";
2300
+ /**
2301
+ * @throws {MethodNotAvailableError} Always — SPP is not yet implemented. The
2302
+ * declared return type is {@link SendReceipt} (never actually returned) so the
2303
+ * adapter's surface matches the {@link DeliveryAdapter} interface exactly.
2304
+ */
2305
+ send(_params: AdapterSendParams): Promise<SendReceipt>;
2306
+ /** @throws {MethodNotAvailableError} Always — SPP is not yet implemented. */
2307
+ scan(_keys: StealthKeys, _cursor?: string): Promise<{
2308
+ payments: Payment[];
2309
+ cursor?: string;
2310
+ }>;
2311
+ /** @throws {MethodNotAvailableError} Always — SPP is not yet implemented. */
2312
+ claim(_payment: Payment, _destination: string, _opts: ClaimOpts): Promise<ClaimReceipt>;
2313
+ }
2314
+
2315
+ export { AccountAdapter, type AdapterSendParams, AnnouncementNotFoundError, type AnnouncementsPage, type Balance, ClaimAmountError, ClaimAmountRequiresNoMergeError, type ClaimOpts, type ClaimReceipt, type ClientConfig, ContractIdRequiredError, type CreditChallenge, type CreditView, DEFAULT_APP_ID, DEFAULT_KEY_SCOPE, type DeliveryAdapter, type DeliveryMethod, DestinationTrustlineError, EntryArchivedRestoringError, FeePayerAddressRequiredError, FeePayerRequiredError, type FetchLike, type FundingSigner, type HorizonAccount, type HorizonClaimableBalance, type HorizonClaimant, HorizonClient, type HorizonOp, type HorizonPredicate, type HorizonTx, type IndexerAnnouncement, IndexerClient, type IndexerHealth, IndexerHttpError, IndexerNetworkError, InvalidAmountError, type KVStorage, MethodNotAvailableError, MethodNotEnabledError, MethodRequiredError, type MethodScanMeta, MinimumAmountError, NETWORKS, type NetworkConfig, type NetworkDefinition, type NetworkName, NoBalanceError, NoHealthyRelayerError, type Payment, PoolAdapter, type ProbeOutcome, type RawStealthKeys, type RawStealthMetaAddress, type RebuildInvocation, type RelayOpts, type RelayerCallCtx, RelayerClient, type RelayerHealth, RelayerHttpError, RelayerNetworkError, RelayerPool, type RelayerPoolOpts, type RelayerSelection, type RestoreNotify, type RestoreSigner, type RestoreSubmit, type ScanCursor, type ScanOpts, type ScanResult, type ScanSegmentMeta, type ScanState, type SendOpts, type SendReceipt, SessionIntegrityError, ShadeError, type SponsorClaimPrepareArgs, type SponsorClaimPrepared, type SponsorOpts, SponsoredClaimMismatchError, SppAdapter, StealthAccountNotFoundError, StealthClient, type StealthKeys, StealthSession, type StealthSessionOpts, TransactionRetryableError, type TransactionSigner, type TransactionStatusSource, TransactionTimeoutError, UnsupportedNetworkError, type WalletKeysOpts, type WalletSigner, type WithdrawOpts, type WithdrawReceipt, WrongPasswordError, buildWithdrawMessage, challengeMessage, createSimulationTx, formatStroops, getNetworkConfig, keysFromWalletSignature, labelForToken, networkNameForPassphrase, normalizeRelayList, numberToStroops, parseStroops, prepareWithRestore, resolveTokenAddress, simulateReadOnly, stealthKeysFromRaw, waitForTransaction };