@arkade-os/swap 0.0.9 → 0.0.10

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.
@@ -639,10 +639,8 @@ async function requestLightningSend(wallet, arkServerUrl, transport, params) {
639
639
  const rfqId = params.rfqId ?? newRfqId();
640
640
  const secrets = await provisionRefundKey(wallet);
641
641
  const senderPubkey = secrets.pubkey;
642
- const [info, refundAddress] = await Promise.all([
643
- new RestArkProvider(arkServerUrl).getInfo(),
644
- wallet.getAddress()
645
- ]);
642
+ const refundAddress = secrets.address;
643
+ const info = await new RestArkProvider(arkServerUrl).getInfo();
646
644
  const quote = await transport.requestQuote(
647
645
  lightningSendRequest({ rfqId, invoice: params.invoice.raw, refundAddress, senderPubkey })
648
646
  );
@@ -677,7 +675,7 @@ async function requestLightningSend(wallet, arkServerUrl, transport, params) {
677
675
  ),
678
676
  senderPubkey,
679
677
  receiverPkScript: solverHex(receiverPkScriptHex, "profile.receiver_pk_script"),
680
- refundPkScript: ArkAddress2.decode(refundAddress).pkScript
678
+ refundPkScript: secrets.pkScript
681
679
  };
682
680
  const script = lightningSendVtxoScript(treeParams);
683
681
  const address = script.address(network.hrp, serverPubkey).encode();
package/dist/index.cjs CHANGED
@@ -609,8 +609,8 @@ async function cancelOffer(wallet, arkServerUrl, offerHex, opts) {
609
609
  // script and the payout script comes from wallet.getAddress(), so the
610
610
  // client's network (which only shapes address derivation) is unused here
611
611
  });
612
- const serverKey = swapAddress ? import_sdk2.ArkAddress.decode(swapAddress).serverPubKey : client.serverKey;
613
- const { program, args, keys } = swapProgramBinding(offer, serverKey);
612
+ const operatorPubkey = swapAddress ? import_sdk2.ArkAddress.decode(swapAddress).serverPubKey : client.serverKey;
613
+ const { program, args, keys } = swapProgramBinding(offer, operatorPubkey);
614
614
  const rebuilt = new import_sdk2.arkade.ArkadeProgramScript(program, args, keys);
