@satora/swap 1.7.1 → 1.8.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
@@ -396,6 +396,81 @@ interface ContractManager {
396
396
  dispose(): void;
397
397
  }
398
398
 
399
+ /**
400
+ * A standalone subscriber to the server's swap-status WebSocket (`GET /ws`).
401
+ *
402
+ * Holds a single reconnecting connection, tracks a live set of subscribed swap
403
+ * ids, and emits `{ swapId, status }` on every status transition the server
404
+ * pushes — including the snapshot the server sends right after a subscribe. This
405
+ * is a *hint* feed only: it neither trusts nor acts on the status. A consumer is
406
+ * expected to verify against the chain (the contract managers) before doing
407
+ * anything with it.
408
+ *
409
+ * Protocol (see `swap/src/api/ws.rs`):
410
+ * → {"op":"subscribe","channel":"swap_status","args":[<uuid>,...]} (≤64 ids/frame)
411
+ * ← {"channel":"swap_status","data":{"<uuid>":"<status>",...}}
412
+ * ← {"op":"subscribed",...} (ack) ← {"op":...,"message":...} (error)
413
+ * A single connection carries up to 256 subscriptions; that cap is enforced
414
+ * client-side (see {@link WsStatusSource.subscribe}) because the server silently
415
+ * drops the overflow.
416
+ */
417
+
418
+ /** A status update pushed by the server for one swap. */
419
+ type SwapStatusUpdate = {
420
+ swapId: string;
421
+ status: SwapStatus;
422
+ };
423
+ /** The minimal WebSocket surface the source needs (the global `WebSocket` satisfies it). */
424
+ interface WebSocketLike {
425
+ send(data: string): void;
426
+ close(): void;
427
+ onopen: (() => void) | null;
428
+ onmessage: ((event: {
429
+ data: unknown;
430
+ }) => void) | null;
431
+ onclose: (() => void) | null;
432
+ onerror: ((event: unknown) => void) | null;
433
+ }
434
+ type WsStatusSourceOptions = {
435
+ /** Server base URL (http/https); the `ws(s)://…/ws` endpoint is derived from it. */
436
+ serverUrl: string;
437
+ /** WebSocket factory — defaults to the global `WebSocket` (browsers, Node ≥ 22). */
438
+ webSocketFactory?: (url: string) => WebSocketLike;
439
+ /** Reconnect backoff floor/ceiling (ms). Defaults: 1_000 / 30_000. */
440
+ reconnectMinMs?: number;
441
+ reconnectMaxMs?: number;
442
+ };
443
+ declare class WsStatusSource {
444
+ #private;
445
+ constructor(options: WsStatusSourceOptions);
446
+ /** Open the connection and begin (re)subscribing. Idempotent. */
447
+ start(): void;
448
+ /** Close the connection and stop reconnecting. */
449
+ stop(): void;
450
+ /** Emit for each pushed status update. Returns an unsubscribe fn. */
451
+ onStatus(cb: (update: SwapStatusUpdate) => void): () => void;
452
+ /**
453
+ * Emit whenever the socket comes (back) up after a disruption — a drop, or a
454
+ * first connect that had to be retried. NOT emitted on a pristine first open:
455
+ * hints were never live before it, so there is no gap to recover from. While
456
+ * the socket was down, pushed transitions were lost for good (the server only
457
+ * snapshots on subscribe), so a consumer should re-verify its world against
458
+ * the chain on this signal.
459
+ */
460
+ onReconnect(cb: () => void): () => void;
461
+ /**
462
+ * Track these swap ids — subscribe on the socket now if open, else on next open.
463
+ *
464
+ * Enforces the server's per-connection cap locally. The server silently stops
465
+ * adding once it is full (it breaks out of the loop but still acks every id),
466
+ * so sending more would leave us believing ids are subscribed that will never
467
+ * receive an update. Overflow is queued instead and promoted as slots free.
468
+ */
469
+ subscribe(swapIds: string[]): void;
470
+ /** Stop tracking these swap ids, promoting queued ones into the freed slots. */
471
+ unsubscribe(swapIds: string[]): void;
472
+ }
473
+
399
474
  /**
400
475
  * The reactive orchestration.
401
476
  *
@@ -533,6 +608,8 @@ type TrackingConfig = {
533
608
  * remain the source of truth.
534
609
  */
535
610
  serverUrl?: string;
611
+ /** WebSocket factory for the hint feed; defaults to the global `WebSocket` (testing). */
612
+ webSocketFactory?: (url: string) => WebSocketLike;
536
613
  /** Explicit per-ledger managers, bypassing auto-construction (advanced/testing). */
537
614
  managers?: Map<Ledger, ContractManager>;
538
615
  /** Ark server URL override; defaults to the mainnet server when unset. */
