@quaivault/sdk 0.1.1 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts 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.js';
8
10
 
@@ -61,8 +63,36 @@ interface NetworkConfig {
61
63
  * is removed (approval epochs), so they must never gate a signature.
62
64
  */
63
65
  type Consistency = 'auto' | 'indexed' | 'chain';
66
+ /**
67
+ * Source of "now", in Unix **seconds**, for decisions compared against chain time.
68
+ *
69
+ * The contracts decide with `block.timestamp`. Everything the SDK derives locally —
70
+ * a transaction's `ready` vs `timelocked`, what a caller may do next, whether a
71
+ * recovery period has elapsed — is a *prediction* of that, and a machine whose clock is
72
+ * wrong predicts wrongly. Containers, CI runners and VMs resumed from a snapshot drift
73
+ * in ways a desktop usually does not.
74
+ *
75
+ * Supplying a clock lets a consumer that has measured the offset feed it back in:
76
+ *
77
+ * ```ts
78
+ * const skew = localSeconds - blockTimestamp; // positive: local clock is ahead
79
+ * connect({ now: () => Date.now() / 1000 - skew });
80
+ * ```
81
+ *
82
+ * A function rather than a scalar offset on purpose. An offset needs a sign convention,
83
+ * and getting it backwards doubles the error instead of cancelling it — silently. Here
84
+ * the arithmetic sits in the caller's own code, where it reads as what it means.
85
+ *
86
+ * This never affects elapsed-time measurement (retry backoff, timeouts, poll
87
+ * intervals). Those are durations: if the clock is 12 seconds fast, 30 seconds is still
88
+ * 30 seconds.
89
+ *
90
+ * Detection is deliberately not the SDK's job — it would mean an RPC call on behalf of
91
+ * a consumer who may not want one, and a cached value with no clear invalidation.
92
+ */
93
+ type Clock = () => number;
64
94
  interface ClientOptions {
65
- network?: NetworkConfig | keyof typeof networks;
95
+ network?: NetworkConfig | NetworkName;
66
96
  provider?: Provider;
67
97
  signer?: Signer;
68
98
  /** Private key for a local signer. Prefer the `QUAIVAULT_PRIVATE_KEY` env var. */
@@ -75,6 +105,11 @@ interface ClientOptions {
75
105
  maxIndexerLagBlocks?: number;
76
106
  /** Read env vars for anything not passed explicitly. Default true. */
77
107
  useEnv?: boolean;
108
+ /**
109
+ * Source of "now" for time compared against chain timestamps. See {@link Clock}.
110
+ * Defaults to the local clock.
111
+ */
112
+ now?: Clock;
78
113
  /**
79
114
  * Retry policy for transient RPC and indexer failures. Applies to reads only —
80
115
  * writes are never retried, since a resubmit risks a double broadcast.
@@ -129,8 +164,16 @@ interface VaultTransaction {
129
164
  value: bigint;
130
165
  data: Hex;
131
166
  proposer: Address;
132
- /** Unix seconds when the proposal was recorded on chain. */
167
+ /**
168
+ * Unix seconds when the proposal was recorded on chain, or 0 when unknown.
169
+ *
170
+ * Only chain reads carry a timestamp: the vault stores one in the transaction
171
+ * struct, but the indexer records the *block* instead. On an indexed read this is 0
172
+ * and {@link proposedAtBlock} carries the position. Never render 0 as a date.
173
+ */
133
174
  proposedAt: number;
175
+ /** Block the proposal was recorded in. Only populated on indexer reads. */
176
+ proposedAtBlock?: number;
134
177
  kind: TransactionKind;
135
178
  decoded?: DecodedCall;
136
179
  /** One-line human-readable description. */
@@ -283,8 +326,10 @@ interface RecoveryRequest {
283
326
  expiration: number;
284
327
  status: RecoveryStatus;
285
328
  executed: boolean;
286
- /** Guardians who have approved. Only populated on indexer reads. */
287
- approvedBy?: Address[];
329
+ /**
330
+ * Who initiated the recovery. Only populated on indexer reads — the module's struct
331
+ * does not retain it.
332
+ */
288
333
  initiator?: Address;
289
334
  source: 'indexer' | 'chain';
290
335
  }
@@ -302,7 +347,15 @@ interface Pagination {
302
347
  }
303
348
  interface Page<T> {
304
349
  data: T[];
350
+ /**
351
+ * Approximate size of the full result set.
352
+ *
353
+ * Taken from the query planner rather than a full scan, so it is exact on small
354
+ * tables and an estimate on large ones. Use it to size a progress bar, not to decide
355
+ * whether to keep paging — {@link hasMore} is what answers that.
356
+ */
305
357
  total: number;
358
+ /** Whether another page exists. Always exact. */
306
359
  hasMore: boolean;
307
360
  }
308
361
  interface IndexerHealth {
@@ -315,13 +368,6 @@ interface IndexerHealth {
315
368
  error?: string;
316
369
  }
317
370
 
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
371
  interface RetryOptions {
326
372
  /** Total attempts including the first. Default 3. */
327
373
  maxAttempts?: number;
@@ -388,6 +434,11 @@ interface ResolvedConfig {
388
434
  consistency: Consistency;
389
435
  maxIndexerLagBlocks: number;
390
436
  retry: RetryOptions;
437
+ /**
438
+ * Resolved source of "now" in Unix seconds. Always set — falls back to the local
439
+ * clock — so consumers never branch on its presence.
440
+ */
441
+ now: Clock;
391
442
  }
392
443
  /** A `ResolvedConfig` with the private key removed. Safe to log. */
393
444
  type PublicConfig = Omit<ResolvedConfig, 'privateKey'> & {
@@ -424,6 +475,13 @@ declare class Connection {
424
475
  vault(address: Address, write?: boolean): Contract;
425
476
  factory(address: Address, write?: boolean): Contract;
426
477
  socialRecovery(address: Address, write?: boolean): Contract;
478
+ /**
479
+ * Read-only handle to a token contract.
480
+ *
481
+ * No write variant: the SDK never calls a token directly. Moving tokens goes
482
+ * through the vault's proposal flow, which encodes the transfer as calldata.
483
+ */
484
+ token(address: Address, standard: 'ERC20' | 'ERC721' | 'ERC1155'): Contract;
427
485
  }
428
486
 
429
487
  interface MineSaltOptions {
@@ -462,9 +520,31 @@ interface ResolvedMineJob {
462
520
  */
463
521
  declare const syncStrategy: MiningStrategy;
464
522
  /**
465
- * Node-only multi-core miner. Falls back to {@link syncStrategy} when
466
- * `node:worker_threads` is unavailable (browsers, restricted runtimes).
523
+ * Node-only multi-core miner, degrading to {@link syncStrategy} whenever workers turn
524
+ * out not to be usable.
525
+ *
526
+ * Two distinct failures land here. `node:worker_threads` may not exist at all
527
+ * (browsers, restricted runtimes). Or it exists, but the worker cannot boot — the
528
+ * source below is inlined as a string and resolves `quais` with a bare `require`,
529
+ * which no bundler traces, so a packaged consumer's worker dies on its first line.
530
+ *
531
+ * Both retreat to the sync miner rather than failing, and that retreat is always safe:
532
+ * mining is pure computation over random salts with no side effects and no on-chain
533
+ * state, so re-running it in process yields an equally valid answer. Deployment
534
+ * getting slower is a far better outcome than deployment becoming impossible.
535
+ */
536
+ interface WorkerRuntime {
537
+ Worker: typeof node_worker_threads.Worker;
538
+ parallelism: number;
539
+ }
540
+ /**
541
+ * Build the strategy over an injectable worker runtime.
542
+ *
543
+ * The injection point exists so the retreat path can be tested: a worker that dies on
544
+ * its first line is the single most likely failure in a packaged consumer, and a
545
+ * fallback nothing exercises is a fallback nobody can trust.
467
546
  */
547
+ declare function createWorkerThreadsStrategy(load?: () => Promise<WorkerRuntime | null>): MiningStrategy;
468
548
  declare const workerThreadsStrategy: MiningStrategy;
469
549
  /** Prefer worker_threads on Node; the sync miner degrades gracefully everywhere else. */
470
550
  declare function defaultStrategy(): MiningStrategy;
@@ -541,13 +621,19 @@ declare class Factory {
541
621
  }
542
622
 
543
623
  /**
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.
624
+ * The indexer uses one Postgres schema per network, chosen at runtime, so the client
625
+ * cannot be pinned to postgrest-js's default `"public"` schema generic.
546
626
  */
547
- type AnySupabaseClient = SupabaseClient<any, any, any, any, any>;
627
+ type AnyPostgrestClient = PostgrestClient<any, any, any>;
548
628
  /**
549
629
  * Thin wrapper over the indexer's Supabase project.
550
630
  *
631
+ * Built on `@supabase/postgrest-js` and `@supabase/realtime-js` directly rather than
632
+ * the `@supabase/supabase-js` umbrella. The umbrella's constructor instantiates auth,
633
+ * storage and functions clients unconditionally, so no bundler can shake them out —
634
+ * and this SDK uses none of the three. Going direct costs 57 KB → 22 KB gzip, and the
635
+ * only thing given up is `createClient`'s URL and header wiring, reproduced below.
636
+ *
551
637
  * The anon key is a public read-only credential: the indexer's RLS grants `anon`
552
638
  * SELECT on every table and nothing else, with all writes reserved for
553
639
  * `service_role`. It carries no authority beyond reading already-public chain data.
@@ -555,16 +641,34 @@ type AnySupabaseClient = SupabaseClient<any, any, any, any, any>;
555
641
  declare class IndexerClient {
556
642
  readonly config: IndexerConfig;
557
643
  private readonly client;
644
+ private realtime;
558
645
  private healthCache;
559
646
  private inflightHealth;
560
- /** Health results are reused for this long to keep read paths cheap. */
647
+ /**
648
+ * Health results are reused for this long to keep read paths cheap.
649
+ *
650
+ * This and `waitForBlock`'s deadline are elapsed-time arithmetic, so they use the
651
+ * raw local clock rather than `ClientOptions.now` — see the note in `chain/retry.ts`.
652
+ */
561
653
  readonly healthCacheMs: number;
562
654
  constructor(config: IndexerConfig, options?: {
563
655
  healthCacheMs?: number;
564
656
  });
565
- /** Raw Supabase client, for queries the SDK does not wrap. */
566
- get raw(): AnySupabaseClient;
657
+ /** The PostgREST client, for queries the SDK does not wrap. */
658
+ get rest(): AnyPostgrestClient;
567
659
  from(table: string): _supabase_postgrest_js.PostgrestQueryBuilder<any, any, any, string, unknown>;
660
+ /**
661
+ * Realtime client, constructed on first use.
662
+ *
663
+ * Lazy because it opens a WebSocket and starts a heartbeat, and the large majority
664
+ * of SDK usage never calls `watch()`. Nothing here should cost a socket unless
665
+ * somebody asked to follow a vault.
666
+ */
667
+ private realtimeClient;
668
+ /** Open a Realtime channel. See `watchVault`, which is what callers should use. */
669
+ channel(name: string): RealtimeChannel;
670
+ /** Close a channel opened by {@link channel}. */
671
+ removeChannel(channel: RealtimeChannel): Promise<void>;
568
672
  /**
569
673
  * Run a query, parse each row, and surface failures as `IndexerQueryError`.
570
674
  *
@@ -575,15 +679,36 @@ declare class IndexerClient {
575
679
  * multiplied attempts if it were ever made to work. Chain reads, which do reject,
576
680
  * are retried in `src/chain/retry.ts`.
577
681
  */
578
- select<S extends z.ZodTypeAny>(table: string, schema: S, build: (q: ReturnType<AnySupabaseClient["from"]>) => PromiseLike<{
682
+ select<S extends z.ZodTypeAny>(table: string, schema: S, build: (q: ReturnType<AnyPostgrestClient["from"]>) => PromiseLike<{
579
683
  data: unknown[] | null;
580
684
  error: {
581
685
  message: string;
582
686
  code?: string;
583
687
  } | null;
584
688
  }>): Promise<Array<z.infer<S>>>;
689
+ /**
690
+ * Paged select: an exact `hasMore`, an approximate `total`.
691
+ *
692
+ * Both halves of that are deliberate. An `exact` count makes Postgres scan every
693
+ * matching row on every page request, which is invisible on a young vault and
694
+ * quadratic on a busy one — so the count is `estimated`, cheap and good enough for
695
+ * "roughly how much history is there".
696
+ *
697
+ * But `hasMore` derived from an estimate would be a lie a caller can act on: a
698
+ * paging loop that stops early drops real rows. So callers request one row beyond
699
+ * the page and this trims it, which answers "is there another page" exactly, for
700
+ * the cost of a single extra row.
701
+ */
702
+ selectPage<S extends z.ZodTypeAny>(table: string, schema: S, limit: number, build: (q: ReturnType<AnyPostgrestClient['from']>) => PromiseLike<{
703
+ data: unknown[] | null;
704
+ error: {
705
+ message: string;
706
+ code?: string;
707
+ } | null;
708
+ count?: number | null;
709
+ }>): Promise<Page<z.infer<S>>>;
585
710
  /** 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<{
711
+ selectOne<S extends z.ZodTypeAny>(table: string, schema: S, build: (q: ReturnType<AnyPostgrestClient["from"]>) => PromiseLike<{
587
712
  data: unknown | null;
588
713
  error: {
589
714
  message: string;
@@ -1132,24 +1257,56 @@ declare class IndexerQueries {
1132
1257
  vaultsForGuardian(guardian: Address, options?: Pagination): Promise<WalletRow[]>;
1133
1258
  pendingTransactions(vault: Address, options?: Pagination): Promise<TransactionRow[]>;
1134
1259
  transaction(vault: Address, txHash: string): Promise<TransactionRow | null>;
1260
+ /**
1261
+ * Several transactions by hash, chunked for the same reasons as
1262
+ * {@link activeConfirmationsBatch} — hashes are long, and they go in the query string.
1263
+ *
1264
+ * Rows come back in no particular order and hashes with no row are simply absent;
1265
+ * the caller matches them up.
1266
+ */
1267
+ transactionsByHash(vault: Address, txHashes: string[]): Promise<TransactionRow[]>;
1135
1268
  transactionHistory(vault: Address, options?: Pagination & {
1136
1269
  status?: string[];
1137
1270
  }): Promise<Page<TransactionRow>>;
1138
1271
  /** All confirmations for one transaction, including revoked ones. */
1139
1272
  confirmations(vault: Address, txHash: string): Promise<ConfirmationRow[]>;
1140
1273
  /**
1141
- * Active confirmations for many transactions in one round trip.
1274
+ * Active confirmations for many transactions, in as few round trips as is safe.
1142
1275
  *
1143
1276
  * "Active" here means not revoked. It does NOT mean the confirming address is
1144
1277
  * still an owner — the indexer does not deactivate confirmations when an owner is
1145
1278
  * removed, while the contract invalidates them via approval epochs. Callers must
1146
1279
  * intersect with the active owner set; `Vault` does this for you.
1280
+ *
1281
+ * Chunked rather than issued as one `in(...)`, because a single filter fails two
1282
+ * ways at scale. PostgREST reads filters from the query string, and each 32-byte
1283
+ * hash costs ~70 characters there — a full {@link MAX_LIMIT} page of hashes builds a
1284
+ * ~14 KB request line, past the 8 KB cap most reverse proxies apply by default. The
1285
+ * response is bounded too: {@link CONFIRMATION_CHUNK} transactions can carry at most
1286
+ * `chunk × MAX_OWNERS` rows, and the chunk is sized to keep that under the
1287
+ * `max-rows` ceiling PostgREST deployments commonly set, so a busy vault cannot be
1288
+ * silently short-served.
1147
1289
  */
1148
1290
  activeConfirmationsBatch(vault: Address, txHashes: string[]): Promise<Map<string, ConfirmationRow[]>>;
1149
1291
  modules(vault: Address): Promise<string[]>;
1150
1292
  delegatecallTargets(vault: Address): Promise<string[]>;
1151
1293
  deposits(vault: Address, options?: Pagination): Promise<Page<DepositRow>>;
1152
1294
  tokenTransfers(vault: Address, options?: Pagination): Promise<Page<TokenTransferRow>>;
1295
+ /**
1296
+ * Scan up to `budget` of the most recent transfer rows, paging past {@link MAX_LIMIT}.
1297
+ *
1298
+ * `tokenTransfers` clamps a single request, so a caller asking for more than the cap
1299
+ * silently received a short page — and then computed "did I see everything?" from
1300
+ * that short page's `hasMore`, which reported truncation the caller had not actually
1301
+ * hit. Paging here keeps the caller's budget meaningful and makes the returned
1302
+ * `hasMore` mean what it says: rows exist beyond the budget that was scanned.
1303
+ *
1304
+ * Pages by offset over a descending order, so a transfer landing mid-scan can shift
1305
+ * rows by one. That is acceptable for token *discovery* — a duplicate collapses into
1306
+ * the same map key and a missed row costs at most one candidate that the next call
1307
+ * picks up. Do not reuse this for anything that must see each row exactly once.
1308
+ */
1309
+ tokenTransferScan(vault: Address, budget: number): Promise<Page<TokenTransferRow>>;
1153
1310
  tokens(addresses: string[]): Promise<TokenRow[]>;
1154
1311
  signedMessages(vault: Address): Promise<SignedMessageRow[]>;
1155
1312
  recoveryConfig(vault: Address): Promise<{
@@ -1167,6 +1324,8 @@ interface RecoveryModuleContext {
1167
1324
  queries: IndexerQueries | null;
1168
1325
  vaultAddress: Address;
1169
1326
  moduleAddress: Address | undefined;
1327
+ /** Source of "now" for comparisons against chain timestamps. See {@link Clock}. */
1328
+ now?: Clock;
1170
1329
  }
1171
1330
  /**
1172
1331
  * Guardian-based recovery for one vault.
@@ -1183,6 +1342,16 @@ declare class RecoveryModule {
1183
1342
  get address(): Address;
1184
1343
  private contract;
1185
1344
  private get vault();
1345
+ /**
1346
+ * The vault this module acts on, through the same retrying facade every other vault
1347
+ * read in the SDK uses. Recovery reaches into the vault for three checks — is the
1348
+ * module enabled, is the caller an owner, who are the current owners — and each of
1349
+ * them gates a write, so none of them should be the one read that silently skips
1350
+ * transient-failure handling.
1351
+ */
1352
+ /** Now, in Unix seconds, for anything compared against a chain timestamp. */
1353
+ private now;
1354
+ private vaultContract;
1186
1355
  /** Whether the module is currently enabled on the vault. */
1187
1356
  isEnabled(): Promise<boolean>;
1188
1357
  config(): Promise<RecoveryConfig>;
@@ -1252,7 +1421,7 @@ declare class RecoveryModule {
1252
1421
  chainTxHash: Hex;
1253
1422
  }>;
1254
1423
  /** What `caller` may do to a recovery right now, and when blocked actions unlock. */
1255
- affordances(recoveryHash: Bytes32, caller?: Address): Promise<RecoveryAffordance[]>;
1424
+ affordances(recoveryHash: Bytes32, caller?: Address, at?: number): Promise<RecoveryAffordance[]>;
1256
1425
  private buildFromChain;
1257
1426
  private validateNewOwners;
1258
1427
  private assertLive;
@@ -1265,7 +1434,13 @@ declare class RecoveryModule {
1265
1434
  interface TokenBalance {
1266
1435
  token: Address;
1267
1436
  standard: 'ERC20' | 'ERC721' | 'ERC1155';
1437
+ /**
1438
+ * Ticker as the token reports it — deployer-controlled, so passed through
1439
+ * {@link sanitizeText} and capped. Falls back to `???` when the token has none or
1440
+ * supplied nothing printable.
1441
+ */
1268
1442
  symbol: string;
1443
+ /** Display name. Same provenance and treatment as {@link symbol}. */
1269
1444
  name: string;
1270
1445
  decimals: number;
1271
1446
  /** ERC20: raw units. ERC721: number held. ERC1155: total across ids. */
@@ -1306,12 +1481,22 @@ interface BalanceOptions {
1306
1481
  /**
1307
1482
  * How many recent transfer rows to scan when discovering tokens. Default 500.
1308
1483
  *
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}.
1484
+ * Scanned across as many indexer requests as it takes, so this is a real budget
1485
+ * rather than a single page size. A vault with more transfers than this may not
1486
+ * surface its oldest holdings, so raise it for long-lived vaults. Reported back in
1487
+ * {@link VaultBalances.truncated}.
1311
1488
  */
1312
1489
  transferScanLimit?: number;
1313
1490
  /** Cap on ERC721 ids ownership-checked per token. Default 100. */
1314
1491
  maxTokenIdChecks?: number;
1492
+ /**
1493
+ * Ceiling on in-flight RPC reads. Default 8.
1494
+ *
1495
+ * Applied at both fan-out levels independently, so the true ceiling is the square
1496
+ * of this. Raise it against a private node; lower it if a shared endpoint is
1497
+ * rate-limiting you.
1498
+ */
1499
+ concurrency?: number;
1315
1500
  }
1316
1501
  /**
1317
1502
  * Aggregate a vault's holdings.
@@ -1404,8 +1589,10 @@ declare const interfaces: {
1404
1589
  declare const MAX_EXECUTION_DELAY: number;
1405
1590
  declare const MAX_OWNERS = 20;
1406
1591
  declare const MAX_MODULES = 50;
1407
- /** Head of the Zodiac module linked list. */
1592
+ /** Head of the Zodiac module linked list. Never valid as an owner, module or guardian. */
1408
1593
  declare const SENTINEL_MODULES = "0x0000000000000000000000000000000000000001";
1594
+ /** Rejected everywhere an address is committed to a role. */
1595
+ declare const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";
1409
1596
  declare const selfCall: {
1410
1597
  addOwner: (owner: Address) => Hex;
1411
1598
  removeOwner: (owner: Address) => Hex;
@@ -1486,6 +1673,34 @@ interface VaultContext {
1486
1673
  contracts: ContractAddresses;
1487
1674
  consistency: Consistency;
1488
1675
  maxIndexerLagBlocks: number;
1676
+ /**
1677
+ * Source of "now" for comparisons against chain timestamps. See {@link Clock}.
1678
+ * Optional so a hand-built context still works; defaults to the local clock.
1679
+ */
1680
+ now?: Clock;
1681
+ /** Set by {@link Vault.pinned}. Never consulted by a write precondition. */
1682
+ view?: VaultView;
1683
+ }
1684
+ /**
1685
+ * Owners and threshold, captured at one instant.
1686
+ *
1687
+ * The SDK deliberately holds no read cache: the owner set decides which approvals
1688
+ * count toward the threshold, and serving a stale one silently would reintroduce the
1689
+ * exact bug `buildTransaction` intersects confirmations to avoid. An implicit TTL
1690
+ * would make that failure invisible and intermittent.
1691
+ *
1692
+ * So the staleness window is the caller's to open, and to see. `capturedAt` is right
1693
+ * there in the object; a CLI rendering one screen can pin a view for that screen and
1694
+ * know precisely what it pinned.
1695
+ */
1696
+ interface VaultView {
1697
+ owners: Address[];
1698
+ threshold: number;
1699
+ /** Indexer head when this was taken; absent if the snapshot came from chain. */
1700
+ indexedAtBlock?: number;
1701
+ /** Unix seconds. How stale the snapshot is allowed to get is the caller's call. */
1702
+ capturedAt: number;
1703
+ source: 'indexer' | 'chain';
1489
1704
  }
1490
1705
  /** A handle to one deployed vault. Cheap to construct; performs no I/O until used. */
1491
1706
  declare class Vault {
@@ -1503,13 +1718,84 @@ declare class Vault {
1503
1718
  */
1504
1719
  private useIndexer;
1505
1720
  private requireQueries;
1721
+ /**
1722
+ * The indexer's head, for stamping onto records read from it.
1723
+ *
1724
+ * Reads the cached health result rather than querying `indexer_state` directly.
1725
+ * `useIndexer()` has just called `health()` on this same code path and the result is
1726
+ * cached, so this is free — whereas a `state()` call is a second round trip per
1727
+ * hydration, paid on every indexed read purely to fill one advisory field.
1728
+ *
1729
+ * Undefined when the indexer is not answering: a head of 0 would read as "indexed at
1730
+ * genesis" rather than "unknown".
1731
+ */
1732
+ private indexerHead;
1733
+ /**
1734
+ * Now, in Unix seconds, for anything compared against a chain timestamp.
1735
+ *
1736
+ * Never used to measure elapsed time — see the duration sites in
1737
+ * {@link waitForExecutable}, which stay on the raw local clock deliberately.
1738
+ */
1739
+ private now;
1506
1740
  private contract;
1507
1741
  /** Owners, threshold, timelock floor, nonce and balance. Always read from chain. */
1508
1742
  info(): Promise<VaultInfo>;
1509
- /** Active owners. Prefers the indexer, falls back to chain. */
1743
+ /**
1744
+ * Capture owners and threshold once, for reuse across a burst of reads.
1745
+ *
1746
+ * Pair with {@link pinned}:
1747
+ *
1748
+ * ```ts
1749
+ * const view = await vault.view();
1750
+ * const pinned = vault.pinned(view);
1751
+ * // every read below shares one owner/threshold read
1752
+ * const txs = await pinned.transactions(hashes);
1753
+ * ```
1754
+ */
1755
+ view(): Promise<VaultView>;
1756
+ /**
1757
+ * A handle that answers {@link owners} and {@link threshold} from `view` instead of
1758
+ * re-reading them.
1759
+ *
1760
+ * Reads only. Write preconditions go through `chainOwners` / `chainThreshold`, which
1761
+ * bypass the snapshot by construction — pinning a view can never cause this SDK to
1762
+ * sign against a stale owner set. Everything else about the handle, including the
1763
+ * connection and the recovery module, is shared with the original.
1764
+ */
1765
+ pinned(view: VaultView): Vault;
1766
+ /** Active owners. Prefers a pinned view, then the indexer, then chain. */
1510
1767
  owners(): Promise<Address[]>;
1768
+ /**
1769
+ * Owners straight from the chain, bypassing the indexer entirely.
1770
+ *
1771
+ * {@link owners} prefers the indexer, which is right for display and wrong for
1772
+ * anything that gates a write. A lagging indexer would let the SDK build a proposal
1773
+ * against an owner set that no longer exists — admitting a `removeOwner` that drops
1774
+ * the vault below its threshold, or rejecting one that is perfectly valid. Every
1775
+ * propose-time precondition reads through here instead.
1776
+ */
1777
+ private chainOwners;
1511
1778
  isOwner(address: Address): Promise<boolean>;
1779
+ /**
1780
+ * Whether `owner`'s approval on `txHash` currently counts toward the threshold.
1781
+ *
1782
+ * Reads the chain, and accounts for approval-epoch invalidation: removing an owner
1783
+ * bumps the vault's epoch and voids every approval that address made, so a
1784
+ * confirmation the indexer still lists may already be dead. This is the authoritative
1785
+ * answer.
1786
+ *
1787
+ * Public because it is one of the two inputs {@link computeAffordances} needs. Without
1788
+ * it a consumer wanting affordances at an adjusted time had no way to assemble an
1789
+ * `AffordanceContext` short of reimplementing this method against the raw contract.
1790
+ */
1791
+ hasApproved(txHash: Bytes32, owner: Address): Promise<boolean>;
1792
+ /** Approvals required to execute. Prefers a pinned view, then the indexer, then chain. */
1512
1793
  threshold(): Promise<number>;
1794
+ /**
1795
+ * Threshold straight from the chain. The write-path counterpart to {@link threshold},
1796
+ * for the same reason {@link chainOwners} exists.
1797
+ */
1798
+ private chainThreshold;
1513
1799
  balance(): Promise<bigint>;
1514
1800
  /**
1515
1801
  * Enabled modules, in linked-list order.
@@ -1527,6 +1813,20 @@ declare class Vault {
1527
1813
  /** The vault-transaction hash for a given call, at a given nonce. */
1528
1814
  transactionHash(to: Address, value: bigint, data: Hex, nonce?: number): Promise<Bytes32>;
1529
1815
  transaction(txHash: Bytes32): Promise<VaultTransaction>;
1816
+ /**
1817
+ * Several transactions at once, keyed by hash.
1818
+ *
1819
+ * The plural form exists because the singular one is expensive to loop: each
1820
+ * `transaction()` re-reads the owner set, the threshold and the indexer head, so
1821
+ * fetching fifty costs fifty times that. This resolves the whole set the way the
1822
+ * listing methods already do — one query for the rows, one owner/threshold read
1823
+ * shared across them, one batched confirmations query.
1824
+ *
1825
+ * Hashes the indexer does not have fall back to chain reads, bounded by a pool.
1826
+ * Hashes that exist nowhere are simply absent from the map rather than throwing:
1827
+ * one unknown hash should not lose the caller the other forty-nine.
1828
+ */
1829
+ transactions(txHashes: Bytes32[]): Promise<Map<Bytes32, VaultTransaction>>;
1530
1830
  pendingTransactions(options?: Pagination): Promise<VaultTransaction[]>;
1531
1831
  transactionHistory(options?: Pagination & {
1532
1832
  status?: string[];
@@ -1550,7 +1850,17 @@ declare class Vault {
1550
1850
  * What `caller` may legally do to `txHash` right now, and when blocked actions
1551
1851
  * become available. Defaults to the connected signer's address.
1552
1852
  */
1553
- affordances(txHash: Bytes32, caller?: Address): Promise<Affordance[]>;
1853
+ affordances(txHash: Bytes32, caller?: Address, at?: number): Promise<Affordance[]>;
1854
+ /**
1855
+ * Shared body of {@link affordances}, taking the transaction either as a value or as
1856
+ * an in-flight promise.
1857
+ *
1858
+ * The promise form is the point: a caller that already holds the record (like
1859
+ * {@link describe}) passes it and skips a redundant fetch, while `affordances()`
1860
+ * passes the unawaited promise so the transaction read still overlaps the two
1861
+ * ownership probes rather than serialising behind them.
1862
+ */
1863
+ private resolveAffordances;
1554
1864
  /**
1555
1865
  * Proposal builders.
1556
1866
  *
@@ -1770,6 +2080,11 @@ declare class Vault {
1770
2080
  * called, so building a client is cheap and safe at module scope.
1771
2081
  */
1772
2082
  declare class QuaiVaultClient {
2083
+ /**
2084
+ * Source of "now" for time compared against chain timestamps. Resolved once from
2085
+ * {@link ClientOptions.now}, defaulting to the local clock. See {@link Clock}.
2086
+ */
2087
+ readonly now: Clock;
1773
2088
  /**
1774
2089
  * Resolved configuration, with the private key stripped and the indexer key
1775
2090
  * masked — safe to log. The key itself is consumed once when building the signer.
@@ -1828,6 +2143,23 @@ declare const QuaiVault: {
1828
2143
  connect: typeof connect;
1829
2144
  };
1830
2145
 
2146
+ /**
2147
+ * Map with a bounded number of in-flight promises, preserving input order.
2148
+ *
2149
+ * A plain `Promise.all` over a fan-out the caller does not control is a liability:
2150
+ * `balances()` can reach `maxTokens × maxTokenIdChecks` — 5,000 with the defaults —
2151
+ * concurrent RPC reads from one call, and a batched transaction read fans out over
2152
+ * however many hashes the caller passed. Public endpoints rate-limit long before
2153
+ * either, and the failures come back as transient errors on reads that would each
2154
+ * have succeeded on their own.
2155
+ *
2156
+ * Rejections propagate: the first failure rejects the whole call, matching
2157
+ * `Promise.all`. Callers that want per-item tolerance catch inside `fn`.
2158
+ */
2159
+ declare function mapPooled<T, R>(items: readonly T[], limit: number, fn: (item: T, index: number) => Promise<R>): Promise<R[]>;
2160
+ /** Default ceiling on concurrent chain reads issued by one SDK call. */
2161
+ declare const DEFAULT_CONCURRENCY = 8;
2162
+
1831
2163
  /**
1832
2164
  * Raw `Transaction` struct as returned by `QuaiVault.transactions(bytes32)`.
1833
2165
  * Field names match the Solidity struct.
@@ -1923,6 +2255,25 @@ declare class FactoryContract {
1923
2255
  registerWallet(wallet: Address): Promise<ContractTransactionResponse>;
1924
2256
  }
1925
2257
 
2258
+ /**
2259
+ * Typed, retrying facade over an ERC20/721/1155 contract.
2260
+ *
2261
+ * Tokens are only ever read, never written to — the vault's own proposal flow is what
2262
+ * moves them — so there is no write half here and no signer involved.
2263
+ */
2264
+ declare class TokenContract {
2265
+ readonly contract: Contract;
2266
+ private readonly retry;
2267
+ constructor(contract: Contract, retry?: RetryOptions);
2268
+ private read;
2269
+ /** ERC20 units held, or — for ERC721 — the number of tokens held. */
2270
+ balanceOf(owner: Address): Promise<bigint>;
2271
+ /** ERC721 only. Reverts for a burned or never-minted id. */
2272
+ ownerOf(tokenId: string | bigint): Promise<string>;
2273
+ /** ERC1155 only. One call for many ids, so it stays a single round trip. */
2274
+ balanceOfBatch(owners: Address[], ids: Array<string | bigint>): Promise<bigint[]>;
2275
+ }
2276
+
1926
2277
  /** Raw `RecoveryConfig` struct from `getRecoveryConfig(address)`. */
1927
2278
  interface RawRecoveryConfig {
1928
2279
  guardians: string[];
@@ -2141,6 +2492,38 @@ declare function predictVaultAddress(args: {
2141
2492
  */
2142
2493
  declare function shardPrefixOf(address: Address): string;
2143
2494
 
2495
+ /**
2496
+ * Display sanitisation for attacker-controlled strings.
2497
+ *
2498
+ * Two values reach a consumer's screen without ever having been vetted by anyone:
2499
+ *
2500
+ * - **Token metadata.** `symbol()` and `name()` are whatever the token's deployer
2501
+ * chose. Getting one indexed against a vault costs one transfer of one unit.
2502
+ * - **Revert reasons.** `Error(string)` data comes from the contract a vault called,
2503
+ * which for an external call is an arbitrary address the proposer picked.
2504
+ *
2505
+ * In a browser both are inert — the DOM escapes them. In a terminal they are not. An
2506
+ * ANSI escape can move the cursor up and overwrite lines the SDK already printed, so
2507
+ * a token named `"\x1b[2A\x1b[KAll checks passed"` can forge SDK output. Bidi
2508
+ * overrides can reverse a rendered address while leaving the underlying bytes intact,
2509
+ * which is the same trick that makes malicious source look benign.
2510
+ *
2511
+ * Sanitising here rather than in each consumer means a CLI, a TUI and a log pipeline
2512
+ * all inherit it, and none of them has to remember. Only human-facing fields are
2513
+ * scrubbed — machine-readable ones (`DecodedRevert.args`) keep the raw bytes, because
2514
+ * a consumer comparing against an expected value needs exactly what was on chain.
2515
+ */
2516
+ /** Default cap. Long enough for any legitimate token name, short enough to not wrap. */
2517
+ declare const DEFAULT_TEXT_LIMIT = 128;
2518
+ /**
2519
+ * Strip control and direction-manipulating characters, collapse surrounding
2520
+ * whitespace, and cap the length.
2521
+ *
2522
+ * Returns `''` for a non-string or for input that was entirely unsafe, so callers can
2523
+ * `sanitizeText(x) || fallback`.
2524
+ */
2525
+ declare function sanitizeText(value: unknown, maxLength?: number): string;
2526
+
2144
2527
  /**
2145
2528
  * Address validation for Quai's dual-ledger, sharded address space.
2146
2529
  *
@@ -2189,7 +2572,7 @@ declare function assertQuaiAddress(value: unknown, label?: string): Address;
2189
2572
  declare function assertQuaiAddresses(values: readonly unknown[], label: string): Address[];
2190
2573
 
2191
2574
  /** 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';
2575
+ 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
2576
  declare class QuaiVaultError extends Error {
2194
2577
  readonly code: QuaiVaultErrorCode;
2195
2578
  /** What the caller can do about it, in plain language. */
@@ -2205,6 +2588,18 @@ declare class QuaiVaultError extends Error {
2205
2588
  /** Structured form, for logs and machine consumers. */
2206
2589
  toJSON(): Record<string, unknown>;
2207
2590
  }
2591
+ /**
2592
+ * A caller-supplied `AbortSignal` fired.
2593
+ *
2594
+ * Every long-running operation the SDK exposes — retries, indexer polling, salt
2595
+ * mining, waiting for a timelock — accepts a signal, and each used to report abort
2596
+ * differently: a bare `Error('Aborted')` in two places, a `PreconditionError` in a
2597
+ * third. Consumers switching on `err.code` to distinguish "the user pressed Ctrl-C"
2598
+ * from "this genuinely failed" could not do so. They can now.
2599
+ */
2600
+ declare class AbortError extends QuaiVaultError {
2601
+ constructor(operation?: string);
2602
+ }
2208
2603
  declare class ConfigError extends QuaiVaultError {
2209
2604
  constructor(message: string, remediation?: string);
2210
2605
  }
@@ -2268,4 +2663,4 @@ declare function knownErrorSelectors(): Array<{
2268
2663
  signature: string;
2269
2664
  }>;
2270
2665
 
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 };
2666
+ export { AbortError, type Address, type AddressCheck, type AddressLedger, type Affordance, type AffordanceBlocker, type AffordanceContext, type ApprovalRecord, type BalanceOptions, type BatchCall, type Bytes32, type ClientOptions, type Clock, 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 };