615
615
  if (import_base2.hex.encode(rebuilt.pkScript) !== import_base2.hex.encode(offer.swapPkScript)) {
616
616
  throw new Error(
@@ -2135,10 +2135,8 @@ async function requestLightningSend(wallet, arkServerUrl, transport, params) {
2135
2135
  const rfqId = params.rfqId ?? newRfqId();
2136
2136
  const secrets = await (0, import_sdk10.provisionRefundKey)(wallet);
2137
2137
  const senderPubkey = secrets.pubkey;
2138
- const [info, refundAddress] = await Promise.all([
2139
- new import_sdk9.RestArkProvider(arkServerUrl).getInfo(),
2140
- wallet.getAddress()
2141
- ]);
2138
+ const refundAddress = secrets.address;
2139
+ const info = await new import_sdk9.RestArkProvider(arkServerUrl).getInfo();
2142
2140
  const quote = await transport.requestQuote(
2143
2141
  lightningSendRequest({ rfqId, invoice: params.invoice.raw, refundAddress, senderPubkey })
2144
2142
  );
@@ -2173,7 +2171,7 @@ async function requestLightningSend(wallet, arkServerUrl, transport, params) {
2173
2171
  ),
2174
2172
  senderPubkey,
2175
2173
  receiverPkScript: solverHex(receiverPkScriptHex, "profile.receiver_pk_script"),
2176
- refundPkScript: import_sdk9.ArkAddress.decode(refundAddress).pkScript
2174
+ refundPkScript: secrets.pkScript
2177
2175
  };
2178
2176
  const script = lightningSendVtxoScript(treeParams);
2179
2177
  const address = script.address(network.hrp, serverPubkey).encode();
@@ -2747,6 +2745,7 @@ async function findLockupVtxos(indexer, swapPkScript) {
2747
2745
  [recoverable.vtxos ?? [], true]
2748
2746
  ]) {
2749
2747
  for (const vtxo of vtxos) {
2748
+ if (vtxo.isUnrolled) continue;
2750
2749
  const key = `${vtxo.txid}:${vtxo.vout}`;
2751
2750
  if (seen.has(key)) continue;
2752
2751
  seen.add(key);
@@ -2769,10 +2768,17 @@ async function readLockupFate(indexer, input) {
2769
2768
  const { vtxos } = await indexer.getVtxos({ scripts: [import_base11.hex.encode(input.swapPkScript)] });
2770
2769
  const all = vtxos ?? [];
2771
2770
  if (all.length === 0) return { fate: "unknown" };
2771
+ const exited = all.filter((vtxo) => vtxo.isUnrolled && !(0, import_sdk11.hasTerminalSpend)(vtxo));
2772
+ if (exited.length > 0) {
2773
+ return {
2774
+ fate: "exited",
2775
+ outpoints: exited.map((vtxo) => ({ txid: vtxo.txid, vout: vtxo.vout }))
2776
+ };
2777
+ }
2772
2778
  const spentBy = /* @__PURE__ */ new Map();
2773
2779
  let everySpendNamed = true;
2774
2780
  for (const vtxo of all) {
2775
- if (!vtxo.isSpent && !vtxo.spentBy && !vtxo.settledBy) return { fate: "open" };
2781
+ if (!(0, import_sdk11.hasTerminalSpend)(vtxo)) return { fate: "open" };
2776
2782
  if (vtxo.spentBy)
2777
2783
  spentBy.set(vtxo.spentBy, {
2778
2784
  checkpointTxid: vtxo.spentBy,
@@ -3660,12 +3666,36 @@ var RfqSwapManager = class {
3660
3666
  this.setState(swap, fate.fate === "claimed" ? "settled" : "refunded");
3661
3667
  return;
3662
3668
  }
3663
- if (swap.kind === "lightning_receive") return this.driveReceiveClaim(swap);
3669
+ if (swap.kind === "lightning_receive") {
3670
+ return fate.fate === "exited" ? this.blockExitedLockup(swap, fate) : this.driveReceiveClaim(swap);
3671
+ }
3664
3672
  if (swap.kind === "onchain_send" && swap.state !== "claimed") {
3665
3673
  if (await this.driveOnchain(swap) === "handled") return;
3666
3674
  }
3675
+ if (fate.fate === "exited") {
3676
+ const claiming = swap.state === "claimable" || swap.state === "claimed";
3677
+ if (this.config.now() < swap.refundLocktime && claiming) return;
3678
+ return this.blockExitedLockup(swap, fate);
3679
+ }
3667
3680
  await this.driveArkadeRefund(swap);
3668
3681
  }
3682
+ /**
3683
+ * The lockup was unilaterally exited: its outputs sit onchain under the
3684
+ * VHTLC script, where no offchain claim or refund can reach them.
3685
+ *
3686
+ * `needs_counterparty` rather than a terminal state, because the money still
3687
+ * needs action and the swap can still end either way — an onchain claim can
3688
+ * reveal the preimage, an onchain refund can return it — and that state is
3689
+ * documented as re-checked every pass. It must be set from HERE and not from
3690
+ * inside `driveArkadeRefund`, whose two `unblock` calls would lift it again
3691
+ * on the very next pass.
3692
+ */
3693
+ blockExitedLockup(swap, fate) {
3694
+ this.block(
3695
+ swap,
3696
+ `the lockup was unilaterally exited (${fate.outpoints.length} output(s) onchain), so no offchain spend can move it \u2014 complete the unroll and spend it onchain`
3697
+ );
3698
+ }
3669
3699
  /**
3670
3700
  * The receive leg's whole state machine: claim the solver-funded lockup
3671
3701
  * while the window is open, and recognise the shapes in which it can be
package/dist/index.d.cts CHANGED
@@ -1,9 +1,9 @@
1
1
  import { ProvisionedKey, ProvisionedClaimSecret, asset, IWallet, arkade, RestIndexerProvider, Transaction, IContractManager, VHTLC, Identity, ActivityResolver } from '@arkade-os/sdk';
2
- import { S as SwapSecretsProjection, R as RfqSwapRecord, A as AssetSwapRepository, a as AssetSwap, M as MarketsCacheEntry, b as RefundArkProvider, c as RefundIndexer, L as LockupVtxo, d as RfqSwap, e as ArkadeRefundResult, f as LockupSpendIndexer, g as RfqSwapState } from './repository-DEHLtD9l.cjs';
3
- export { h as AssetSwapStatus, i as AvailableRfqSwapManagerCallbacks, B as BTC_ASSET_ID, I as InMemoryAssetSwapRepository, j as LightningReceiveSwap, k as LightningSendSwap, l as LockupFate, m as LockupNeedsRecoveryError, n as LockupParams, o as LockupSpend, O as OnchainSendAction, p as OnchainSendSwap, P as PersistableRfqSwap, q as PreimageBlockedReason, r as PreimageNotRecoverableError, s as REFUND_MTP_LAG_SECONDS, t as RFQ_RESOLVED_STATES, u as RFQ_SWAP_RETENTION_SECONDS, v as RFQ_SWAP_TERMINAL_STATES, w as RefundOutcome, x as RfqRestoreFailure, y as RfqRestoreOptions, z as RfqRestoreResult, C as RfqSwapActionName, D as RfqSwapLockup, E as RfqSwapManager, F as RfqSwapManagerCallbacks, G as RfqSwapManagerConfig, H as RfqSwapManagerDeps, J as RfqSwapManagerEvents, K as RfqSwapOrigin, N as RfqSwapOriginRequired, Q as RfqSwapOutcome, T as RfqSwapRecordStore, U as SwapContractRegistry, V as addAssetSwap, W as awaitRfqResolution, X as createRfqSwapRecord, Y as findLockupVtxos, Z as getAssetSwaps, _ as getAssetSwapsOrThrow, $ as isRfqSwapTerminal, a0 as isRfqTerminal, a1 as nextOnchainAction, a2 as preimageForSwapRecord, a3 as pushRefundWithoutReceiver, a4 as readLockupFate, a5 as rebuildRfqSwap, a6 as refundIfUnresolved, a7 as rfqSwapOriginOf, a8 as shouldRetainRfqSwap, a9 as swapSecretsToRecord, aa as updateAssetSwap, ab as updateAssetSwapBestEffort, ac as updateRfqSwapRecord } from './repository-DEHLtD9l.cjs';
2
+ import { S as SwapSecretsProjection, R as RfqSwapRecord, A as AssetSwapRepository, a as AssetSwap, M as MarketsCacheEntry, b as RefundArkProvider, c as RefundIndexer, L as LockupVtxo, d as RfqSwap, e as ArkadeRefundResult, f as LockupSpendIndexer, g as RfqSwapState } from './repository-C8FXlmHY.cjs';
3
+ export { h as AssetSwapStatus, i as AvailableRfqSwapManagerCallbacks, B as BTC_ASSET_ID, I as InMemoryAssetSwapRepository, j as LightningReceiveSwap, k as LightningSendSwap, l as LockupFate, m as LockupNeedsRecoveryError, n as LockupParams, o as LockupSpend, O as OnchainSendAction, p as OnchainSendSwap, P as PersistableRfqSwap, q as PreimageBlockedReason, r as PreimageNotRecoverableError, s as REFUND_MTP_LAG_SECONDS, t as RFQ_RESOLVED_STATES, u as RFQ_SWAP_RETENTION_SECONDS, v as RFQ_SWAP_TERMINAL_STATES, w as RefundOutcome, x as RfqRestoreFailure, y as RfqRestoreOptions, z as RfqRestoreResult, C as RfqSwapActionName, D as RfqSwapLockup, E as RfqSwapManager, F as RfqSwapManagerCallbacks, G as RfqSwapManagerConfig, H as RfqSwapManagerDeps, J as RfqSwapManagerEvents, K as RfqSwapOrigin, N as RfqSwapOriginRequired, Q as RfqSwapOutcome, T as RfqSwapRecordStore, U as SwapContractRegistry, V as addAssetSwap, W as awaitRfqResolution, X as createRfqSwapRecord, Y as findLockupVtxos, Z as getAssetSwaps, _ as getAssetSwapsOrThrow, $ as isRfqSwapTerminal, a0 as isRfqTerminal, a1 as nextOnchainAction, a2 as preimageForSwapRecord, a3 as pushRefundWithoutReceiver, a4 as readLockupFate, a5 as rebuildRfqSwap, a6 as refundIfUnresolved, a7 as rfqSwapOriginOf, a8 as shouldRetainRfqSwap, a9 as swapSecretsToRecord, aa as updateAssetSwap, ab as updateAssetSwapBestEffort, ac as updateRfqSwapRecord } from './repository-C8FXlmHY.cjs';
4
4
  import { Network, LocalCardInput, DiscoveredMarket, Side, OfferPlan } from '@arkade-os/solver-discovery';
5
- import { O as OnchainNetwork, a as OnchainHtlc, b as OnchainHtlcParams } from './rfq-hbzhTWHT.cjs';
6
- export { A as ARKADE_ASSET, c as ARKADE_BTC, d as AddressMismatch, C as ChainSource, e as ChainUtxo, H as HtlcUtxo, I as InvoiceFacts, L as LIGHTNING_BTC, f as LIGHTNING_RECEIVE_PAIR, g as LIGHTNING_SEND_PAIR, h as LOCKTIME_THRESHOLD, i as LightningReceiveTreeParams, j as LightningSendTreeParams, M as MAX_MIN_CONFIRMATIONS, k as MIN_CLAIM_WINDOW_SECONDS, l as MIN_HEADROOM_SECONDS, m as ONCHAIN_BTC, n as ONCHAIN_CLAIM_MARGIN_SECONDS, o as ONCHAIN_DUST_SATS, p as ONCHAIN_ORDER_MARGIN_SECONDS, q as ONCHAIN_RECEIVE_PAIR, r as ONCHAIN_SECONDS_PER_BLOCK, s as ONCHAIN_SEND_PAIR, t as OnchainHtlcPhase, R as RFQ_TERMINAL_STATES, u as RelaySocket, v as RfqQuote, w as RfqRefusalReason, x as RfqStatus, y as RfqTransport, S as SOLO_REFUND_HEADROOM_SECONDS, z as SwapRefusal, B as arkadeAssetLeg, D as arkadeSwapRequest, E as assertFundable, F as assertReceivable, G as awaitOnchainFill, J as buildHtlcClaim, K as buildHtlcRefund, N as claimOnchainFill, P as classifyOnchainHtlc, Q as deriveLightningReceive, T as deriveOnchainReceive, U as deriveOnchainSend, V as extractPreimage, W as httpTransport, X as lightningReceiveRequest, Y as lightningSendRequest, Z as lightningSendVtxoScript, _ as newPreimage, $ as newRfqId, a0 as offerTermsFromQuote, a1 as onchainHtlcScript, a2 as onchainReceiveRequest, a3 as onchainSendRequest, a4 as paymentHashOf, a5 as receiveVtxoScript, a6 as relayTransport, a7 as requestLightningReceive, a8 as requestLightningSend, a9 as requestOnchainReceive, aa as requestOnchainSend, ab as rfqPair, ac as unilateralClaimDelay, ad as unilateralRefundDelay, ae as unilateralRefundWithoutReceiverDelay, af as verifyLockupAddress, ag as verifyReceiveInvoice } from './rfq-hbzhTWHT.cjs';
5
+ import { O as OnchainNetwork, a as OnchainHtlc, b as OnchainHtlcParams } from './rfq-DglvHMNC.cjs';
6
+ export { A as ARKADE_ASSET, c as ARKADE_BTC, d as AddressMismatch, C as ChainSource, e as ChainUtxo, H as HtlcUtxo, I as InvoiceFacts, L as LIGHTNING_BTC, f as LIGHTNING_RECEIVE_PAIR, g as LIGHTNING_SEND_PAIR, h as LOCKTIME_THRESHOLD, i as LightningReceiveTreeParams, j as LightningSendTreeParams, M as MAX_MIN_CONFIRMATIONS, k as MIN_CLAIM_WINDOW_SECONDS, l as MIN_HEADROOM_SECONDS, m as ONCHAIN_BTC, n as ONCHAIN_CLAIM_MARGIN_SECONDS, o as ONCHAIN_DUST_SATS, p as ONCHAIN_ORDER_MARGIN_SECONDS, q as ONCHAIN_RECEIVE_PAIR, r as ONCHAIN_SECONDS_PER_BLOCK, s as ONCHAIN_SEND_PAIR, t as OnchainHtlcPhase, R as RFQ_TERMINAL_STATES, u as RelaySocket, v as RfqQuote, w as RfqRefusalReason, x as RfqStatus, y as RfqTransport, S as SOLO_REFUND_HEADROOM_SECONDS, z as SwapRefusal, B as arkadeAssetLeg, D as arkadeSwapRequest, E as assertFundable, F as assertReceivable, G as awaitOnchainFill, J as buildHtlcClaim, K as buildHtlcRefund, N as claimOnchainFill, P as classifyOnchainHtlc, Q as deriveLightningReceive, T as deriveOnchainReceive, U as deriveOnchainSend, V as extractPreimage, W as httpTransport, X as lightningReceiveRequest, Y as lightningSendRequest, Z as lightningSendVtxoScript, _ as newPreimage, $ as newRfqId, a0 as offerTermsFromQuote, a1 as onchainHtlcScript, a2 as onchainReceiveRequest, a3 as onchainSendRequest, a4 as paymentHashOf, a5 as receiveVtxoScript, a6 as relayTransport, a7 as requestLightningReceive, a8 as requestLightningSend, a9 as requestOnchainReceive, aa as requestOnchainSend, ab as rfqPair, ac as unilateralClaimDelay, ad as unilateralRefundDelay, ae as unilateralRefundWithoutReceiverDelay, af as verifyLockupAddress, ag as verifyReceiveInvoice } from './rfq-DglvHMNC.cjs';
7
7
 
8
8
  /**
9
9
  * Which wallet key signs this leg. Stored at `profile.signer`.
package/dist/index.d.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  import { ProvisionedKey, ProvisionedClaimSecret, asset, IWallet, arkade, RestIndexerProvider, Transaction, IContractManager, VHTLC, Identity, ActivityResolver } from '@arkade-os/sdk';
2
- import { S as SwapSecretsProjection, R as RfqSwapRecord, A as AssetSwapRepository, a as AssetSwap, M as MarketsCacheEntry, b as RefundArkProvider, c as RefundIndexer, L as LockupVtxo, d as RfqSwap, e as ArkadeRefundResult, f as LockupSpendIndexer, g as RfqSwapState } from './repository-DIAr5XYk.js';
3
- export { h as AssetSwapStatus, i as AvailableRfqSwapManagerCallbacks, B as BTC_ASSET_ID, I as InMemoryAssetSwapRepository, j as LightningReceiveSwap, k as LightningSendSwap, l as LockupFate, m as LockupNeedsRecoveryError, n as LockupParams, o as LockupSpend, O as OnchainSendAction, p as OnchainSendSwap, P as PersistableRfqSwap, q as PreimageBlockedReason, r as PreimageNotRecoverableError, s as REFUND_MTP_LAG_SECONDS, t as RFQ_RESOLVED_STATES, u as RFQ_SWAP_RETENTION_SECONDS, v as RFQ_SWAP_TERMINAL_STATES, w as RefundOutcome, x as RfqRestoreFailure, y as RfqRestoreOptions, z as RfqRestoreResult, C as RfqSwapActionName, D as RfqSwapLockup, E as RfqSwapManager, F as RfqSwapManagerCallbacks, G as RfqSwapManagerConfig, H as RfqSwapManagerDeps, J as RfqSwapManagerEvents, K as RfqSwapOrigin, N as RfqSwapOriginRequired, Q as RfqSwapOutcome, T as RfqSwapRecordStore, U as SwapContractRegistry, V as addAssetSwap, W as awaitRfqResolution, X as createRfqSwapRecord, Y as findLockupVtxos, Z as getAssetSwaps, _ as getAssetSwapsOrThrow, $ as isRfqSwapTerminal, a0 as isRfqTerminal, a1 as nextOnchainAction, a2 as preimageForSwapRecord, a3 as pushRefundWithoutReceiver, a4 as readLockupFate, a5 as rebuildRfqSwap, a6 as refundIfUnresolved, a7 as rfqSwapOriginOf, a8 as shouldRetainRfqSwap, a9 as swapSecretsToRecord, aa as updateAssetSwap, ab as updateAssetSwapBestEffort, ac as updateRfqSwapRecord } from './repository-DIAr5XYk.js';
2
+ import { S as SwapSecretsProjection, R as RfqSwapRecord, A as AssetSwapRepository, a as AssetSwap, M as MarketsCacheEntry, b as RefundArkProvider, c as RefundIndexer, L as LockupVtxo, d as RfqSwap, e as ArkadeRefundResult, f as LockupSpendIndexer, g as RfqSwapState } from './repository-Cw2EvvZG.js';
3
+ export { h as AssetSwapStatus, i as AvailableRfqSwapManagerCallbacks, B as BTC_ASSET_ID, I as InMemoryAssetSwapRepository, j as LightningReceiveSwap, k as LightningSendSwap, l as LockupFate, m as LockupNeedsRecoveryError, n as LockupParams, o as LockupSpend, O as OnchainSendAction, p as OnchainSendSwap, P as PersistableRfqSwap, q as PreimageBlockedReason, r as PreimageNotRecoverableError, s as REFUND_MTP_LAG_SECONDS, t as RFQ_RESOLVED_STATES, u as RFQ_SWAP_RETENTION_SECONDS, v as RFQ_SWAP_TERMINAL_STATES, w as RefundOutcome, x as RfqRestoreFailure, y as RfqRestoreOptions, z as RfqRestoreResult, C as RfqSwapActionName, D as RfqSwapLockup, E as RfqSwapManager, F as RfqSwapManagerCallbacks, G as RfqSwapManagerConfig, H as RfqSwapManagerDeps, J as RfqSwapManagerEvents, K as RfqSwapOrigin, N as RfqSwapOriginRequired, Q as RfqSwapOutcome, T as RfqSwapRecordStore, U as SwapContractRegistry, V as addAssetSwap, W as awaitRfqResolution, X as createRfqSwapRecord, Y as findLockupVtxos, Z as getAssetSwaps, _ as getAssetSwapsOrThrow, $ as isRfqSwapTerminal, a0 as isRfqTerminal, a1 as nextOnchainAction, a2 as preimageForSwapRecord, a3 as pushRefundWithoutReceiver, a4 as readLockupFate, a5 as rebuildRfqSwap, a6 as refundIfUnresolved, a7 as rfqSwapOriginOf, a8 as shouldRetainRfqSwap, a9 as swapSecretsToRecord, aa as updateAssetSwap, ab as updateAssetSwapBestEffort, ac as updateRfqSwapRecord } from './repository-Cw2EvvZG.js';
4
4
  import { Network, LocalCardInput, DiscoveredMarket, Side, OfferPlan } from '@arkade-os/solver-discovery';
5
- import { O as OnchainNetwork, a as OnchainHtlc, b as OnchainHtlcParams } from './rfq-hbzhTWHT.js';
6
- export { A as ARKADE_ASSET, c as ARKADE_BTC, d as AddressMismatch, C as ChainSource, e as ChainUtxo, H as HtlcUtxo, I as InvoiceFacts, L as LIGHTNING_BTC, f as LIGHTNING_RECEIVE_PAIR, g as LIGHTNING_SEND_PAIR, h as LOCKTIME_THRESHOLD, i as LightningReceiveTreeParams, j as LightningSendTreeParams, M as MAX_MIN_CONFIRMATIONS, k as MIN_CLAIM_WINDOW_SECONDS, l as MIN_HEADROOM_SECONDS, m as ONCHAIN_BTC, n as ONCHAIN_CLAIM_MARGIN_SECONDS, o as ONCHAIN_DUST_SATS, p as ONCHAIN_ORDER_MARGIN_SECONDS, q as ONCHAIN_RECEIVE_PAIR, r as ONCHAIN_SECONDS_PER_BLOCK, s as ONCHAIN_SEND_PAIR, t as OnchainHtlcPhase, R as RFQ_TERMINAL_STATES, u as RelaySocket, v as RfqQuote, w as RfqRefusalReason, x as RfqStatus, y as RfqTransport, S as SOLO_REFUND_HEADROOM_SECONDS, z as SwapRefusal, B as arkadeAssetLeg, D as arkadeSwapRequest, E as assertFundable, F as assertReceivable, G as awaitOnchainFill, J as buildHtlcClaim, K as buildHtlcRefund, N as claimOnchainFill, P as classifyOnchainHtlc, Q as deriveLightningReceive, T as deriveOnchainReceive, U as deriveOnchainSend, V as extractPreimage, W as httpTransport, X as lightningReceiveRequest, Y as lightningSendRequest, Z as lightningSendVtxoScript, _ as newPreimage, $ as newRfqId, a0 as offerTermsFromQuote, a1 as onchainHtlcScript, a2 as onchainReceiveRequest, a3 as onchainSendRequest, a4 as paymentHashOf, a5 as receiveVtxoScript, a6 as relayTransport, a7 as requestLightningReceive, a8 as requestLightningSend, a9 as requestOnchainReceive, aa as requestOnchainSend, ab as rfqPair, ac as unilateralClaimDelay, ad as unilateralRefundDelay, ae as unilateralRefundWithoutReceiverDelay, af as verifyLockupAddress, ag as verifyReceiveInvoice } from './rfq-hbzhTWHT.js';
5
+ import { O as OnchainNetwork, a as OnchainHtlc, b as OnchainHtlcParams } from './rfq-DglvHMNC.js';
6
+ export { A as ARKADE_ASSET, c as ARKADE_BTC, d as AddressMismatch, C as ChainSource, e as ChainUtxo, H as HtlcUtxo, I as InvoiceFacts, L as LIGHTNING_BTC, f as LIGHTNING_RECEIVE_PAIR, g as LIGHTNING_SEND_PAIR, h as LOCKTIME_THRESHOLD, i as LightningReceiveTreeParams, j as LightningSendTreeParams, M as MAX_MIN_CONFIRMATIONS, k as MIN_CLAIM_WINDOW_SECONDS, l as MIN_HEADROOM_SECONDS, m as ONCHAIN_BTC, n as ONCHAIN_CLAIM_MARGIN_SECONDS, o as ONCHAIN_DUST_SATS, p as ONCHAIN_ORDER_MARGIN_SECONDS, q as ONCHAIN_RECEIVE_PAIR, r as ONCHAIN_SECONDS_PER_BLOCK, s as ONCHAIN_SEND_PAIR, t as OnchainHtlcPhase, R as RFQ_TERMINAL_STATES, u as RelaySocket, v as RfqQuote, w as RfqRefusalReason, x as RfqStatus, y as RfqTransport, S as SOLO_REFUND_HEADROOM_SECONDS, z as SwapRefusal, B as arkadeAssetLeg, D as arkadeSwapRequest, E as assertFundable, F as assertReceivable, G as awaitOnchainFill, J as buildHtlcClaim, K as buildHtlcRefund, N as claimOnchainFill, P as classifyOnchainHtlc, Q as deriveLightningReceive, T as deriveOnchainReceive, U as deriveOnchainSend, V as extractPreimage, W as httpTransport, X as lightningReceiveRequest, Y as lightningSendRequest, Z as lightningSendVtxoScript, _ as newPreimage, $ as newRfqId, a0 as offerTermsFromQuote, a1 as onchainHtlcScript, a2 as onchainReceiveRequest, a3 as onchainSendRequest, a4 as paymentHashOf, a5 as receiveVtxoScript, a6 as relayTransport, a7 as requestLightningReceive, a8 as requestLightningSend, a9 as requestOnchainReceive, aa as requestOnchainSend, ab as rfqPair, ac as unilateralClaimDelay, ad as unilateralRefundDelay, ae as unilateralRefundWithoutReceiverDelay, af as verifyLockupAddress, ag as verifyReceiveInvoice } from './rfq-DglvHMNC.js';
7
7
 
8
8
  /**
9
9
  * Which wallet key signs this leg. Stored at `profile.signer`.
package/dist/index.js CHANGED
@@ -63,7 +63,7 @@ import {
63
63
  unilateralRefundWithoutReceiverDelay,
64
64
  verifyLockupAddress,
65
65
  verifyReceiveInvoice
66
- } from "./chunk-2FEMUOIH.js";
66
+ } from "./chunk-5NPYNQ5V.js";
67
67
  import {
68
68
  InMemoryAssetSwapRepository,
69
69
  marketsCacheKey
@@ -527,8 +527,8 @@ async function cancelOffer(wallet, arkServerUrl, offerHex, opts) {
527
527
  // script and the payout script comes from wallet.getAddress(), so the
528
528
  // client's network (which only shapes address derivation) is unused here
529
529
  });
530
- const serverKey = swapAddress ? ArkAddress.decode(swapAddress).serverPubKey : client.serverKey;
531
- const { program, args, keys } = swapProgramBinding(offer, serverKey);
530
+ const operatorPubkey = swapAddress ? ArkAddress.decode(swapAddress).serverPubKey : client.serverKey;
531
+ const { program, args, keys } = swapProgramBinding(offer, operatorPubkey);
532
532
  const rebuilt = new arkade.ArkadeProgramScript(program, args, keys);
533
533
  if (hex2.encode(rebuilt.pkScript) !== hex2.encode(offer.swapPkScript)) {
534
534
  throw new Error(
@@ -1412,6 +1412,7 @@ import {
1412
1412
  assertSubmittedArkTxid,
1413
1413
  buildOffchainTx,
1414
1414
  getArkPsbtFields,
1415
+ hasTerminalSpend,
1415
1416
  matchServerCheckpoints
1416
1417
  } from "@arkade-os/sdk";
1417
1418
  var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
@@ -1472,6 +1473,7 @@ async function findLockupVtxos(indexer, swapPkScript) {
1472
1473
  [recoverable.vtxos ?? [], true]
1473
1474
  ]) {
1474
1475
  for (const vtxo of vtxos) {
1476
+ if (vtxo.isUnrolled) continue;
1475
1477
  const key = `${vtxo.txid}:${vtxo.vout}`;
1476
1478
  if (seen.has(key)) continue;
1477
1479
  seen.add(key);
@@ -1494,10 +1496,17 @@ async function readLockupFate(indexer, input) {
1494
1496
  const { vtxos } = await indexer.getVtxos({ scripts: [hex7.encode(input.swapPkScript)] });
1495
1497
  const all = vtxos ?? [];
1496
1498
  if (all.length === 0) return { fate: "unknown" };
1499
+ const exited = all.filter((vtxo) => vtxo.isUnrolled && !hasTerminalSpend(vtxo));
1500
+ if (exited.length > 0) {
1501
+ return {
1502
+ fate: "exited",
1503
+ outpoints: exited.map((vtxo) => ({ txid: vtxo.txid, vout: vtxo.vout }))
1504
+ };
1505
+ }
1497
1506
  const spentBy = /* @__PURE__ */ new Map();
1498
1507
  let everySpendNamed = true;
1499
1508
  for (const vtxo of all) {
1500
- if (!vtxo.isSpent && !vtxo.spentBy && !vtxo.settledBy) return { fate: "open" };
1509
+ if (!hasTerminalSpend(vtxo)) return { fate: "open" };
1501
1510
  if (vtxo.spentBy)
1502
1511
  spentBy.set(vtxo.spentBy, {
1503
1512
  checkpointTxid: vtxo.spentBy,
@@ -2389,12 +2398,36 @@ var RfqSwapManager = class {
2389
2398
  this.setState(swap, fate.fate === "claimed" ? "settled" : "refunded");
2390
2399
  return;
2391
2400
  }
2392
- if (swap.kind === "lightning_receive") return this.driveReceiveClaim(swap);
2401
+ if (swap.kind === "lightning_receive") {
2402
+ return fate.fate === "exited" ? this.blockExitedLockup(swap, fate) : this.driveReceiveClaim(swap);
2403
+ }
2393
2404
  if (swap.kind === "onchain_send" && swap.state !== "claimed") {
2394
2405
  if (await this.driveOnchain(swap) === "handled") return;
2395
2406
  }
2407
+ if (fate.fate === "exited") {
2408
+ const claiming = swap.state === "claimable" || swap.state === "claimed";
2409
+ if (this.config.now() < swap.refundLocktime && claiming) return;
2410
+ return this.blockExitedLockup(swap, fate);
2411
+ }
2396
2412
  await this.driveArkadeRefund(swap);
2397
2413
  }
2414
+ /**
2415
+ * The lockup was unilaterally exited: its outputs sit onchain under the
2416
+ * VHTLC script, where no offchain claim or refund can reach them.
2417
+ *
2418
+ * `needs_counterparty` rather than a terminal state, because the money still
2419
+ * needs action and the swap can still end either way — an onchain claim can
2420
+ * reveal the preimage, an onchain refund can return it — and that state is
2421
+ * documented as re-checked every pass. It must be set from HERE and not from
2422
+ * inside `driveArkadeRefund`, whose two `unblock` calls would lift it again
2423
+ * on the very next pass.
2424
+ */
2425
+ blockExitedLockup(swap, fate) {
2426
+ this.block(
2427
+ swap,
2428
+ `the lockup was unilaterally exited (${fate.outpoints.length} output(s) onchain), so no offchain spend can move it \u2014 complete the unroll and spend it onchain`
2429
+ );
2430
+ }
2398
2431
  /**
2399
2432
  * The receive leg's whole state machine: claim the solver-funded lockup
2400
2433
  * while the window is open, and recognise the shapes in which it can be
package/dist/nostr.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { y as RfqTransport } from './rfq-hbzhTWHT.cjs';
1
+ import { y as RfqTransport } from './rfq-DglvHMNC.cjs';
2
2
  import { SimplePool } from 'nostr-tools';
3
3
  import '@arkade-os/sdk';
4
4
 
package/dist/nostr.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { y as RfqTransport } from './rfq-hbzhTWHT.js';
1
+ import { y as RfqTransport } from './rfq-DglvHMNC.js';
2
2
  import { SimplePool } from 'nostr-tools';
3
3
  import '@arkade-os/sdk';
4
4
 
package/dist/nostr.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  expectQuote,
3
3
  pairOf
4
- } from "./chunk-2FEMUOIH.js";
4
+ } from "./chunk-5NPYNQ5V.js";
5
5
 
6
6
  // src/nostr.ts
7
7
  import {
@@ -1,8 +1,8 @@
1
1
  import { RealmLike } from '@arkade-os/sdk/repositories/realm';
2
- import { A as AssetSwapRepository, a as AssetSwap, R as RfqSwapRecord, M as MarketsCacheEntry } from '../../repository-DEHLtD9l.cjs';
2
+ import { A as AssetSwapRepository, a as AssetSwap, R as RfqSwapRecord, M as MarketsCacheEntry } from '../../repository-C8FXlmHY.cjs';
3
3
  import '@arkade-os/solver-discovery';
4
4
  import '@arkade-os/sdk';
5
- import '../../rfq-hbzhTWHT.cjs';
5
+ import '../../rfq-DglvHMNC.cjs';
6
6
 
7
7
  /**
8
8
  * Realm backend for React Native.
@@ -1,8 +1,8 @@
1
1
  import { RealmLike } from '@arkade-os/sdk/repositories/realm';
2
- import { A as AssetSwapRepository, a as AssetSwap, R as RfqSwapRecord, M as MarketsCacheEntry } from '../../repository-DIAr5XYk.js';
2
+ import { A as AssetSwapRepository, a as AssetSwap, R as RfqSwapRecord, M as MarketsCacheEntry } from '../../repository-Cw2EvvZG.js';
3
3
  import '@arkade-os/solver-discovery';
4
4
  import '@arkade-os/sdk';
5
- import '../../rfq-hbzhTWHT.js';
5
+ import '../../rfq-DglvHMNC.js';
6
6
 
7
7
  /**
8
8
  * Realm backend for React Native.
@@ -1,8 +1,8 @@
1
1
  import { SQLExecutor } from '@arkade-os/sdk/repositories/sqlite';
2
- import { A as AssetSwapRepository, a as AssetSwap, R as RfqSwapRecord, M as MarketsCacheEntry } from '../../repository-DEHLtD9l.cjs';
2
+ import { A as AssetSwapRepository, a as AssetSwap, R as RfqSwapRecord, M as MarketsCacheEntry } from '../../repository-C8FXlmHY.cjs';
3
3
  import '@arkade-os/solver-discovery';
4
4
  import '@arkade-os/sdk';
5
- import '../../rfq-hbzhTWHT.cjs';
5
+ import '../../rfq-DglvHMNC.cjs';
6
6
 
7
7
  /**
8
8
  * SQLite backend over the SDK's `SQLExecutor`, so any driver plugs in
@@ -1,8 +1,8 @@
1
1
  import { SQLExecutor } from '@arkade-os/sdk/repositories/sqlite';
2
- import { A as AssetSwapRepository, a as AssetSwap, R as RfqSwapRecord, M as MarketsCacheEntry } from '../../repository-DIAr5XYk.js';
2
+ import { A as AssetSwapRepository, a as AssetSwap, R as RfqSwapRecord, M as MarketsCacheEntry } from '../../repository-Cw2EvvZG.js';
3
3
  import '@arkade-os/solver-discovery';
4
4
  import '@arkade-os/sdk';
5
- import '../../rfq-hbzhTWHT.js';
5
+ import '../../rfq-DglvHMNC.js';
6
6
 
7
7
  /**
8
8
  * SQLite backend over the SDK's `SQLExecutor`, so any driver plugs in
@@ -1,6 +1,6 @@
1
1
  import { DiscoveredMarket } from '@arkade-os/solver-discovery';
2
2
  import { IWallet, ProvisionedKey, ProvisionedClaimSecret, RestArkProvider, RestIndexerProvider, VHTLC, Identity, IContractManager } from '@arkade-os/sdk';
3
- import { x as RfqStatus, y as RfqTransport, a as OnchainHtlc, e as ChainUtxo, C as ChainSource, t as OnchainHtlcPhase } from './rfq-hbzhTWHT.js';
3
+ import { x as RfqStatus, y as RfqTransport, a as OnchainHtlc, e as ChainUtxo, C as ChainSource, t as OnchainHtlcPhase } from './rfq-DglvHMNC.cjs';
4
4
 
5
5
  type AssetSwapStatus = "pending" | "cancelling" | "fulfilled" | "cancelled" | "recoverable" | "awaiting_fill" | "claimable" | "claimed" | "refunded_l1";
6
6
  /** The sentinel asset id for BTC itself, as opposed to a 68-hex asset id.
@@ -227,12 +227,15 @@ interface LockupVtxo {
227
227
  * It is still the trader's money and it is still visible, which is why
228
228
  * {@link findLockupVtxos} returns it. What it is not is refundable by
229
229
  * {@link pushRefundWithoutReceiver}: that builds an offchain Ark
230
- * transaction, and the SDK's own predicates make the two states mutually
231
- * exclusive — `canSpendOffchain` is false exactly when `canRecoverOnchain`
232
- * is true (`wallet/vtxo.ts`), and the latter is documented as "must be
233
- * recovered into a fresh batch rather than spent offchain". Holding the
234
- * trader's `sender` key does not change that; a sweep removes the leaf from
235
- * the live tree, not the signature from the trader.
230
+ * transaction, and the SDK's own predicates put the two states on opposite
231
+ * sides — `canSpendOffchain` is false whenever `canRecoverOnchain` is true
232
+ * (`wallet/vtxo.ts`), and the latter is documented as "must be recovered
233
+ * into a fresh batch rather than spent offchain". Holding the trader's
234
+ * `sender` key does not change that; a sweep removes the leaf from the live
235
+ * tree, not the signature from the trader.
236
+ *
237
+ * Opposite, but not exhaustive: an unrolled output satisfies neither, and
238
+ * {@link findLockupVtxos} drops those before they reach this type at all.
236
239
  *
237
240
  * `packages/boltz-swap` splits on exactly this fact rather than working
238
241
  * around it: `settleRefundWithoutReceiver` sends a live VTXO through an
@@ -321,6 +324,18 @@ declare class LockupNeedsRecoveryError extends Error {
321
324
  * That function refuses the recoverable ones by name rather than submitting a
322
325
  * spend the server must reject.
323
326
  *
327
+ * **Unrolled outputs are the one exception, and are dropped.** A unilaterally
328
+ * exited output lives onchain behind its CSV; no offchain spend of any leaf can
329
+ * reach it, and `LockupVtxo` carries no field to say so, so a caller could not
330
+ * tell it apart from a live one. Whether arkd returns such an output under
331
+ * `spendableOnly` is not determinable from here, so the exclusion is made
332
+ * defensively rather than assumed. It costs the two waiting callers nothing
333
+ * they wanted: `awaitLockupFunding` keeps waiting for a claimable lockup
334
+ * instead of publishing `P` into a spend that cannot land, and
335
+ * `refundIfUnresolved` reports `nothing_to_refund` instead of grinding a doomed
336
+ * push to its deadline. The manager reads the exit through
337
+ * {@link readLockupFate}, which queries unfiltered and reports it as `exited`.
338
+ *
324
339
  * This read — not the RFQ's reported state — is the authority on whether
325
340
  * there is anything left at the lockup.
326
341
  *
@@ -374,6 +389,29 @@ type LockupFate =
374
389
  fate: "returned";
375
390
  spends: readonly LockupSpend[];
376
391
  }
392
+ /**
393
+ * At least one output was unilaterally exited: it sits onchain under the
394
+ * VHTLC script, where no offchain claim or refund can reach it.
395
+ *
396
+ * Not terminal, and not a loss. The money is still under the same script
397
+ * with the same leaves, so `completeUnroll` plus an onchain spend can still
398
+ * end the swap either way — which is why this outranks `open`: an output
399
+ * that is "still unspent" but unreachable is not a swap that is merely
400
+ * running.
401
+ *
402
+ * It outranks a verdict too, on a lockup where a sibling output was claimed
403
+ * or returned. That is the rule `open` already sets, not a new one: the
404
+ * unspent test short-circuits before any witness is read, so a partially
405
+ * resolved lockup has never reported `claimed`/`returned`. What changes is
406
+ * only that such a lockup now says why it is unresolved.
407
+ */
408
+ | {
409
+ fate: "exited";
410
+ outpoints: readonly {
411
+ txid: string;
412
+ vout: number;
413
+ }[];
414
+ }
377
415
  /** Nothing was learned: no outputs visible, an output spent by nothing the
378
416
  * indexer names, a spend it could not produce, or a blob that would not
379
417
  * decode. Never an answer. */
@@ -409,6 +447,11 @@ type LockupFate =
409
447
  * response to `unknown` is the same as to `open`: keep watching, and let the
410
448
  * refund timelock — which no outage can move — be what ends the wait.
411
449
  *
450
+ * **An exit is read before anything else, and over the whole set.** It is the
451
+ * one fact that makes an unspent output unreachable, so it outranks `open`; and
452
+ * it is scanned across every output rather than in outpoint order, so which
453
+ * output happens to come first cannot change the answer.
454
+ *
412
455
  * Ask-the-indexer, don't-trust-local-state: read fresh on every poll, never
413
456
  * cached, the same posture {@link findLockupVtxos} already establishes.
414
457
  */
@@ -447,9 +490,9 @@ declare function readLockupFate(indexer: LockupSpendIndexer, input: {
447
490
  * {@link refundIfUnresolved}, which retries.
448
491
  *
449
492
  * **Swept outputs are refused, not attempted.** This is an OFFCHAIN spend, and
450
- * a swept output is no longer a live leaf: `canSpendOffchain` and
451
- * `canRecoverOnchain` are mutually exclusive by construction, so a recoverable
452
- * input cannot be spent this way whatever key signs it (see
493
+ * a swept output is no longer a live leaf: `canSpendOffchain` is false wherever
494
+ * `canRecoverOnchain` is true, so a recoverable input cannot be spent this way
495
+ * whatever key signs it (see
453
496
  * {@link LockupVtxo.recoverable}). Because every input lands in ONE aggregate
454
497
  * transaction, a single swept output would take the live ones down with it —
455
498
  * so the whole push is refused with {@link LockupNeedsRecoveryError} naming the
@@ -1162,7 +1205,9 @@ interface RfqSwapManagerDeps {
1162
1205
  * `settled`; a lockup fully spent by anything else ends it `refunded`.
1163
1206
  * Anything the indexer could not answer is `unknown`, which is NOT an
1164
1207
  * answer: the pass carries on to the steps below, whose deadlines an indexer
1165
- * outage has no bearing on.
1208
+ * outage has no bearing on. `exited` — an output unilaterally taken onchain
1209
+ * — ends neither the swap nor the pass: it blocks the Arkade half below,
1210
+ * with the L1 half left running.
1166
1211
  * 2. **Drive the trader's claim.** On an onchain send that is the L1 fill — see
1167
1212
  * {@link nextOnchainAction}. On a receive it is the lockup itself, and it
1168
1213
  * ends the pass: that leg has no step 3.
@@ -1493,6 +1538,18 @@ declare class RfqSwapManager {
1493
1538
  private arm;
1494
1539
  private pollSwap;
1495
1540
  private runPass;
1541
+ /**
1542
+ * The lockup was unilaterally exited: its outputs sit onchain under the
1543
+ * VHTLC script, where no offchain claim or refund can reach them.
1544
+ *
1545
+ * `needs_counterparty` rather than a terminal state, because the money still
1546
+ * needs action and the swap can still end either way — an onchain claim can
1547
+ * reveal the preimage, an onchain refund can return it — and that state is
1548
+ * documented as re-checked every pass. It must be set from HERE and not from
1549
+ * inside `driveArkadeRefund`, whose two `unblock` calls would lift it again
1550
+ * on the very next pass.
1551
+ */
1552
+ private blockExitedLockup;
1496
1553
  /**
1497
1554
  * The receive leg's whole state machine: claim the solver-funded lockup
1498
1555
  * while the window is open, and recognise the shapes in which it can be
@@ -1,6 +1,6 @@
1
1
  import { DiscoveredMarket } from '@arkade-os/solver-discovery';
2
2
  import { IWallet, ProvisionedKey, ProvisionedClaimSecret, RestArkProvider, RestIndexerProvider, VHTLC, Identity, IContractManager } from '@arkade-os/sdk';
3
- import { x as RfqStatus, y as RfqTransport, a as OnchainHtlc, e as ChainUtxo, C as ChainSource, t as OnchainHtlcPhase } from './rfq-hbzhTWHT.cjs';
3
+ import { x as RfqStatus, y as RfqTransport, a as OnchainHtlc, e as ChainUtxo, C as ChainSource, t as OnchainHtlcPhase } from './rfq-DglvHMNC.js';
4
4
 
5
5
  type AssetSwapStatus = "pending" | "cancelling" | "fulfilled" | "cancelled" | "recoverable" | "awaiting_fill" | "claimable" | "claimed" | "refunded_l1";
6
6
  /** The sentinel asset id for BTC itself, as opposed to a 68-hex asset id.
@@ -227,12 +227,15 @@ interface LockupVtxo {
227
227
  * It is still the trader's money and it is still visible, which is why
228
228
  * {@link findLockupVtxos} returns it. What it is not is refundable by
229
229
  * {@link pushRefundWithoutReceiver}: that builds an offchain Ark
230
- * transaction, and the SDK's own predicates make the two states mutually
231
- * exclusive — `canSpendOffchain` is false exactly when `canRecoverOnchain`
232
- * is true (`wallet/vtxo.ts`), and the latter is documented as "must be
233
- * recovered into a fresh batch rather than spent offchain". Holding the
234
- * trader's `sender` key does not change that; a sweep removes the leaf from
235
- * the live tree, not the signature from the trader.
230
+ * transaction, and the SDK's own predicates put the two states on opposite
231
+ * sides — `canSpendOffchain` is false whenever `canRecoverOnchain` is true
232
+ * (`wallet/vtxo.ts`), and the latter is documented as "must be recovered
233
+ * into a fresh batch rather than spent offchain". Holding the trader's
234
+ * `sender` key does not change that; a sweep removes the leaf from the live
235
+ * tree, not the signature from the trader.
236
+ *
237
+ * Opposite, but not exhaustive: an unrolled output satisfies neither, and
238
+ * {@link findLockupVtxos} drops those before they reach this type at all.
236
239
  *
237
240
  * `packages/boltz-swap` splits on exactly this fact rather than working
238
241
  * around it: `settleRefundWithoutReceiver` sends a live VTXO through an
@@ -321,6 +324,18 @@ declare class LockupNeedsRecoveryError extends Error {
321
324
  * That function refuses the recoverable ones by name rather than submitting a
322
325
  * spend the server must reject.
323
326
  *
327
+ * **Unrolled outputs are the one exception, and are dropped.** A unilaterally
328
+ * exited output lives onchain behind its CSV; no offchain spend of any leaf can
329
+ * reach it, and `LockupVtxo` carries no field to say so, so a caller could not
330
+ * tell it apart from a live one. Whether arkd returns such an output under
331
+ * `spendableOnly` is not determinable from here, so the exclusion is made
332
+ * defensively rather than assumed. It costs the two waiting callers nothing
333
+ * they wanted: `awaitLockupFunding` keeps waiting for a claimable lockup
334
+ * instead of publishing `P` into a spend that cannot land, and
335
+ * `refundIfUnresolved` reports `nothing_to_refund` instead of grinding a doomed
336
+ * push to its deadline. The manager reads the exit through
337
+ * {@link readLockupFate}, which queries unfiltered and reports it as `exited`.
338
+ *
324
339
  * This read — not the RFQ's reported state — is the authority on whether
325
340
  * there is anything left at the lockup.
326
341
  *
@@ -374,6 +389,29 @@ type LockupFate =
374
389
  fate: "returned";
375
390
  spends: readonly LockupSpend[];
376
391
  }
392
+ /**
393
+ * At least one output was unilaterally exited: it sits onchain under the
394
+ * VHTLC script, where no offchain claim or refund can reach it.
395
+ *
396
+ * Not terminal, and not a loss. The money is still under the same script
397
+ * with the same leaves, so `completeUnroll` plus an onchain spend can still
398
+ * end the swap either way — which is why this outranks `open`: an output
399
+ * that is "still unspent" but unreachable is not a swap that is merely
400
+ * running.
401
+ *
402
+ * It outranks a verdict too, on a lockup where a sibling output was claimed
403
+ * or returned. That is the rule `open` already sets, not a new one: the
404
+ * unspent test short-circuits before any witness is read, so a partially
405
+ * resolved lockup has never reported `claimed`/`returned`. What changes is
406
+ * only that such a lockup now says why it is unresolved.
407
+ */
408
+ | {
409
+ fate: "exited";
410
+ outpoints: readonly {
411
+ txid: string;
412
+ vout: number;
413
+ }[];
414
+ }
377
415
  /** Nothing was learned: no outputs visible, an output spent by nothing the
378
416
  * indexer names, a spend it could not produce, or a blob that would not
379
417
  * decode. Never an answer. */
@@ -409,6 +447,11 @@ type LockupFate =
409
447
  * response to `unknown` is the same as to `open`: keep watching, and let the
410
448
  * refund timelock — which no outage can move — be what ends the wait.
411
449
  *
450
+ * **An exit is read before anything else, and over the whole set.** It is the
451
+ * one fact that makes an unspent output unreachable, so it outranks `open`; and
452
+ * it is scanned across every output rather than in outpoint order, so which
453
+ * output happens to come first cannot change the answer.
454
+ *
412
455
  * Ask-the-indexer, don't-trust-local-state: read fresh on every poll, never
413
456
  * cached, the same posture {@link findLockupVtxos} already establishes.
414
457
  */
@@ -447,9 +490,9 @@ declare function readLockupFate(indexer: LockupSpendIndexer, input: {
447
490
  * {@link refundIfUnresolved}, which retries.
448
491
  *
449
492
  * **Swept outputs are refused, not attempted.** This is an OFFCHAIN spend, and
450
- * a swept output is no longer a live leaf: `canSpendOffchain` and
451
- * `canRecoverOnchain` are mutually exclusive by construction, so a recoverable
452
- * input cannot be spent this way whatever key signs it (see
493
+ * a swept output is no longer a live leaf: `canSpendOffchain` is false wherever
494
+ * `canRecoverOnchain` is true, so a recoverable input cannot be spent this way
495
+ * whatever key signs it (see
453
496
  * {@link LockupVtxo.recoverable}). Because every input lands in ONE aggregate
454
497
  * transaction, a single swept output would take the live ones down with it —
455
498
  * so the whole push is refused with {@link LockupNeedsRecoveryError} naming the
@@ -1162,7 +1205,9 @@ interface RfqSwapManagerDeps {
1162
1205
  * `settled`; a lockup fully spent by anything else ends it `refunded`.
1163
1206
  * Anything the indexer could not answer is `unknown`, which is NOT an
1164
1207
  * answer: the pass carries on to the steps below, whose deadlines an indexer
1165
- * outage has no bearing on.
1208
+ * outage has no bearing on. `exited` — an output unilaterally taken onchain
1209
+ * — ends neither the swap nor the pass: it blocks the Arkade half below,
1210
+ * with the L1 half left running.
1166
1211
  * 2. **Drive the trader's claim.** On an onchain send that is the L1 fill — see
1167
1212
  * {@link nextOnchainAction}. On a receive it is the lockup itself, and it
1168
1213
  * ends the pass: that leg has no step 3.
@@ -1493,6 +1538,18 @@ declare class RfqSwapManager {
1493
1538
  private arm;
1494
1539
  private pollSwap;
1495
1540
  private runPass;
1541
+ /**
1542
+ * The lockup was unilaterally exited: its outputs sit onchain under the
1543
+ * VHTLC script, where no offchain claim or refund can reach them.
1544
+ *
1545
+ * `needs_counterparty` rather than a terminal state, because the money still
1546
+ * needs action and the swap can still end either way — an onchain claim can
1547
+ * reveal the preimage, an onchain refund can return it — and that state is
1548
+ * documented as re-checked every pass. It must be set from HERE and not from
1549
+ * inside `driveArkadeRefund`, whose two `unblock` calls would lift it again
1550
+ * on the very next pass.
1551
+ */
1552
+ private blockExitedLockup;
1496
1553
  /**
1497
1554
  * The receive leg's whole state machine: claim the solver-funded lockup
1498
1555
  * while the window is open, and recognise the shapes in which it can be
@@ -487,12 +487,11 @@ interface InvoiceFacts {
487
487
  * throws while nothing is funded. `RfqSwapManager` re-registers as a backstop
488
488
  * for older records; a repeat write is a no-op.
489
489
  *
490
- * The `sender` key comes from the wallet a fresh HD descriptor per call, or
491
- * the wallet's static key and is returned as `senderPubkey` plus `secrets`.
492
- * `secrets` holds only a public descriptor; the signer re-derives from the
493
- * wallet, so nothing secret is at rest. Persist `secrets` with the record
494
- * anyway: it is how the refund signer is found again. `nonInteractiveRefund`
495
- * recovers the funds even without it — but it needs the SOLVER's active
490
+ * The `sender` key is the wallet's identity key, reused by {@link
491
+ * provisionRefundKey} — returned as `senderPubkey` plus `secrets`. `secrets`
492
+ * holds only a public descriptor; the signer re-derives from the wallet, so
493
+ * nothing secret is at rest. Persist `secrets` with the record anyway: it is
494
+ * how the refund signer is found again. `nonInteractiveRefund` recovers the funds even without it — but it needs the SOLVER's active
496
495
  * cooperation, not just infrastructure uptime.
497
496
  */
498
497
  declare function requestLightningSend(wallet: IWallet, arkServerUrl: string, transport: RfqTransport, params: {
@@ -515,7 +514,8 @@ declare function requestLightningSend(wallet: IWallet, arkServerUrl: string, tra
515
514
  * `lockup` (with `address`): without it the manager can only poll, and
516
515
  * cannot retire the row this call just wrote. */
517
516
  script: InstanceType<typeof VHTLC.ScriptV2>;
518
- /** Where a failed swap refunds. */
517
+ /** Where a failed swap refunds — the same address `secrets.pkScript` was
518
+ * decoded from, so the quote and the covenant always name one script. */
519
519
  refundAddress: string;
520
520
  /** The VHTLC `sender` x-only key, bound into the covenant. Public. */
521
521
  senderPubkey: Uint8Array;
@@ -528,7 +528,9 @@ declare function requestLightningSend(wallet: IWallet, arkServerUrl: string, tra
528
528
  * Returned so a consumer can persist the swap without re-deriving any of
529
529
  * it. Half of these are not on the quote: `serverPubkey` and `claimDelay`
530
530
  * come from this wallet's own `getInfo()`, `emulatorPubkey` from a
531
- * per-network pin, `refundPkScript` from decoding an address.
531
+ * per-network pin, `refundPkScript` from `secrets` decoded from the
532
+ * refund address at provisioning time, the same address this call returns
533
+ * as `refundAddress`.
532
534
  *
533
535
  * All public. Persisting them is optional: this call also registers the
534
536
  * lockup as a contract, and that row is where `rebuildRfqSwap` takes its
@@ -487,12 +487,11 @@ interface InvoiceFacts {
487
487
  * throws while nothing is funded. `RfqSwapManager` re-registers as a backstop
488
488
  * for older records; a repeat write is a no-op.
489
489
  *
490
- * The `sender` key comes from the wallet a fresh HD descriptor per call, or
491
- * the wallet's static key and is returned as `senderPubkey` plus `secrets`.
492
- * `secrets` holds only a public descriptor; the signer re-derives from the
493
- * wallet, so nothing secret is at rest. Persist `secrets` with the record
494
- * anyway: it is how the refund signer is found again. `nonInteractiveRefund`
495
- * recovers the funds even without it — but it needs the SOLVER's active
490
+ * The `sender` key is the wallet's identity key, reused by {@link
491
+ * provisionRefundKey} — returned as `senderPubkey` plus `secrets`. `secrets`
492
+ * holds only a public descriptor; the signer re-derives from the wallet, so
493
+ * nothing secret is at rest. Persist `secrets` with the record anyway: it is
494
+ * how the refund signer is found again. `nonInteractiveRefund` recovers the funds even without it — but it needs the SOLVER's active
496
495
  * cooperation, not just infrastructure uptime.
497
496
  */
498
497
  declare function requestLightningSend(wallet: IWallet, arkServerUrl: string, transport: RfqTransport, params: {
@@ -515,7 +514,8 @@ declare function requestLightningSend(wallet: IWallet, arkServerUrl: string, tra
515
514
  * `lockup` (with `address`): without it the manager can only poll, and
516
515
  * cannot retire the row this call just wrote. */
517
516
  script: InstanceType<typeof VHTLC.ScriptV2>;
518
- /** Where a failed swap refunds. */
517
+ /** Where a failed swap refunds — the same address `secrets.pkScript` was
518
+ * decoded from, so the quote and the covenant always name one script. */
519
519
  refundAddress: string;
520
520
  /** The VHTLC `sender` x-only key, bound into the covenant. Public. */
521
521
  senderPubkey: Uint8Array;
@@ -528,7 +528,9 @@ declare function requestLightningSend(wallet: IWallet, arkServerUrl: string, tra
528
528
  * Returned so a consumer can persist the swap without re-deriving any of
529
529
  * it. Half of these are not on the quote: `serverPubkey` and `claimDelay`
530
530
  * come from this wallet's own `getInfo()`, `emulatorPubkey` from a
531
- * per-network pin, `refundPkScript` from decoding an address.
531
+ * per-network pin, `refundPkScript` from `secrets` decoded from the
532
+ * refund address at provisioning time, the same address this call returns
533
+ * as `refundAddress`.
532
534
  *
533
535
  * All public. Persisting them is optional: this call also registers the
534
536
  * lockup as a contract, and that row is where `rebuildRfqSwap` takes its
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arkade-os/swap",
3
- "version": "0.0.9",
3
+ "version": "0.0.10",
4
4
  "type": "module",
5
5
  "description": "Client-side Arkade Intents asset swaps: discover markets, quote, create/track/cancel offers, restore from chain.",
6
6
  "repository": {
@@ -70,7 +70,7 @@
70
70
  "@noble/hashes": "2.0.1",
71
71
  "@scure/base": "2.0.0",
72
72
  "@scure/btc-signer": "2.0.1",
73
- "@arkade-os/sdk": "0.4.66"
73
+ "@arkade-os/sdk": "0.4.67"
74
74
  },
75
75
  "peerDependencies": {
76
76
  "nostr-tools": "^2.12.0"
@@ -91,8 +91,8 @@
91
91
  "scripts": {
92
92
  "build": "tsup src/index.ts src/nostr.ts src/repositories/sqlite/index.ts src/repositories/realm/index.ts --format esm,cjs --dts --clean",
93
93
  "typecheck": "tsc --noEmit",
94
- "format": "prettier --write src test",
95
- "lint": "prettier --check src test",
94
+ "format": "biome format --write src test",
95
+ "lint": "biome format src test",
96
96
  "test": "vitest run --exclude test/e2e",
97
97
  "test:unit": "vitest run --exclude test/e2e",
98
98
  "test:integration": "vitest run test/e2e/** --exclude test/e2e/rfq*.test.ts",