@satora/swap 1.0.0 → 1.1.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/README.md CHANGED
@@ -94,7 +94,7 @@ await client.claim(swapId);
94
94
 
95
95
  // Refund after timeout
96
96
  await client.refundSwap(swapId, {destinationAddress: "bc1q…"}); // or "ark1…"
97
- await client.collabRefundEvmWithSigner(swapId, signer, "swap-back"); // EVM, instant/gasless
97
+ await client.collabRefundEvmWithSigner(swapId, signer); // EVM, instant/gasless
98
98
  ```
99
99
 
100
100
  ## License
@@ -0,0 +1,7 @@
1
+ import {
2
+ electrumReader
3
+ } from "./chunk-FTPIQSRW.js";
4
+ export {
5
+ electrumReader
6
+ };
7
+ //# sourceMappingURL=bitcoin-reader-electrum-PY7DHD6T.js.map
@@ -2,10 +2,10 @@ import {
2
2
  DEFAULT_ESPLORA_URLS,
3
3
  esploraReader,
4
4
  htlcFactsFromEsploraTxs
5
- } from "./chunk-EIWELJJK.js";
5
+ } from "./chunk-THI4UO6R.js";
6
6
  export {
7
7
  DEFAULT_ESPLORA_URLS,
8
8
  esploraReader,
9
9
  htlcFactsFromEsploraTxs
10
10
  };
11
- //# sourceMappingURL=bitcoin-reader-esplora-HV6CANAA.js.map
11
+ //# sourceMappingURL=bitcoin-reader-esplora-ZLPMM5FF.js.map
@@ -62,7 +62,17 @@ function electrumReader(client, opts) {
62
62
  }
63
63
  return indices;
64
64
  };
65
- const funding = txs.find(({ tx }) => outputIndicesPaying(tx).length > 0);
65
+ let funding;
66
+ let fundedSats = 0;
67
+ for (const candidate of txs) {
68
+ let paid = 0;
69
+ for (const i of outputIndicesPaying(candidate.tx))
70
+ paid += Number(candidate.tx.getOutput(i).amount ?? 0n);
71
+ if (paid > fundedSats) {
72
+ funding = candidate;
73
+ fundedSats = paid;
74
+ }
75
+ }
66
76
  if (!funding) return { funding: "absent", fundedSats: 0 };
67
77
  const fundedVouts = outputIndicesPaying(funding.tx);
68
78
  for (const { tx } of txs) {
@@ -80,9 +90,6 @@ function electrumReader(client, opts) {
80
90
  }
81
91
  }
82
92
  }
83
- let fundedSats = 0;
84
- for (const i of fundedVouts)
85
- fundedSats += Number(funding.tx.getOutput(i).amount ?? 0n);
86
93
  const height = funding.entry.height;
87
94
  const isConfirmed = height > 0;
88
95
  let deepEnough;
@@ -142,4 +149,4 @@ function electrumReader(client, opts) {
142
149
  export {
143
150
  electrumReader
144
151
  };
145
- //# sourceMappingURL=chunk-VHXEU534.js.map
152
+ //# sourceMappingURL=chunk-FTPIQSRW.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/contracts/bitcoin-reader-electrum.ts"],"sourcesContent":["/**\n * An Electrum-backed {@link BitcoinChainReader} — reads HTLC facts from our own\n * Fulcrum over WebSocket instead of public esplora REST endpoints.\n *\n * Two advantages over the esplora reader:\n *\n * - **Freshness**: our Fulcrum sees a transaction the moment its bitcoind\n * accepts it, while public explorers index with a lag — the exact lag that\n * made a server-funding hint's targeted verify come back \"absent\" and park\n * the claim on the 60s at-risk poller.\n * - **Push**: implements the reader's optional `subscribe` capability via\n * `blockchain.scripthash.subscribe`, so the manager reconciles the moment\n * an HTLC address's history changes instead of waiting for a poll tick.\n *\n * Reads fall back to a wrapped reader (typically the esplora one) whenever the\n * Electrum server errors, so a Fulcrum outage degrades to today's behavior\n * rather than blinding the tracker.\n */\n\nimport { sha256 } from \"@noble/hashes/sha2.js\";\nimport { hex } from \"@scure/base\";\nimport * as btc from \"@scure/btc-signer\";\nimport type { BitcoinHtlcFacts } from \"./bitcoin.js\";\nimport type { BitcoinChainReader } from \"./bitcoin-manager.js\";\nimport type { BitcoinConfirmationPolicy } from \"./bitcoin-reader-esplora.js\";\n\n/** One entry of `blockchain.scripthash.get_history`. */\ntype ElectrumHistoryEntry = {\n tx_hash: string;\n /** Block height; 0 for mempool, -1 for mempool with unconfirmed parents. */\n height: number;\n};\n\n/**\n * The Electrum client surface this reader needs. The pure SDK's\n * `ElectrumWsClient` satisfies it; tests inject a fake.\n */\nexport type ElectrumRpc = {\n request<T>(method: string, params: unknown[]): Promise<T>;\n subscribeScriptHash(\n scripthash: string,\n onChange: (status: string | null) => void,\n ): Promise<string | null>;\n unsubscribeScriptHash(\n scripthash: string,\n onChange: (status: string | null) => void,\n ): void;\n};\n\nexport type ElectrumReaderNetwork =\n | \"mainnet\"\n | \"testnet\"\n | \"signet\"\n | \"regtest\";\n\n/** btc-signer only ships mainnet/testnet; regtest differs in bech32 prefix. */\nconst REGTEST_NETWORK = {\n bech32: \"bcrt\",\n pubKeyHash: 0x6f,\n scriptHash: 0xc4,\n wif: 0xef,\n} as const;\n\nfunction toBtcSignerNetwork(network: ElectrumReaderNetwork) {\n switch (network) {\n case \"mainnet\":\n return btc.NETWORK;\n case \"testnet\":\n case \"signet\":\n return btc.TEST_NETWORK;\n case \"regtest\":\n return REGTEST_NETWORK;\n }\n}\n\n/** Electrum scripthash: sha256 of the output script, reversed, hex. */\nfunction scriptHashOf(script: Uint8Array): string {\n return hex.encode(sha256(script).reverse());\n}\n\n/**\n * Build a {@link BitcoinChainReader} over an Electrum connection, with push\n * support and an optional fallback reader for outages.\n */\nexport function electrumReader(\n client: ElectrumRpc,\n opts?: BitcoinConfirmationPolicy & {\n /** Network the HTLC addresses live on; defaults to mainnet. */\n network?: ElectrumReaderNetwork;\n /** Reader consulted when the Electrum server errors (e.g. esplora). */\n fallback?: BitcoinChainReader;\n },\n): BitcoinChainReader {\n const network = opts?.network ?? \"mainnet\";\n const defaultMinConf = opts?.minConfirmations ?? 0;\n const fallback = opts?.fallback;\n\n const scriptFor = (address: string): Uint8Array =>\n btc.OutScript.encode(\n btc.Address(toBtcSignerNetwork(network)).decode(address),\n );\n\n async function getHtlcFactsElectrum(\n address: string,\n required: number,\n ): Promise<BitcoinHtlcFacts> {\n const script = scriptFor(address);\n const scripthash = scriptHashOf(script);\n const history = await client.request<ElectrumHistoryEntry[]>(\n \"blockchain.scripthash.get_history\",\n [scripthash],\n );\n if (!history || history.length === 0)\n return { funding: \"absent\", fundedSats: 0 };\n\n // HTLC addresses see at most a handful of txs (fund + resolve), so\n // fetching each raw tx is cheap.\n const txs = await Promise.all(\n history.map(async (entry) => {\n const raw = await client.request<string>(\"blockchain.transaction.get\", [\n entry.tx_hash,\n ]);\n return {\n entry,\n tx: btc.Transaction.fromRaw(hex.decode(raw), {\n allowUnknownOutputs: true,\n allowUnknownInputs: true,\n disableScriptCheck: true,\n }),\n };\n }),\n );\n\n const scriptHex = hex.encode(script);\n const outputIndicesPaying = (tx: btc.Transaction): number[] => {\n const indices: number[] = [];\n for (let i = 0; i < tx.outputsLength; i++) {\n const out = tx.getOutput(i);\n if (out.script && hex.encode(out.script) === scriptHex) indices.push(i);\n }\n return indices;\n };\n\n // Among all txs paying the address, the one paying the MOST is the\n // funding candidate — same rule as the pure SDK's UTXO lookup. The\n // address is public, so a stray dust payment (whether it landed before\n // or after the real funding) must not be mistaken for the funding and\n // observe the swap as underfunded/invalid.\n let funding: (typeof txs)[number] | undefined;\n let fundedSats = 0;\n for (const candidate of txs) {\n let paid = 0;\n for (const i of outputIndicesPaying(candidate.tx))\n paid += Number(candidate.tx.getOutput(i).amount ?? 0n);\n if (paid > fundedSats) {\n funding = candidate;\n fundedSats = paid;\n }\n }\n if (!funding) return { funding: \"absent\", fundedSats: 0 };\n const fundedVouts = outputIndicesPaying(funding.tx);\n\n // A tx spending one of the funding outputs is the resolving spend; its\n // witness classifies claim vs refund.\n for (const { tx } of txs) {\n for (let i = 0; i < tx.inputsLength; i++) {\n const input = tx.getInput(i);\n if (\n input.txid &&\n hex.encode(input.txid) === funding.entry.tx_hash &&\n input.index !== undefined &&\n fundedVouts.includes(input.index)\n ) {\n return {\n funding: \"confirmed\",\n fundedSats: 0, // already spent — the amount no longer matters\n spendWitness: (input.finalScriptWitness ?? []).map((item) =>\n hex.encode(item),\n ),\n };\n }\n }\n }\n\n // Same depth policy as the esplora reader: 0/1 read straight off the\n // confirmed flag; deeper policies need the tip to compute depth.\n const height = funding.entry.height;\n const isConfirmed = height > 0;\n let deepEnough: boolean;\n if (isConfirmed) {\n if (required <= 1) {\n deepEnough = true;\n } else {\n const tip = await client.request<{ height: number }>(\n \"blockchain.headers.subscribe\",\n [],\n );\n deepEnough = tip.height - height + 1 >= required;\n }\n } else {\n deepEnough = required === 0;\n }\n return { funding: deepEnough ? \"confirmed\" : \"mempool\", fundedSats };\n }\n\n return {\n async getHtlcFacts(address, minConfirmations) {\n const required = minConfirmations ?? defaultMinConf;\n try {\n return await getHtlcFactsElectrum(address, required);\n } catch (error) {\n if (!fallback) throw error;\n console.warn(\n \"electrumReader: falling back to secondary reader:\",\n error instanceof Error ? error.message : error,\n );\n return fallback.getHtlcFacts(address, minConfirmations);\n }\n },\n\n subscribe(address, onChange) {\n // An address the configured network can't decode (e.g. a signet address\n // under a mainnet-configured reader) must not throw out of register()\n // and abort tracking — degrade to the poll cadence instead.\n let scripthash: string;\n try {\n scripthash = scriptHashOf(scriptFor(address));\n } catch (error) {\n console.warn(\n \"electrumReader: subscribe skipped (address decode failed):\",\n error instanceof Error ? error.message : error,\n );\n return () => {};\n }\n const handler = () => onChange();\n // Best-effort: if the subscription fails the tracker's poll cadence\n // remains the backstop, so the error is logged, not thrown.\n client.subscribeScriptHash(scripthash, handler).catch((error) => {\n console.warn(\n \"electrumReader: scripthash subscribe failed:\",\n error instanceof Error ? error.message : error,\n );\n });\n return () => client.unsubscribeScriptHash(scripthash, handler);\n },\n };\n}\n"],"mappings":"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","names":[]}
@@ -12,11 +12,17 @@ function htlcFactsFromEsploraTxs(txs, address, opts) {
12
12
  spendWitness: spend.witness ?? []
13
13
  };
14
14
  }
15
- const funding = txs.find(
16
- (tx) => tx.vout.some((vout) => vout.scriptpubkey_address === address)
17
- );
15
+ const paidSats = (tx) => tx.vout.filter((vout) => vout.scriptpubkey_address === address).reduce((sum, vout) => sum + (vout.value ?? 0), 0);
16
+ let funding;
17
+ let fundedSats = 0;
18
+ for (const tx of txs) {
19
+ const paid = paidSats(tx);
20
+ if (paid > fundedSats) {
21
+ funding = tx;
22
+ fundedSats = paid;
23
+ }
24
+ }
18
25
  if (funding) {
19
- const fundedSats = funding.vout.filter((vout) => vout.scriptpubkey_address === address).reduce((sum, vout) => sum + (vout.value ?? 0), 0);
20
26
  const minConf = opts?.minConfirmations ?? 0;
21
27
  let deepEnough;
22
28
  if (funding.status?.confirmed) {
@@ -78,4 +84,4 @@ export {
78
84
  DEFAULT_ESPLORA_URLS,
79
85
  esploraReader
80
86
  };
81
- //# sourceMappingURL=chunk-EIWELJJK.js.map
87
+ //# sourceMappingURL=chunk-THI4UO6R.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/contracts/bitcoin-reader-esplora.ts"],"sourcesContent":["/**\n * A esplora-backed {@link BitcoinChainReader} — the concrete chain source the\n * {@link BitcoinContractManager} uses in production.\n *\n * Kept separate from the manager so the manager stays free of any HTTP/esplora\n * dependency (and unit-testable against a fake reader). A single\n * `GET /address/{addr}/txs` yields both the funding state and the spending\n * input's witness, so no extra round-trips are needed.\n */\n\nimport type { BitcoinHtlcFacts } from \"./bitcoin.js\";\nimport type { BitcoinChainReader } from \"./bitcoin-manager.js\";\n\n/** The slice of esplora's tx JSON we read. */\ntype EsploraTx = {\n vin: Array<{\n witness?: string[];\n prevout?: { scriptpubkey_address?: string } | null;\n }>;\n vout: Array<{ scriptpubkey_address?: string; value?: number }>;\n status?: { confirmed?: boolean; block_height?: number };\n};\n\n/**\n * When a funding tx counts as `confirmed` for observation purposes.\n *\n * `minConfirmations: 0` (the default) accepts an UNCONFIRMED funding, so a swap\n * claims as soon as the funding hits the mempool instead of waiting ~10min for\n * a block. This TRUSTS the funder not to double-spend: claiming publishes the\n * preimage, so a funder who then replaces its funding tx could take both legs.\n * Our own server therefore broadcasts HTLC fundings without signalling RBF.\n * Note that a non-signalling tx is still replaceable by full-RBF miners — set\n * `1` (or more) for a policy that doesn't rely on the funder's good behaviour;\n * values above 1 make the reader fetch the tip height to compute depth.\n */\nexport type BitcoinConfirmationPolicy = {\n minConfirmations?: number;\n};\n\n/**\n * Reduce an address's esplora tx history to HTLC facts. If a tx spends an output\n * at the address, that's the resolving spend (its witness classifies claim vs\n * refund); otherwise a tx paying the address is the funding, gated by the\n * confirmation policy (see {@link BitcoinConfirmationPolicy}).\n */\nexport function htlcFactsFromEsploraTxs(\n txs: EsploraTx[],\n address: string,\n opts?: BitcoinConfirmationPolicy & { tipHeight?: number },\n): BitcoinHtlcFacts {\n for (const tx of txs) {\n const spend = tx.vin.find(\n (vin) => vin.prevout?.scriptpubkey_address === address,\n );\n if (spend)\n return {\n funding: \"confirmed\",\n fundedSats: 0, // already spent — the amount no longer matters\n spendWitness: spend.witness ?? [],\n };\n }\n // Among all txs paying the address, the one paying the MOST is the funding\n // candidate — matching the pure SDK's UTXO lookup. The address is public,\n // so a stray dust payment must not be mistaken for the funding and observe\n // the swap as underfunded/invalid.\n const paidSats = (tx: EsploraTx): number =>\n tx.vout\n .filter((vout) => vout.scriptpubkey_address === address)\n .reduce((sum, vout) => sum + (vout.value ?? 0), 0);\n let funding: EsploraTx | undefined;\n let fundedSats = 0;\n for (const tx of txs) {\n const paid = paidSats(tx);\n if (paid > fundedSats) {\n funding = tx;\n fundedSats = paid;\n }\n }\n if (funding) {\n const minConf = opts?.minConfirmations ?? 0;\n let deepEnough: boolean;\n if (funding.status?.confirmed) {\n deepEnough =\n minConf <= 1 ||\n (opts?.tipHeight !== undefined &&\n funding.status.block_height !== undefined &&\n opts.tipHeight - funding.status.block_height + 1 >= minConf);\n } else {\n deepEnough = minConf === 0;\n }\n return {\n funding: deepEnough ? \"confirmed\" : \"mempool\",\n fundedSats,\n };\n }\n return { funding: \"absent\", fundedSats: 0 };\n}\n\n/**\n * Public esplora endpoints tried in rotation (mainnet). blockstream.info runs the\n * reference esplora, so it's API-compatible with mempool.space; spreading calls\n * across both halves the per-provider load and survives one being throttled.\n */\nexport const DEFAULT_ESPLORA_URLS = [\n \"https://mempool.space/api\",\n \"https://blockstream.info/api\",\n];\n\n/**\n * Build a {@link BitcoinChainReader} over one or more esplora REST endpoints.\n * With several, each call starts at a rotating endpoint (to spread load across\n * providers) and fails over to the rest on error — so a throttled or failing\n * provider doesn't stall tracking.\n */\nexport function esploraReader(\n esploraUrls: string | string[],\n fetchImpl: typeof fetch = fetch,\n policy?: BitcoinConfirmationPolicy,\n): BitcoinChainReader {\n const bases = (Array.isArray(esploraUrls) ? esploraUrls : [esploraUrls]).map(\n (url) => url.replace(/\\/+$/, \"\"),\n );\n const minConf = policy?.minConfirmations ?? 0;\n let start = 0;\n return {\n async getHtlcFacts(address, minConfirmations) {\n const required = minConfirmations ?? minConf;\n const from = start++ % bases.length; // rotate the primary to spread load\n let lastError: unknown;\n for (let i = 0; i < bases.length; i++) {\n const base = bases[(from + i) % bases.length];\n try {\n const res = await fetchImpl(\n `${base}/address/${encodeURIComponent(address)}/txs`,\n );\n if (!res.ok) throw new Error(`esplora ${res.status} at ${base}`);\n const txs = (await res.json()) as EsploraTx[];\n // Depth policies beyond 1 need the tip to compute confirmations;\n // 0 and 1 read straight off the tx's confirmed flag.\n let tipHeight: number | undefined;\n if (required > 1) {\n const tip = await fetchImpl(`${base}/blocks/tip/height`);\n if (!tip.ok) throw new Error(`esplora ${tip.status} at ${base}`);\n tipHeight = Number(await tip.text());\n }\n return htlcFactsFromEsploraTxs(txs, address, {\n minConfirmations: required,\n tipHeight,\n });\n } catch (error) {\n lastError = error;\n }\n }\n throw lastError ?? new Error(\"no esplora endpoints configured\");\n },\n };\n}\n"],"mappings":";AA6CO,SAAS,wBACd,KACA,SACA,MACkB;AAClB,aAAW,MAAM,KAAK;AACpB,UAAM,QAAQ,GAAG,IAAI;AAAA,MACnB,CAAC,QAAQ,IAAI,SAAS,yBAAyB;AAAA,IACjD;AACA,QAAI;AACF,aAAO;AAAA,QACL,SAAS;AAAA,QACT,YAAY;AAAA;AAAA,QACZ,cAAc,MAAM,WAAW,CAAC;AAAA,MAClC;AAAA,EACJ;AAKA,QAAM,WAAW,CAAC,OAChB,GAAG,KACA,OAAO,CAAC,SAAS,KAAK,yBAAyB,OAAO,EACtD,OAAO,CAAC,KAAK,SAAS,OAAO,KAAK,SAAS,IAAI,CAAC;AACrD,MAAI;AACJ,MAAI,aAAa;AACjB,aAAW,MAAM,KAAK;AACpB,UAAM,OAAO,SAAS,EAAE;AACxB,QAAI,OAAO,YAAY;AACrB,gBAAU;AACV,mBAAa;AAAA,IACf;AAAA,EACF;AACA,MAAI,SAAS;AACX,UAAM,UAAU,MAAM,oBAAoB;AAC1C,QAAI;AACJ,QAAI,QAAQ,QAAQ,WAAW;AAC7B,mBACE,WAAW,KACV,MAAM,cAAc,UACnB,QAAQ,OAAO,iBAAiB,UAChC,KAAK,YAAY,QAAQ,OAAO,eAAe,KAAK;AAAA,IAC1D,OAAO;AACL,mBAAa,YAAY;AAAA,IAC3B;AACA,WAAO;AAAA,MACL,SAAS,aAAa,cAAc;AAAA,MACpC;AAAA,IACF;AAAA,EACF;AACA,SAAO,EAAE,SAAS,UAAU,YAAY,EAAE;AAC5C;AAOO,IAAM,uBAAuB;AAAA,EAClC;AAAA,EACA;AACF;AAQO,SAAS,cACd,aACA,YAA0B,OAC1B,QACoB;AACpB,QAAM,SAAS,MAAM,QAAQ,WAAW,IAAI,cAAc,CAAC,WAAW,GAAG;AAAA,IACvE,CAAC,QAAQ,IAAI,QAAQ,QAAQ,EAAE;AAAA,EACjC;AACA,QAAM,UAAU,QAAQ,oBAAoB;AAC5C,MAAI,QAAQ;AACZ,SAAO;AAAA,IACL,MAAM,aAAa,SAAS,kBAAkB;AAC5C,YAAM,WAAW,oBAAoB;AACrC,YAAM,OAAO,UAAU,MAAM;AAC7B,UAAI;AACJ,eAAS,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;AACrC,cAAM,OAAO,OAAO,OAAO,KAAK,MAAM,MAAM;AAC5C,YAAI;AACF,gBAAM,MAAM,MAAM;AAAA,YAChB,GAAG,IAAI,YAAY,mBAAmB,OAAO,CAAC;AAAA,UAChD;AACA,cAAI,CAAC,IAAI,GAAI,OAAM,IAAI,MAAM,WAAW,IAAI,MAAM,OAAO,IAAI,EAAE;AAC/D,gBAAM,MAAO,MAAM,IAAI,KAAK;AAG5B,cAAI;AACJ,cAAI,WAAW,GAAG;AAChB,kBAAM,MAAM,MAAM,UAAU,GAAG,IAAI,oBAAoB;AACvD,gBAAI,CAAC,IAAI,GAAI,OAAM,IAAI,MAAM,WAAW,IAAI,MAAM,OAAO,IAAI,EAAE;AAC/D,wBAAY,OAAO,MAAM,IAAI,KAAK,CAAC;AAAA,UACrC;AACA,iBAAO,wBAAwB,KAAK,SAAS;AAAA,YAC3C,kBAAkB;AAAA,YAClB;AAAA,UACF,CAAC;AAAA,QACH,SAAS,OAAO;AACd,sBAAY;AAAA,QACd;AAAA,MACF;AACA,YAAM,aAAa,IAAI,MAAM,iCAAiC;AAAA,IAChE;AAAA,EACF;AACF;","names":[]}
package/dist/index.cjs CHANGED
@@ -51,11 +51,17 @@ function htlcFactsFromEsploraTxs(txs, address, opts) {
51
51
  spendWitness: spend.witness ?? []
52
52
  };
53
53
  }
54
- const funding = txs.find(
55
- (tx) => tx.vout.some((vout) => vout.scriptpubkey_address === address)
56
- );
54
+ const paidSats = (tx) => tx.vout.filter((vout) => vout.scriptpubkey_address === address).reduce((sum, vout) => sum + (vout.value ?? 0), 0);
55
+ let funding;
56
+ let fundedSats = 0;
57
+ for (const tx of txs) {
58
+ const paid = paidSats(tx);
59
+ if (paid > fundedSats) {
60
+ funding = tx;
61
+ fundedSats = paid;
62
+ }
63
+ }
57
64
  if (funding) {
58
- const fundedSats = funding.vout.filter((vout) => vout.scriptpubkey_address === address).reduce((sum, vout) => sum + (vout.value ?? 0), 0);
59
65
  const minConf = opts?.minConfirmations ?? 0;
60
66
  let deepEnough;
61
67
  if (funding.status?.confirmed) {
@@ -177,7 +183,17 @@ function electrumReader(client, opts) {
177
183
  }
178
184
  return indices;
179
185
  };
180
- const funding = txs.find(({ tx }) => outputIndicesPaying(tx).length > 0);
186
+ let funding;
187
+ let fundedSats = 0;
188
+ for (const candidate of txs) {
189
+ let paid = 0;
190
+ for (const i of outputIndicesPaying(candidate.tx))
191
+ paid += Number(candidate.tx.getOutput(i).amount ?? 0n);
192
+ if (paid > fundedSats) {
193
+ funding = candidate;
194
+ fundedSats = paid;
195
+ }
196
+ }
181
197
  if (!funding) return { funding: "absent", fundedSats: 0 };
182
198
  const fundedVouts = outputIndicesPaying(funding.tx);
183
199
  for (const { tx } of txs) {
@@ -195,9 +211,6 @@ function electrumReader(client, opts) {
195
211
  }
196
212
  }
197
213
  }
198
- let fundedSats = 0;
199
- for (const i of fundedVouts)
200
- fundedSats += Number(funding.tx.getOutput(i).amount ?? 0n);
201
214
  const height = funding.entry.height;
202
215
  const isConfirmed = height > 0;
203
216
  let deepEnough;
@@ -2079,6 +2092,21 @@ function swapToTracked(stored) {
2079
2092
  // no on-chain client leg
2080
2093
  serverRefundLocktime: ms(r.vhtlc_refund_locktime)
2081
2094
  };
2095
+ // Client funds the Arkade VHTLC (source_amount); the server pays the
2096
+ // Lightning invoice off-chain and claims the VHTLC — no server leg to
2097
+ // watch.
2098
+ case "arkade_to_lightning":
2099
+ return {
2100
+ swapId: r.id,
2101
+ clientHtlc: arkadeLeg(
2102
+ r,
2103
+ r.arkade_vhtlc_address,
2104
+ Number(r.source_amount)
2105
+ ),
2106
+ clientRefundLocktime: ms(r.vhtlc_refund_locktime),
2107
+ serverRefundLocktime: 0
2108
+ // no on-chain server leg
2109
+ };
2082
2110
  default:
2083
2111
  return void 0;
2084
2112
  }
@@ -2605,6 +2633,12 @@ var Client = class {
2605
2633
  );
2606
2634
  }
2607
2635
  /** Delegated to the legacy client (migration checkpoint). */
2636
+ createArkadeToLightningSwap(...args) {
2637
+ return this.#trackAfterCreate(
2638
+ this.#legacy.createArkadeToLightningSwap(...args)
2639
+ );
2640
+ }
2641
+ /** Delegated to the legacy client (migration checkpoint). */
2608
2642
  createEvmToArkadeSwapGeneric(...args) {
2609
2643
  return this.#trackAfterCreate(
2610
2644
  this.#legacy.createEvmToArkadeSwapGeneric(...args)