@@ -675,6 +752,7 @@ declare class Client {
675
752
  /** Delegated to the legacy client (migration checkpoint). */
676
753
  claim(...args: Parameters<Client$1["claim"]>): ReturnType<Client$1["claim"]>;
677
754
  /** Delegated to the legacy client (migration checkpoint). */
755
+ canSponsorClaim(...args: Parameters<Client$1["canSponsorClaim"]>): boolean;
678
756
  claimEvmWithSigner(...args: Parameters<Client$1["claimEvmWithSigner"]>): ReturnType<Client$1["claimEvmWithSigner"]>;
679
757
  /** Delegated to the legacy client (migration checkpoint). */
680
758
  claimViaGasless(...args: Parameters<Client$1["claimViaGasless"]>): ReturnType<Client$1["claimViaGasless"]>;
@@ -689,6 +767,16 @@ declare class Client {
689
767
  /** Delegated to the legacy client (migration checkpoint). */
690
768
  buildCollabRefundEvmTypedData(...args: Parameters<Client$1["buildCollabRefundEvmTypedData"]>): ReturnType<Client$1["buildCollabRefundEvmTypedData"]>;
691
769
  /** Delegated to the legacy client (migration checkpoint). */
770
+ getCollabRefundEvmSignature(...args: Parameters<Client$1["getCollabRefundEvmSignature"]>): ReturnType<Client$1["getCollabRefundEvmSignature"]>;
771
+ /** Delegated to the legacy client (migration checkpoint). */
772
+ getRehashEvmParams(...args: Parameters<Client$1["getRehashEvmParams"]>): ReturnType<Client$1["getRehashEvmParams"]>;
773
+ /** Delegated to the legacy client (migration checkpoint). */
774
+ signRehashEvm(...args: Parameters<Client$1["signRehashEvm"]>): ReturnType<Client$1["signRehashEvm"]>;
775
+ /** Delegated to the legacy client (migration checkpoint). */
776
+ retrySwapWithInvoice(...args: Parameters<Client$1["retrySwapWithInvoice"]>): ReturnType<Client$1["retrySwapWithInvoice"]>;
777
+ /** Delegated to the legacy client (migration checkpoint). */
778
+ retrySwapWithInvoiceWithSigner(...args: Parameters<Client$1["retrySwapWithInvoiceWithSigner"]>): ReturnType<Client$1["retrySwapWithInvoiceWithSigner"]>;
779
+ /** Delegated to the legacy client (migration checkpoint). */
692
780
  collabRefundEvmSwap(...args: Parameters<Client$1["collabRefundEvmSwap"]>): ReturnType<Client$1["collabRefundEvmSwap"]>;
693
781
  /** Delegated to the legacy client (migration checkpoint). */
694
782
  submitCollabRefundEvm(...args: Parameters<Client$1["submitCollabRefundEvm"]>): ReturnType<Client$1["submitCollabRefundEvm"]>;
@@ -718,7 +806,10 @@ declare class Client {
718
806
  getCoordinatorFundingCallDataPermit2(...args: Parameters<Client$1["getCoordinatorFundingCallDataPermit2"]>): ReturnType<Client$1["getCoordinatorFundingCallDataPermit2"]>;
719
807
  /** Delegated to the legacy client (migration checkpoint). */
720
808
  getPermit2FundingParamsUnsigned(...args: Parameters<Client$1["getPermit2FundingParamsUnsigned"]>): ReturnType<Client$1["getPermit2FundingParamsUnsigned"]>;
721
- /** Delegated to the legacy client (migration checkpoint). */
809
+ /**
810
+ * Fund the swap, then re-verify it against the chain so the lock shows up
811
+ * in the derived actions without waiting on a server hint.
812
+ */
722
813
  fundSwap(...args: Parameters<Client$1["fundSwap"]>): ReturnType<Client$1["fundSwap"]>;
723
814
  /** Delegated to the legacy client (migration checkpoint). */
724
815
  fundSwapFromSolana(...args: Parameters<Client$1["fundSwapFromSolana"]>): ReturnType<Client$1["fundSwapFromSolana"]>;
@@ -733,6 +824,8 @@ declare class Client {
733
824
  /** Delegated to the legacy client (migration checkpoint). */
734
825
  fundSwapGasless(...args: Parameters<Client$1["fundSwapGasless"]>): ReturnType<Client$1["fundSwapGasless"]>;
735
826
  /** Delegated to the legacy client (migration checkpoint). */
827
+ getEvmFundingExpectation(...args: Parameters<Client$1["getEvmFundingExpectation"]>): ReturnType<Client$1["getEvmFundingExpectation"]>;
828
+ /** Delegated to the legacy client (migration checkpoint). */
736
829
  getSwapDepositorKey(...args: Parameters<Client$1["getSwapDepositorKey"]>): ReturnType<Client$1["getSwapDepositorKey"]>;
737
830
  /** Delegated to the legacy client (migration checkpoint). */
738
831
  getEvmDepositorKey(...args: Parameters<Client$1["getEvmDepositorKey"]>): ReturnType<Client$1["getEvmDepositorKey"]>;
@@ -1538,7 +1631,7 @@ type SwapWorkerOptions = {
1538
1631
  * Resolve once submitted. OMIT for observe-only mode: hints still trigger
1539
1632
  * chain re-verifies (fast, cheap derivations), but nothing is ever run.
1540
1633
  */
1541
- execute?: (swapId: string, actionId: SwapActionId) => Promise<void>;
1634
+ execute?: (swapId: string, actionId: SwapActionId, actions: SwapActions) => Promise<void>;
1542
1635
  /**
1543
1636
  * Surface an action that needs the user rather than being auto-run — a manual
1544
1637
  * `fund`, a refund the user must confirm, or a claim whose retries are spent.
@@ -1553,83 +1646,16 @@ declare class SwapWorker {
1553
1646
  constructor(options: SwapWorkerOptions);
1554
1647
  /** Watch tracked swaps on the hint feed, wire hints → verify, react to actions. */
1555
1648
  start(): void;
1556
- /** Stop reacting, cancel pending retries, and close the hint feed. */
1557
- stop(): void;
1558
- }
1559
-
1560
- /**
1561
- * A standalone subscriber to the server's swap-status WebSocket (`GET /ws`).
1562
- *
1563
- * Holds a single reconnecting connection, tracks a live set of subscribed swap
1564
- * ids, and emits `{ swapId, status }` on every status transition the server
1565
- * pushes — including the snapshot the server sends right after a subscribe. This
1566
- * is a *hint* feed only: it neither trusts nor acts on the status. A consumer is
1567
- * expected to verify against the chain (the contract managers) before doing
1568
- * anything with it.
1569
- *
1570
- * Protocol (see `swap/src/api/ws.rs`):
1571
- * → {"op":"subscribe","channel":"swap_status","args":[<uuid>,...]} (≤64 ids/frame)
1572
- * ← {"channel":"swap_status","data":{"<uuid>":"<status>",...}}
1573
- * ← {"op":"subscribed",...} (ack) ← {"op":...,"message":...} (error)
1574
- * A single connection carries up to 256 subscriptions; that cap is enforced
1575
- * client-side (see {@link WsStatusSource.subscribe}) because the server silently
1576
- * drops the overflow.
1577
- */
1578
-
1579
- /** A status update pushed by the server for one swap. */
1580
- type SwapStatusUpdate = {
1581
- swapId: string;
1582
- status: SwapStatus;
1583
- };
1584
- /** The minimal WebSocket surface the source needs (the global `WebSocket` satisfies it). */
1585
- interface WebSocketLike {
1586
- send(data: string): void;
1587
- close(): void;
1588
- onopen: (() => void) | null;
1589
- onmessage: ((event: {
1590
- data: unknown;
1591
- }) => void) | null;
1592
- onclose: (() => void) | null;
1593
- onerror: ((event: unknown) => void) | null;
1594
- }
1595
- type WsStatusSourceOptions = {
1596
- /** Server base URL (http/https); the `ws(s)://…/ws` endpoint is derived from it. */
1597
- serverUrl: string;
1598
- /** WebSocket factory — defaults to the global `WebSocket` (browsers, Node ≥ 22). */
1599
- webSocketFactory?: (url: string) => WebSocketLike;
1600
- /** Reconnect backoff floor/ceiling (ms). Defaults: 1_000 / 30_000. */
1601
- reconnectMinMs?: number;
1602
- reconnectMaxMs?: number;
1603
- };
1604
- declare class WsStatusSource {
1605
- #private;
1606
- constructor(options: WsStatusSourceOptions);
1607
- /** Open the connection and begin (re)subscribing. Idempotent. */
1608
- start(): void;
1609
- /** Close the connection and stop reconnecting. */
1610
- stop(): void;
1611
- /** Emit for each pushed status update. Returns an unsubscribe fn. */
1612
- onStatus(cb: (update: SwapStatusUpdate) => void): () => void;
1613
- /**
1614
- * Emit whenever the socket comes (back) up after a disruption — a drop, or a
1615
- * first connect that had to be retried. NOT emitted on a pristine first open:
1616
- * hints were never live before it, so there is no gap to recover from. While
1617
- * the socket was down, pushed transitions were lost for good (the server only
1618
- * snapshots on subscribe), so a consumer should re-verify its world against
1619
- * the chain on this signal.
1620
- */
1621
- onReconnect(cb: () => void): () => void;
1622
1649
  /**
1623
- * Track these swap ids — subscribe on the socket now if open, else on next open.
1624
- *
1625
- * Enforces the server's per-connection cap locally. The server silently stops
1626
- * adding once it is full (it breaks out of the loop but still acks every id),
1627
- * so sending more would leave us believing ids are subscribed that will never
1628
- * receive an update. Overflow is queued instead and promoted as slots free.
1650
+ * Put swaps on the hint feed as soon as they are tracked. {@link #onActions}
1651
+ * subscribes a swap when its first action is emitted, but a swap whose leg
1652
+ * cannot be classified yet (an unfunded lock on a chain without `eth_getLogs`)
1653
+ * emits nothing, and without this it would never receive the server hint
1654
+ * that its funding landed.
1629
1655
  */
1630
1656
  subscribe(swapIds: string[]): void;
1631
- /** Stop tracking these swap ids, promoting queued ones into the freed slots. */
1632
- unsubscribe(swapIds: string[]): void;
1657
+ /** Stop reacting, cancel pending retries, and close the hint feed. */
1658
+ stop(): void;
1633
1659
  }
1634
1660
 
1635
1661
  /**
package/dist/index.d.ts CHANGED
@@ -396,6 +396,81 @@ interface ContractManager {
396
396
  dispose(): void;
397
397
  }
398
398
 
399
+ /**
400
+ * A standalone subscriber to the server's swap-status WebSocket (`GET /ws`).
401
+ *
402
+ * Holds a single reconnecting connection, tracks a live set of subscribed swap
403
+ * ids, and emits `{ swapId, status }` on every status transition the server
404
+ * pushes — including the snapshot the server sends right after a subscribe. This
405
+ * is a *hint* feed only: it neither trusts nor acts on the status. A consumer is
406
+ * expected to verify against the chain (the contract managers) before doing
407
+ * anything with it.
408
+ *
409
+ * Protocol (see `swap/src/api/ws.rs`):
410
+ * → {"op":"subscribe","channel":"swap_status","args":[<uuid>,...]} (≤64 ids/frame)
411
+ * ← {"channel":"swap_status","data":{"<uuid>":"<status>",...}}
412
+ * ← {"op":"subscribed",...} (ack) ← {"op":...,"message":...} (error)
413
+ * A single connection carries up to 256 subscriptions; that cap is enforced
414
+ * client-side (see {@link WsStatusSource.subscribe}) because the server silently
415
+ * drops the overflow.
416
+ */
417
+
418
+ /** A status update pushed by the server for one swap. */
419
+ type SwapStatusUpdate = {
420
+ swapId: string;
421
+ status: SwapStatus;
422
+ };
423
+ /** The minimal WebSocket surface the source needs (the global `WebSocket` satisfies it). */
424
+ interface WebSocketLike {
425
+ send(data: string): void;
426
+ close(): void;
427
+ onopen: (() => void) | null;
428
+ onmessage: ((event: {
429
+ data: unknown;
430
+ }) => void) | null;
431
+ onclose: (() => void) | null;
432
+ onerror: ((event: unknown) => void) | null;
433
+ }
434
+ type WsStatusSourceOptions = {
435
+ /** Server base URL (http/https); the `ws(s)://…/ws` endpoint is derived from it. */
436
+ serverUrl: string;
437
+ /** WebSocket factory — defaults to the global `WebSocket` (browsers, Node ≥ 22). */
438
+ webSocketFactory?: (url: string) => WebSocketLike;
439
+ /** Reconnect backoff floor/ceiling (ms). Defaults: 1_000 / 30_000. */
440
+ reconnectMinMs?: number;
441
+ reconnectMaxMs?: number;
442
+ };
443
+ declare class WsStatusSource {
444
+ #private;
445
+ constructor(options: WsStatusSourceOptions);
446
+ /** Open the connection and begin (re)subscribing. Idempotent. */
447
+ start(): void;
448
+ /** Close the connection and stop reconnecting. */
449
+ stop(): void;
450
+ /** Emit for each pushed status update. Returns an unsubscribe fn. */
451
+ onStatus(cb: (update: SwapStatusUpdate) => void): () => void;
452
+ /**
453
+ * Emit whenever the socket comes (back) up after a disruption — a drop, or a
454
+ * first connect that had to be retried. NOT emitted on a pristine first open:
455
+ * hints were never live before it, so there is no gap to recover from. While
456
+ * the socket was down, pushed transitions were lost for good (the server only
457
+ * snapshots on subscribe), so a consumer should re-verify its world against
458
+ * the chain on this signal.
459
+ */
460
+ onReconnect(cb: () => void): () => void;
461
+ /**
462
+ * Track these swap ids — subscribe on the socket now if open, else on next open.
463
+ *
464
+ * Enforces the server's per-connection cap locally. The server silently stops
465
+ * adding once it is full (it breaks out of the loop but still acks every id),
466
+ * so sending more would leave us believing ids are subscribed that will never
467
+ * receive an update. Overflow is queued instead and promoted as slots free.
468
+ */
469
+ subscribe(swapIds: string[]): void;
470
+ /** Stop tracking these swap ids, promoting queued ones into the freed slots. */
471
+ unsubscribe(swapIds: string[]): void;
472
+ }
473
+
399
474
  /**
400
475
  * The reactive orchestration.
401
476
  *
@@ -533,6 +608,8 @@ type TrackingConfig = {
533
608
  * remain the source of truth.
534
609
  */
535
610
  serverUrl?: string;
611
+ /** WebSocket factory for the hint feed; defaults to the global `WebSocket` (testing). */
612
+ webSocketFactory?: (url: string) => WebSocketLike;
536
613
  /** Explicit per-ledger managers, bypassing auto-construction (advanced/testing). */
537
614
  managers?: Map<Ledger, ContractManager>;
538
615
  /** Ark server URL override; defaults to the mainnet server when unset. */
@@ -675,6 +752,7 @@ declare class Client {
675
752
  /** Delegated to the legacy client (migration checkpoint). */
676
753
  claim(...args: Parameters<Client$1["claim"]>): ReturnType<Client$1["claim"]>;
677
754
  /** Delegated to the legacy client (migration checkpoint). */
755
+ canSponsorClaim(...args: Parameters<Client$1["canSponsorClaim"]>): boolean;
678
756
  claimEvmWithSigner(...args: Parameters<Client$1["claimEvmWithSigner"]>): ReturnType<Client$1["claimEvmWithSigner"]>;
679
757
  /** Delegated to the legacy client (migration checkpoint). */
680
758
  claimViaGasless(...args: Parameters<Client$1["claimViaGasless"]>): ReturnType<Client$1["claimViaGasless"]>;
@@ -689,6 +767,16 @@ declare class Client {
689
767
  /** Delegated to the legacy client (migration checkpoint). */
690
768
  buildCollabRefundEvmTypedData(...args: Parameters<Client$1["buildCollabRefundEvmTypedData"]>): ReturnType<Client$1["buildCollabRefundEvmTypedData"]>;
691
769
  /** Delegated to the legacy client (migration checkpoint). */
770
+ getCollabRefundEvmSignature(...args: Parameters<Client$1["getCollabRefundEvmSignature"]>): ReturnType<Client$1["getCollabRefundEvmSignature"]>;
771
+ /** Delegated to the legacy client (migration checkpoint). */
772
+ getRehashEvmParams(...args: Parameters<Client$1["getRehashEvmParams"]>): ReturnType<Client$1["getRehashEvmParams"]>;
773
+ /** Delegated to the legacy client (migration checkpoint). */
774
+ signRehashEvm(...args: Parameters<Client$1["signRehashEvm"]>): ReturnType<Client$1["signRehashEvm"]>;
775
+ /** Delegated to the legacy client (migration checkpoint). */
776
+ retrySwapWithInvoice(...args: Parameters<Client$1["retrySwapWithInvoice"]>): ReturnType<Client$1["retrySwapWithInvoice"]>;
777
+ /** Delegated to the legacy client (migration checkpoint). */
778
+ retrySwapWithInvoiceWithSigner(...args: Parameters<Client$1["retrySwapWithInvoiceWithSigner"]>): ReturnType<Client$1["retrySwapWithInvoiceWithSigner"]>;
779
+ /** Delegated to the legacy client (migration checkpoint). */
692
780
  collabRefundEvmSwap(...args: Parameters<Client$1["collabRefundEvmSwap"]>): ReturnType<Client$1["collabRefundEvmSwap"]>;
693
781
  /** Delegated to the legacy client (migration checkpoint). */
694
782
  submitCollabRefundEvm(...args: Parameters<Client$1["submitCollabRefundEvm"]>): ReturnType<Client$1["submitCollabRefundEvm"]>;
@@ -718,7 +806,10 @@ declare class Client {
718
806
  getCoordinatorFundingCallDataPermit2(...args: Parameters<Client$1["getCoordinatorFundingCallDataPermit2"]>): ReturnType<Client$1["getCoordinatorFundingCallDataPermit2"]>;
719
807
  /** Delegated to the legacy client (migration checkpoint). */
720
808
  getPermit2FundingParamsUnsigned(...args: Parameters<Client$1["getPermit2FundingParamsUnsigned"]>): ReturnType<Client$1["getPermit2FundingParamsUnsigned"]>;
721
- /** Delegated to the legacy client (migration checkpoint). */
809
+ /**
810
+ * Fund the swap, then re-verify it against the chain so the lock shows up
811
+ * in the derived actions without waiting on a server hint.
812
+ */
722
813
  fundSwap(...args: Parameters<Client$1["fundSwap"]>): ReturnType<Client$1["fundSwap"]>;
723
814
  /** Delegated to the legacy client (migration checkpoint). */
724
815
  fundSwapFromSolana(...args: Parameters<Client$1["fundSwapFromSolana"]>): ReturnType<Client$1["fundSwapFromSolana"]>;
@@ -733,6 +824,8 @@ declare class Client {
733
824
  /** Delegated to the legacy client (migration checkpoint). */
734
825
  fundSwapGasless(...args: Parameters<Client$1["fundSwapGasless"]>): ReturnType<Client$1["fundSwapGasless"]>;
735
826
  /** Delegated to the legacy client (migration checkpoint). */
827
+ getEvmFundingExpectation(...args: Parameters<Client$1["getEvmFundingExpectation"]>): ReturnType<Client$1["getEvmFundingExpectation"]>;
828
+ /** Delegated to the legacy client (migration checkpoint). */
736
829
  getSwapDepositorKey(...args: Parameters<Client$1["getSwapDepositorKey"]>): ReturnType<Client$1["getSwapDepositorKey"]>;
737
830
  /** Delegated to the legacy client (migration checkpoint). */
738
831
  getEvmDepositorKey(...args: Parameters<Client$1["getEvmDepositorKey"]>): ReturnType<Client$1["getEvmDepositorKey"]>;
@@ -1538,7 +1631,7 @@ type SwapWorkerOptions = {
1538
1631
  * Resolve once submitted. OMIT for observe-only mode: hints still trigger
1539
1632
  * chain re-verifies (fast, cheap derivations), but nothing is ever run.
1540
1633
  */
1541
- execute?: (swapId: string, actionId: SwapActionId) => Promise<void>;
1634
+ execute?: (swapId: string, actionId: SwapActionId, actions: SwapActions) => Promise<void>;
1542
1635
  /**
1543
1636
  * Surface an action that needs the user rather than being auto-run — a manual
1544
1637
  * `fund`, a refund the user must confirm, or a claim whose retries are spent.
@@ -1553,83 +1646,16 @@ declare class SwapWorker {
1553
1646
  constructor(options: SwapWorkerOptions);
1554
1647
  /** Watch tracked swaps on the hint feed, wire hints → verify, react to actions. */
1555
1648
  start(): void;
1556
- /** Stop reacting, cancel pending retries, and close the hint feed. */
1557
- stop(): void;
1558
- }
1559
-
1560
- /**
1561
- * A standalone subscriber to the server's swap-status WebSocket (`GET /ws`).
1562
- *
1563
- * Holds a single reconnecting connection, tracks a live set of subscribed swap
1564
- * ids, and emits `{ swapId, status }` on every status transition the server
1565
- * pushes — including the snapshot the server sends right after a subscribe. This
1566
- * is a *hint* feed only: it neither trusts nor acts on the status. A consumer is
1567
- * expected to verify against the chain (the contract managers) before doing
1568
- * anything with it.
1569
- *
1570
- * Protocol (see `swap/src/api/ws.rs`):
1571
- * → {"op":"subscribe","channel":"swap_status","args":[<uuid>,...]} (≤64 ids/frame)
1572
- * ← {"channel":"swap_status","data":{"<uuid>":"<status>",...}}
1573
- * ← {"op":"subscribed",...} (ack) ← {"op":...,"message":...} (error)
1574
- * A single connection carries up to 256 subscriptions; that cap is enforced
1575
- * client-side (see {@link WsStatusSource.subscribe}) because the server silently
1576
- * drops the overflow.
1577
- */
1578
-
1579
- /** A status update pushed by the server for one swap. */
1580
- type SwapStatusUpdate = {
1581
- swapId: string;
1582
- status: SwapStatus;
1583
- };
1584
- /** The minimal WebSocket surface the source needs (the global `WebSocket` satisfies it). */
1585
- interface WebSocketLike {
1586
- send(data: string): void;
1587
- close(): void;
1588
- onopen: (() => void) | null;
1589
- onmessage: ((event: {
1590
- data: unknown;
1591
- }) => void) | null;
1592
- onclose: (() => void) | null;
1593
- onerror: ((event: unknown) => void) | null;
1594
- }
1595
- type WsStatusSourceOptions = {
1596
- /** Server base URL (http/https); the `ws(s)://…/ws` endpoint is derived from it. */
1597
- serverUrl: string;
1598
- /** WebSocket factory — defaults to the global `WebSocket` (browsers, Node ≥ 22). */
1599
- webSocketFactory?: (url: string) => WebSocketLike;
1600
- /** Reconnect backoff floor/ceiling (ms). Defaults: 1_000 / 30_000. */
1601
- reconnectMinMs?: number;
1602
- reconnectMaxMs?: number;
1603
- };
1604
- declare class WsStatusSource {
1605
- #private;
1606
- constructor(options: WsStatusSourceOptions);
1607
- /** Open the connection and begin (re)subscribing. Idempotent. */
1608
- start(): void;
1609
- /** Close the connection and stop reconnecting. */
1610
- stop(): void;
1611
- /** Emit for each pushed status update. Returns an unsubscribe fn. */
1612
- onStatus(cb: (update: SwapStatusUpdate) => void): () => void;
1613
- /**
1614
- * Emit whenever the socket comes (back) up after a disruption — a drop, or a
1615
- * first connect that had to be retried. NOT emitted on a pristine first open:
1616
- * hints were never live before it, so there is no gap to recover from. While
1617
- * the socket was down, pushed transitions were lost for good (the server only
1618
- * snapshots on subscribe), so a consumer should re-verify its world against
1619
- * the chain on this signal.
1620
- */
1621
- onReconnect(cb: () => void): () => void;
1622
1649
  /**
1623
- * Track these swap ids — subscribe on the socket now if open, else on next open.
1624
- *
1625
- * Enforces the server's per-connection cap locally. The server silently stops
1626
- * adding once it is full (it breaks out of the loop but still acks every id),
1627
- * so sending more would leave us believing ids are subscribed that will never
1628
- * receive an update. Overflow is queued instead and promoted as slots free.
1650
+ * Put swaps on the hint feed as soon as they are tracked. {@link #onActions}
1651
+ * subscribes a swap when its first action is emitted, but a swap whose leg
1652
+ * cannot be classified yet (an unfunded lock on a chain without `eth_getLogs`)
1653
+ * emits nothing, and without this it would never receive the server hint
1654
+ * that its funding landed.
1629
1655
  */
1630
1656
  subscribe(swapIds: string[]): void;
1631
- /** Stop tracking these swap ids, promoting queued ones into the freed slots. */
1632
- unsubscribe(swapIds: string[]): void;
1657
+ /** Stop reacting, cancel pending retries, and close the hint feed. */
1658
+ stop(): void;
1633
1659
  }
1634
1660
 
1635
1661
  /**
package/dist/index.js CHANGED
@@ -419,13 +419,10 @@ async function fetchArkadeSpend(source, spentByTxid, paymentHash) {
419
419
 
420
420
  // src/contracts/arkade-manager.ts
421
421
  function isFunded(vtxo) {
422
- const { state } = vtxo.virtualStatus;
423
- return state === "preconfirmed" || state === "settled";
422
+ return !vtxo.isSpent && !vtxo.isSwept;
424
423
  }
425
424
  function spendTxid(vtxos) {
426
- const spent = vtxos.find(
427
- (v) => v.arkTxId || v.spentBy || v.isSpent || v.virtualStatus.state === "spent"
428
- );
425
+ const spent = vtxos.find((v) => v.arkTxId || v.spentBy || v.isSpent);
429
426
  return spent?.arkTxId ?? spent?.spentBy;
430
427
  }
431
428
  var ArkadeContractManager = class _ArkadeContractManager {
@@ -1478,6 +1475,17 @@ var SwapWorker = class {
1478
1475
  )
1479
1476
  );
1480
1477
  }
1478
+ /**
1479
+ * Put swaps on the hint feed as soon as they are tracked. {@link #onActions}
1480
+ * subscribes a swap when its first action is emitted, but a swap whose leg
1481
+ * cannot be classified yet (an unfunded lock on a chain without `eth_getLogs`)
1482
+ * emits nothing, and without this it would never receive the server hint
1483
+ * that its funding landed.
1484
+ */
1485
+ subscribe(swapIds) {
1486
+ if (swapIds.length === 0) return;
1487
+ this.#hintSource.subscribe(swapIds);
1488
+ }
1481
1489
  /** Stop reacting, cancel pending retries, and close the hint feed. */
1482
1490
  stop() {
1483
1491
  this.#started = false;
@@ -1529,7 +1537,7 @@ var SwapWorker = class {
1529
1537
  const actionId = actions.recommended;
1530
1538
  if (!actionId) return;
1531
1539
  this.#executing.add(swapId);
1532
- void execute(swapId, actionId).then(() => this.#cancelRetry(swapId)).catch((error) => {
1540
+ void execute(swapId, actionId, actions).then(() => this.#cancelRetry(swapId)).catch((error) => {
1533
1541
  if (needsWallet(error)) {
1534
1542
  this.#cancelRetry(swapId);
1535
1543
  this.#needsUser.add(swapId);
@@ -1947,7 +1955,7 @@ function swapToTracked(stored) {
1947
1955
  token: r.evm_htlc_kind === "native" ? NATIVE_TOKEN_ADDRESS : lockedTokenFallback(r.evm_chain_id, r.network),
1948
1956
  // The coordinator created the HTLC on the client's behalf, so it
1949
1957
  // is the lock's sender (refund address), as on evm_to_lightning.
1950
- sender: r.evm_coordinator_address,
1958
+ sender: r.evm_htlc_kind === "native" && r.evm_coordinator_address.toLowerCase() === NATIVE_TOKEN_ADDRESS ? r.client_evm_address ?? void 0 : r.evm_coordinator_address,
1951
1959
  timelockSec: r.evm_refund_locktime,
1952
1960
  createdAt: r.created_at
1953
1961
  }),
@@ -1997,7 +2005,7 @@ function swapToTracked(stored) {
1997
2005
  token: r.wbtc_address,
1998
2006
  // The coordinator created the HTLC on the client's behalf, so it
1999
2007
  // is the lock's sender (refund address).
2000
- sender: r.evm_coordinator_address,
2008
+ sender: r.evm_htlc_kind === "native" && r.evm_coordinator_address.toLowerCase() === NATIVE_TOKEN_ADDRESS ? r.client_evm_address ?? void 0 : r.evm_coordinator_address,
2001
2009
  timelockSec: r.evm_refund_locktime,
2002
2010
  createdAt: r.created_at
2003
2011
  }),
@@ -2092,7 +2100,7 @@ function swapToTracked(stored) {
2092
2100
  // the server claims the client's EVM HTLC
2093
2101
  expectedSats: r.evm_expected_sats,
2094
2102
  token: r.wbtc_address,
2095
- sender: r.evm_coordinator_address,
2103
+ sender: r.evm_htlc_kind === "native" && r.evm_coordinator_address.toLowerCase() === NATIVE_TOKEN_ADDRESS ? r.client_evm_address ?? void 0 : r.evm_coordinator_address,
2096
2104
  // the coordinator locks on the client's behalf
2097
2105
  timelockSec: r.evm_refund_locktime,
2098
2106
  createdAt: r.created_at
@@ -2714,6 +2722,9 @@ var Client = class {
2714
2722
  return this.#legacy.claim(...args);
2715
2723
  }
2716
2724
  /** Delegated to the legacy client (migration checkpoint). */
2725
+ canSponsorClaim(...args) {
2726
+ return this.#legacy.canSponsorClaim(...args);
2727
+ }
2717
2728
  claimEvmWithSigner(...args) {
2718
2729
  return this.#legacy.claimEvmWithSigner(...args);
2719
2730
  }
@@ -2742,6 +2753,26 @@ var Client = class {
2742
2753
  return this.#legacy.buildCollabRefundEvmTypedData(...args);
2743
2754
  }
2744
2755
  /** Delegated to the legacy client (migration checkpoint). */
2756
+ getCollabRefundEvmSignature(...args) {
2757
+ return this.#legacy.getCollabRefundEvmSignature(...args);
2758
+ }
2759
+ /** Delegated to the legacy client (migration checkpoint). */
2760
+ getRehashEvmParams(...args) {
2761
+ return this.#legacy.getRehashEvmParams(...args);
2762
+ }
2763
+ /** Delegated to the legacy client (migration checkpoint). */
2764
+ signRehashEvm(...args) {
2765
+ return this.#legacy.signRehashEvm(...args);
2766
+ }
2767
+ /** Delegated to the legacy client (migration checkpoint). */
2768
+ retrySwapWithInvoice(...args) {
2769
+ return this.#legacy.retrySwapWithInvoice(...args);
2770
+ }
2771
+ /** Delegated to the legacy client (migration checkpoint). */
2772
+ retrySwapWithInvoiceWithSigner(...args) {
2773
+ return this.#legacy.retrySwapWithInvoiceWithSigner(...args);
2774
+ }
2775
+ /** Delegated to the legacy client (migration checkpoint). */
2745
2776
  collabRefundEvmSwap(...args) {
2746
2777
  return this.#legacy.collabRefundEvmSwap(...args);
2747
2778
  }
@@ -2817,9 +2848,14 @@ var Client = class {
2817
2848
  getPermit2FundingParamsUnsigned(...args) {
2818
2849
  return this.#legacy.getPermit2FundingParamsUnsigned(...args);
2819
2850
  }
2820
- /** Delegated to the legacy client (migration checkpoint). */
2821
- fundSwap(...args) {
2822
- return this.#legacy.fundSwap(...args);
2851
+ /**
2852
+ * Fund the swap, then re-verify it against the chain so the lock shows up
2853
+ * in the derived actions without waiting on a server hint.
2854
+ */
2855
+ async fundSwap(...args) {
2856
+ const result = await this.#legacy.fundSwap(...args);
2857
+ this.#reverifyAfterFunding(args[0]);
2858
+ return result;
2823
2859
  }
2824
2860
  /** Delegated to the legacy client (migration checkpoint). */
2825
2861
  fundSwapFromSolana(...args) {
@@ -2846,6 +2882,10 @@ var Client = class {
2846
2882
  return this.#legacy.fundSwapGasless(...args);
2847
2883
  }
2848
2884
  /** Delegated to the legacy client (migration checkpoint). */
2885
+ getEvmFundingExpectation(...args) {
2886
+ return this.#legacy.getEvmFundingExpectation(...args);
2887
+ }
2888
+ /** Delegated to the legacy client (migration checkpoint). */
2849
2889
  getSwapDepositorKey(...args) {
2850
2890
  return this.#legacy.getSwapDepositorKey(...args);
2851
2891
  }
@@ -3046,11 +3086,37 @@ var Client = class {
3046
3086
  for (const swap of swaps) {
3047
3087
  try {
3048
3088
  await tracker.track(swap);
3089
+ if (tracker.trackedSwapIds().includes(swap.swapId))
3090
+ this.#worker?.subscribe([swap.swapId]);
3049
3091
  } catch (error) {
3050
3092
  console.warn(`Client: tracking swap ${swap.swapId} failed:`, error);
3051
3093
  }
3052
3094
  }
3053
3095
  }
3096
+ /**
3097
+ * Re-verify a swap against the chain right after this client funded it.
3098
+ * The funding is mined when `fundSwap` resolves, but nothing else tells the
3099
+ * tracker so: a server hint arrives only after the server's own monitor
3100
+ * sees the lock (on Rootstock, a 30 s poll plus confirmations), and the
3101
+ * tracker's at-risk poll does not re-read an `absent` client leg. One
3102
+ * targeted read moves the swap to `clientfunded` at once. Fire-and-forget:
3103
+ * the funding succeeded whatever the re-verify does, and a swap that is not
3104
+ * tracked is a no-op.
3105
+ *
3106
+ * Chain-verified tracking only: a hints-only tracker has no chain to read,
3107
+ * its `applyHint` re-fetches the server's stored status, which lags the
3108
+ * lock by the same server-side delay this works around.
3109
+ */
3110
+ #reverifyAfterFunding(swapId) {
3111
+ if (typeof swapId !== "string" || !(this.#tracker instanceof SwapTracker))
3112
+ return;
3113
+ void this.#tracker.applyHint(swapId).catch((error) => {
3114
+ console.warn(
3115
+ `Client: re-verify after funding ${swapId} failed (the tracker's next read catches up):`,
3116
+ error
3117
+ );
3118
+ });
3119
+ }
3054
3120
  /**
3055
3121
  * Wire the server status WebSocket into a {@link SwapWorker} whenever a server
3056
3122
  * URL is configured: pushed status transitions trigger a targeted chain
@@ -3063,7 +3129,10 @@ var Client = class {
3063
3129
  const serverUrl = this.#tracking.serverUrl ?? legacyBaseUrl(this.#legacy);
3064
3130
  if (!serverUrl) return;
3065
3131
  const autoClaim = this.#tracking.autoClaim;
3066
- const hintSource = new WsStatusSource({ serverUrl });
3132
+ const hintSource = new WsStatusSource({
3133
+ serverUrl,
3134
+ webSocketFactory: this.#tracking.webSocketFactory
3135
+ });
3067
3136
  const worker = new SwapWorker({
3068
3137
  tracker,
3069
3138
  hintSource,