@quaivault/sdk 0.1.0 → 0.2.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,8 +1,10 @@
1
1
  import * as quais from 'quais';
2
2
  import { Provider, Signer, Contract, Interface } from 'quais';
3
3
  export { Provider, Signer } from 'quais';
4
+ import * as node_worker_threads from 'node:worker_threads';
4
5
  import * as _supabase_postgrest_js from '@supabase/postgrest-js';
5
- import { SupabaseClient } from '@supabase/supabase-js';
6
+ import { PostgrestClient } from '@supabase/postgrest-js';
7
+ import { RealtimeChannel } from '@supabase/realtime-js';
6
8
  import { z } from 'zod';
7
9
  export { Erc1155Abi, Erc20Abi, Erc721Abi, MultiSendCallOnlyAbi, QuaiVaultAbi, QuaiVaultFactoryAbi, QuaiVaultProxyAbi, QuaiVaultProxyBytecode, SocialRecoveryModuleAbi } from './abi/index.cjs';
8
10
 
@@ -62,7 +64,7 @@ interface NetworkConfig {
62
64
  */
63
65
  type Consistency = 'auto' | 'indexed' | 'chain';
64
66
  interface ClientOptions {
65
- network?: NetworkConfig | keyof typeof networks;
67
+ network?: NetworkConfig | NetworkName;
66
68
  provider?: Provider;
67
69
  signer?: Signer;
68
70
  /** Private key for a local signer. Prefer the `QUAIVAULT_PRIVATE_KEY` env var. */
@@ -129,8 +131,16 @@ interface VaultTransaction {
129
131
  value: bigint;
130
132
  data: Hex;
131
133
  proposer: Address;
132
- /** Unix seconds when the proposal was recorded on chain. */
134
+ /**
135
+ * Unix seconds when the proposal was recorded on chain, or 0 when unknown.
136
+ *
137
+ * Only chain reads carry a timestamp: the vault stores one in the transaction
138
+ * struct, but the indexer records the *block* instead. On an indexed read this is 0
139
+ * and {@link proposedAtBlock} carries the position. Never render 0 as a date.
140
+ */
133
141
  proposedAt: number;
142
+ /** Block the proposal was recorded in. Only populated on indexer reads. */
143
+ proposedAtBlock?: number;
134
144
  kind: TransactionKind;
135
145
  decoded?: DecodedCall;
136
146
  /** One-line human-readable description. */
@@ -283,8 +293,10 @@ interface RecoveryRequest {
283
293
  expiration: number;
284
294
  status: RecoveryStatus;
285
295
  executed: boolean;
286
- /** Guardians who have approved. Only populated on indexer reads. */
287
- approvedBy?: Address[];
296
+ /**
297
+ * Who initiated the recovery. Only populated on indexer reads — the module's struct
298
+ * does not retain it.
299
+ */
288
300
  initiator?: Address;
289
301
  source: 'indexer' | 'chain';
290
302
  }
@@ -302,7 +314,15 @@ interface Pagination {
302
314
  }
303
315
  interface Page<T> {
304
316
  data: T[];
317
+ /**
318
+ * Approximate size of the full result set.
319
+ *
320
+ * Taken from the query planner rather than a full scan, so it is exact on small
321
+ * tables and an estimate on large ones. Use it to size a progress bar, not to decide
322
+ * whether to keep paging — {@link hasMore} is what answers that.
323
+ */
305
324
  total: number;
325
+ /** Whether another page exists. Always exact. */
306
326
  hasMore: boolean;
307
327
  }
308
328
  interface IndexerHealth {
@@ -315,13 +335,6 @@ interface IndexerHealth {
315
335
  error?: string;
316
336
  }
317
337
 
318
- /**
319
- * Retry policy for transient RPC and indexer failures.
320
- *
321
- * Scoped deliberately to **reads**. Retrying a write risks broadcasting the same
322
- * transaction twice — a resubmit that looks like a timeout to the client may already
323
- * be in the mempool — so every write path in this SDK calls through unretried.
324
- */
325
338
  interface RetryOptions {
326
339
  /** Total attempts including the first. Default 3. */
327
340
  maxAttempts?: number;
@@ -424,6 +437,13 @@ declare class Connection {
424
437
  vault(address: Address, write?: boolean): Contract;
425
438
  factory(address: Address, write?: boolean): Contract;
426
439
  socialRecovery(address: Address, write?: boolean): Contract;
440
+ /**
441
+ * Read-only handle to a token contract.
442
+ *
443
+ * No write variant: the SDK never calls a token directly. Moving tokens goes
444
+ * through the vault's proposal flow, which encodes the transfer as calldata.
445
+ */
446
+ token(address: Address, standard: 'ERC20' | 'ERC721' | 'ERC1155'): Contract;
427
447
  }
428
448
 
429
449
  interface MineSaltOptions {
@@ -462,9 +482,31 @@ interface ResolvedMineJob {
462
482
  */
463
483
  declare const syncStrategy: MiningStrategy;
464
484
  /**
465
- * Node-only multi-core miner. Falls back to {@link syncStrategy} when
466
- * `node:worker_threads` is unavailable (browsers, restricted runtimes).
485
+ * Node-only multi-core miner, degrading to {@link syncStrategy} whenever workers turn
486
+ * out not to be usable.
487
+ *
488
+ * Two distinct failures land here. `node:worker_threads` may not exist at all
489
+ * (browsers, restricted runtimes). Or it exists, but the worker cannot boot — the
490
+ * source below is inlined as a string and resolves `quais` with a bare `require`,
491
+ * which no bundler traces, so a packaged consumer's worker dies on its first line.
492
+ *
493
+ * Both retreat to the sync miner rather than failing, and that retreat is always safe:
494
+ * mining is pure computation over random salts with no side effects and no on-chain
495
+ * state, so re-running it in process yields an equally valid answer. Deployment
496
+ * getting slower is a far better outcome than deployment becoming impossible.
467
497
  */
498
+ interface WorkerRuntime {
499
+ Worker: typeof node_worker_threads.Worker;
500
+ parallelism: number;
501
+ }
502
+ /**
503
+ * Build the strategy over an injectable worker runtime.
504
+ *
505
+ * The injection point exists so the retreat path can be tested: a worker that dies on
506
+ * its first line is the single most likely failure in a packaged consumer, and a
507
+ * fallback nothing exercises is a fallback nobody can trust.
508
+ */
509
+ declare function createWorkerThreadsStrategy(load?: () => Promise<WorkerRuntime | null>): MiningStrategy;
468
510
  declare const workerThreadsStrategy: MiningStrategy;
469
511
  /** Prefer worker_threads on Node; the sync miner degrades gracefully everywhere else. */
470
512
  declare function defaultStrategy(): MiningStrategy;
@@ -541,13 +583,19 @@ declare class Factory {
541
583
  }
542
584
 
543
585
  /**
544
- * The indexer uses one Postgres schema per network, chosen at runtime, so the
545
- * client cannot be pinned to supabase-js's default `"public"` schema generic.
586
+ * The indexer uses one Postgres schema per network, chosen at runtime, so the client
587
+ * cannot be pinned to postgrest-js's default `"public"` schema generic.
546
588
  */
547
- type AnySupabaseClient = SupabaseClient<any, any, any, any, any>;
589
+ type AnyPostgrestClient = PostgrestClient<any, any, any>;
548
590
  /**
549
591
  * Thin wrapper over the indexer's Supabase project.
550
592
  *
593
+ * Built on `@supabase/postgrest-js` and `@supabase/realtime-js` directly rather than
594
+ * the `@supabase/supabase-js` umbrella. The umbrella's constructor instantiates auth,
595
+ * storage and functions clients unconditionally, so no bundler can shake them out —
596
+ * and this SDK uses none of the three. Going direct costs 57 KB → 22 KB gzip, and the
597
+ * only thing given up is `createClient`'s URL and header wiring, reproduced below.
598
+ *
551
599
  * The anon key is a public read-only credential: the indexer's RLS grants `anon`
552
600
  * SELECT on every table and nothing else, with all writes reserved for
553
601
  * `service_role`. It carries no authority beyond reading already-public chain data.
@@ -555,6 +603,7 @@ type AnySupabaseClient = SupabaseClient<any, any, any, any, any>;
555
603
  declare class IndexerClient {
556
604
  readonly config: IndexerConfig;
557
605
  private readonly client;
606
+ private realtime;
558
607
  private healthCache;
559
608
  private inflightHealth;
560
609
  /** Health results are reused for this long to keep read paths cheap. */
@@ -562,9 +611,21 @@ declare class IndexerClient {
562
611
  constructor(config: IndexerConfig, options?: {
563
612
  healthCacheMs?: number;
564
613
  });
565
- /** Raw Supabase client, for queries the SDK does not wrap. */
566
- get raw(): AnySupabaseClient;
614
+ /** The PostgREST client, for queries the SDK does not wrap. */
615
+ get rest(): AnyPostgrestClient;
567
616
  from(table: string): _supabase_postgrest_js.PostgrestQueryBuilder<any, any, any, string, unknown>;
617
+ /**
618
+ * Realtime client, constructed on first use.
619
+ *
620
+ * Lazy because it opens a WebSocket and starts a heartbeat, and the large majority
621
+ * of SDK usage never calls `watch()`. Nothing here should cost a socket unless
622
+ * somebody asked to follow a vault.
623
+ */
624
+ private realtimeClient;
625
+ /** Open a Realtime channel. See `watchVault`, which is what callers should use. */
626
+ channel(name: string): RealtimeChannel;
627
+ /** Close a channel opened by {@link channel}. */
628
+ removeChannel(channel: RealtimeChannel): Promise<void>;
568
629
  /**
569
630
  * Run a query, parse each row, and surface failures as `IndexerQueryError`.
570
631
  *
@@ -575,15 +636,36 @@ declare class IndexerClient {
575
636
  * multiplied attempts if it were ever made to work. Chain reads, which do reject,
576
637
  * are retried in `src/chain/retry.ts`.
577
638
  */
578
- select<S extends z.ZodTypeAny>(table: string, schema: S, build: (q: ReturnType<AnySupabaseClient["from"]>) => PromiseLike<{
639
+ select<S extends z.ZodTypeAny>(table: string, schema: S, build: (q: ReturnType<AnyPostgrestClient["from"]>) => PromiseLike<{
579
640
  data: unknown[] | null;
580
641
  error: {
581
642
  message: string;
582
643
  code?: string;
583
644
  } | null;
584
645
  }>): Promise<Array<z.infer<S>>>;
646
+ /**
647
+ * Paged select: an exact `hasMore`, an approximate `total`.
648
+ *
649
+ * Both halves of that are deliberate. An `exact` count makes Postgres scan every
650
+ * matching row on every page request, which is invisible on a young vault and
651
+ * quadratic on a busy one — so the count is `estimated`, cheap and good enough for
652
+ * "roughly how much history is there".
653
+ *
654
+ * But `hasMore` derived from an estimate would be a lie a caller can act on: a
655
+ * paging loop that stops early drops real rows. So callers request one row beyond
656
+ * the page and this trims it, which answers "is there another page" exactly, for
657
+ * the cost of a single extra row.
658
+ */
659
+ selectPage<S extends z.ZodTypeAny>(table: string, schema: S, limit: number, build: (q: ReturnType<AnyPostgrestClient['from']>) => PromiseLike<{
660
+ data: unknown[] | null;
661
+ error: {
662
+ message: string;
663
+ code?: string;
664
+ } | null;
665
+ count?: number | null;
666
+ }>): Promise<Page<z.infer<S>>>;
585
667
  /** Single-row variant. Returns null for PostgREST's "no rows" code. */
586
- selectOne<S extends z.ZodTypeAny>(table: string, schema: S, build: (q: ReturnType<AnySupabaseClient["from"]>) => PromiseLike<{
668
+ selectOne<S extends z.ZodTypeAny>(table: string, schema: S, build: (q: ReturnType<AnyPostgrestClient["from"]>) => PromiseLike<{
587
669
  data: unknown | null;
588
670
  error: {
589
671
  message: string;
@@ -1132,24 +1214,56 @@ declare class IndexerQueries {
1132
1214
  vaultsForGuardian(guardian: Address, options?: Pagination): Promise<WalletRow[]>;
1133
1215
  pendingTransactions(vault: Address, options?: Pagination): Promise<TransactionRow[]>;
1134
1216
  transaction(vault: Address, txHash: string): Promise<TransactionRow | null>;
1217
+ /**
1218
+ * Several transactions by hash, chunked for the same reasons as
1219
+ * {@link activeConfirmationsBatch} — hashes are long, and they go in the query string.
1220
+ *
1221
+ * Rows come back in no particular order and hashes with no row are simply absent;
1222
+ * the caller matches them up.
1223
+ */
1224
+ transactionsByHash(vault: Address, txHashes: string[]): Promise<TransactionRow[]>;
1135
1225
  transactionHistory(vault: Address, options?: Pagination & {
1136
1226
  status?: string[];
1137
1227
  }): Promise<Page<TransactionRow>>;
1138
1228
  /** All confirmations for one transaction, including revoked ones. */
1139
1229
  confirmations(vault: Address, txHash: string): Promise<ConfirmationRow[]>;
1140
1230
  /**
1141
- * Active confirmations for many transactions in one round trip.
1231
+ * Active confirmations for many transactions, in as few round trips as is safe.
1142
1232
  *
1143
1233
  * "Active" here means not revoked. It does NOT mean the confirming address is
1144
1234
  * still an owner — the indexer does not deactivate confirmations when an owner is
1145
1235
  * removed, while the contract invalidates them via approval epochs. Callers must
1146
1236
  * intersect with the active owner set; `Vault` does this for you.
1237
+ *
1238
+ * Chunked rather than issued as one `in(...)`, because a single filter fails two
1239
+ * ways at scale. PostgREST reads filters from the query string, and each 32-byte
1240
+ * hash costs ~70 characters there — a full {@link MAX_LIMIT} page of hashes builds a
1241
+ * ~14 KB request line, past the 8 KB cap most reverse proxies apply by default. The
1242
+ * response is bounded too: {@link CONFIRMATION_CHUNK} transactions can carry at most
1243
+ * `chunk × MAX_OWNERS` rows, and the chunk is sized to keep that under the
1244
+ * `max-rows` ceiling PostgREST deployments commonly set, so a busy vault cannot be
1245
+ * silently short-served.
1147
1246
  */
1148
1247
  activeConfirmationsBatch(vault: Address, txHashes: string[]): Promise<Map<string, ConfirmationRow[]>>;
1149
1248
  modules(vault: Address): Promise<string[]>;
1150
1249
  delegatecallTargets(vault: Address): Promise<string[]>;
1151
1250
  deposits(vault: Address, options?: Pagination): Promise<Page<DepositRow>>;
1152
1251
  tokenTransfers(vault: Address, options?: Pagination): Promise<Page<TokenTransferRow>>;
1252
+ /**
1253
+ * Scan up to `budget` of the most recent transfer rows, paging past {@link MAX_LIMIT}.
1254
+ *
1255
+ * `tokenTransfers` clamps a single request, so a caller asking for more than the cap
1256
+ * silently received a short page — and then computed "did I see everything?" from
1257
+ * that short page's `hasMore`, which reported truncation the caller had not actually
1258
+ * hit. Paging here keeps the caller's budget meaningful and makes the returned
1259
+ * `hasMore` mean what it says: rows exist beyond the budget that was scanned.
1260
+ *
1261
+ * Pages by offset over a descending order, so a transfer landing mid-scan can shift
1262
+ * rows by one. That is acceptable for token *discovery* — a duplicate collapses into
1263
+ * the same map key and a missed row costs at most one candidate that the next call
1264
+ * picks up. Do not reuse this for anything that must see each row exactly once.
1265
+ */
1266
+ tokenTransferScan(vault: Address, budget: number): Promise<Page<TokenTransferRow>>;
1153
1267
  tokens(addresses: string[]): Promise<TokenRow[]>;
1154
1268
  signedMessages(vault: Address): Promise<SignedMessageRow[]>;
1155
1269
  recoveryConfig(vault: Address): Promise<{
@@ -1183,6 +1297,14 @@ declare class RecoveryModule {
1183
1297
  get address(): Address;
1184
1298
  private contract;
1185
1299
  private get vault();
1300
+ /**
1301
+ * The vault this module acts on, through the same retrying facade every other vault
1302
+ * read in the SDK uses. Recovery reaches into the vault for three checks — is the
1303
+ * module enabled, is the caller an owner, who are the current owners — and each of
1304
+ * them gates a write, so none of them should be the one read that silently skips
1305
+ * transient-failure handling.
1306
+ */
1307
+ private vaultContract;
1186
1308
  /** Whether the module is currently enabled on the vault. */
1187
1309
  isEnabled(): Promise<boolean>;
1188
1310
  config(): Promise<RecoveryConfig>;
@@ -1265,7 +1387,13 @@ declare class RecoveryModule {
1265
1387
  interface TokenBalance {
1266
1388
  token: Address;
1267
1389
  standard: 'ERC20' | 'ERC721' | 'ERC1155';
1390
+ /**
1391
+ * Ticker as the token reports it — deployer-controlled, so passed through
1392
+ * {@link sanitizeText} and capped. Falls back to `???` when the token has none or
1393
+ * supplied nothing printable.
1394
+ */
1268
1395
  symbol: string;
1396
+ /** Display name. Same provenance and treatment as {@link symbol}. */
1269
1397
  name: string;
1270
1398
  decimals: number;
1271
1399
  /** ERC20: raw units. ERC721: number held. ERC1155: total across ids. */
@@ -1306,12 +1434,22 @@ interface BalanceOptions {
1306
1434
  /**
1307
1435
  * How many recent transfer rows to scan when discovering tokens. Default 500.
1308
1436
  *
1309
- * A vault with more transfers than this may not surface its oldest holdings, so
1310
- * raise it for long-lived vaults. Reported back in {@link VaultBalances.truncated}.
1437
+ * Scanned across as many indexer requests as it takes, so this is a real budget
1438
+ * rather than a single page size. A vault with more transfers than this may not
1439
+ * surface its oldest holdings, so raise it for long-lived vaults. Reported back in
1440
+ * {@link VaultBalances.truncated}.
1311
1441
  */
1312
1442
  transferScanLimit?: number;
1313
1443
  /** Cap on ERC721 ids ownership-checked per token. Default 100. */
1314
1444
  maxTokenIdChecks?: number;
1445
+ /**
1446
+ * Ceiling on in-flight RPC reads. Default 8.
1447
+ *
1448
+ * Applied at both fan-out levels independently, so the true ceiling is the square
1449
+ * of this. Raise it against a private node; lower it if a shared endpoint is
1450
+ * rate-limiting you.
1451
+ */
1452
+ concurrency?: number;
1315
1453
  }
1316
1454
  /**
1317
1455
  * Aggregate a vault's holdings.
@@ -1404,8 +1542,10 @@ declare const interfaces: {
1404
1542
  declare const MAX_EXECUTION_DELAY: number;
1405
1543
  declare const MAX_OWNERS = 20;
1406
1544
  declare const MAX_MODULES = 50;
1407
- /** Head of the Zodiac module linked list. */
1545
+ /** Head of the Zodiac module linked list. Never valid as an owner, module or guardian. */
1408
1546
  declare const SENTINEL_MODULES = "0x0000000000000000000000000000000000000001";
1547
+ /** Rejected everywhere an address is committed to a role. */
1548
+ declare const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";
1409
1549
  declare const selfCall: {
1410
1550
  addOwner: (owner: Address) => Hex;
1411
1551
  removeOwner: (owner: Address) => Hex;
@@ -1486,6 +1626,29 @@ interface VaultContext {
1486
1626
  contracts: ContractAddresses;
1487
1627
  consistency: Consistency;
1488
1628
  maxIndexerLagBlocks: number;
1629
+ /** Set by {@link Vault.pinned}. Never consulted by a write precondition. */
1630
+ view?: VaultView;
1631
+ }
1632
+ /**
1633
+ * Owners and threshold, captured at one instant.
1634
+ *
1635
+ * The SDK deliberately holds no read cache: the owner set decides which approvals
1636
+ * count toward the threshold, and serving a stale one silently would reintroduce the
1637
+ * exact bug `buildTransaction` intersects confirmations to avoid. An implicit TTL
1638
+ * would make that failure invisible and intermittent.
1639
+ *
1640
+ * So the staleness window is the caller's to open, and to see. `capturedAt` is right
1641
+ * there in the object; a CLI rendering one screen can pin a view for that screen and
1642
+ * know precisely what it pinned.
1643
+ */
1644
+ interface VaultView {
1645
+ owners: Address[];
1646
+ threshold: number;
1647
+ /** Indexer head when this was taken; absent if the snapshot came from chain. */
1648
+ indexedAtBlock?: number;
1649
+ /** Unix seconds. How stale the snapshot is allowed to get is the caller's call. */
1650
+ capturedAt: number;
1651
+ source: 'indexer' | 'chain';
1489
1652
  }
1490
1653
  /** A handle to one deployed vault. Cheap to construct; performs no I/O until used. */
1491
1654
  declare class Vault {
@@ -1503,13 +1666,64 @@ declare class Vault {
1503
1666
  */
1504
1667
  private useIndexer;
1505
1668
  private requireQueries;
1669
+ /**
1670
+ * The indexer's head, for stamping onto records read from it.
1671
+ *
1672
+ * Reads the cached health result rather than querying `indexer_state` directly.
1673
+ * `useIndexer()` has just called `health()` on this same code path and the result is
1674
+ * cached, so this is free — whereas a `state()` call is a second round trip per
1675
+ * hydration, paid on every indexed read purely to fill one advisory field.
1676
+ *
1677
+ * Undefined when the indexer is not answering: a head of 0 would read as "indexed at
1678
+ * genesis" rather than "unknown".
1679
+ */
1680
+ private indexerHead;
1506
1681
  private contract;
1507
1682
  /** Owners, threshold, timelock floor, nonce and balance. Always read from chain. */
1508
1683
  info(): Promise<VaultInfo>;
1509
- /** Active owners. Prefers the indexer, falls back to chain. */
1684
+ /**
1685
+ * Capture owners and threshold once, for reuse across a burst of reads.
1686
+ *
1687
+ * Pair with {@link pinned}:
1688
+ *
1689
+ * ```ts
1690
+ * const view = await vault.view();
1691
+ * const pinned = vault.pinned(view);
1692
+ * // every read below shares one owner/threshold read
1693
+ * const txs = await pinned.transactions(hashes);
1694
+ * ```
1695
+ */
1696
+ view(): Promise<VaultView>;
1697
+ /**
1698
+ * A handle that answers {@link owners} and {@link threshold} from `view` instead of
1699
+ * re-reading them.
1700
+ *
1701
+ * Reads only. Write preconditions go through `chainOwners` / `chainThreshold`, which
1702
+ * bypass the snapshot by construction — pinning a view can never cause this SDK to
1703
+ * sign against a stale owner set. Everything else about the handle, including the
1704
+ * connection and the recovery module, is shared with the original.
1705
+ */
1706
+ pinned(view: VaultView): Vault;
1707
+ /** Active owners. Prefers a pinned view, then the indexer, then chain. */
1510
1708
  owners(): Promise<Address[]>;
1709
+ /**
1710
+ * Owners straight from the chain, bypassing the indexer entirely.
1711
+ *
1712
+ * {@link owners} prefers the indexer, which is right for display and wrong for
1713
+ * anything that gates a write. A lagging indexer would let the SDK build a proposal
1714
+ * against an owner set that no longer exists — admitting a `removeOwner` that drops
1715
+ * the vault below its threshold, or rejecting one that is perfectly valid. Every
1716
+ * propose-time precondition reads through here instead.
1717
+ */
1718
+ private chainOwners;
1511
1719
  isOwner(address: Address): Promise<boolean>;
1720
+ /** Approvals required to execute. Prefers a pinned view, then the indexer, then chain. */
1512
1721
  threshold(): Promise<number>;
1722
+ /**
1723
+ * Threshold straight from the chain. The write-path counterpart to {@link threshold},
1724
+ * for the same reason {@link chainOwners} exists.
1725
+ */
1726
+ private chainThreshold;
1513
1727
  balance(): Promise<bigint>;
1514
1728
  /**
1515
1729
  * Enabled modules, in linked-list order.
@@ -1527,6 +1741,20 @@ declare class Vault {
1527
1741
  /** The vault-transaction hash for a given call, at a given nonce. */
1528
1742
  transactionHash(to: Address, value: bigint, data: Hex, nonce?: number): Promise<Bytes32>;
1529
1743
  transaction(txHash: Bytes32): Promise<VaultTransaction>;
1744
+ /**
1745
+ * Several transactions at once, keyed by hash.
1746
+ *
1747
+ * The plural form exists because the singular one is expensive to loop: each
1748
+ * `transaction()` re-reads the owner set, the threshold and the indexer head, so
1749
+ * fetching fifty costs fifty times that. This resolves the whole set the way the
1750
+ * listing methods already do — one query for the rows, one owner/threshold read
1751
+ * shared across them, one batched confirmations query.
1752
+ *
1753
+ * Hashes the indexer does not have fall back to chain reads, bounded by a pool.
1754
+ * Hashes that exist nowhere are simply absent from the map rather than throwing:
1755
+ * one unknown hash should not lose the caller the other forty-nine.
1756
+ */
1757
+ transactions(txHashes: Bytes32[]): Promise<Map<Bytes32, VaultTransaction>>;
1530
1758
  pendingTransactions(options?: Pagination): Promise<VaultTransaction[]>;
1531
1759
  transactionHistory(options?: Pagination & {
1532
1760
  status?: string[];
@@ -1551,6 +1779,16 @@ declare class Vault {
1551
1779
  * become available. Defaults to the connected signer's address.
1552
1780
  */
1553
1781
  affordances(txHash: Bytes32, caller?: Address): Promise<Affordance[]>;
1782
+ /**
1783
+ * Shared body of {@link affordances}, taking the transaction either as a value or as
1784
+ * an in-flight promise.
1785
+ *
1786
+ * The promise form is the point: a caller that already holds the record (like
1787
+ * {@link describe}) passes it and skips a redundant fetch, while `affordances()`
1788
+ * passes the unawaited promise so the transaction read still overlaps the two
1789
+ * ownership probes rather than serialising behind them.
1790
+ */
1791
+ private resolveAffordances;
1554
1792
  /**
1555
1793
  * Proposal builders.
1556
1794
  *
@@ -1828,6 +2066,23 @@ declare const QuaiVault: {
1828
2066
  connect: typeof connect;
1829
2067
  };
1830
2068
 
2069
+ /**
2070
+ * Map with a bounded number of in-flight promises, preserving input order.
2071
+ *
2072
+ * A plain `Promise.all` over a fan-out the caller does not control is a liability:
2073
+ * `balances()` can reach `maxTokens × maxTokenIdChecks` — 5,000 with the defaults —
2074
+ * concurrent RPC reads from one call, and a batched transaction read fans out over
2075
+ * however many hashes the caller passed. Public endpoints rate-limit long before
2076
+ * either, and the failures come back as transient errors on reads that would each
2077
+ * have succeeded on their own.
2078
+ *
2079
+ * Rejections propagate: the first failure rejects the whole call, matching
2080
+ * `Promise.all`. Callers that want per-item tolerance catch inside `fn`.
2081
+ */
2082
+ declare function mapPooled<T, R>(items: readonly T[], limit: number, fn: (item: T, index: number) => Promise<R>): Promise<R[]>;
2083
+ /** Default ceiling on concurrent chain reads issued by one SDK call. */
2084
+ declare const DEFAULT_CONCURRENCY = 8;
2085
+
1831
2086
  /**
1832
2087
  * Raw `Transaction` struct as returned by `QuaiVault.transactions(bytes32)`.
1833
2088
  * Field names match the Solidity struct.
@@ -1923,6 +2178,25 @@ declare class FactoryContract {
1923
2178
  registerWallet(wallet: Address): Promise<ContractTransactionResponse>;
1924
2179
  }
1925
2180
 
2181
+ /**
2182
+ * Typed, retrying facade over an ERC20/721/1155 contract.
2183
+ *
2184
+ * Tokens are only ever read, never written to — the vault's own proposal flow is what
2185
+ * moves them — so there is no write half here and no signer involved.
2186
+ */
2187
+ declare class TokenContract {
2188
+ readonly contract: Contract;
2189
+ private readonly retry;
2190
+ constructor(contract: Contract, retry?: RetryOptions);
2191
+ private read;
2192
+ /** ERC20 units held, or — for ERC721 — the number of tokens held. */
2193
+ balanceOf(owner: Address): Promise<bigint>;
2194
+ /** ERC721 only. Reverts for a burned or never-minted id. */
2195
+ ownerOf(tokenId: string | bigint): Promise<string>;
2196
+ /** ERC1155 only. One call for many ids, so it stays a single round trip. */
2197
+ balanceOfBatch(owners: Address[], ids: Array<string | bigint>): Promise<bigint[]>;
2198
+ }
2199
+
1926
2200
  /** Raw `RecoveryConfig` struct from `getRecoveryConfig(address)`. */
1927
2201
  interface RawRecoveryConfig {
1928
2202
  guardians: string[];
@@ -2141,6 +2415,38 @@ declare function predictVaultAddress(args: {
2141
2415
  */
2142
2416
  declare function shardPrefixOf(address: Address): string;
2143
2417
 
2418
+ /**
2419
+ * Display sanitisation for attacker-controlled strings.
2420
+ *
2421
+ * Two values reach a consumer's screen without ever having been vetted by anyone:
2422
+ *
2423
+ * - **Token metadata.** `symbol()` and `name()` are whatever the token's deployer
2424
+ * chose. Getting one indexed against a vault costs one transfer of one unit.
2425
+ * - **Revert reasons.** `Error(string)` data comes from the contract a vault called,
2426
+ * which for an external call is an arbitrary address the proposer picked.
2427
+ *
2428
+ * In a browser both are inert — the DOM escapes them. In a terminal they are not. An
2429
+ * ANSI escape can move the cursor up and overwrite lines the SDK already printed, so
2430
+ * a token named `"\x1b[2A\x1b[KAll checks passed"` can forge SDK output. Bidi
2431
+ * overrides can reverse a rendered address while leaving the underlying bytes intact,
2432
+ * which is the same trick that makes malicious source look benign.
2433
+ *
2434
+ * Sanitising here rather than in each consumer means a CLI, a TUI and a log pipeline
2435
+ * all inherit it, and none of them has to remember. Only human-facing fields are
2436
+ * scrubbed — machine-readable ones (`DecodedRevert.args`) keep the raw bytes, because
2437
+ * a consumer comparing against an expected value needs exactly what was on chain.
2438
+ */
2439
+ /** Default cap. Long enough for any legitimate token name, short enough to not wrap. */
2440
+ declare const DEFAULT_TEXT_LIMIT = 128;
2441
+ /**
2442
+ * Strip control and direction-manipulating characters, collapse surrounding
2443
+ * whitespace, and cap the length.
2444
+ *
2445
+ * Returns `''` for a non-string or for input that was entirely unsafe, so callers can
2446
+ * `sanitizeText(x) || fallback`.
2447
+ */
2448
+ declare function sanitizeText(value: unknown, maxLength?: number): string;
2449
+
2144
2450
  /**
2145
2451
  * Address validation for Quai's dual-ledger, sharded address space.
2146
2452
  *
@@ -2189,7 +2495,7 @@ declare function assertQuaiAddress(value: unknown, label?: string): Address;
2189
2495
  declare function assertQuaiAddresses(values: readonly unknown[], label: string): Address[];
2190
2496
 
2191
2497
  /** Machine-readable error codes. Stable across releases. */
2192
- type QuaiVaultErrorCode = 'CONFIG' | 'NOT_CONNECTED' | 'NO_SIGNER' | 'NO_INDEXER' | 'INDEXER_QUERY' | 'INDEXER_STALE' | 'VALIDATION' | 'NOT_FOUND' | 'PRECONDITION' | 'REVERT' | 'SALT_MINING' | 'STALE_PROPOSAL' | 'UNSUPPORTED';
2498
+ type QuaiVaultErrorCode = 'ABORTED' | 'CONFIG' | 'NOT_CONNECTED' | 'NO_SIGNER' | 'NO_INDEXER' | 'INDEXER_QUERY' | 'INDEXER_STALE' | 'VALIDATION' | 'NOT_FOUND' | 'PRECONDITION' | 'REVERT' | 'SALT_MINING' | 'STALE_PROPOSAL' | 'UNSUPPORTED';
2193
2499
  declare class QuaiVaultError extends Error {
2194
2500
  readonly code: QuaiVaultErrorCode;
2195
2501
  /** What the caller can do about it, in plain language. */
@@ -2205,6 +2511,18 @@ declare class QuaiVaultError extends Error {
2205
2511
  /** Structured form, for logs and machine consumers. */
2206
2512
  toJSON(): Record<string, unknown>;
2207
2513
  }
2514
+ /**
2515
+ * A caller-supplied `AbortSignal` fired.
2516
+ *
2517
+ * Every long-running operation the SDK exposes — retries, indexer polling, salt
2518
+ * mining, waiting for a timelock — accepts a signal, and each used to report abort
2519
+ * differently: a bare `Error('Aborted')` in two places, a `PreconditionError` in a
2520
+ * third. Consumers switching on `err.code` to distinguish "the user pressed Ctrl-C"
2521
+ * from "this genuinely failed" could not do so. They can now.
2522
+ */
2523
+ declare class AbortError extends QuaiVaultError {
2524
+ constructor(operation?: string);
2525
+ }
2208
2526
  declare class ConfigError extends QuaiVaultError {
2209
2527
  constructor(message: string, remediation?: string);
2210
2528
  }
@@ -2268,4 +2586,4 @@ declare function knownErrorSelectors(): Array<{
2268
2586
  signature: string;
2269
2587
  }>;
2270
2588
 
2271
- export { type Address, type AddressCheck, type AddressLedger, type Affordance, type AffordanceBlocker, type AffordanceContext, type ApprovalRecord, type BalanceOptions, type BatchCall, type Bytes32, type ClientOptions, ConfigError, Connection, type Consistency, type ContractAddresses, type CreateProgress, type CreateVaultParams, type CreateVaultResult, type DecodeContext, type DecodeResult, type DecodedBatchCall, type DecodedCall, type DecodedRevert, type DryRunResult, ENV_VARS, type ExecuteOutcome, type ExecuteResult, Factory, type FactoryContext, FactoryContract, type Hex, IndexerClient, type IndexerConfig, type IndexerHealth, IndexerQueries, IndexerQueryError, MAX_EXECUTION_DELAY, MAX_MODULES, MAX_OWNERS, type MineSaltOptions, type MinedSalt, type MiningStrategy, type NetworkConfig, type NetworkName, NoIndexerError, NoSignerError, NotFoundError, Operation, type Page, type Pagination, PreconditionError, type ProposeOptions, type ProposeResult, QuaiVault, QuaiVaultClient, QuaiVaultError, type QuaiVaultErrorCode, type RawRecovery, type RawRecoveryConfig, type RawRecoveryState, type RawTransactionState, type RawTransactionStruct, type ReceiptLike, type RecoveryAction, type RecoveryAffordance, type RecoveryConfig, RecoveryContract, RecoveryModule, type RecoveryModuleContext, type RecoveryRequest, type RecoveryStatus, type ResolvedConfig, type RetryOptions, RevertError, SENTINEL_MODULES, SaltMiningError, StaleProposalError, type Subscription, type TokenBalance, type TransactionKind, type TransactionStatus, ValidationError, Vault, type VaultAction, type VaultBalances, type VaultContext, VaultContract, type VaultInfo, type VaultTransaction, type WatchEvent, type WatchOptions, type WatchTopic, allowedActions, assertQuaiAddress, assertQuaiAddresses, classifyExecution, computeAffordances, computeBytecodeHash, computeFullSalt, connect, decodeCall, decodeMultiSendPayload, decodeRevert, decodeRevertFromError, defaultStrategy, deriveRecoveryStatus, deriveStatus, encodeInitData, encodeMultiSend, encodeMultiSendPayload, executableAfterOf, extractProposedTxHash, schemas as indexerSchemas, inspectAddress, interfaces, isNetworkName, isTerminal, isTransient, isUsableQuaiAddress, knownErrorSelectors, loadBalances, mainnet, mineSalt, minimumExpiration, networks, nowSeconds, predictVaultAddress, recoveryCall, remediationFor, resolveConfig, selfCall, shardPrefixOf, syncStrategy, testnet, token as tokenCalls, watchVault, withRetry, workerThreadsStrategy };
2589
+ export { AbortError, type Address, type AddressCheck, type AddressLedger, type Affordance, type AffordanceBlocker, type AffordanceContext, type ApprovalRecord, type BalanceOptions, type BatchCall, type Bytes32, type ClientOptions, ConfigError, Connection, type Consistency, type ContractAddresses, type CreateProgress, type CreateVaultParams, type CreateVaultResult, DEFAULT_CONCURRENCY, DEFAULT_TEXT_LIMIT, type DecodeContext, type DecodeResult, type DecodedBatchCall, type DecodedCall, type DecodedRevert, type DryRunResult, ENV_VARS, type ExecuteOutcome, type ExecuteResult, Factory, type FactoryContext, FactoryContract, type Hex, IndexerClient, type IndexerConfig, type IndexerHealth, IndexerQueries, IndexerQueryError, MAX_EXECUTION_DELAY, MAX_MODULES, MAX_OWNERS, type MineSaltOptions, type MinedSalt, type MiningStrategy, type NetworkConfig, type NetworkName, NoIndexerError, NoSignerError, NotFoundError, Operation, type Page, type Pagination, PreconditionError, type ProposeOptions, type ProposeResult, QuaiVault, QuaiVaultClient, QuaiVaultError, type QuaiVaultErrorCode, type RawRecovery, type RawRecoveryConfig, type RawRecoveryState, type RawTransactionState, type RawTransactionStruct, type ReceiptLike, type RecoveryAction, type RecoveryAffordance, type RecoveryConfig, RecoveryContract, RecoveryModule, type RecoveryModuleContext, type RecoveryRequest, type RecoveryStatus, type ResolvedConfig, type RetryOptions, RevertError, SENTINEL_MODULES, SaltMiningError, StaleProposalError, type Subscription, type TokenBalance, TokenContract, type TransactionKind, type TransactionStatus, ValidationError, Vault, type VaultAction, type VaultBalances, type VaultContext, VaultContract, type VaultInfo, type VaultTransaction, type VaultView, type WatchEvent, type WatchOptions, type WatchTopic, type WorkerRuntime, ZERO_ADDRESS, allowedActions, assertQuaiAddress, assertQuaiAddresses, classifyExecution, computeAffordances, computeBytecodeHash, computeFullSalt, connect, createWorkerThreadsStrategy, decodeCall, decodeMultiSendPayload, decodeRevert, decodeRevertFromError, defaultStrategy, deriveRecoveryStatus, deriveStatus, encodeInitData, encodeMultiSend, encodeMultiSendPayload, executableAfterOf, extractProposedTxHash, schemas as indexerSchemas, inspectAddress, interfaces, isNetworkName, isTerminal, isTransient, isUsableQuaiAddress, knownErrorSelectors, loadBalances, mainnet, mapPooled, mineSalt, minimumExpiration, networks, nowSeconds, predictVaultAddress, recoveryCall, remediationFor, resolveConfig, sanitizeText, selfCall, shardPrefixOf, syncStrategy, testnet, token as tokenCalls, watchVault, withRetry, workerThreadsStrategy };