@satora/swap 0.2.0 → 0.3.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,7 +1,7 @@
1
1
  import { SwapStatus, Client as Client$1, ClientBuilder as ClientBuilder$1, StoredSwap } from '@lendasat/lendaswap-sdk-pure';
2
2
  export * from '@lendasat/lendaswap-sdk-pure';
3
3
  import { VirtualCoin } from '@arkade-os/sdk';
4
- import { Log, Block } from 'viem';
4
+ import { Block } from 'viem';
5
5
 
6
6
  /**
7
7
  * The derived "what should the client do next?" model.
@@ -44,8 +44,37 @@ type CctpClaimEvidence = {
44
44
  burnTxHash: `0x${string}`;
45
45
  attestationAvailable: boolean;
46
46
  };
47
+ /**
48
+ * What a `wait` is waiting FOR — machine-readable, so a consumer can pick the
49
+ * right surface (invoice page, "deposit seen…", processing spinner, refund
50
+ * countdown) by switching on this field instead of re-deriving it from
51
+ * status + observations.
52
+ *
53
+ * One caveat: `client_payment` covers the whole Lightning-funded window — the
54
+ * chain cannot see whether the invoice was already paid (the server is the
55
+ * only witness for that split), so a consumer that wants "invoice shown" vs
56
+ * "payment in flight" as separate screens still refines this one value with
57
+ * the server status.
58
+ */
59
+ type WaitingOn =
60
+ /** A Lightning payment to be made / locked in as a funded HTLC. */
61
+ "client_payment"
62
+ /** The client's funding tx was seen and is awaiting confirmation. */
63
+ | "client_funding_confirmation"
64
+ /** The client funded; the server's HTLC hasn't appeared yet. */
65
+ | "server_funding"
66
+ /** The client's claim is in flight, awaiting confirmation. */
67
+ | "claim_confirmation"
68
+ /** Nothing will progress — waiting for the client refund timelock to pass. */
69
+ | "refund_timelock";
70
+ /**
71
+ * How a finished swap ended — so a consumer can render success vs refund vs
72
+ * expiry from the terminal `none` action without interpreting `SwapStatus`.
73
+ */
74
+ type SwapOutcome = "completed" | "refunded" | "expired";
47
75
  type WaitAction = SwapActionBase & {
48
76
  id: "wait";
77
+ waitingOn: WaitingOn;
49
78
  };
