@satora/swap 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
@@ -272,6 +272,15 @@ declare function htlcKey(ref: HtlcRef): string;
272
272
  interface ContractManager {
273
273
  /** The ledger this manager observes. */
274
274
  readonly ledger: Ledger;
275
+ /**
276
+ * Whether this manager can observe `ref` at all — a permanent capability check,
277
+ * not a transient health probe. `false` means the ledger/chain is not configured
278
+ * (e.g. an EVM chain with no RPC), so the ref can never be observed and the
279
+ * tracker should skip the swap rather than {@link register} it (which throws).
280
+ * Distinct from a register failure, which is a recoverable error on an
281
+ * observable ref.
282
+ */
283
+ canObserve(ref: HtlcRef): boolean;
275
284
  /** Start tracking an HTLC (idempotent); seeds its state from the ledger. */
276
285
  register(ref: HtlcRef): Promise<void>;
277
286
  /** Stop tracking an HTLC. */
@@ -292,6 +301,13 @@ interface ContractManager {
292
301
  onEvent(cb: (ref: HtlcRef, state: HtlcObservation) => void): () => void;
293
302
  /** Force a reconcile from the ledger. */
294
303
  refresh(): Promise<void>;
304
+ /**
305
+ * Re-observe a single tracked HTLC from the ledger now — a targeted refresh, so
306
+ * a faster server hint can trigger an immediate on-chain verification of just
307
+ * that swap's legs without re-scanning every tracked HTLC. A no-op if the ref
308
+ * isn't tracked.
309
+ */
310
+ reconcile(ref: HtlcRef): Promise<void>;
295
311
  /** Tear down subscriptions and background work. */
296
312
  dispose(): void;
297
313
  }
@@ -321,8 +337,36 @@ type SwapTrackerOptions = {
321
337
  declare class SwapTracker {
322
338
  #private;
323
339
  constructor(managers: Map<Ledger, ContractManager>, options?: SwapTrackerOptions);
340
+ /**
341
+ * Whether every on-chain leg of `swap` is observable — i.e. the tracker can
342
+ * derive its status from chain. `false` means a leg is on a ledger/chain this
343
+ * client can't reach (no manager, or an EVM chain with no RPC), so the swap
344
+ * can't be tracked. Pure predicate — callers decide the policy (skip vs reject).
345
+ */
346
+ canObserve(swap: TrackedSwap): boolean;
324
347
  /** Register each swap's on-chain leg(s), subscribe to manager events, and seed state. */
325
348
  startTracking(swaps: TrackedSwap[]): Promise<void>;
349
+ /**
350
+ * Track one more swap after {@link startTracking} — register its on-chain
351
+ * leg(s), seed the managers they touch, and derive its first action. Idempotent:
352
+ * a swap already tracked, or one already seen through to a terminal action, is
353
+ * ignored. New swaps are picked up by the existing per-manager event listeners
354
+ * and poll timer, so nothing else needs re-wiring. Lets a swap created after
355
+ * start (e.g. via `createSwap`) be tracked without a restart.
356
+ */
357
+ track(swap: TrackedSwap): Promise<void>;
358
+ /** The swap ids currently tracked — e.g. to subscribe them to a hint feed. */
359
+ trackedSwapIds(): string[];
360
+ /**
361
+ * Re-verify one swap against the chain right now — the hook a faster server
362
+ * status hint drives. Reconciles just that swap's legs (targeted, on the
363
+ * managers), then recomputes its action. The hint is only a *trigger*: the
364
+ * chain stays the source of truth, so a premature hint that the chain doesn't
365
+ * yet reflect changes nothing. A no-op for an untracked swap.
366
+ */
367
+ applyHint(swapId: string, opts?: {
368
+ force?: boolean;
369
+ }): Promise<void>;
326
370
  /** Notify `cb` of the current action for each tracked swap, then on every change. */
327
371
  subscribeToActions(cb: ActionSubscriber): () => void;
328
372
  /**
@@ -347,6 +391,22 @@ declare class SwapTracker {
347
391
  * individual delegators get replaced with native implementations over time.
348
392
  */
349
393
 
394
+ /**
395
+ * Hint-driven auto-execution, opt-in via {@link ClientBuilder.withAutoClaim}.
396
+ *
397
+ * When set, tracking also opens the server status WebSocket (a faster trigger
398
+ * than the chain poll) and runs a {@link SwapWorker}: it auto-claims a swap the
399
+ * moment the chain confirms it is claimable, and surfaces the rest — a manual
400
+ * `fund`, or a refund the user must confirm — via {@link onActionRequired}.
401
+ */
402
+ type AutoClaimConfig = {
403
+ /**
404
+ * Surface an action that needs the user rather than being auto-run — a manual
405
+ * `fund`, or a refund to confirm. The seam a frontend notification center
406
+ * plugs into. Omit to only auto-claim and ignore the rest.
407
+ */
408
+ onActionRequired?: (swapId: string, actions: SwapActions) => void;
409
+ };
350
410
  /** How the client should set up observe-mode tracking. */
351
411
  type TrackingConfig = {
352
412
  /** Whether tracking is enabled at all (default on; `withoutTracking()` clears). */
@@ -361,6 +421,8 @@ type TrackingConfig = {
361
421
  esploraUrl?: string;
362
422
  /** Poll interval (ms) for advancing observations/clocks; defaults to 5s. */
363
423
  refreshIntervalMs?: number;
424
+ /** Opt-in hint-driven auto-claim; unset leaves tracking observe-only. */
425
+ autoClaim?: AutoClaimConfig;
364
426
  };
365
427
  declare class Client {
366
428
  #private;
@@ -511,6 +573,10 @@ declare class Client {
511
573
  /** Delegated to the legacy client (migration checkpoint). */
512
574
  getEvmDepositorKey(...args: Parameters<Client$1["getEvmDepositorKey"]>): ReturnType<Client$1["getEvmDepositorKey"]>;
513
575
  /** Delegated to the legacy client (migration checkpoint). */
576
+ getRefundedEvmSwapContinuation(...args: Parameters<Client$1["getRefundedEvmSwapContinuation"]>): ReturnType<Client$1["getRefundedEvmSwapContinuation"]>;
577
+ /** Delegated to the legacy client (migration checkpoint). */
578
+ continueRefundedEvmSwap(...args: Parameters<Client$1["continueRefundedEvmSwap"]>): ReturnType<Client$1["continueRefundedEvmSwap"]>;
579
+ /** Delegated to the legacy client (migration checkpoint). */
514
580
  recoverGaslessFunds(...args: Parameters<Client$1["recoverGaslessFunds"]>): ReturnType<Client$1["recoverGaslessFunds"]>;
515
581
  /** Delegated to the legacy client (migration checkpoint). */
516
582
  hasReceivedVtxo(...args: Parameters<Client$1["hasReceivedVtxo"]>): ReturnType<Client$1["hasReceivedVtxo"]>;
@@ -545,6 +611,17 @@ declare class ClientBuilder {
545
611
  * defaults kept as fallbacks.
546
612
  */
547
613
  withEvmRpcUrls(urls: Record<number, string>): this;
614
+ /**
615
+ * Opt in to hint-driven auto-claim. Tracking then also subscribes to the
616
+ * server status WebSocket (a faster trigger than the chain poll) and, when the
617
+ * chain confirms a swap is claimable, claims it automatically. Actions that
618
+ * need the user — a manual `fund`, or a refund to confirm — are surfaced via
619
+ * `onActionRequired` instead of being run. Off by default: this auto-spends on
620
+ * the user's behalf, so it must be explicit.
621
+ */
622
+ withAutoClaim(options?: {
623
+ onActionRequired?: (swapId: string, actions: SwapActions) => void;
624
+ }): this;
548
625
  /** Turn observe-mode tracking off. */
549
626
  withoutTracking(): this;
550
627
  /**
@@ -633,12 +710,14 @@ declare class ArkadeContractManager implements ContractManager {
633
710
  static fromDeps(deps: ArkadeContractManagerDeps): ArkadeContractManager;
634
711
  /** Construct a manager backed by a `RestIndexerProvider` over the Ark server. */
635
712
  static create(config: ArkadeCreateConfig): Promise<ArkadeContractManager>;
713
+ canObserve(ref: HtlcRef): boolean;
636
714
  register(ref: HtlcRef): Promise<void>;
637
715
  unregister(ref: HtlcRef): Promise<void>;
638
716
  getState(ref: HtlcRef): HtlcObservation | undefined;
639
717
  chainNow(_ref: HtlcRef): number | undefined;
640
718
  onEvent(cb: (ref: HtlcRef, state: HtlcObservation) => void): () => void;
641
719
  refresh(): Promise<void>;
720
+ reconcile(ref: HtlcRef): Promise<void>;
642
721
  dispose(): void;
643
722
  /** The verified preimage recovered from a claim spend, if one was seen. */
644
723
  getPreimage(ref: HtlcRef): Uint8Array | undefined;
@@ -721,12 +800,14 @@ declare class BitcoinContractManager implements ContractManager {
721
800
  private constructor();
722
801
  static fromDeps(deps: BitcoinContractManagerDeps): BitcoinContractManager;
723
802
  static create(config: BitcoinCreateConfig): Promise<BitcoinContractManager>;
803
+ canObserve(ref: HtlcRef): boolean;
724
804
  register(ref: HtlcRef): Promise<void>;
725
805
  unregister(ref: HtlcRef): Promise<void>;
726
806
  getState(ref: HtlcRef): HtlcObservation | undefined;
727
807
  chainNow(_ref: HtlcRef): number | undefined;
728
808
  onEvent(cb: (ref: HtlcRef, state: HtlcObservation) => void): () => void;
729
809
  refresh(): Promise<void>;
810
+ reconcile(ref: HtlcRef): Promise<void>;
730
811
  dispose(): void;
731
812
  /** The preimage a claim revealed on this HTLC, if one was seen. */
732
813
  getPreimage(ref: HtlcRef): Uint8Array | undefined;
@@ -834,12 +915,14 @@ declare class EvmContractManager implements ContractManager {
834
915
  readonly ledger: Ledger;
835
916
  private constructor();
836
917
  static fromDeps(deps: EvmContractManagerDeps): EvmContractManager;
918
+ canObserve(ref: HtlcRef): boolean;
837
919
  register(ref: HtlcRef): Promise<void>;
838
920
  unregister(ref: HtlcRef): Promise<void>;
839
921
  getState(ref: HtlcRef): HtlcObservation | undefined;
840
922
  chainNow(ref: HtlcRef): number | undefined;
841
923
  onEvent(cb: (ref: HtlcRef, state: HtlcObservation) => void): () => void;
842
924
  refresh(): Promise<void>;
925
+ reconcile(ref: HtlcRef): Promise<void>;
843
926
  dispose(): void;
844
927
  /** The preimage a claim revealed on this HTLC, if one was seen. */
845
928
  getPreimage(ref: HtlcRef): `0x${string}` | undefined;
@@ -938,6 +1021,144 @@ declare function createEvmRpcReader(rpcUrls: string | string[]): EvmChainReader;
938
1021
  */
939
1022
  declare function defaultEvmReaders(overrides?: Record<number, string>): Map<number, EvmChainReader>;
940
1023
 
1024
+ /**
1025
+ * Ties the hint feed and the action model together into auto-execution.
1026
+ *
1027
+ * On a server status hint it verifies the swap against the chain
1028
+ * (`tracker.applyHint`) — the hint only makes the derivation faster, the chain
1029
+ * stays the source of truth. Then, on every derived action, it AUTO-RUNS the
1030
+ * recommended action when it is auto-safe (a claim) and SURFACES the rest — a
1031
+ * manual `fund`, or a refund the user must confirm — via `onActionRequired`, the
1032
+ * seam a frontend notification center plugs into.
1033
+ *
1034
+ * A failed auto-claim is RETRIED with backoff. Each retry first re-verifies
1035
+ * against chain (a forced `applyHint`) and only re-runs if the swap is still
1036
+ * claimable — so we never blindly re-submit a claim (which on EVM would waste gas
1037
+ * or double-submit). After the attempt budget is spent the swap is surfaced via
1038
+ * `onActionRequired`, so it is never silently stuck.
1039
+ */
1040
+
1041
+ /** The tracker surface the worker drives. */
1042
+ interface WorkerTracker {
1043
+ trackedSwapIds(): string[];
1044
+ /** Reconcile a swap's legs from chain, then recompute. `force` re-emits even if unchanged. */
1045
+ applyHint(swapId: string, opts?: {
1046
+ force?: boolean;
1047
+ }): Promise<void>;
1048
+ subscribeToActions(cb: (swapId: string, actions: SwapActions) => void): () => void;
1049
+ }
1050
+ /** The hint feed surface the worker consumes (`WsStatusSource` satisfies it). */
1051
+ interface WorkerHintSource {
1052
+ start(): void;
1053
+ stop(): void;
1054
+ subscribe(swapIds: string[]): void;
1055
+ unsubscribe(swapIds: string[]): void;
1056
+ onStatus(cb: (update: {
1057
+ swapId: string;
1058
+ }) => void): () => void;
1059
+ }
1060
+ /** Backoff policy for retrying a failed auto-claim. */
1061
+ type SwapWorkerRetryOptions = {
1062
+ /** Attempts before giving up and surfacing the swap. Default 5. */
1063
+ maxAttempts?: number;
1064
+ /** Base backoff (ms), doubled each attempt. Default 2000. */
1065
+ baseDelayMs?: number;
1066
+ /** Backoff ceiling (ms). Default 30000. */
1067
+ maxDelayMs?: number;
1068
+ };
1069
+ type SwapWorkerOptions = {
1070
+ tracker: WorkerTracker;
1071
+ hintSource: WorkerHintSource;
1072
+ /**
1073
+ * Run the recommended auto action for a swap (today: claim). Reject to signal
1074
+ * failure — the worker re-verifies against chain and retries with backoff.
1075
+ * Resolve once submitted.
1076
+ */
1077
+ execute: (swapId: string, actionId: SwapActionId) => Promise<void>;
1078
+ /**
1079
+ * Surface an action that needs the user rather than being auto-run — a manual
1080
+ * `fund`, a refund the user must confirm, or a claim whose retries are spent.
1081
+ * Where a UI notification hooks in.
1082
+ */
1083
+ onActionRequired?: (swapId: string, actions: SwapActions) => void;
1084
+ /** Auto-claim retry policy; sensible defaults otherwise. */
1085
+ retry?: SwapWorkerRetryOptions;
1086
+ };
1087
+ declare class SwapWorker {
1088
+ #private;
1089
+ constructor(options: SwapWorkerOptions);
1090
+ /** Watch tracked swaps on the hint feed, wire hints → verify, react to actions. */
1091
+ start(): void;
1092
+ /** Stop reacting, cancel pending retries, and close the hint feed. */
1093
+ stop(): void;
1094
+ }
1095
+
1096
+ /**
1097
+ * A standalone subscriber to the server's swap-status WebSocket (`GET /ws`).
1098
+ *
1099
+ * Holds a single reconnecting connection, tracks a live set of subscribed swap
1100
+ * ids, and emits `{ swapId, status }` on every status transition the server
1101
+ * pushes — including the snapshot the server sends right after a subscribe. This
1102
+ * is a *hint* feed only: it neither trusts nor acts on the status. A consumer is
1103
+ * expected to verify against the chain (the contract managers) before doing
1104
+ * anything with it.
1105
+ *
1106
+ * Protocol (see `swap/src/api/ws.rs`):
1107
+ * → {"op":"subscribe","channel":"swap_status","args":[<uuid>,...]} (≤64 ids/frame)
1108
+ * ← {"channel":"swap_status","data":{"<uuid>":"<status>",...}}
1109
+ * ← {"op":"subscribed",...} (ack) ← {"op":...,"message":...} (error)
1110
+ * A single connection carries up to 256 subscriptions; that cap is enforced
1111
+ * client-side (see {@link WsStatusSource.subscribe}) because the server silently
1112
+ * drops the overflow.
1113
+ */
1114
+
1115
+ /** A status update pushed by the server for one swap. */
1116
+ type SwapStatusUpdate = {
1117
+ swapId: string;
1118
+ status: SwapStatus;
1119
+ };
1120
+ /** The minimal WebSocket surface the source needs (the global `WebSocket` satisfies it). */
1121
+ interface WebSocketLike {
1122
+ send(data: string): void;
1123
+ close(): void;
1124
+ onopen: (() => void) | null;
1125
+ onmessage: ((event: {
1126
+ data: unknown;
1127
+ }) => void) | null;
1128
+ onclose: (() => void) | null;
1129
+ onerror: ((event: unknown) => void) | null;
1130
+ }
1131
+ type WsStatusSourceOptions = {
1132
+ /** Server base URL (http/https); the `ws(s)://…/ws` endpoint is derived from it. */
1133
+ serverUrl: string;
1134
+ /** WebSocket factory — defaults to the global `WebSocket` (browsers, Node ≥ 22). */
1135
+ webSocketFactory?: (url: string) => WebSocketLike;
1136
+ /** Reconnect backoff floor/ceiling (ms). Defaults: 1_000 / 30_000. */
1137
+ reconnectMinMs?: number;
1138
+ reconnectMaxMs?: number;
1139
+ };
1140
+ declare class WsStatusSource {
1141
+ #private;
1142
+ constructor(options: WsStatusSourceOptions);
1143
+ /** Open the connection and begin (re)subscribing. Idempotent. */
1144
+ start(): void;
1145
+ /** Close the connection and stop reconnecting. */
1146
+ stop(): void;
1147
+ /** Emit for each pushed status update. Returns an unsubscribe fn. */
1148
+ onStatus(cb: (update: SwapStatusUpdate) => void): () => void;
1149
+ /**
1150
+ * Track these swap ids — subscribe on the socket now if open, else on next open.
1151
+ *
1152
+ * Enforces the server's per-connection cap locally. The server silently stops
1153
+ * adding once it is full (it breaks out of the loop but still acks every id),
1154
+ * so sending more would leave us believing ids are subscribed that will never
1155
+ * receive an update. Overflow is queued instead and promoted as slots free.
1156
+ */
1157
+ subscribe(swapIds: string[]): void;
1158
+ /** Stop tracking these swap ids, promoting queued ones into the freed slots. */
1159
+ unsubscribe(swapIds: string[]): void;
1160
+ }
1161
+
941
1162
  /**
942
1163
  * Map a stored swap to the {@link TrackedSwap} the {@link SwapTracker} watches.
943
1164
  *
@@ -953,4 +1174,4 @@ declare function defaultEvmReaders(overrides?: Record<number, string>): Map<numb
953
1174
 
954
1175
  declare function swapToTracked(stored: StoredSwap): TrackedSwap | undefined;
955
1176
 
956
- 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, SwapTracker, type TrackedSwap, type WaitAction, buildArkadeVhtlcRef, createEvmRpcReader, defaultArkadeServerUrl, defaultEvmReaders, deriveSwapActions, deriveSwapStatus, esploraReader, evmReaderFromClient, htlcFactsFromEsploraTxs, htlcKey, swapToTracked };
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 };
package/dist/index.d.ts CHANGED
@@ -272,6 +272,15 @@ declare function htlcKey(ref: HtlcRef): string;
272
272
  interface ContractManager {
273
273
  /** The ledger this manager observes. */
274
274
  readonly ledger: Ledger;
275
+ /**
276
+ * Whether this manager can observe `ref` at all — a permanent capability check,
277
+ * not a transient health probe. `false` means the ledger/chain is not configured
278
+ * (e.g. an EVM chain with no RPC), so the ref can never be observed and the
279
+ * tracker should skip the swap rather than {@link register} it (which throws).
280
+ * Distinct from a register failure, which is a recoverable error on an
281
+ * observable ref.
282
+ */
283
+ canObserve(ref: HtlcRef): boolean;
275
284
  /** Start tracking an HTLC (idempotent); seeds its state from the ledger. */
276
285
  register(ref: HtlcRef): Promise<void>;
277
286
  /** Stop tracking an HTLC. */
@@ -292,6 +301,13 @@ interface ContractManager {
292
301
  onEvent(cb: (ref: HtlcRef, state: HtlcObservation) => void): () => void;
293
302
  /** Force a reconcile from the ledger. */
294
303
  refresh(): Promise<void>;
304
+ /**
305
+ * Re-observe a single tracked HTLC from the ledger now — a targeted refresh, so
306
+ * a faster server hint can trigger an immediate on-chain verification of just
307
+ * that swap's legs without re-scanning every tracked HTLC. A no-op if the ref
308
+ * isn't tracked.
309
+ */
310
+ reconcile(ref: HtlcRef): Promise<void>;
295
311
  /** Tear down subscriptions and background work. */
296
312
  dispose(): void;
297
313
  }
@@ -321,8 +337,36 @@ type SwapTrackerOptions = {
321
337
  declare class SwapTracker {
322
338
  #private;
323
339
  constructor(managers: Map<Ledger, ContractManager>, options?: SwapTrackerOptions);
340
+ /**
341
+ * Whether every on-chain leg of `swap` is observable — i.e. the tracker can
342
+ * derive its status from chain. `false` means a leg is on a ledger/chain this
343
+ * client can't reach (no manager, or an EVM chain with no RPC), so the swap
344
+ * can't be tracked. Pure predicate — callers decide the policy (skip vs reject).
345
+ */
346
+ canObserve(swap: TrackedSwap): boolean;
324
347
  /** Register each swap's on-chain leg(s), subscribe to manager events, and seed state. */
325
348
  startTracking(swaps: TrackedSwap[]): Promise<void>;
349
+ /**
350
+ * Track one more swap after {@link startTracking} — register its on-chain
351
+ * leg(s), seed the managers they touch, and derive its first action. Idempotent:
352
+ * a swap already tracked, or one already seen through to a terminal action, is
353
+ * ignored. New swaps are picked up by the existing per-manager event listeners
354
+ * and poll timer, so nothing else needs re-wiring. Lets a swap created after
355
+ * start (e.g. via `createSwap`) be tracked without a restart.
356
+ */
357
+ track(swap: TrackedSwap): Promise<void>;
358
+ /** The swap ids currently tracked — e.g. to subscribe them to a hint feed. */
359
+ trackedSwapIds(): string[];
360
+ /**
361
+ * Re-verify one swap against the chain right now — the hook a faster server
362
+ * status hint drives. Reconciles just that swap's legs (targeted, on the
363
+ * managers), then recomputes its action. The hint is only a *trigger*: the
364
+ * chain stays the source of truth, so a premature hint that the chain doesn't
365
+ * yet reflect changes nothing. A no-op for an untracked swap.
366
+ */
367
+ applyHint(swapId: string, opts?: {
368
+ force?: boolean;
369
+ }): Promise<void>;
326
370
  /** Notify `cb` of the current action for each tracked swap, then on every change. */
327
371
  subscribeToActions(cb: ActionSubscriber): () => void;
328
372
  /**
@@ -347,6 +391,22 @@ declare class SwapTracker {
347
391
  * individual delegators get replaced with native implementations over time.
348
392
  */
349
393
 
394
+ /**
395
+ * Hint-driven auto-execution, opt-in via {@link ClientBuilder.withAutoClaim}.
396
+ *
397
+ * When set, tracking also opens the server status WebSocket (a faster trigger
398
+ * than the chain poll) and runs a {@link SwapWorker}: it auto-claims a swap the
399
+ * moment the chain confirms it is claimable, and surfaces the rest — a manual
400
+ * `fund`, or a refund the user must confirm — via {@link onActionRequired}.
401
+ */
402
+ type AutoClaimConfig = {
403
+ /**
404
+ * Surface an action that needs the user rather than being auto-run — a manual
405
+ * `fund`, or a refund to confirm. The seam a frontend notification center
406
+ * plugs into. Omit to only auto-claim and ignore the rest.
407
+ */
408
+ onActionRequired?: (swapId: string, actions: SwapActions) => void;
409
+ };
350
410
  /** How the client should set up observe-mode tracking. */
351
411
  type TrackingConfig = {
352
412
  /** Whether tracking is enabled at all (default on; `withoutTracking()` clears). */
@@ -361,6 +421,8 @@ type TrackingConfig = {
361
421
  esploraUrl?: string;
362
422
  /** Poll interval (ms) for advancing observations/clocks; defaults to 5s. */
363
423
  refreshIntervalMs?: number;
424
+ /** Opt-in hint-driven auto-claim; unset leaves tracking observe-only. */
425
+ autoClaim?: AutoClaimConfig;
364
426
  };
365
427
  declare class Client {
366
428
  #private;
@@ -511,6 +573,10 @@ declare class Client {
511
573
  /** Delegated to the legacy client (migration checkpoint). */
512
574
  getEvmDepositorKey(...args: Parameters<Client$1["getEvmDepositorKey"]>): ReturnType<Client$1["getEvmDepositorKey"]>;
513
575
  /** Delegated to the legacy client (migration checkpoint). */
576
+ getRefundedEvmSwapContinuation(...args: Parameters<Client$1["getRefundedEvmSwapContinuation"]>): ReturnType<Client$1["getRefundedEvmSwapContinuation"]>;
577
+ /** Delegated to the legacy client (migration checkpoint). */
578
+ continueRefundedEvmSwap(...args: Parameters<Client$1["continueRefundedEvmSwap"]>): ReturnType<Client$1["continueRefundedEvmSwap"]>;
579
+ /** Delegated to the legacy client (migration checkpoint). */
514
580
  recoverGaslessFunds(...args: Parameters<Client$1["recoverGaslessFunds"]>): ReturnType<Client$1["recoverGaslessFunds"]>;
515
581
  /** Delegated to the legacy client (migration checkpoint). */
516
582
  hasReceivedVtxo(...args: Parameters<Client$1["hasReceivedVtxo"]>): ReturnType<Client$1["hasReceivedVtxo"]>;
@@ -545,6 +611,17 @@ declare class ClientBuilder {
545
611
  * defaults kept as fallbacks.
546
612
  */
547
613
  withEvmRpcUrls(urls: Record<number, string>): this;
614
+ /**
615
+ * Opt in to hint-driven auto-claim. Tracking then also subscribes to the
616
+ * server status WebSocket (a faster trigger than the chain poll) and, when the
617
+ * chain confirms a swap is claimable, claims it automatically. Actions that
618
+ * need the user — a manual `fund`, or a refund to confirm — are surfaced via
619
+ * `onActionRequired` instead of being run. Off by default: this auto-spends on
620
+ * the user's behalf, so it must be explicit.
621
+ */
622
+ withAutoClaim(options?: {
623
+ onActionRequired?: (swapId: string, actions: SwapActions) => void;
624
+ }): this;
548
625
  /** Turn observe-mode tracking off. */
549
626
  withoutTracking(): this;
550
627
  /**
@@ -633,12 +710,14 @@ declare class ArkadeContractManager implements ContractManager {
633
710
  static fromDeps(deps: ArkadeContractManagerDeps): ArkadeContractManager;
634
711
  /** Construct a manager backed by a `RestIndexerProvider` over the Ark server. */
635
712
  static create(config: ArkadeCreateConfig): Promise<ArkadeContractManager>;
713
+ canObserve(ref: HtlcRef): boolean;
636
714
  register(ref: HtlcRef): Promise<void>;
637
715
  unregister(ref: HtlcRef): Promise<void>;
638
716
  getState(ref: HtlcRef): HtlcObservation | undefined;
639
717
  chainNow(_ref: HtlcRef): number | undefined;
640
718
  onEvent(cb: (ref: HtlcRef, state: HtlcObservation) => void): () => void;
641
719
  refresh(): Promise<void>;
720
+ reconcile(ref: HtlcRef): Promise<void>;
642
721
  dispose(): void;
643
722
  /** The verified preimage recovered from a claim spend, if one was seen. */
644
723
  getPreimage(ref: HtlcRef): Uint8Array | undefined;
@@ -721,12 +800,14 @@ declare class BitcoinContractManager implements ContractManager {
721
800
  private constructor();
722
801
  static fromDeps(deps: BitcoinContractManagerDeps): BitcoinContractManager;
723
802
  static create(config: BitcoinCreateConfig): Promise<BitcoinContractManager>;
803
+ canObserve(ref: HtlcRef): boolean;
724
804
  register(ref: HtlcRef): Promise<void>;
725
805
  unregister(ref: HtlcRef): Promise<void>;
726
806
  getState(ref: HtlcRef): HtlcObservation | undefined;
727
807
  chainNow(_ref: HtlcRef): number | undefined;
728
808
  onEvent(cb: (ref: HtlcRef, state: HtlcObservation) => void): () => void;
729
809
  refresh(): Promise<void>;
810
+ reconcile(ref: HtlcRef): Promise<void>;
730
811
  dispose(): void;
731
812
  /** The preimage a claim revealed on this HTLC, if one was seen. */
732
813
  getPreimage(ref: HtlcRef): Uint8Array | undefined;
@@ -834,12 +915,14 @@ declare class EvmContractManager implements ContractManager {
834
915
  readonly ledger: Ledger;
835
916
  private constructor();
836
917
  static fromDeps(deps: EvmContractManagerDeps): EvmContractManager;
918
+ canObserve(ref: HtlcRef): boolean;
837
919
  register(ref: HtlcRef): Promise<void>;
838
920
  unregister(ref: HtlcRef): Promise<void>;
839
921
  getState(ref: HtlcRef): HtlcObservation | undefined;
840
922
  chainNow(ref: HtlcRef): number | undefined;
841
923
  onEvent(cb: (ref: HtlcRef, state: HtlcObservation) => void): () => void;
842
924
  refresh(): Promise<void>;
925
+ reconcile(ref: HtlcRef): Promise<void>;
843
926
  dispose(): void;
844
927
  /** The preimage a claim revealed on this HTLC, if one was seen. */
845
928
  getPreimage(ref: HtlcRef): `0x${string}` | undefined;
@@ -938,6 +1021,144 @@ declare function createEvmRpcReader(rpcUrls: string | string[]): EvmChainReader;
938
1021
  */
939
1022
  declare function defaultEvmReaders(overrides?: Record<number, string>): Map<number, EvmChainReader>;
940
1023
 
1024
+ /**
1025
+ * Ties the hint feed and the action model together into auto-execution.
1026
+ *
1027
+ * On a server status hint it verifies the swap against the chain
1028
+ * (`tracker.applyHint`) — the hint only makes the derivation faster, the chain
1029
+ * stays the source of truth. Then, on every derived action, it AUTO-RUNS the
1030
+ * recommended action when it is auto-safe (a claim) and SURFACES the rest — a
1031
+ * manual `fund`, or a refund the user must confirm — via `onActionRequired`, the
1032
+ * seam a frontend notification center plugs into.
1033
+ *
1034
+ * A failed auto-claim is RETRIED with backoff. Each retry first re-verifies
1035
+ * against chain (a forced `applyHint`) and only re-runs if the swap is still
1036
+ * claimable — so we never blindly re-submit a claim (which on EVM would waste gas
1037
+ * or double-submit). After the attempt budget is spent the swap is surfaced via
1038
+ * `onActionRequired`, so it is never silently stuck.
1039
+ */
1040
+
1041
+ /** The tracker surface the worker drives. */
1042
+ interface WorkerTracker {
1043
+ trackedSwapIds(): string[];
1044
+ /** Reconcile a swap's legs from chain, then recompute. `force` re-emits even if unchanged. */
1045
+ applyHint(swapId: string, opts?: {
1046
+ force?: boolean;
1047
+ }): Promise<void>;
1048
+ subscribeToActions(cb: (swapId: string, actions: SwapActions) => void): () => void;
1049
+ }
1050
+ /** The hint feed surface the worker consumes (`WsStatusSource` satisfies it). */
1051
+ interface WorkerHintSource {
1052
+ start(): void;
1053
+ stop(): void;
1054
+ subscribe(swapIds: string[]): void;
1055
+ unsubscribe(swapIds: string[]): void;
1056
+ onStatus(cb: (update: {
1057
+ swapId: string;
1058
+ }) => void): () => void;
1059
+ }
1060
+ /** Backoff policy for retrying a failed auto-claim. */
1061
+ type SwapWorkerRetryOptions = {
1062
+ /** Attempts before giving up and surfacing the swap. Default 5. */
1063
+ maxAttempts?: number;
1064
+ /** Base backoff (ms), doubled each attempt. Default 2000. */
1065
+ baseDelayMs?: number;
1066
+ /** Backoff ceiling (ms). Default 30000. */
1067
+ maxDelayMs?: number;
1068
+ };
1069
+ type SwapWorkerOptions = {
1070
+ tracker: WorkerTracker;
1071
+ hintSource: WorkerHintSource;
1072
+ /**
1073
+ * Run the recommended auto action for a swap (today: claim). Reject to signal
1074
+ * failure — the worker re-verifies against chain and retries with backoff.
1075
+ * Resolve once submitted.
1076
+ */
1077
+ execute: (swapId: string, actionId: SwapActionId) => Promise<void>;
1078
+ /**
1079
+ * Surface an action that needs the user rather than being auto-run — a manual
1080
+ * `fund`, a refund the user must confirm, or a claim whose retries are spent.
1081
+ * Where a UI notification hooks in.
1082
+ */
1083
+ onActionRequired?: (swapId: string, actions: SwapActions) => void;
1084
+ /** Auto-claim retry policy; sensible defaults otherwise. */
1085
+ retry?: SwapWorkerRetryOptions;
1086
+ };
1087
+ declare class SwapWorker {
1088
+ #private;
1089
+ constructor(options: SwapWorkerOptions);
1090
+ /** Watch tracked swaps on the hint feed, wire hints → verify, react to actions. */
1091
+ start(): void;
1092
+ /** Stop reacting, cancel pending retries, and close the hint feed. */
1093
+ stop(): void;
1094
+ }
1095
+
1096
+ /**
1097
+ * A standalone subscriber to the server's swap-status WebSocket (`GET /ws`).
1098
+ *
1099
+ * Holds a single reconnecting connection, tracks a live set of subscribed swap
1100
+ * ids, and emits `{ swapId, status }` on every status transition the server
1101
+ * pushes — including the snapshot the server sends right after a subscribe. This
1102
+ * is a *hint* feed only: it neither trusts nor acts on the status. A consumer is
1103
+ * expected to verify against the chain (the contract managers) before doing
1104
+ * anything with it.
1105
+ *
1106
+ * Protocol (see `swap/src/api/ws.rs`):
1107
+ * → {"op":"subscribe","channel":"swap_status","args":[<uuid>,...]} (≤64 ids/frame)
1108
+ * ← {"channel":"swap_status","data":{"<uuid>":"<status>",...}}
1109
+ * ← {"op":"subscribed",...} (ack) ← {"op":...,"message":...} (error)
1110
+ * A single connection carries up to 256 subscriptions; that cap is enforced
1111
+ * client-side (see {@link WsStatusSource.subscribe}) because the server silently
1112
+ * drops the overflow.
1113
+ */
1114
+
1115
+ /** A status update pushed by the server for one swap. */
1116
+ type SwapStatusUpdate = {
1117
+ swapId: string;
1118
+ status: SwapStatus;
1119
+ };
1120
+ /** The minimal WebSocket surface the source needs (the global `WebSocket` satisfies it). */
1121
+ interface WebSocketLike {
1122
+ send(data: string): void;
1123
+ close(): void;
1124
+ onopen: (() => void) | null;
1125
+ onmessage: ((event: {
1126
+ data: unknown;
1127
+ }) => void) | null;
1128
+ onclose: (() => void) | null;
1129
+ onerror: ((event: unknown) => void) | null;
1130
+ }
1131
+ type WsStatusSourceOptions = {
1132
+ /** Server base URL (http/https); the `ws(s)://…/ws` endpoint is derived from it. */
1133
+ serverUrl: string;
1134
+ /** WebSocket factory — defaults to the global `WebSocket` (browsers, Node ≥ 22). */
1135
+ webSocketFactory?: (url: string) => WebSocketLike;
1136
+ /** Reconnect backoff floor/ceiling (ms). Defaults: 1_000 / 30_000. */
1137
+ reconnectMinMs?: number;
1138
+ reconnectMaxMs?: number;
1139
+ };
1140
+ declare class WsStatusSource {
1141
+ #private;
1142
+ constructor(options: WsStatusSourceOptions);
1143
+ /** Open the connection and begin (re)subscribing. Idempotent. */
1144
+ start(): void;
1145
+ /** Close the connection and stop reconnecting. */
1146
+ stop(): void;
1147
+ /** Emit for each pushed status update. Returns an unsubscribe fn. */
1148
+ onStatus(cb: (update: SwapStatusUpdate) => void): () => void;
1149
+ /**
1150
+ * Track these swap ids — subscribe on the socket now if open, else on next open.
1151
+ *
1152
+ * Enforces the server's per-connection cap locally. The server silently stops
1153
+ * adding once it is full (it breaks out of the loop but still acks every id),
1154
+ * so sending more would leave us believing ids are subscribed that will never
1155
+ * receive an update. Overflow is queued instead and promoted as slots free.
1156
+ */
1157
+ subscribe(swapIds: string[]): void;
1158
+ /** Stop tracking these swap ids, promoting queued ones into the freed slots. */
1159
+ unsubscribe(swapIds: string[]): void;
1160
+ }
1161
+
941
1162
  /**
942
1163
  * Map a stored swap to the {@link TrackedSwap} the {@link SwapTracker} watches.
943
1164
  *
@@ -953,4 +1174,4 @@ declare function defaultEvmReaders(overrides?: Record<number, string>): Map<numb
953
1174
 
954
1175
  declare function swapToTracked(stored: StoredSwap): TrackedSwap | undefined;
955
1176
 
956
- 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, SwapTracker, type TrackedSwap, type WaitAction, buildArkadeVhtlcRef, createEvmRpcReader, defaultArkadeServerUrl, defaultEvmReaders, deriveSwapActions, deriveSwapStatus, esploraReader, evmReaderFromClient, htlcFactsFromEsploraTxs, htlcKey, swapToTracked };
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 };