50
79
  type FundAction = SwapActionBase & {
51
80
  id: "fund";
@@ -66,6 +95,7 @@ type RecoverCctpClaimAction = SwapActionBase & {
66
95
  };
67
96
  type NoneAction = SwapActionBase & {
68
97
  id: "none";
98
+ outcome: SwapOutcome;
69
99
  };
70
100
  /** A single next-action option. Discriminated union on `id`. */
71
101
  type SwapAction = WaitAction | FundAction | ClaimAction | RefundCollaborativeAction | RefundUnilateralAction | RecoverCctpClaimAction | NoneAction;
@@ -76,6 +106,14 @@ type SwapActions = {
76
106
  /** id of the recommended action, if any; convenience pointer into `actions`. */
77
107
  recommended?: SwapActionId;
78
108
  actions: SwapAction[];
109
+ /**
110
+ * The raw chain observations these actions were derived from (set by the
111
+ * tracker; absent from a bare `deriveSwapActions` call). Chain FACTS — which
112
+ * leg is funded/spent — not protocol states: lets a UI render progress from
113
+ * chain truth (even when the server is stale or unreachable) without ever
114
+ * interpreting the `SwapStatus` enum.
115
+ */
116
+ observations?: SwapObservations;
79
117
  };
80
118
  /**
81
119
  * The pure input to {@link deriveSwapActions}: a plain, fully-resolved snapshot.
@@ -228,15 +266,28 @@ type HtlcRef = {
228
266
  preimageHash: string;
229
267
  /** Expected funding amount in sats; a funding below this is `invalid`. */
230
268
  expectedSats: number;
269
+ /**
270
+ * Confirmations this leg's funding needs before it observes as
271
+ * `confirmed`, overriding the reader's default.
272
+ *
273
+ * Set per leg because the two directions are not symmetric. A funding the
274
+ * SERVER made can be taken at 0-conf: the server does not double-spend its
275
+ * own HTLC, and waiting a block would stall every claim. A funding the
276
+ * CLIENT made is read back under the rule the server applies to it — the
277
+ * server does not act until the tx has a blocktime, so calling it funded
278
+ * sooner reports progress the other side has not agreed to.
279
+ */
280
+ minConfirmations?: number;
231
281
  } | {
232
282
  ledger: "evm";
233
283
  chainId: number;
234
284
  /** The HTLCErc20 contract holding this leg — where its events are emitted. */
235
285
  htlc: `0x${string}`;
236
286
  /**
237
- * SHA256 preimage hash (`0x`-prefixed) — the indexed topic that identifies
238
- * this swap's HTLC in the contract's `SwapCreated`/`SwapRedeemed`/
239
- * `SwapRefunded` events. Serves as the contract's unique id here.
287
+ * SHA256 preimage hash (`0x`-prefixed) — the indexed topic the contract's
288
+ * `SwapCreated`/`SwapRedeemed`/`SwapRefunded` events are filtered by. It
289
+ * narrows a log scan but identifies no single HTLC: any number can share
290
+ * one hash, so a swap is told apart by the rest of the tuple below.
240
291
  */
241
292
  preimageHash: `0x${string}`;
242
293
  /**
@@ -254,6 +305,22 @@ type HtlcRef = {
254
305
  expectedAmount: bigint;
255
306
  /** Expected token address; the funding is `invalid` if it locks another token. */
256
307
  expectedToken?: `0x${string}`;
308
+ /**
309
+ * The funder (the HTLC's refund address). Together with the other fields it
310
+ * completes the contract's swap-key tuple, enabling the cheap `isActive`
311
+ * eth_call fast path (which verifies the terms implicitly — the key hashes
312
+ * amount/token/addresses/timelock). Legs whose response omits a tuple field
313
+ * fall back to log scans.
314
+ */
315
+ sender?: `0x${string}`;
316
+ /** The HTLC refund timelock in unix SECONDS, exactly as funded on-chain. */
317
+ timelockSec?: number;
318
+ /**
319
+ * Swap creation time (ms). Lower-bounds log scans: the HTLC can have no
320
+ * events before the swap existed, so the scan starts near the corresponding
321
+ * block instead of genesis.
322
+ */
323
+ createdAtMs?: number;
257
324
  } | {
258
325
  ledger: "lightning";
259
326
  /** Payment hash — the contract's unique id. */
@@ -328,11 +395,20 @@ type TrackedSwap = {
328
395
  type ActionSubscriber = (swapId: string, actions: SwapActions) => void;
329
396
  type SwapTrackerOptions = {
330
397
  /**
331
- * How often (ms) to re-poll every manager to advance clocks and reconcile
332
- * observations. Needed because some ledgers (Arkade) have no event push, so
333
- * state only moves on a poll. `0` disables the timer (tests drive `refresh`).
398
+ * The local tick interval (ms). Each tick recomputes every tracked swap from
399
+ * extrapolated chain clocks — pure local work, NO chain reads — so timelock
400
+ * flips (a refund unlocking, a stale pending swap turning reapable) surface
401
+ * between hints. `0` disables the timer (tests drive `refresh` manually).
334
402
  */
335
403
  refreshIntervalMs?: number;
404
+ /**
405
+ * Minimum ms between chain reconciles of AT-RISK swaps — those whose client
406
+ * leg holds funds on-chain (≥ clientfunded, unresolved), where refund safety
407
+ * must not depend on the server. Everything else advances on server hints
408
+ * only ({@link applyHint}); a swap with nothing at stake costs zero chain
409
+ * reads. Default 60s.
410
+ */
411
+ atRiskReconcileIntervalMs?: number;
336
412
  };
337
413
  declare class SwapTracker {
338
414
  #private;
@@ -411,6 +487,13 @@ type AutoClaimConfig = {
411
487
  type TrackingConfig = {
412
488
  /** Whether tracking is enabled at all (default on; `withoutTracking()` clears). */
413
489
  enabled: boolean;
490
+ /**
491
+ * The swap server URL (from `withBaseUrl`). When set, tracking opens the
492
+ * server's status WebSocket as a HINT feed: a pushed transition triggers a
493
+ * targeted chain verify, so the chain pollers can stay slow. Chain reads
494
+ * remain the source of truth.
495
+ */
496
+ serverUrl?: string;
414
497
  /** Explicit per-ledger managers, bypassing auto-construction (advanced/testing). */
415
498
  managers?: Map<Ledger, ContractManager>;
416
499
  /** Ark server URL override; defaults to the mainnet server when unset. */
@@ -419,7 +502,17 @@ type TrackingConfig = {
419
502
  evmRpcUrls?: Record<number, string>;
420
503
  /** Esplora REST URL override for the Bitcoin manager; defaults to mainnet mempool.space. */
421
504
  esploraUrl?: string;
422
- /** Poll interval (ms) for advancing observations/clocks; defaults to 5s. */
505
+ /**
506
+ * Confirmations a Bitcoin funding tx needs before it observes as
507
+ * `confirmed` (gating e.g. the evm→bitcoin claim). Default `0`: accept
508
+ * 0-conf, trusting the funder not to double-spend. `1` = wait for a block.
509
+ */
510
+ bitcoinMinConfirmations?: number;
511
+ /**
512
+ * Local tick interval (ms) — recomputes actions off extrapolated clocks, no
513
+ * chain reads. Chain reads happen on server hints and, for swaps whose
514
+ * client leg holds funds, on the tracker's at-risk cadence. Defaults to 5s.
515
+ */
423
516
  refreshIntervalMs?: number;
424
517
  /** Opt-in hint-driven auto-claim; unset leaves tracking observe-only. */
425
518
  autoClaim?: AutoClaimConfig;
@@ -639,6 +732,15 @@ declare class ClientBuilder {
639
732
  * otherwise defaults to mainnet mempool.space; set this for a dev/regtest node.
640
733
  */
641
734
  withEsploraUrl(url: string): this;
735
+ /**
736
+ * Confirmations a Bitcoin funding tx needs before it observes as `confirmed`
737
+ * (gating e.g. the evm→bitcoin claim). Default `0`: claim as soon as the
738
+ * funding hits the mempool, which TRUSTS the funder not to double-spend it
739
+ * (claiming publishes the preimage, so a funder that then replaced its
740
+ * funding could take both legs). Set `1` (or more) for a block-depth policy
741
+ * that doesn't rely on the funder's good behaviour.
742
+ */
743
+ withBitcoinMinConfirmations(minConfirmations: number): this;
642
744
  /**
643
745
  * Override the Ark server URL. Optional for tracking — it otherwise defaults to
644
746
  * the mainnet server; set this to track a non-mainnet (dev/signet) deployment.
@@ -663,7 +765,7 @@ type VirtualTxSource = {
663
765
 
664
766
  /**
665
767
  * The Arkade {@link ContractManager} — the stateful I/O adapter that observes a
666
- * swap's VHTLC by polling the Ark indexer.
768
+ * swap's VHTLC via the Ark indexer.
667
769
  *
668
770
  * We deliberately do NOT use `@arkade-os/sdk`'s `ContractManager`: its vtxo
669
771
  * annotation calls `script.forfeit()` on every contract, but a VHTLC script has
@@ -672,11 +774,14 @@ type VirtualTxSource = {
672
774
  * the legacy SDK's claim/refund paths do — and map them with the pure helpers in
673
775
  * `./arkade.js`.
674
776
  *
675
- * There is no event push here, so observations advance when {@link refresh} runs;
676
- * the SwapTracker drives that (initial prime + periodic poll).
777
+ * Observation is on-demand: {@link refresh} (tracker start / newly tracked
778
+ * swap) and targeted {@link reconcile}s (server hints, the tracker's at-risk
779
+ * safety net). There is no standing subscription or periodic poll.
677
780
  */
678
781
 
679
- /** The Ark indexer surface the observer needs (RestIndexerProvider satisfies it). */
782
+ /**
783
+ * The Ark indexer surface the observer needs (RestIndexerProvider satisfies it).
784
+ */
680
785
  type ArkadeIndexer = VirtualTxSource & {
681
786
  getVtxos(opts: {
682
787
  scripts: string[];
@@ -716,7 +821,13 @@ declare class ArkadeContractManager implements ContractManager {
716
821
  getState(ref: HtlcRef): HtlcObservation | undefined;
717
822
  chainNow(_ref: HtlcRef): number | undefined;
718
823
  onEvent(cb: (ref: HtlcRef, state: HtlcObservation) => void): () => void;
824
+ /**
825
+ * Full reconcile of every tracked ref (clock + vtxos). Called on demand
826
+ * (tracker start / newly tracked swap), NOT periodically: onward, hints and
827
+ * the tracker's at-risk safety net drive targeted `reconcile`s.
828
+ */
719
829
  refresh(): Promise<void>;
830
+ /** Targeted verify (hint / pre-action path) — never gated. */
720
831
  reconcile(ref: HtlcRef): Promise<void>;
721
832
  dispose(): void;
722
833
  /** The verified preimage recovered from a claim spend, if one was seen. */
@@ -730,6 +841,15 @@ declare class ArkadeContractManager implements ContractManager {
730
841
  */
731
842
  declare function defaultArkadeServerUrl(network: string): string | undefined;
732
843
 
844
+ /**
845
+ * Build the Arkade {@link HtlcRef} for a swap's VHTLC leg from the plain swap
846
+ * fields, deriving the pkScript and serialized contract params the
847
+ * `ArkadeContractManager` needs to register it for watching.
848
+ *
849
+ * This is the one place that reconstructs the VHTLC from a swap, mirroring the
850
+ * SDK VHTLC construction, so the mapper and manager stay free of VHTLC internals.
851
+ */
852
+
733
853
  type ArkadeRef = Extract<HtlcRef, {
734
854
  ledger: "arkade";
735
855
  }>;
@@ -754,6 +874,8 @@ type ArkadeVhtlcInput = {
754
874
  unilateralRefundWithoutReceiverDelay: number;
755
875
  /** Expected funding amount in sats (a short funding is `invalid`, not confirmed). */
756
876
  expectedSats: number;
877
+ /** Arkade HTLC script version. Missing means legacy v0 for old stored swaps. */
878
+ arkadeHtlcScriptVersion?: number;
757
879
  };
758
880
  /**
759
881
  * The VHTLC always commits to `ripemd160(sha256(preimage))` (HASH160). When
@@ -782,13 +904,25 @@ type BitcoinHtlcFacts = {
782
904
  /** The esplora surface the observer needs (a `BitcoinReaderEsplora` satisfies it). */
783
905
  type BitcoinChainReader = {
784
906
  /** Funding + spend facts for an HTLC witness-script address. */
785
- getHtlcFacts(address: string): Promise<BitcoinHtlcFacts>;
907
+ /**
908
+ * Funding + spend facts for an HTLC witness-script address. `minConfirmations`
909
+ * overrides the reader's default for this address alone — the two legs of a
910
+ * swap do not share one rule.
911
+ */
912
+ getHtlcFacts(address: string, minConfirmations?: number): Promise<BitcoinHtlcFacts>;
786
913
  };
787
914
  type BitcoinCreateConfig = {
788
915
  /** One or more esplora REST base URLs; several are tried in rotation with failover. */
789
916
  esploraUrl: string | string[];
790
917
  /** The current Bitcoin MTP (ms); typically `async () => (await client.getMtp()).mtp * 1000`. */
791
918
  chainTime?: () => Promise<number>;
919
+ /**
920
+ * Confirmations a funding tx needs before it observes as `confirmed`.
921
+ * Default `0`: accept an unconfirmed funding, trusting the funder not to
922
+ * double-spend it (the claim reveals the preimage against it). `1` = wait
923
+ * for a block.
924
+ */
925
+ minConfirmations?: number;
792
926
  };
793
927
  type BitcoinContractManagerDeps = {
794
928
  reader: BitcoinChainReader;
@@ -837,14 +971,33 @@ type EsploraTx = {
837
971
  }>;
838
972
  status?: {
839
973
  confirmed?: boolean;
974
+ block_height?: number;
840
975
  };
841
976
  };
977
+ /**
978
+ * When a funding tx counts as `confirmed` for observation purposes.
979
+ *
980
+ * `minConfirmations: 0` (the default) accepts an UNCONFIRMED funding, so a swap
981
+ * claims as soon as the funding hits the mempool instead of waiting ~10min for
982
+ * a block. This TRUSTS the funder not to double-spend: claiming publishes the
983
+ * preimage, so a funder who then replaces its funding tx could take both legs.
984
+ * Our own server therefore broadcasts HTLC fundings without signalling RBF.
985
+ * Note that a non-signalling tx is still replaceable by full-RBF miners — set
986
+ * `1` (or more) for a policy that doesn't rely on the funder's good behaviour;
987
+ * values above 1 make the reader fetch the tip height to compute depth.
988
+ */
989
+ type BitcoinConfirmationPolicy = {
990
+ minConfirmations?: number;
991
+ };
842
992
  /**
843
993
  * Reduce an address's esplora tx history to HTLC facts. If a tx spends an output
844
994
  * at the address, that's the resolving spend (its witness classifies claim vs
845
- * refund); otherwise a tx paying the address is the funding.
995
+ * refund); otherwise a tx paying the address is the funding, gated by the
996
+ * confirmation policy (see {@link BitcoinConfirmationPolicy}).
846
997
  */
847
- declare function htlcFactsFromEsploraTxs(txs: EsploraTx[], address: string): BitcoinHtlcFacts;
998
+ declare function htlcFactsFromEsploraTxs(txs: EsploraTx[], address: string, opts?: BitcoinConfirmationPolicy & {
999
+ tipHeight?: number;
1000
+ }): BitcoinHtlcFacts;
848
1001
  /**
849
1002
  * Public esplora endpoints tried in rotation (mainnet). blockstream.info runs the
850
1003
  * reference esplora, so it's API-compatible with mempool.space; spreading calls
@@ -857,7 +1010,7 @@ declare const DEFAULT_ESPLORA_URLS: string[];
857
1010
  * providers) and fails over to the rest on error — so a throttled or failing
858
1011
  * provider doesn't stall tracking.
859
1012
  */
860
- declare function esploraReader(esploraUrls: string | string[], fetchImpl?: typeof fetch): BitcoinChainReader;
1013
+ declare function esploraReader(esploraUrls: string | string[], fetchImpl?: typeof fetch, policy?: BitcoinConfirmationPolicy): BitcoinChainReader;
861
1014
 
862
1015
  /**
863
1016
  * The `EvmContractManager`'s pure core.
@@ -894,17 +1047,80 @@ type EvmHtlcEvent = {
894
1047
  * {@link chainNow} is ref-scoped.
895
1048
  */
896
1049
 
1050
+ /** Identifies one HTLC to read: contract + preimageHash (+ the claim guard). */
1051
+ type EvmHtlcQuery = {
1052
+ htlc: `0x${string}`;
1053
+ preimageHash: `0x${string}`;
1054
+ /** A `SwapCreated` counts only when it pays this address (term check). */
1055
+ claimAddress: `0x${string}`;
1056
+ /**
1057
+ * The rest of the swap-key tuple, when the swap response exposed it. It makes
1058
+ * {@link htlcQueryKey} unique to this HTLC, so two swaps sharing a preimage
1059
+ * hash hold separate entries rather than one overwriting the other. It does
1060
+ * not change how settlements are attributed — those go by hash either way.
1061
+ */
1062
+ terms?: EvmHtlcTerms;
1063
+ };
1064
+ /**
1065
+ * The result key for one query in a batch.
1066
+ *
1067
+ * `preimageHash` identifies no single HTLC — any number can share one — so the
1068
+ * rest of the swap-key tuple is what separates two HTLCs on the same contract.
1069
+ * A caller that knows the tuple gets a key unique to its HTLC; one that does not
1070
+ * falls back to contract + hash, which is ambiguous by construction — two such
1071
+ * callers on one hash share an entry.
1072
+ */
1073
+ declare function htlcQueryKey(q: {
1074
+ htlc: `0x${string}`;
1075
+ preimageHash: `0x${string}`;
1076
+ claimAddress?: `0x${string}`;
1077
+ terms?: EvmHtlcTerms;
1078
+ }): string;
1079
+ /** The swap-key tuple fields beyond contract, hash and claim address. */
1080
+ type EvmHtlcTerms = {
1081
+ amount: bigint;
1082
+ token: `0x${string}`;
1083
+ sender: `0x${string}`;
1084
+ timelockSec: number;
1085
+ };
1086
+ /**
1087
+ * The contract's full swap-key tuple for one HTLC. `isActive` hashes all of it
1088
+ * into the swap key, so an `isActive == true` with these EXPECTED values also
1089
+ * proves the HTLC was funded on exactly the expected terms.
1090
+ */
1091
+ type EvmActiveQuery = {
1092
+ htlc: `0x${string}`;
1093
+ preimageHash: `0x${string}`;
1094
+ amount: bigint;
1095
+ token: `0x${string}`;
1096
+ sender: `0x${string}`;
1097
+ claimAddress: `0x${string}`;
1098
+ timelockSec: number;
1099
+ };
897
1100
  /** Reads `HTLCErc20` state for one EVM chain. Implemented over viem/ethers/etc. */
898
1101
  type EvmChainReader = {
899
1102
  /**
900
- * The decoded lifecycle events (oldest→newest) for the HTLC identified by
901
- * `(htlc contract, preimageHash)`.
1103
+ * The decoded lifecycle events for every queried HTLC, keyed by
1104
+ * {@link htlcQueryKey}. Batched so a whole chain scan costs one RPC request
1105
+ * regardless of how many swaps are tracked; a queried HTLC with no events maps
1106
+ * to an empty array. `fromBlock` lower-bounds the scan (default genesis) —
1107
+ * pass the swaps' creation-block estimate so providers never see an
1108
+ * unbounded log query.
902
1109
  */
903
- getHtlcEvents(htlc: `0x${string}`, preimageHash: `0x${string}`, claimAddress: `0x${string}`): Promise<EvmHtlcEvent[]>;
904
- /** The latest block's `block.timestamp`, in ms. */
905
- getBlockTimeMs(): Promise<number>;
906
- /** Fire `cb` when new blocks/logs may have changed watched HTLCs; returns unsubscribe. */
907
- watch(cb: () => void): () => void;
1110
+ getHtlcEventsBatch(queries: EvmHtlcQuery[], fromBlock?: bigint): Promise<Map<string, EvmHtlcEvent[]>>;
1111
+ /**
1112
+ * Whether each queried HTLC is currently open, keyed by {@link htlcQueryKey}.
1113
+ * The cheap routine check (`eth_call`, batched via Multicall3): `true` means
1114
+ * funded-and-unspent on exactly the queried terms; `false` is ambiguous
1115
+ * (never funded / claimed / refunded) and needs {@link getHtlcEventsBatch}
1116
+ * to classify.
1117
+ */
1118
+ isActiveBatch(queries: EvmActiveQuery[]): Promise<Map<string, boolean>>;
1119
+ /** The latest block's timestamp (ms) and number. */
1120
+ getLatestBlock(): Promise<{
1121
+ timeMs: number;
1122
+ number: bigint;
1123
+ }>;
908
1124
  };
909
1125
  type EvmContractManagerDeps = {
910
1126
  /** A chain reader per EVM `chainId` this manager serves. */
@@ -921,7 +1137,14 @@ declare class EvmContractManager implements ContractManager {
921
1137
  getState(ref: HtlcRef): HtlcObservation | undefined;
922
1138
  chainNow(ref: HtlcRef): number | undefined;
923
1139
  onEvent(cb: (ref: HtlcRef, state: HtlcObservation) => void): () => void;
1140
+ /**
1141
+ * Full reconcile of every tracked chain — seeds clocks + observations. Called
1142
+ * on demand (tracker start / newly tracked swap), NOT periodically: onward,
1143
+ * hints drive targeted `reconcile`s and the tracker's at-risk safety net
1144
+ * re-reads only swaps with client funds on the line.
1145
+ */
924
1146
  refresh(): Promise<void>;
1147
+ /** Targeted verify (hint / pre-action path) — never gated. */
925
1148
  reconcile(ref: HtlcRef): Promise<void>;
926
1149
  dispose(): void;
927
1150
  /** The preimage a claim revealed on this HTLC, if one was seen. */
@@ -935,83 +1158,58 @@ declare class EvmContractManager implements ContractManager {
935
1158
  * Kept separate from the manager so the manager itself stays free of any
936
1159
  * chain-library dependency (and unit-testable against a fake reader). This module
937
1160
  * is the only place that touches viem and the `HTLCErc20` ABI.
1161
+ *
1162
+ * All HTLCs on a chain are read with a SINGLE `eth_getLogs` call: the three
1163
+ * `HTLCErc20` lifecycle events all index `preimageHash` as topic1, so one filter
1164
+ * ORs the three event signatures on topic0 and the tracked preimage hashes on
1165
+ * topic1 across all HTLC contract addresses. Request count per scan is constant
1166
+ * in the number of tracked swaps — this matters, since public RPCs rate-limit by
1167
+ * request count.
938
1168
  */
939
1169
 
940
1170
  /**
941
1171
  * Tested public RPC endpoints per supported chainId, tried in order via viem's
942
- * ranked `fallback`. Mirrors the frontend's `evmTransport.ts` list — the client
1172
+ * `fallback`. Mirrors the frontend's `evmTransport.ts` list — the client
943
1173
  * uses these by default so Arkade↔EVM tracking works out of the box; a caller can
944
1174
  * override per chain with `ClientBuilder.withEvmRpcUrls`.
945
1175
  */
946
1176
  declare const DEFAULT_EVM_RPCS: Record<number, string[]>;
947
- declare const SWAP_CREATED: {
948
- readonly name: "SwapCreated";
949
- readonly type: "event";
950
- readonly inputs: readonly [{
951
- readonly type: "bytes32";
952
- readonly name: "preimageHash";
953
- readonly indexed: true;
954
- }, {
955
- readonly type: "address";
956
- readonly name: "refundAddress";
957
- readonly indexed: true;
958
- }, {
959
- readonly type: "address";
960
- readonly name: "claimAddress";
961
- readonly indexed: true;
962
- }, {
963
- readonly type: "address";
964
- readonly name: "token";
965
- }, {
966
- readonly type: "uint256";
967
- readonly name: "amount";
968
- }, {
969
- readonly type: "uint256";
970
- readonly name: "timelock";
971
- }];
972
- };
973
- declare const SWAP_REDEEMED: {
974
- readonly name: "SwapRedeemed";
975
- readonly type: "event";
976
- readonly inputs: readonly [{
977
- readonly type: "bytes32";
978
- readonly name: "preimageHash";
979
- readonly indexed: true;
980
- }, {
981
- readonly type: "bytes32";
982
- readonly name: "preimage";
983
- }];
984
- };
985
- declare const SWAP_REFUNDED: {
986
- readonly name: "SwapRefunded";
987
- readonly type: "event";
988
- readonly inputs: readonly [{
989
- readonly type: "bytes32";
990
- readonly name: "preimageHash";
991
- readonly indexed: true;
992
- }];
1177
+ /** The undecoded log shape `eth_getLogs` returns. */
1178
+ type RawLog = {
1179
+ address: `0x${string}`;
1180
+ topics: [`0x${string}`, ...`0x${string}`[]];
1181
+ data: `0x${string}`;
993
1182
  };
994
1183
  /** The viem surface the reader needs — a seam so it can be faked in tests. */
995
1184
  type EvmLogClient = {
996
- getLogs(args: {
997
- address: `0x${string}`;
998
- event: typeof SWAP_CREATED | typeof SWAP_REDEEMED | typeof SWAP_REFUNDED;
999
- args: {
1000
- preimageHash: `0x${string}`;
1001
- claimAddress?: `0x${string}`;
1002
- };
1003
- fromBlock: bigint | "earliest";
1004
- }): Promise<Log[]>;
1005
- getBlock(): Promise<Pick<Block, "timestamp">>;
1006
- watchBlocks(args: {
1007
- onBlock: () => void;
1008
- }): () => void;
1185
+ request(args: {
1186
+ method: "eth_getLogs";
1187
+ params: [
1188
+ {
1189
+ address: `0x${string}`[];
1190
+ topics: (`0x${string}`[] | null)[];
1191
+ fromBlock: `0x${string}`;
1192
+ }
1193
+ ];
1194
+ }): Promise<RawLog[]>;
1195
+ /** A read-only `eth_call`. */
1196
+ call(args: {
1197
+ to: `0x${string}`;
1198
+ data: `0x${string}`;
1199
+ }): Promise<{
1200
+ data?: `0x${string}`;
1201
+ }>;
1202
+ getBlock(): Promise<Pick<Block, "timestamp" | "number">>;
1009
1203
  };
1010
1204
  /** Build an {@link EvmChainReader} over an existing viem-like client. */
1011
1205
  declare function evmReaderFromClient(client: EvmLogClient): EvmChainReader;
1012
1206
  /**
1013
1207
  * Build an {@link EvmChainReader} over one or more EVM JSON-RPC endpoints. With
1014
- * several, viem's ranked `fallback` picks the healthiest and fails over.
1208
+ * several, viem's `fallback` tries them in order and fails over on error.
1209
+ * Deliberately NOT ranked: ranking health-pings every listed endpoint on an
1210
+ * interval from every open tab, which is exactly the background burst that got
1211
+ * public RPCs rate-limiting us (publicnode 403s) — and it contradicts this
1212
+ * package's near-zero-RPC design.
1015
1213
  */
1016
1214
  declare function createEvmRpcReader(rpcUrls: string | string[]): EvmChainReader;
1017
1215
  /**
@@ -1056,6 +1254,12 @@ interface WorkerHintSource {
1056
1254
  onStatus(cb: (update: {
1057
1255
  swapId: string;
1058
1256
  }) => void): () => void;
1257
+ /**
1258
+ * Optional: fires when the feed comes back up after an outage, during which
1259
+ * pushed transitions were lost for good. The worker answers by re-verifying
1260
+ * every tracked swap against the chain.
1261
+ */
1262
+ onReconnect?(cb: () => void): () => void;
1059
1263
  }
1060
1264
  /** Backoff policy for retrying a failed auto-claim. */
1061
1265
  type SwapWorkerRetryOptions = {
@@ -1072,9 +1276,10 @@ type SwapWorkerOptions = {
1072
1276
  /**
1073
1277
  * Run the recommended auto action for a swap (today: claim). Reject to signal
1074
1278
  * failure — the worker re-verifies against chain and retries with backoff.
1075
- * Resolve once submitted.
1279
+ * Resolve once submitted. OMIT for observe-only mode: hints still trigger
1280
+ * chain re-verifies (fast, cheap derivations), but nothing is ever run.
1076
1281
  */
1077
- execute: (swapId: string, actionId: SwapActionId) => Promise<void>;
1282
+ execute?: (swapId: string, actionId: SwapActionId) => Promise<void>;
1078
1283
  /**
1079
1284
  * Surface an action that needs the user rather than being auto-run — a manual
1080
1285
  * `fund`, a refund the user must confirm, or a claim whose retries are spent.
@@ -1146,6 +1351,15 @@ declare class WsStatusSource {
1146
1351
  stop(): void;
1147
1352
  /** Emit for each pushed status update. Returns an unsubscribe fn. */
1148
1353
  onStatus(cb: (update: SwapStatusUpdate) => void): () => void;
1354
+ /**
1355
+ * Emit whenever the socket comes (back) up after a disruption — a drop, or a
1356
+ * first connect that had to be retried. NOT emitted on a pristine first open:
1357
+ * hints were never live before it, so there is no gap to recover from. While
1358
+ * the socket was down, pushed transitions were lost for good (the server only
1359
+ * snapshots on subscribe), so a consumer should re-verify its world against
1360
+ * the chain on this signal.
1361
+ */
1362
+ onReconnect(cb: () => void): () => void;
1149
1363
  /**
1150
1364
  * Track these swap ids — subscribe on the socket now if open, else on next open.
1151
1365
  *
@@ -1174,4 +1388,4 @@ declare class WsStatusSource {
1174
1388
 
1175
1389
  declare function swapToTracked(stored: StoredSwap): TrackedSwap | undefined;
1176
1390
 
1177
- export { type ActionSubscriber, ArkadeContractManager, type ArkadeContractManagerDeps, type ArkadeCreateConfig, type ArkadeVhtlcInput, type BitcoinChainReader, BitcoinContractManager, type BitcoinContractManagerDeps, type BitcoinCreateConfig, type BlockedBy, type BlockedReason, type CctpClaimEvidence, type ClaimAction, Client, ClientBuilder, type ContractManager, DEFAULT_ESPLORA_URLS, DEFAULT_EVM_RPCS, type EvmChainReader, EvmContractManager, type EvmContractManagerDeps, type EvmLogClient, type FundAction, type HtlcObservation, type HtlcRef, type Ledger, type NoneAction, type RecoverCctpClaimAction, type RefundCollaborativeAction, type RefundUnilateralAction, type SwapAction, type SwapActionAutomation, type SwapActionId, type SwapActionInput, type SwapActions, type SwapObservations, type SwapStatusUpdate, SwapTracker, SwapWorker, type SwapWorkerOptions, type TrackedSwap, type WaitAction, type WebSocketLike, type WorkerHintSource, type WorkerTracker, WsStatusSource, type WsStatusSourceOptions, buildArkadeVhtlcRef, createEvmRpcReader, defaultArkadeServerUrl, defaultEvmReaders, deriveSwapActions, deriveSwapStatus, esploraReader, evmReaderFromClient, htlcFactsFromEsploraTxs, htlcKey, swapToTracked };
1391
+ export { type ActionSubscriber, ArkadeContractManager, type ArkadeContractManagerDeps, type ArkadeCreateConfig, type ArkadeVhtlcInput, type BitcoinChainReader, type BitcoinConfirmationPolicy, BitcoinContractManager, type BitcoinContractManagerDeps, type BitcoinCreateConfig, type BlockedBy, type BlockedReason, type CctpClaimEvidence, type ClaimAction, Client, ClientBuilder, type ContractManager, DEFAULT_ESPLORA_URLS, DEFAULT_EVM_RPCS, type EvmChainReader, EvmContractManager, type EvmContractManagerDeps, type EvmHtlcQuery, type EvmLogClient, type FundAction, type HtlcObservation, type HtlcRef, type Ledger, type NoneAction, type RawLog, type RecoverCctpClaimAction, type RefundCollaborativeAction, type RefundUnilateralAction, type SwapAction, type SwapActionAutomation, type SwapActionId, type SwapActionInput, type SwapActions, type SwapObservations, type SwapOutcome, type SwapStatusUpdate, SwapTracker, SwapWorker, type SwapWorkerOptions, type TrackedSwap, type WaitAction, type WaitingOn, type WebSocketLike, type WorkerHintSource, type WorkerTracker, WsStatusSource, type WsStatusSourceOptions, buildArkadeVhtlcRef, createEvmRpcReader, defaultArkadeServerUrl, defaultEvmReaders, deriveSwapActions, deriveSwapStatus, esploraReader, evmReaderFromClient, htlcFactsFromEsploraTxs, htlcKey, htlcQueryKey, swapToTracked };