@lendasat/lendaswap-sdk-pure 0.2.41 → 0.2.43

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.
Files changed (252) hide show
  1. package/dist/_esm-6K2AP5GG.js +3663 -0
  2. package/dist/_esm-6K2AP5GG.js.map +1 -0
  3. package/dist/_esm-UGRDSVEV.cjs +3662 -0
  4. package/dist/_esm-UGRDSVEV.cjs.map +1 -0
  5. package/dist/api/client.d.ts +0 -4
  6. package/dist/api/client.d.ts.map +1 -1
  7. package/dist/base-2KYNAFR3.cjs +109 -0
  8. package/dist/base-2KYNAFR3.cjs.map +1 -0
  9. package/dist/base-HMHH3XPM.js +9 -0
  10. package/dist/base-HMHH3XPM.js.map +1 -0
  11. package/dist/ccip-EBXWV2U3.js +8 -0
  12. package/dist/ccip-EBXWV2U3.js.map +1 -0
  13. package/dist/ccip-PD37NJHA.cjs +26 -0
  14. package/dist/ccip-PD37NJHA.cjs.map +1 -0
  15. package/dist/cctp-bridge/index.cjs +67 -0
  16. package/dist/cctp-bridge/index.cjs.map +1 -0
  17. package/dist/cctp-bridge/index.js +53 -15
  18. package/dist/cctp-bridge/index.js.map +1 -1
  19. package/dist/cctp-inbound/client.d.ts +21 -19
  20. package/dist/cctp-inbound/client.d.ts.map +1 -1
  21. package/dist/cctp-inbound/smartAccount.d.ts +21 -19
  22. package/dist/cctp-inbound/smartAccount.d.ts.map +1 -1
  23. package/dist/chunk-4JXN4UPB.js +554 -0
  24. package/dist/chunk-4JXN4UPB.js.map +1 -0
  25. package/dist/chunk-A4GKKURK.cjs +430 -0
  26. package/dist/chunk-A4GKKURK.cjs.map +1 -0
  27. package/dist/chunk-BZU3R5DJ.cjs +554 -0
  28. package/dist/chunk-BZU3R5DJ.cjs.map +1 -0
  29. package/dist/chunk-DBBCKPZ5.js +421 -0
  30. package/dist/chunk-DBBCKPZ5.js.map +1 -0
  31. package/dist/chunk-DNSJDKTP.cjs +96 -0
  32. package/dist/chunk-DNSJDKTP.cjs.map +1 -0
  33. package/dist/chunk-EIK6BZWG.js +90 -0
  34. package/dist/chunk-EIK6BZWG.js.map +1 -0
  35. package/dist/chunk-GVAPVS4Z.js +6144 -0
  36. package/dist/chunk-GVAPVS4Z.js.map +1 -0
  37. package/dist/chunk-I7TGLGN6.js +46 -0
  38. package/dist/chunk-I7TGLGN6.js.map +1 -0
  39. package/dist/chunk-JBVKWKD4.cjs +50 -0
  40. package/dist/chunk-JBVKWKD4.cjs.map +1 -0
  41. package/dist/chunk-LA3YX2HD.js +47096 -0
  42. package/dist/chunk-LA3YX2HD.js.map +1 -0
  43. package/dist/chunk-PKDJZJMV.cjs +2214 -0
  44. package/dist/chunk-PKDJZJMV.cjs.map +1 -0
  45. package/dist/chunk-PRN5VZOM.js +370 -0
  46. package/dist/chunk-PRN5VZOM.js.map +1 -0
  47. package/dist/chunk-RIOT5VOJ.cjs +47240 -0
  48. package/dist/chunk-RIOT5VOJ.cjs.map +1 -0
  49. package/dist/chunk-WVZSHRV7.cjs +431 -0
  50. package/dist/chunk-WVZSHRV7.cjs.map +1 -0
  51. package/dist/chunk-YLWPBHMV.cjs +6311 -0
  52. package/dist/chunk-YLWPBHMV.cjs.map +1 -0
  53. package/dist/chunk-Z2FCTNRC.js +2210 -0
  54. package/dist/chunk-Z2FCTNRC.js.map +1 -0
  55. package/dist/client.d.ts +68 -1
  56. package/dist/client.d.ts.map +1 -1
  57. package/dist/compose-quote.d.ts +77 -0
  58. package/dist/compose-quote.d.ts.map +1 -0
  59. package/dist/delegate.cjs +23 -0
  60. package/dist/delegate.cjs.map +1 -0
  61. package/dist/delegate.js +8 -269
  62. package/dist/delegate.js.map +1 -1
  63. package/dist/dist-PWXNFVDF.js +8 -0
  64. package/dist/dist-PWXNFVDF.js.map +1 -0
  65. package/dist/dist-VEHYC67F.cjs +550 -0
  66. package/dist/dist-VEHYC67F.cjs.map +1 -0
  67. package/dist/esplora.d.ts +14 -0
  68. package/dist/esplora.d.ts.map +1 -1
  69. package/dist/generated/api.d.ts +235 -38
  70. package/dist/generated/api.d.ts.map +1 -1
  71. package/dist/index.cjs +35461 -0
  72. package/dist/index.cjs.map +1 -0
  73. package/dist/index.d.ts +2 -1
  74. package/dist/index.d.ts.map +1 -1
  75. package/dist/index.js +35312 -32
  76. package/dist/index.js.map +1 -1
  77. package/dist/mine.wasm-LUTPOAD2.js +12 -0
  78. package/dist/mine.wasm-LUTPOAD2.js.map +1 -0
  79. package/dist/mine.wasm-Z3ORSBKN.cjs +12 -0
  80. package/dist/mine.wasm-Z3ORSBKN.cjs.map +1 -0
  81. package/dist/node.cjs +166 -0
  82. package/dist/node.cjs.map +1 -0
  83. package/dist/node.js +159 -18
  84. package/dist/node.js.map +1 -1
  85. package/dist/secp256k1-B6OW5ZCM.js +8 -0
  86. package/dist/secp256k1-B6OW5ZCM.js.map +1 -0
  87. package/dist/secp256k1-PJCSE4N6.cjs +30 -0
  88. package/dist/secp256k1-PJCSE4N6.cjs.map +1 -0
  89. package/dist/tokens.d.ts +2 -1
  90. package/dist/tokens.d.ts.map +1 -1
  91. package/dist/types/chain-config.d.ts +50 -0
  92. package/dist/types/chain-config.d.ts.map +1 -0
  93. package/dist/types/chain.d.ts +11 -0
  94. package/dist/types/chain.d.ts.map +1 -0
  95. package/dist/types/dex-quote.d.ts +85 -0
  96. package/dist/types/dex-quote.d.ts.map +1 -0
  97. package/dist/types/index.d.ts +15 -0
  98. package/dist/types/index.d.ts.map +1 -0
  99. package/dist/types/network-fees.d.ts +43 -0
  100. package/dist/types/network-fees.d.ts.map +1 -0
  101. package/dist/types/quote.d.ts +87 -0
  102. package/dist/types/quote.d.ts.map +1 -0
  103. package/dist/types/swap-pairs.d.ts +37 -0
  104. package/dist/types/swap-pairs.d.ts.map +1 -0
  105. package/dist/version.d.ts +2 -2
  106. package/dist/wallet-4YGTKWNZ.js +7 -0
  107. package/dist/wallet-4YGTKWNZ.js.map +1 -0
  108. package/dist/wallet-SZJDXAY7.cjs +45 -0
  109. package/dist/wallet-SZJDXAY7.cjs.map +1 -0
  110. package/package.json +23 -10
  111. package/dist/api/client.js +0 -13
  112. package/dist/api/client.js.map +0 -1
  113. package/dist/arkade-network.js +0 -40
  114. package/dist/arkade-network.js.map +0 -1
  115. package/dist/arkade.js +0 -61
  116. package/dist/arkade.js.map +0 -1
  117. package/dist/cctp/attestation.js +0 -149
  118. package/dist/cctp/attestation.js.map +0 -1
  119. package/dist/cctp/constants.js +0 -92
  120. package/dist/cctp/constants.js.map +0 -1
  121. package/dist/cctp/fee.js +0 -70
  122. package/dist/cctp/fee.js.map +0 -1
  123. package/dist/cctp/index.js +0 -13
  124. package/dist/cctp/index.js.map +0 -1
  125. package/dist/cctp/types.js +0 -5
  126. package/dist/cctp/types.js.map +0 -1
  127. package/dist/cctp/utils.js +0 -91
  128. package/dist/cctp/utils.js.map +0 -1
  129. package/dist/cctp-bridge/bridge.js +0 -74
  130. package/dist/cctp-bridge/bridge.js.map +0 -1
  131. package/dist/cctp-inbound/approveAndBurn.js +0 -95
  132. package/dist/cctp-inbound/approveAndBurn.js.map +0 -1
  133. package/dist/cctp-inbound/burn.js +0 -66
  134. package/dist/cctp-inbound/burn.js.map +0 -1
  135. package/dist/cctp-inbound/chainMap.js +0 -128
  136. package/dist/cctp-inbound/chainMap.js.map +0 -1
  137. package/dist/cctp-inbound/client.js +0 -125
  138. package/dist/cctp-inbound/client.js.map +0 -1
  139. package/dist/cctp-inbound/eip2612.js +0 -95
  140. package/dist/cctp-inbound/eip2612.js.map +0 -1
  141. package/dist/cctp-inbound/evmSignerAdapter.js +0 -82
  142. package/dist/cctp-inbound/evmSignerAdapter.js.map +0 -1
  143. package/dist/cctp-inbound/fundSwap.js +0 -95
  144. package/dist/cctp-inbound/fundSwap.js.map +0 -1
  145. package/dist/cctp-inbound/index.js +0 -34
  146. package/dist/cctp-inbound/index.js.map +0 -1
  147. package/dist/cctp-inbound/permit2.js +0 -32
  148. package/dist/cctp-inbound/permit2.js.map +0 -1
  149. package/dist/cctp-inbound/preflight.js +0 -84
  150. package/dist/cctp-inbound/preflight.js.map +0 -1
  151. package/dist/cctp-inbound/recover.js +0 -177
  152. package/dist/cctp-inbound/recover.js.map +0 -1
  153. package/dist/cctp-inbound/smartAccount.js +0 -117
  154. package/dist/cctp-inbound/smartAccount.js.map +0 -1
  155. package/dist/cctp-inbound/submit.js +0 -136
  156. package/dist/cctp-inbound/submit.js.map +0 -1
  157. package/dist/cctp-inbound/types.js +0 -5
  158. package/dist/cctp-inbound/types.js.map +0 -1
  159. package/dist/cctp-inbound/userOp.js +0 -151
  160. package/dist/cctp-inbound/userOp.js.map +0 -1
  161. package/dist/client.js +0 -4126
  162. package/dist/client.js.map +0 -1
  163. package/dist/create/arkade-to-lightning.js +0 -76
  164. package/dist/create/arkade-to-lightning.js.map +0 -1
  165. package/dist/create/arkade.js +0 -86
  166. package/dist/create/arkade.js.map +0 -1
  167. package/dist/create/bitcoin-to-arkade.js +0 -76
  168. package/dist/create/bitcoin-to-arkade.js.map +0 -1
  169. package/dist/create/bitcoin.js +0 -77
  170. package/dist/create/bitcoin.js.map +0 -1
  171. package/dist/create/evm-to-arkade.js +0 -80
  172. package/dist/create/evm-to-arkade.js.map +0 -1
  173. package/dist/create/evm-to-bitcoin.js +0 -82
  174. package/dist/create/evm-to-bitcoin.js.map +0 -1
  175. package/dist/create/evm-to-lightning.js +0 -89
  176. package/dist/create/evm-to-lightning.js.map +0 -1
  177. package/dist/create/index.js +0 -21
  178. package/dist/create/index.js.map +0 -1
  179. package/dist/create/lightning-to-arkade.js +0 -69
  180. package/dist/create/lightning-to-arkade.js.map +0 -1
  181. package/dist/create/lightning.js +0 -61
  182. package/dist/create/lightning.js.map +0 -1
  183. package/dist/create/retry.js +0 -71
  184. package/dist/create/retry.js.map +0 -1
  185. package/dist/create/types.js +0 -5
  186. package/dist/create/types.js.map +0 -1
  187. package/dist/escrow/index.js +0 -115
  188. package/dist/escrow/index.js.map +0 -1
  189. package/dist/esplora.js +0 -47
  190. package/dist/esplora.js.map +0 -1
  191. package/dist/evm/coordinator.js +0 -714
  192. package/dist/evm/coordinator.js.map +0 -1
  193. package/dist/evm/htlc.js +0 -278
  194. package/dist/evm/htlc.js.map +0 -1
  195. package/dist/evm/index.js +0 -10
  196. package/dist/evm/index.js.map +0 -1
  197. package/dist/evm/signing.js +0 -70
  198. package/dist/evm/signing.js.map +0 -1
  199. package/dist/evm/wallet.js +0 -133
  200. package/dist/evm/wallet.js.map +0 -1
  201. package/dist/generated/api.js +0 -6
  202. package/dist/generated/api.js.map +0 -1
  203. package/dist/logging.js +0 -130
  204. package/dist/logging.js.map +0 -1
  205. package/dist/price-calculations.js +0 -135
  206. package/dist/price-calculations.js.map +0 -1
  207. package/dist/redeem/arkade.js +0 -416
  208. package/dist/redeem/arkade.js.map +0 -1
  209. package/dist/redeem/ethereum.js +0 -206
  210. package/dist/redeem/ethereum.js.map +0 -1
  211. package/dist/redeem/gasless.js +0 -73
  212. package/dist/redeem/gasless.js.map +0 -1
  213. package/dist/redeem/index.js +0 -189
  214. package/dist/redeem/index.js.map +0 -1
  215. package/dist/redeem/types.js +0 -36
  216. package/dist/redeem/types.js.map +0 -1
  217. package/dist/refund/arkade.js +0 -200
  218. package/dist/refund/arkade.js.map +0 -1
  219. package/dist/refund/collab-arkade-evm.js +0 -307
  220. package/dist/refund/collab-arkade-evm.js.map +0 -1
  221. package/dist/refund/collab-arkade-lightning.js +0 -245
  222. package/dist/refund/collab-arkade-lightning.js.map +0 -1
  223. package/dist/refund/index.js +0 -12
  224. package/dist/refund/index.js.map +0 -1
  225. package/dist/refund/onchain.js +0 -366
  226. package/dist/refund/onchain.js.map +0 -1
  227. package/dist/signer/index.js +0 -249
  228. package/dist/signer/index.js.map +0 -1
  229. package/dist/storage/idb.js +0 -236
  230. package/dist/storage/idb.js.map +0 -1
  231. package/dist/storage/index.js +0 -98
  232. package/dist/storage/index.js.map +0 -1
  233. package/dist/storage/sqlite.js +0 -206
  234. package/dist/storage/sqlite.js.map +0 -1
  235. package/dist/storage/types.js +0 -9
  236. package/dist/storage/types.js.map +0 -1
  237. package/dist/tokens.js +0 -322
  238. package/dist/tokens.js.map +0 -1
  239. package/dist/usd-price.js +0 -96
  240. package/dist/usd-price.js.map +0 -1
  241. package/dist/usdt0-bridge/constants.js +0 -71
  242. package/dist/usdt0-bridge/constants.js.map +0 -1
  243. package/dist/usdt0-bridge/index.js +0 -11
  244. package/dist/usdt0-bridge/index.js.map +0 -1
  245. package/dist/usdt0-bridge/tracking.js +0 -123
  246. package/dist/usdt0-bridge/tracking.js.map +0 -1
  247. package/dist/usdt0-bridge/utils.js +0 -51
  248. package/dist/usdt0-bridge/utils.js.map +0 -1
  249. package/dist/version.js +0 -6
  250. package/dist/version.js.map +0 -1
  251. package/dist/ws.js +0 -145
  252. package/dist/ws.js.map +0 -1
package/dist/client.js DELETED
@@ -1,4126 +0,0 @@
1
- import { createApiClient, } from "./api/client.js";
2
- import { getVhtlcAmounts } from "./arkade.js";
3
- import { USDC_ADDRESSES } from "./cctp/constants.js";
4
- import { computeCctpFastFee, getCachedCctpFee } from "./cctp/fee.js";
5
- import { cctpMetaForChainId, isCctpOnlySource, } from "./cctp-inbound/chainMap.js";
6
- import { CctpInboundClient } from "./cctp-inbound/client.js";
7
- import { createArkadeToEvmSwapGeneric, createArkadeToLightningSwap, createBitcoinToArkadeSwap, createBitcoinToEvmSwap, createEvmToArkadeSwapGeneric, createEvmToBitcoinSwap, createEvmToLightningSwapGeneric, createLightningToArkadeSwap, createLightningToEvmSwapGeneric, } from "./create/index.js";
8
- import { delegateClaim, delegateRefund } from "./delegate.js";
9
- import { broadcastTransaction, findOutputByAddress } from "./esplora.js";
10
- import { buildCollabRefundEvmDigest, buildCollabRefundEvmTypedData, buildEip2612PermitDigest, buildPermit2FundingDigest, buildPermit2TypedData, deriveEvmAddress, encodeApproveCallData, encodeExecuteAndCreateWithPermit2, encodeHtlcErc20RefundCallData, PERMIT2_ADDRESS, signEvmDigest, } from "./evm/index.js";
11
- import { decodeUint256, encodeAllowanceCall, encodeBalanceOfCall, encodeMaxApproveData, getRevertReason, parseSignature, simulateTransaction, } from "./evm/wallet.js";
12
- import { createSdkLogger, } from "./logging.js";
13
- import { buildArkadeClaim, continueArkadeClaim, claimViaGasless as gaslessClaim, claim as redeemClaim, } from "./redeem/index.js";
14
- import { buildArkadeRefund, buildOnchainClaimTransaction, buildOnchainRefundTransaction, collabRefundArkadeToEvmDelegate, collabRefundArkadeToEvmOffchain, collabRefundArkadeToLightningOffchain, verifyHtlcAddress, } from "./refund/index.js";
15
- import { bytesToHex, hexToBytes, Signer, } from "./signer/index.js";
16
- import { SWAP_STORAGE_VERSION, } from "./storage/index.js";
17
- import { isArkade, isBridgeOnlyChain, isBtcOnchain, isBtcPegged, isEvmToken, isLightning, isSourceEvmChain, toChainName, } from "./tokens.js";
18
- import { USDT0_ADDRESSES } from "./usdt0-bridge/constants.js";
19
- import { createSwapStatusWatcher, } from "./ws.js";
20
- // Re-export coordinator utilities for Arkade-to-EVM redeemAndExecute flow
21
- export { buildCollabRefundEvmDigest, buildCollabRefundEvmTypedData, buildEip2612PermitDigest, buildPermit2FundingDigest, buildRedeemCalls, buildRedeemDigest, encodeExecuteAndCreateWithPermit2, encodeRedeemAndExecute, encodeRefundAndExecute, encodeRefundTo, PERMIT2_ADDRESS, } from "./evm/index.js";
22
- function isStatusResponse(value) {
23
- return (typeof value === "object" &&
24
- value !== null &&
25
- "healthy" in value &&
26
- "services" in value);
27
- }
28
- const DEFAULT_BASE_URL = "https://api.satora.io/";
29
- /** Default Esplora URLs by network */
30
- const DEFAULT_ESPLORA_URLS = {
31
- mainnet: "https://mempool.space/api",
32
- signet: "https://mutinynet.com/api",
33
- regtest: "http://localhost:3000",
34
- };
35
- /**
36
- * Builder for creating a Lendaswap client with a fluent API.
37
- *
38
- * The `build()` method is async and returns a fully initialized client.
39
- *
40
- * @example
41
- * ```ts
42
- * // Create client with new wallet (generates mnemonic)
43
- * const client = await Client.builder()
44
- * .withSignerStorage(new IdbWalletStorage())
45
- * .build();
46
- *
47
- * // Create client with existing mnemonic
48
- * const client = await Client.builder()
49
- * .withSignerStorage(new IdbWalletStorage())
50
- * .withMnemonic("abandon abandon abandon ...")
51
- * .build();
52
- *
53
- * // Create client from a BIP32 extended private key (ephemeral — never persisted).
54
- * // Useful when the secret lives in an env var, KMS, or external secure store.
55
- * const client = await Client.builder()
56
- * .withXprv("xprv9s21ZrQH143K...")
57
- * .build();
58
- *
59
- * // Create client without storage (stateless, generates new mnemonic)
60
- * const client = await Client.builder().build();
61
- * ```
62
- */
63
- export class ClientBuilder {
64
- #baseUrl = DEFAULT_BASE_URL;
65
- #referralCode;
66
- #defaultHeaders;
67
- #esploraUrl;
68
- #arkadeServerUrl;
69
- #signerStorage;
70
- #swapStorage;
71
- #mnemonic;
72
- #xprv;
73
- #aa;
74
- #logger;
75
- #logLevel;
76
- /**
77
- * Sets the base URL for the API.
78
- * @param baseUrl - The base URL of the Lendaswap API.
79
- * @returns The builder instance for chaining.
80
- */
81
- withBaseUrl(baseUrl) {
82
- this.#baseUrl = baseUrl;
83
- return this;
84
- }
85
- /**
86
- * Sets a default referral code for every `createSwap` / `getQuote`
87
- * call. Per-call `referralCode` always wins.
88
- * @returns The builder instance for chaining.
89
- */
90
- withReferralCode(referralCode) {
91
- this.#referralCode = referralCode;
92
- return this;
93
- }
94
- /**
95
- * @deprecated Use {@link withReferralCode}. Kept as a backwards-compat
96
- * alias for SDK consumers that haven't migrated from the old
97
- * `X-Org-Code` model — both sides flow through the same per-call
98
- * `referral_code` plumbing now.
99
- */
100
- withOrgCode(orgCode) {
101
- return this.withReferralCode(orgCode);
102
- }
103
- /**
104
- * Sets default headers to send with SDK API requests.
105
- * @param headers - Headers merged into API requests made by the SDK.
106
- * @returns The builder instance for chaining.
107
- */
108
- withDefaultHeaders(headers) {
109
- this.#defaultHeaders = {
110
- ...this.#defaultHeaders,
111
- ...headers,
112
- };
113
- return this;
114
- }
115
- /**
116
- * Sets the Esplora API URL for broadcasting Bitcoin transactions.
117
- *
118
- * If not set, defaults will be used based on the network:
119
- * - mainnet: https://mempool.space/api
120
- * - testnet: https://mempool.space/testnet/api
121
- * - signet: https://mempool.space/signet/api
122
- *
123
- * @param esploraUrl - The Esplora API base URL.
124
- * @returns The builder instance for chaining.
125
- */
126
- withEsploraUrl(esploraUrl) {
127
- this.#esploraUrl = esploraUrl;
128
- return this;
129
- }
130
- /**
131
- * Sets the Arkade server URL for VHTLC operations (claim, refund, amounts).
132
- *
133
- * If not set, defaults are used based on the network:
134
- * - bitcoin: https://arkade.computer
135
- * - signet: wa
136
- *
137
- * @param arkadeServerUrl - The Arkade server base URL.
138
- * @returns The builder instance for chaining.
139
- */
140
- withArkadeServerUrl(arkadeServerUrl) {
141
- this.#arkadeServerUrl = arkadeServerUrl;
142
- return this;
143
- }
144
- /**
145
- * Sets the storage backend for signer data (mnemonic and key index).
146
- * @param storage - The storage implementation to use.
147
- * @returns The builder instance for chaining.
148
- */
149
- withSignerStorage(storage) {
150
- this.#signerStorage = storage;
151
- return this;
152
- }
153
- /**
154
- * Sets the storage backend for swap data.
155
- *
156
- * When configured, swaps will be automatically persisted after creation.
157
- *
158
- * @param storage - The swap storage implementation to use.
159
- * @returns The builder instance for chaining.
160
- */
161
- withSwapStorage(storage) {
162
- this.#swapStorage = storage;
163
- return this;
164
- }
165
- /**
166
- * Sets the mnemonic phrase to use for the signer.
167
- *
168
- * If provided, this mnemonic will be used instead of loading from storage
169
- * or generating a new one. The mnemonic will be persisted to storage if
170
- * storage is configured.
171
- *
172
- * @param mnemonic - The BIP39 mnemonic phrase (12, 15, 18, 21, or 24 words).
173
- * @returns The builder instance for chaining.
174
- */
175
- withMnemonic(mnemonic) {
176
- this.#mnemonic = mnemonic;
177
- return this;
178
- }
179
- /**
180
- * Sets a BIP32 extended private key (xprv) to use for the signer.
181
- *
182
- * The xprv is treated as ephemeral: it is **never** persisted to signer
183
- * storage, even if storage is configured. The caller is responsible for
184
- * supplying it on every `build()` (e.g. from an env var, KMS, or vault).
185
- * Storage, if configured, is still used for the swap key index counter.
186
- *
187
- * Cannot be combined with `withMnemonic()`.
188
- *
189
- * @param xprv - The base58check-encoded extended private key.
190
- * @returns The builder instance for chaining.
191
- * @throws Error if `xprv` is empty or whitespace-only — fail fast on a
192
- * misconfigured secret rather than silently falling back to storage.
193
- */
194
- withXprv(xprv) {
195
- if (typeof xprv !== "string" || xprv.trim().length === 0) {
196
- throw new Error("withXprv() requires a non-empty xprv string");
197
- }
198
- this.#xprv = xprv;
199
- return this;
200
- }
201
- /**
202
- * Sets the account-abstraction config (bundler + Gas Manager policy).
203
- *
204
- * Required when using the CCTP-inbound swap flow (any non-Arbitrum
205
- * EVM chain as the source). The settlement UserOp — `receiveMessage`
206
- * + `USDC.approve(Permit2)` + `executeAndCreateWithPermit2` — is
207
- * submitted via the Kernel smart account owned by the consumer's
208
- * connected wallet, sponsored by the Gas Manager so users need no
209
- * ETH on Arbitrum.
210
- *
211
- * @param aa - Bundler URL + paymaster policy id.
212
- * @returns The builder instance for chaining.
213
- */
214
- withAa(aa) {
215
- this.#aa = aa;
216
- return this;
217
- }
218
- /**
219
- * Sets the logger sink used by the SDK.
220
- *
221
- * The SDK is silent by default. Pair this with `withLogLevel()` to opt in
222
- * to runtime logs.
223
- */
224
- withLogger(logger) {
225
- this.#logger = logger;
226
- return this;
227
- }
228
- /**
229
- * Sets the minimum log level emitted by the SDK.
230
- *
231
- * Defaults to `silent`.
232
- */
233
- withLogLevel(logLevel) {
234
- this.#logLevel = logLevel;
235
- return this;
236
- }
237
- /**
238
- * Builds and returns a fully initialized Client instance.
239
- *
240
- * Initialization order:
241
- * 1. If `withXprv()` was called, use that xprv (ephemeral, never stored)
242
- * 2. Else if `withMnemonic()` was called, use that mnemonic
243
- * 3. Else if storage is configured and contains a mnemonic, load it
244
- * 4. Else generate a new mnemonic
245
- *
246
- * The mnemonic is persisted to storage if storage is configured. An xprv
247
- * is never persisted.
248
- *
249
- * @returns A promise that resolves to a fully initialized Client.
250
- * @throws Error if the provided mnemonic or xprv is invalid, or if both
251
- * `withMnemonic()` and `withXprv()` were called.
252
- */
253
- async build() {
254
- if (this.#xprv && this.#mnemonic) {
255
- throw new Error("withMnemonic() and withXprv() are mutually exclusive — pick one");
256
- }
257
- let signer;
258
- if (this.#xprv) {
259
- // Ephemeral xprv — never touch signer storage for the secret
260
- signer = Signer.fromXprv(this.#xprv);
261
- }
262
- else if (this.#mnemonic) {
263
- // Use provided mnemonic
264
- signer = Signer.fromMnemonic(this.#mnemonic);
265
- if (this.#signerStorage) {
266
- await this.#signerStorage.setMnemonic(this.#mnemonic);
267
- }
268
- }
269
- else if (this.#signerStorage) {
270
- // Try to load from storage
271
- const storedMnemonic = await this.#signerStorage.getMnemonic();
272
- if (storedMnemonic) {
273
- signer = Signer.fromMnemonic(storedMnemonic);
274
- }
275
- else {
276
- // Generate new and persist
277
- signer = Signer.generate();
278
- // Signer.generate() always populates mnemonic
279
- await this.#signerStorage.setMnemonic(signer.mnemonic);
280
- }
281
- }
282
- else {
283
- // No storage, generate new (stateless mode)
284
- signer = Signer.generate();
285
- }
286
- return new Client({
287
- baseUrl: this.#baseUrl.replace(/\/+$/, ""),
288
- referralCode: this.#referralCode,
289
- defaultHeaders: this.#defaultHeaders,
290
- esploraUrl: this.#esploraUrl?.replace(/\/+$/, ""),
291
- arkadeServerUrl: this.#arkadeServerUrl?.replace(/\/+$/, ""),
292
- aa: this.#aa,
293
- logger: this.#logger,
294
- logLevel: this.#logLevel,
295
- }, signer, this.#signerStorage, this.#swapStorage);
296
- }
297
- }
298
- /**
299
- * Main client for interacting with the Lendaswap API.
300
- *
301
- * The client manages:
302
- * - API communication
303
- * - Signer (HD wallet) for key derivation
304
- * - Storage for persisting mnemonic and key index
305
- *
306
- * Use `Client.builder()` to create a new instance.
307
- *
308
- * @example
309
- * ```ts
310
- * const client = await Client.builder()
311
- * .withSignerStorage(new IdbWalletStorage())
312
- * .build();
313
- *
314
- * // Get mnemonic (for backup)
315
- * const mnemonic = client.getMnemonic();
316
- *
317
- * // Derive swap parameters
318
- * const params = await client.deriveSwapParams();
319
- * ```
320
- */
321
- export class Client {
322
- #apiClient;
323
- #config;
324
- #signer;
325
- #signerStorage;
326
- #swapStorage;
327
- #statusWatcher = null;
328
- #cctpInbound = null;
329
- #logger;
330
- /**
331
- * Creates a new Client instance.
332
- *
333
- * Use `Client.builder()` instead of calling this constructor directly.
334
- *
335
- * @internal
336
- */
337
- constructor(config, signer, signerStorage, swapStorage) {
338
- this.#config = config;
339
- this.#apiClient = createApiClient({
340
- baseUrl: config.baseUrl,
341
- defaultHeaders: config.defaultHeaders,
342
- });
343
- this.#signer = signer;
344
- this.#signerStorage = signerStorage;
345
- this.#swapStorage = swapStorage;
346
- this.#logger = createSdkLogger({
347
- logger: config.logger,
348
- logLevel: config.logLevel,
349
- });
350
- }
351
- /**
352
- * Subscribe to status updates for one or more swaps over a shared WebSocket.
353
- * The socket is opened lazily on first call and closed automatically when
354
- * the last subscriber unsubscribes.
355
- *
356
- * @param swapIds Swap ids to watch.
357
- * @param onUpdate Fires with `(swapId, status)` for every status change of
358
- * any id in the set.
359
- * @returns Unsubscribe function that removes `onUpdate` from all of the
360
- * passed ids at once.
361
- */
362
- subscribeToSwaps(swapIds, onUpdate) {
363
- if (!this.#statusWatcher) {
364
- this.#statusWatcher = createSwapStatusWatcher(this.#config.baseUrl);
365
- }
366
- return this.#statusWatcher.subscribe(swapIds, onUpdate);
367
- }
368
- /**
369
- * Remove `onUpdate` from the given swap ids.
370
- */
371
- unsubscribeFromSwaps(swapIds, onUpdate) {
372
- this.#statusWatcher?.unsubscribe(swapIds, onUpdate);
373
- }
374
- /** Force-close the shared swap-status socket. */
375
- closeSwapStatusSocket() {
376
- this.#statusWatcher?.close();
377
- this.#statusWatcher = null;
378
- }
379
- /**
380
- * Creates a new ClientBuilder for fluent configuration.
381
- * @returns A new ClientBuilder instance.
382
- */
383
- static builder() {
384
- return new ClientBuilder();
385
- }
386
- /** The underlying typed API client for direct API access. */
387
- get api() {
388
- return this.#apiClient;
389
- }
390
- /** The base URL of the API. */
391
- get baseUrl() {
392
- return this.#config.baseUrl;
393
- }
394
- /**
395
- * Namespace for CCTP-inbound swap primitives (source-chain burn,
396
- * IRIS attestation, settlement UserOp). Requires `withAa(...)` on
397
- * the builder — throws with a clear error otherwise.
398
- *
399
- * For simple integrations prefer `Client.fundSwap(swapId, signer)`
400
- * which auto-dispatches to the CCTP path when needed; drop down to
401
- * `client.cctpInbound.*` when you need step-by-step progress control.
402
- */
403
- get cctpInbound() {
404
- if (!this.#cctpInbound) {
405
- if (!this.#config.aa) {
406
- throw new Error("CCTP-inbound flow requires AA config. Call `.withAa({ bundlerUrl, paymasterPolicyId })` on the ClientBuilder before `.build()`.");
407
- }
408
- this.#cctpInbound = new CctpInboundClient({
409
- apiClient: this.#apiClient,
410
- aa: this.#config.aa,
411
- logger: this.#config.logger,
412
- logLevel: this.#config.logLevel,
413
- });
414
- }
415
- return this.#cctpInbound;
416
- }
417
- /** The swap storage, if configured. */
418
- get swapStorage() {
419
- return this.#swapStorage;
420
- }
421
- // =========================================================================
422
- // Signer Methods
423
- // =========================================================================
424
- /**
425
- * Gets the mnemonic phrase.
426
- *
427
- * Store this securely - it's the only way to recover the wallet.
428
- *
429
- * @returns The BIP39 mnemonic phrase.
430
- * @throws Error if the wallet was initialized from an xprv (no mnemonic exists).
431
- */
432
- getMnemonic() {
433
- const mnemonic = this.#signer.mnemonic;
434
- if (!mnemonic) {
435
- throw new Error("No mnemonic available — wallet was initialized from an xprv");
436
- }
437
- return mnemonic;
438
- }
439
- /**
440
- * Loads a mnemonic phrase, replacing the current signer.
441
- *
442
- * The new mnemonic is persisted to storage if storage is configured.
443
- *
444
- * @param mnemonic - The BIP39 mnemonic phrase to load.
445
- * @throws Error if the mnemonic is invalid.
446
- */
447
- async loadMnemonic(mnemonic) {
448
- this.#signer = Signer.fromMnemonic(mnemonic);
449
- if (this.#signerStorage) {
450
- await this.#signerStorage.setMnemonic(mnemonic);
451
- }
452
- }
453
- /**
454
- * Derives a deterministic Nostr private key from the wallet mnemonic.
455
- *
456
- * Uses the NIP-06 derivation path so the same mnemonic always produces
457
- * the same Nostr identity.
458
- *
459
- * @returns The 32-byte Nostr private key as a hex string.
460
- */
461
- getNostrKeyHex() {
462
- return this.#signer.deriveNostrKeyHex();
463
- }
464
- /**
465
- * Gets the user ID extended public key for wallet recovery.
466
- *
467
- * This can be shared with the server for recovering swap history.
468
- *
469
- * @returns The hex-encoded user ID xpub.
470
- */
471
- getUserIdXpub() {
472
- return this.#signer.getUserIdXpubString();
473
- }
474
- /**
475
- * Derives swap parameters at the next available index.
476
- *
477
- * Automatically increments the key index in storage (if configured).
478
- *
479
- * @returns The derived swap parameters.
480
- */
481
- async deriveSwapParams() {
482
- let index = 0;
483
- if (this.#signerStorage) {
484
- index = await this.#signerStorage.incrementKeyIndex();
485
- }
486
- return this.#signer.deriveSwapParams(index);
487
- }
488
- /**
489
- * Derives swap parameters at a specific index.
490
- *
491
- * Does not modify the stored key index. Useful for recovery scenarios.
492
- *
493
- * @param index - The key index to derive.
494
- * @returns The derived swap parameters.
495
- */
496
- deriveSwapParamsAtIndex(index) {
497
- return this.#signer.deriveSwapParams(index);
498
- }
499
- /**
500
- * Get the deterministic EVM address for this SDK key.
501
- *
502
- * This address is reused across all gasless swaps, so a single
503
- * Permit2 approval is sufficient for multiple swaps.
504
- *
505
- * @returns Checksummed EVM address (0x-prefixed).
506
- */
507
- getEvmAddress() {
508
- const { secretKey } = this.#signer.deriveEvmKey();
509
- return deriveEvmAddress(secretKey);
510
- }
511
- /**
512
- * Get the EVM signing key, derived deterministically from the mnemonic.
513
- *
514
- * @internal
515
- */
516
- #getEvmSigningKey() {
517
- return bytesToHex(this.#signer.deriveEvmKey().secretKey);
518
- }
519
- /**
520
- * Gets the current key index from storage.
521
- * @returns The current key index, or 0 if no storage is configured.
522
- */
523
- async getKeyIndex() {
524
- if (this.#signerStorage) {
525
- return this.#signerStorage.getKeyIndex();
526
- }
527
- return 0;
528
- }
529
- /**
530
- * Sets the key index in storage.
531
- *
532
- * Useful for recovery scenarios where you need to set the index
533
- * to a specific value.
534
- *
535
- * @param index - The new key index.
536
- * @throws Error if no storage is configured.
537
- */
538
- async setKeyIndex(index) {
539
- if (!this.#signerStorage) {
540
- throw new Error("No signer storage configured");
541
- }
542
- await this.#signerStorage.setKeyIndex(index);
543
- }
544
- // =========================================================================
545
- // Health & Info
546
- // =========================================================================
547
- /**
548
- * Gets the aggregated status of the API and its external dependencies.
549
- *
550
- * Returns the status body for both healthy (200) and unhealthy (503) responses.
551
- * @throws Error if the status response is missing or malformed.
552
- */
553
- async getStatus() {
554
- const { data, error } = await this.#apiClient.GET("/status");
555
- if (error) {
556
- // openapi-fetch puts non-2xx response bodies in `error`.
557
- // /status returns StatusResponse for both 200 and expected 503 responses.
558
- if (isStatusResponse(error)) {
559
- return error;
560
- }
561
- throw new Error(`Failed to get status: ${JSON.stringify(error)}`);
562
- }
563
- return data;
564
- }
565
- /**
566
- * Checks the health status of the API.
567
- * @deprecated Use {@link getStatus} for detailed dependency health.
568
- * @returns A promise that resolves to "ok" if the API is healthy.
569
- * @throws Error if the health check fails.
570
- */
571
- async healthCheck() {
572
- const { data, error } = await this.#apiClient.GET("/health");
573
- if (error) {
574
- throw new Error(`Health check failed: ${JSON.stringify(error)}`);
575
- }
576
- return data ?? "ok";
577
- }
578
- /**
579
- * Gets the version information of the API.
580
- * @returns A promise that resolves to the version info containing tag and commit hash.
581
- * @throws Error if the request fails.
582
- */
583
- async getVersion() {
584
- const { data, error } = await this.#apiClient.GET("/version");
585
- if (error) {
586
- throw new Error(`Failed to get version: ${JSON.stringify(error)}`);
587
- }
588
- if (!data) {
589
- throw new Error("No version data returned");
590
- }
591
- return data;
592
- }
593
- /**
594
- * Gets the list of support agent npubs from the backend config.
595
- * @returns A promise that resolves to an array of support agent info objects.
596
- * @throws Error if the request fails.
597
- */
598
- async getSupportAgents() {
599
- const baseUrl = this.#config.baseUrl.replace(/\/$/, "");
600
- const resp = await fetch(`${baseUrl}/support-agents`);
601
- if (!resp.ok) {
602
- throw new Error(`Failed to get support agents: ${resp.status}`);
603
- }
604
- const data = await resp.json();
605
- return data.agents;
606
- }
607
- /**
608
- * Gets the current Median Time Past (MTP) and tip block height.
609
- * @returns A promise that resolves to the MTP timestamp and tip height.
610
- * @throws Error if the request fails or MTP is not yet available.
611
- */
612
- async getMtp() {
613
- const { data, error } = await this.#apiClient.GET("/mtp");
614
- if (error) {
615
- throw new Error(`Failed to get MTP: ${JSON.stringify(error)}`);
616
- }
617
- if (!data) {
618
- throw new Error("MTP not available yet");
619
- }
620
- return data;
621
- }
622
- // =========================================================================
623
- // Tokens & Asset Pairs
624
- // =========================================================================
625
- /**
626
- * Gets the list of supported tokens.
627
- * @returns A promise that resolves to an array of token information.
628
- * @throws Error if the request fails.
629
- */
630
- async getTokens() {
631
- const { data, error } = await this.#apiClient.GET("/tokens");
632
- if (error || !data) {
633
- throw new Error(`Failed to get tokens: ${JSON.stringify(error)}`);
634
- }
635
- return data;
636
- }
637
- /**
638
- * Gets all supported swap pairs with their limits (in satoshis) and base fee percentages.
639
- * @returns A promise that resolves to all swap pairs with limits and fees.
640
- * @throws Error if the request fails.
641
- */
642
- async getSwapPairs() {
643
- const { data, error } = await this.#apiClient.GET("/swap-pairs");
644
- if (error || !data) {
645
- throw new Error(`Failed to get swap pairs: ${JSON.stringify(error)}`);
646
- }
647
- return data;
648
- }
649
- // =========================================================================
650
- // Quotes
651
- // =========================================================================
652
- /**
653
- * Gets a quote for swapping between two tokens.
654
- * @param params - Quote parameters.
655
- * @param params.sourceChain - Source blockchain (e.g., "Arkade", "Polygon").
656
- * @param params.sourceToken - Source token: contract address for EVM tokens, or "btc" for BTC.
657
- * @param params.targetChain - Target blockchain (e.g., "Polygon", "Lightning").
658
- * @param params.targetToken - Target token: contract address for EVM tokens, or "btc" for BTC.
659
- * @param params.sourceAmount - Amount in smallest unit of source token (mutually exclusive with targetAmount).
660
- * @param params.targetAmount - Amount in smallest unit of target token (mutually exclusive with sourceAmount).
661
- * @param params.referralCode - Optional referral code to apply referral pricing to the quote.
662
- * @returns A promise that resolves to the quote response with pricing details.
663
- * @throws Error if the request fails.
664
- */
665
- async getQuote(params) {
666
- // If the target is a bridge-only chain (e.g. USDC on Base), remap the
667
- // quote request to the token on Arbitrum (source chain the backend knows).
668
- // Pass bridge_target_chain so the backend includes the bridge fee.
669
- let targetChain = params.targetChain;
670
- let targetToken = params.targetToken;
671
- let bridgeTargetChain;
672
- if (isBridgeOnlyChain(targetChain)) {
673
- bridgeTargetChain = toChainName(targetChain);
674
- // Determine if this is a USDT0 or USDC bridge token by checking
675
- // if the target token matches a known USDT0 address on the destination.
676
- const isUsdt0 = Object.values(USDT0_ADDRESSES).some((addr) => addr.toLowerCase() === params.targetToken.toLowerCase());
677
- // Remap to Arbitrum for the DEX quote (cheapest gas, good liquidity).
678
- targetChain = "42161";
679
- targetToken = isUsdt0
680
- ? USDT0_ADDRESSES.Arbitrum
681
- : USDC_ADDRESSES.Arbitrum;
682
- }
683
- // CCTP-only source (USDC on Optimism / Base / Linea / …): the quote
684
- // endpoint expects the source chain/token the DEX actually runs on
685
- // (Arbitrum + native USDC), plus `bridge_source_chain` + address so
686
- // the backend can apply the CCTPv2 fast-transfer fee. All gross-vs-net
687
- // math happens server-side.
688
- let sourceChain = params.sourceChain;
689
- let sourceToken = params.sourceToken;
690
- let bridgeSourceChain;
691
- let bridgeSourceTokenAddress;
692
- const parsedSourceChainId = Number.parseInt(params.sourceChain, 10);
693
- if (!Number.isNaN(parsedSourceChainId) &&
694
- isCctpOnlySource(parsedSourceChainId)) {
695
- const source = cctpMetaForChainId(parsedSourceChainId);
696
- if (params.sourceToken.toLowerCase() !== source.usdc.toLowerCase()) {
697
- throw new Error(`Quote on ${source.name} requires native USDC (${source.usdc}); got ${params.sourceToken}. Only USDC is bridgeable via CCTP.`);
698
- }
699
- bridgeSourceChain = source.name;
700
- bridgeSourceTokenAddress = source.usdc;
701
- sourceChain = "42161";
702
- sourceToken = USDC_ADDRESSES.Arbitrum;
703
- }
704
- const query = {
705
- source_chain: sourceChain,
706
- source_token: sourceToken,
707
- target_chain: targetChain,
708
- target_token: targetToken,
709
- source_amount: params.sourceAmount,
710
- target_amount: params.targetAmount,
711
- bridge_target_chain: bridgeTargetChain,
712
- bridge_source_chain: bridgeSourceChain,
713
- bridge_source_token_address: bridgeSourceTokenAddress,
714
- bridge_recipient_setup: params.bridgeRecipientSetup,
715
- // Per-call referralCode wins; fall back to the client config default
716
- // (set via withReferralCode / withOrgCode).
717
- ref: params.referralCode ?? this.#config.referralCode,
718
- extra_fees: params.extraFees,
719
- };
720
- const { data, error } = await this.#apiClient.GET("/quote", {
721
- params: { query },
722
- });
723
- if (error) {
724
- throw new Error(`Failed to get quote: ${JSON.stringify(error)}`);
725
- }
726
- if (!data) {
727
- throw new Error("No quote data returned");
728
- }
729
- return data;
730
- }
731
- // =========================================================================
732
- // Swap Status
733
- // =========================================================================
734
- /**
735
- * Gets the status and details of a swap by its ID.
736
- * @param id - The UUID of the swap.
737
- * @param options - Optional settings.
738
- * @param options.updateStorage - If true, updates the swap in storage after fetching.
739
- * @returns A promise that resolves to the swap details.
740
- * @throws Error if the request fails or swap is not found.
741
- */
742
- async getSwap(id, options) {
743
- const { data, error } = await this.#apiClient.GET("/swap/{id}", {
744
- params: { path: { id } },
745
- });
746
- if (error) {
747
- throw new Error(`Failed to get swap: ${JSON.stringify(error)}`);
748
- }
749
- if (!data) {
750
- throw new Error("No swap data returned");
751
- }
752
- if (options?.updateStorage && this.#swapStorage) {
753
- await this.#swapStorage.update(id, data);
754
- }
755
- return data;
756
- }
757
- /**
758
- * Gets a swap from local storage without making a server request.
759
- *
760
- * Use this when you need swap data but don't need the latest status
761
- * from the server. The stored swap includes the preimage, keys, and
762
- * the last known swap response.
763
- *
764
- * @param id - The UUID of the swap.
765
- * @returns The stored swap data, or null if not found.
766
- *
767
- * @example
768
- * ```ts
769
- * const stored = await client.getStoredSwap(swapId);
770
- * if (stored) {
771
- * console.log("Target:", stored.response.target_token);
772
- * console.log("Status:", stored.response.status);
773
- * }
774
- * ```
775
- */
776
- async getStoredSwap(id) {
777
- if (!this.#swapStorage) {
778
- return null;
779
- }
780
- return this.#swapStorage.get(id);
781
- }
782
- /**
783
- * Gets all stored swaps from local storage.
784
- *
785
- * @returns Array of all stored swap data, or empty array if no storage is configured.
786
- */
787
- async listAllSwaps() {
788
- if (!this.#swapStorage) {
789
- return [];
790
- }
791
- return this.#swapStorage.getAll();
792
- }
793
- async deleteSwap(id) {
794
- if (!this.#swapStorage) {
795
- return;
796
- }
797
- await this.#swapStorage.delete(id);
798
- }
799
- async clearSwapStorage() {
800
- if (!this.#swapStorage) {
801
- return;
802
- }
803
- await this.#swapStorage.clear();
804
- }
805
- /**
806
- * Recovers all swaps associated with the current wallet from the server.
807
- *
808
- * Sends the user's xpub to the server, which returns all swaps belonging
809
- * to that wallet. For each recovered swap, re-derives the keys using the
810
- * swap's derivation index and stores it locally.
811
- *
812
- * After recovery, the key index is set to `next_index` so that new swaps
813
- * don't reuse derivation indices.
814
- *
815
- * @returns Recovery metadata and the recovered swaps stored locally.
816
- */
817
- async recoverSwaps(options = {}) {
818
- const xpub = this.getUserIdXpub();
819
- this.#logger.info({
820
- event: "client.recover.start",
821
- message: "Recovering swaps for wallet",
822
- data: { xpub, startIndex: options.startIndex },
823
- });
824
- const { data, error } = await this.#apiClient.POST("/swap/recover", {
825
- body: { xpub, start_index: options.startIndex },
826
- });
827
- if (error) {
828
- throw new Error(`Failed to recover swaps: ${JSON.stringify(error)}`);
829
- }
830
- if (!data) {
831
- throw new Error("No recovery data returned");
832
- }
833
- const storedSwaps = [];
834
- this.#logger.debug({
835
- event: "client.recover.response",
836
- message: "Recovered swap data from server",
837
- data: {
838
- recoveredSwapCount: data.swaps.length,
839
- highestIndex: data.highest_index,
840
- nextIndex: data.next_index,
841
- scannedUntil: data.scanned_until,
842
- complete: data.complete,
843
- },
844
- });
845
- for (const recoveredSwap of data.swaps) {
846
- const { index, ...response } = recoveredSwap;
847
- const swapParams = this.deriveSwapParamsAtIndex(index);
848
- await this.#storeSwap(response.id, swapParams, response);
849
- const stored = await this.getStoredSwap(response.id);
850
- if (stored) {
851
- storedSwaps.push(stored);
852
- }
853
- }
854
- // Update key index so new swaps don't reuse indices, but never regress an
855
- // existing local index. Recovery may be run from an old startIndex or return
856
- // no swaps, in which case data.next_index can be lower than the local index.
857
- const currentKeyIndex = await this.getKeyIndex();
858
- const nextKeyIndex = Math.max(currentKeyIndex, data.next_index);
859
- if (nextKeyIndex !== currentKeyIndex) {
860
- await this.setKeyIndex(nextKeyIndex);
861
- }
862
- return {
863
- swaps: storedSwaps,
864
- highestIndex: data.highest_index,
865
- nextIndex: data.next_index,
866
- scannedUntil: data.scanned_until,
867
- complete: data.complete,
868
- };
869
- }
870
- /**
871
- * Recovers every swap discoverable for the current wallet.
872
- *
873
- * This repeatedly calls `recoverSwaps`, continuing from the last scanned
874
- * derivation index, until the server reports that recovery is complete or a
875
- * recovery scan fails. On failure, it returns the progress made so far instead
876
- * of throwing.
877
- *
878
- * Use this for fresh wallets that cannot rely on local recovery state.
879
- *
880
- * @returns Aggregated recovery metadata and all recovered swaps stored locally.
881
- */
882
- async recoverAllSwaps(options = {}) {
883
- let startIndex = options.startIndex ?? 0;
884
- let scans = 0;
885
- const swapsById = new Map();
886
- let nextIndex = startIndex;
887
- let scannedUntil = null;
888
- while (true) {
889
- let result;
890
- try {
891
- result = await this.recoverSwaps({ startIndex });
892
- }
893
- catch (error) {
894
- return {
895
- swaps: [...swapsById.values()],
896
- nextIndex,
897
- scannedUntil,
898
- complete: false,
899
- stoppedReason: "error",
900
- scans,
901
- errorMessage: error instanceof Error ? error.message : String(error),
902
- error,
903
- };
904
- }
905
- scans += 1;
906
- for (const swap of result.swaps) {
907
- swapsById.set(swap.swapId, swap);
908
- }
909
- nextIndex = Math.max(nextIndex, result.nextIndex);
910
- scannedUntil = result.scannedUntil;
911
- if (result.complete) {
912
- return {
913
- swaps: [...swapsById.values()],
914
- nextIndex,
915
- scannedUntil,
916
- complete: true,
917
- stoppedReason: "complete",
918
- scans,
919
- };
920
- }
921
- startIndex = result.scannedUntil + 1;
922
- }
923
- }
924
- /**
925
- * Gets VHTLC amounts for an Arkade swap.
926
- *
927
- * Queries the Arkade indexer for spendable, spent, and recoverable balances
928
- * at the VHTLC address associated with a swap. Works for:
929
- * - BTC → EVM swaps where the source asset is Arkade
930
- * - EVM → BTC swaps where the target asset is Arkade
931
- *
932
- * Reads swap data from local storage (does not contact the server).
933
- *
934
- * @param id - The UUID of the swap.
935
- * @returns The VHTLC amounts in satoshis.
936
- */
937
- async amountsForSwap(id) {
938
- const stored = await this.getStoredSwap(id);
939
- if (!stored) {
940
- throw new Error(`Swap not found in local storage: ${id}`);
941
- }
942
- const swap = stored.response;
943
- if (swap.direction !== "btc_to_arkade" &&
944
- swap.direction !== "arkade_to_evm" &&
945
- swap.direction !== "arkade_to_lightning" &&
946
- swap.direction !== "evm_to_arkade" &&
947
- swap.direction !== "lightning_to_arkade") {
948
- throw new Error(`amountsForSwap only applies to VHTLC-based swaps, got ${swap.direction}`);
949
- }
950
- // Get VHTLC address based on swap direction
951
- let vhtlcAddress;
952
- if (swap.direction === "btc_to_arkade") {
953
- vhtlcAddress = swap.arkade_vhtlc_address;
954
- }
955
- else if (swap.direction === "lightning_to_arkade") {
956
- vhtlcAddress = swap
957
- .arkade_vhtlc_address;
958
- }
959
- else if (swap.direction === "arkade_to_lightning") {
960
- vhtlcAddress = swap
961
- .arkade_vhtlc_address;
962
- }
963
- else if (swap.direction === "arkade_to_evm" ||
964
- swap.direction === "evm_to_arkade") {
965
- vhtlcAddress = swap.btc_vhtlc_address;
966
- }
967
- if (!vhtlcAddress) {
968
- throw new Error("Swap does not have an Arkade VHTLC address");
969
- }
970
- return getVhtlcAmounts({
971
- vhtlcAddress,
972
- network: swap.network,
973
- arkadeServerUrl: this.#config.arkadeServerUrl,
974
- });
975
- }
976
- // =========================================================================
977
- // Redeem
978
- // =========================================================================
979
- /**
980
- * Claims a swap by revealing the preimage.
981
- *
982
- * Reads swap data and preimage from local storage. The claim method
983
- * depends on the swap direction and target chain:
984
- * - **Arkade/Lightning-to-EVM**: Gasless claim via server
985
- * - **Other EVM swaps**: Returns call data for manual claiming
986
- * - **Arkade**: Claims via Arkade protocol
987
- *
988
- * @param id - The UUID of the swap.
989
- * @param options - For Arkade/Lightning-to-EVM, destination is set at swap creation.
990
- * @returns A ClaimResult with the outcome.
991
- *
992
- * @example
993
- * ```ts
994
- * // Arkade-to-EVM (gasless via server, uses stored target address)
995
- * const result = await client.claim(swapId);
996
- *
997
- * // Other swap types
998
- * const result = await client.claim(swapId);
999
- * if (result.success) {
1000
- * console.log("Claim TX:", result.txHash);
1001
- * }
1002
- * ```
1003
- */
1004
- async claim(id, options) {
1005
- // Check swap storage is configured
1006
- if (!this.#swapStorage) {
1007
- return {
1008
- success: false,
1009
- message: "Swap storage is not configured. Cannot retrieve swap data needed for claim.",
1010
- };
1011
- }
1012
- // Get stored swap data (contains preimage, keys, and swap response)
1013
- const storedSwap = await this.#swapStorage.get(id);
1014
- if (!storedSwap) {
1015
- return {
1016
- success: false,
1017
- message: `Swap ${id} not found in local storage. Cannot claim without stored data.`,
1018
- };
1019
- }
1020
- const swap = storedSwap.response;
1021
- const secret = storedSwap.preimage;
1022
- // EVM-targeted swaps: use gasless claim via server (SDK signs internally)
1023
- // The destination is always the stored target_evm_address (set at swap creation time)
1024
- if (swap.direction === "arkade_to_evm" ||
1025
- swap.direction === "lightning_to_evm" ||
1026
- swap.direction === "bitcoin_to_evm") {
1027
- const evmSwap = swap;
1028
- // For Solana bridge targets the stored `target_evm_address` actually
1029
- // holds a base58 SPL pubkey, which can't satisfy the EIP-712
1030
- // `address destination` field. Fall back to `client_evm_address`
1031
- // (the user's EVM-side wallet at swap creation, used for refunds);
1032
- // the actual Solana destination rides on `bridgeRecipient`.
1033
- const bridgeTargetChain = evmSwap
1034
- .bridge_target_chain;
1035
- const isSolanaBridge = bridgeTargetChain === "Solana";
1036
- const destination = isSolanaBridge
1037
- ? evmSwap.client_evm_address
1038
- : (evmSwap.target_evm_address ?? evmSwap.client_evm_address);
1039
- if (!destination) {
1040
- return {
1041
- success: false,
1042
- message: "Gasless claim failed: no target address found. " +
1043
- "This swap may have been created before target address storage was implemented.",
1044
- };
1045
- }
1046
- if (isSolanaBridge && !options?.bridgeRecipient) {
1047
- return {
1048
- success: false,
1049
- message: "Solana bridge claim requires `bridgeRecipient` (the recipient's USDC ATA) " +
1050
- "in claim options. Derive it via `deriveSolanaUsdcAta` before calling claim.",
1051
- };
1052
- }
1053
- const gaslessResult = await this.claimViaGasless(id, destination, {
1054
- bridgeRecipient: options?.bridgeRecipient,
1055
- bridgeRecipientWallet: options?.bridgeRecipientWallet,
1056
- });
1057
- return {
1058
- success: true,
1059
- message: gaslessResult.message,
1060
- txHash: gaslessResult.txHash,
1061
- };
1062
- }
1063
- // EVM-to-Bitcoin: user claims BTC from on-chain Taproot HTLC with preimage
1064
- if (swap.direction === "evm_to_bitcoin") {
1065
- return this.#claimOnchainBtc(id, options);
1066
- }
1067
- // Check if target is Arkade (handle both string "btc_arkade" and TokenInfo object)
1068
- const isArkadeTarget = swap.target_token.chain === "Arkade";
1069
- if (isArkadeTarget) {
1070
- // Determine destination address based on swap direction
1071
- let destinationAddress;
1072
- if (swap.direction === "btc_to_arkade") {
1073
- const btcToArkadeSwap = swap;
1074
- destinationAddress = btcToArkadeSwap.target_arkade_address;
1075
- }
1076
- else if (swap.direction === "evm_to_arkade" ||
1077
- swap.direction === "lightning_to_arkade") {
1078
- // For evm_to_arkade swaps, check if we have target_arkade_address in stored response
1079
- // Check if we have target_arkade_address in the stored response.
1080
- const storedResponse = swap;
1081
- if (storedResponse.target_arkade_address) {
1082
- destinationAddress = storedResponse.target_arkade_address;
1083
- }
1084
- else {
1085
- // Fetch from API to get the full response with target_arkade_address
1086
- const freshSwap = await this.getSwap(id);
1087
- const evmToArkadeSwap = freshSwap;
1088
- destinationAddress = evmToArkadeSwap.target_arkade_address;
1089
- }
1090
- }
1091
- if (!destinationAddress) {
1092
- return {
1093
- success: false,
1094
- message: "No Arkade destination address found in swap. Use claimArkade() with explicit destinationAddress.",
1095
- };
1096
- }
1097
- const arkadeResult = await this.claimArkade(id, { destinationAddress });
1098
- // Convert to ClaimResult format
1099
- return {
1100
- success: arkadeResult.success,
1101
- message: arkadeResult.message,
1102
- chain: "arkade",
1103
- txHash: arkadeResult.txId,
1104
- };
1105
- }
1106
- // For EVM chains, use the existing claim logic
1107
- return redeemClaim(id, secret, {
1108
- apiClient: this.#apiClient,
1109
- getSwap: () => Promise.resolve(swap),
1110
- });
1111
- }
1112
- /**
1113
- * Claims an Arkade-to-EVM swap gaslessly via the server.
1114
- *
1115
- * The SDK builds the EIP-712 digest, signs it with the swap's internally
1116
- * derived EVM key, and sends the signature + secret to the server. The
1117
- * server submits the `coordinator.redeemAndExecute` transaction.
1118
- *
1119
- * @param id - The UUID of the swap.
1120
- * @param destination - The EVM address where tokens should be sent.
1121
- * @returns The gasless claim result with transaction hash.
1122
- *
1123
- * @example
1124
- * ```ts
1125
- * const result = await client.claimViaGasless(swapId, "0xYourAddress");
1126
- * console.log("Claimed! TX:", result.txHash);
1127
- * ```
1128
- */
1129
- async claimViaGasless(id, destination, { bridgeRecipient, bridgeRecipientWallet, } = {}) {
1130
- if (!this.#swapStorage) {
1131
- throw new Error("Swap storage is not configured. Cannot retrieve preimage needed for gasless claim.");
1132
- }
1133
- // Fetch all data upfront
1134
- const stored = await this.#swapStorage.get(id);
1135
- if (!stored) {
1136
- throw new Error(`Swap ${id} not found in local storage.`);
1137
- }
1138
- const swap = (await this.getSwap(id, {
1139
- updateStorage: true,
1140
- }));
1141
- if (swap.direction !== "arkade_to_evm" &&
1142
- swap.direction !== "lightning_to_evm" &&
1143
- swap.direction !== "bitcoin_to_evm") {
1144
- throw new Error(`Expected arkade_to_evm or lightning_to_evm swap, got ${swap.direction}. claimViaGasless is for EVM-targeted swaps.`);
1145
- }
1146
- // Always fetch redeem calldata from the server to get calls_hash.
1147
- // For non-WBTC targets this also returns DEX calldata.
1148
- // bridge_recipient is forwarded for non-EVM bridge targets (e.g.
1149
- // Solana) so the server's calls_hash matches what claim-gasless
1150
- // will rebuild below.
1151
- const calldataResponse = await this.#apiClient.GET("/swap/{id}/redeem-and-swap-calldata", {
1152
- params: {
1153
- path: { id },
1154
- query: {
1155
- destination,
1156
- ...(bridgeRecipient ? { bridge_recipient: bridgeRecipient } : {}),
1157
- ...(bridgeRecipientWallet
1158
- ? { bridge_recipient_wallet: bridgeRecipientWallet }
1159
- : {}),
1160
- },
1161
- },
1162
- });
1163
- if (calldataResponse.error) {
1164
- throw new Error(`Failed to fetch redeem calldata: ${calldataResponse.error.error}`);
1165
- }
1166
- // Cast to the updated response shape (includes calls_hash and optional dex_calldata).
1167
- // The generated API types may lag behind the server; this will align after regeneration.
1168
- const responseData = calldataResponse.data;
1169
- const targetTokenAddress = String(swap.target_token.token_id);
1170
- const needsDexSwap = targetTokenAddress.toLowerCase() !== swap.wbtc_address.toLowerCase();
1171
- let dexCalldata;
1172
- if (needsDexSwap && responseData?.dex_calldata) {
1173
- dexCalldata = {
1174
- to: responseData.dex_calldata.to,
1175
- data: responseData.dex_calldata.data,
1176
- value: responseData.dex_calldata.value,
1177
- };
1178
- }
1179
- const callsHash = responseData.calls_hash;
1180
- const minAmountOut = BigInt(responseData.min_amount_out);
1181
- return gaslessClaim({
1182
- baseUrl: this.#config.baseUrl,
1183
- preimage: stored.preimage,
1184
- secretKey: hexToBytes(this.#getEvmSigningKey()),
1185
- swap,
1186
- destination,
1187
- dexCalldata,
1188
- minAmountOut,
1189
- callsHash,
1190
- bridgeRecipient,
1191
- bridgeRecipientWallet,
1192
- });
1193
- }
1194
- /**
1195
- * Claims an Arkade (off-chain) VHTLC swap by revealing the preimage.
1196
- *
1197
- * Automatically selects the best claim method based on VTXO status:
1198
- * - **spendable** VTXOs → offchain spend (submitTx/finalizeTx)
1199
- * - **recoverable** or **mixed** VTXOs → delegated settlement via backend
1200
- *
1201
- * This is used for EVM-to-Arkade and BTC-to-Arkade swaps where the user
1202
- * claims BTC on Arkade after the server has funded the VHTLC.
1203
- *
1204
- * @param id - The UUID of the swap.
1205
- * @param options - Claim options including destination address.
1206
- * @returns The claim result with transaction ID and amount.
1207
- *
1208
- * @example
1209
- * ```ts
1210
- * const result = await client.claimArkade(swapId, {
1211
- * destinationAddress: "ark1q...", // Where to receive BTC
1212
- * });
1213
- * if (result.success) {
1214
- * console.log("Claim TX:", result.txId);
1215
- * console.log("Amount:", result.claimAmount);
1216
- * }
1217
- * ```
1218
- */
1219
- async claimArkade(id, options) {
1220
- // Validate options
1221
- if (!options?.destinationAddress) {
1222
- return {
1223
- success: false,
1224
- message: "Destination address is required for Arkade claims. " +
1225
- 'Provide it via the options parameter: { destinationAddress: "ark1..." }',
1226
- };
1227
- }
1228
- // Check swap storage is configured
1229
- if (!this.#swapStorage) {
1230
- return {
1231
- success: false,
1232
- message: "Swap storage is not configured. Cannot retrieve the preimage needed for claim.",
1233
- };
1234
- }
1235
- // Get stored swap data (contains preimage and secret key)
1236
- const storedSwap = await this.#swapStorage.get(id);
1237
- if (!storedSwap) {
1238
- return {
1239
- success: false,
1240
- message: `Swap ${id} not found in local storage. The preimage is required to claim.`,
1241
- };
1242
- }
1243
- const swap = storedSwap.response;
1244
- // Ensure we have an Arkade-target swap
1245
- if (swap.direction !== "btc_to_arkade" &&
1246
- swap.direction !== "evm_to_arkade" &&
1247
- swap.direction !== "lightning_to_arkade") {
1248
- return {
1249
- success: false,
1250
- message: `Expected btc_to_arkade, lightning_to_arkade or evm_to_arkade swap, got ${swap.direction}. claimArkade is for swaps targeting Arkade.`,
1251
- };
1252
- }
1253
- // Extract common VHTLC parameters
1254
- const claimParams = this.#extractArkadeClaimParams(id, storedSwap);
1255
- // Query VTXO status to determine claim method, polling briefly while
1256
- // the Arkade indexer catches up if the funding tx hasn't been seen yet.
1257
- const vtxoStatus = await this.#waitForVtxoStatus(id, options.waitForVtxoMs ?? 30_000);
1258
- if (vtxoStatus === "not_funded") {
1259
- return {
1260
- success: false,
1261
- message: `No VTXOs found at the VHTLC address ${claimParams.vhtlcAddress}. The swap may not have been funded yet.`,
1262
- };
1263
- }
1264
- if (vtxoStatus === "spent") {
1265
- // A submitted-but-not-finalized Arkade offchain tx can make the original
1266
- // VHTLC inputs look spent. Try to resume that pending tx before declaring
1267
- // the claim already spent.
1268
- const continued = await this.continueArkadeClaimSwap(id);
1269
- if (continued.success)
1270
- return continued;
1271
- return {
1272
- success: false,
1273
- message: "All VTXOs have already been spent.",
1274
- };
1275
- }
1276
- // Route based on VTXO status:
1277
- // - spendable: offchain spend (faster, no backend dependency)
1278
- // - recoverable/mixed: delegated settlement (handles expired batches)
1279
- if (vtxoStatus === "spendable") {
1280
- return this.#claimArkadeOffchain(claimParams, options);
1281
- }
1282
- // recoverable or mixed → delegate
1283
- return this.#claimArkadeDelegate(id, claimParams, options);
1284
- }
1285
- /**
1286
- * Poll `amountsForSwap` until the VTXO status leaves `not_funded` or
1287
- * the timeout elapses. Returns the most recent status observed.
1288
- *
1289
- * Uses an exponential-ish backoff capped at 2s between probes so the
1290
- * usual case (indexer catches up within a few seconds) feels snappy
1291
- * without hammering the server.
1292
- */
1293
- async #waitForVtxoStatus(id, timeoutMs) {
1294
- const deadline = Date.now() + Math.max(0, timeoutMs);
1295
- let delayMs = 500;
1296
- // First probe is immediate; subsequent probes back off.
1297
- while (true) {
1298
- const amounts = await this.amountsForSwap(id);
1299
- if (amounts.vtxoStatus !== "not_funded")
1300
- return amounts.vtxoStatus;
1301
- if (Date.now() >= deadline)
1302
- return amounts.vtxoStatus;
1303
- const remaining = deadline - Date.now();
1304
- const sleepMs = Math.min(delayMs, remaining);
1305
- if (sleepMs <= 0)
1306
- return amounts.vtxoStatus;
1307
- await new Promise((resolve) => setTimeout(resolve, sleepMs));
1308
- delayMs = Math.min(delayMs * 2, 2_000);
1309
- }
1310
- }
1311
- /**
1312
- * Continue (finalize) a pending Arkade claim.
1313
- *
1314
- * For Arkade-destination swaps (EVM/Bitcoin/Lightning → Arkade), this
1315
- * fetches pending transactions from the Arkade server and finalizes them.
1316
- * Use this when `claimArkade` submitted a claim but it wasn't finalized
1317
- * (e.g. due to a page reload or network interruption).
1318
- *
1319
- * @param id - The UUID of the swap
1320
- * @returns Result with txId and claimAmount on success
1321
- */
1322
- async continueArkadeClaimSwap(id) {
1323
- if (!this.#swapStorage) {
1324
- return {
1325
- success: false,
1326
- message: "Swap storage is not configured.",
1327
- };
1328
- }
1329
- const storedSwap = await this.#swapStorage.get(id);
1330
- if (!storedSwap) {
1331
- return {
1332
- success: false,
1333
- message: `Swap ${id} not found in local storage.`,
1334
- };
1335
- }
1336
- const swap = storedSwap.response;
1337
- if (swap.direction !== "btc_to_arkade" &&
1338
- swap.direction !== "evm_to_arkade" &&
1339
- swap.direction !== "lightning_to_arkade") {
1340
- return {
1341
- success: false,
1342
- message: `Expected an Arkade-destination swap, got ${swap.direction}.`,
1343
- };
1344
- }
1345
- const claimParams = this.#extractArkadeClaimParams(id, storedSwap);
1346
- try {
1347
- const result = await continueArkadeClaim({
1348
- ...claimParams,
1349
- destinationAddress: "", // not needed for continue
1350
- arkadeServerUrl: this.#config.arkadeServerUrl,
1351
- logger: this.#config.logger,
1352
- logLevel: this.#config.logLevel,
1353
- });
1354
- return {
1355
- success: true,
1356
- message: `Arkade claim finalized: ${result.txId}`,
1357
- txId: result.txId,
1358
- claimAmount: result.claimAmount,
1359
- };
1360
- }
1361
- catch (err) {
1362
- return {
1363
- success: false,
1364
- message: err instanceof Error ? err.message : String(err),
1365
- };
1366
- }
1367
- }
1368
- /**
1369
- * Extracts VHTLC claim parameters from a stored swap.
1370
- * @internal
1371
- */
1372
- #extractArkadeClaimParams(_id, storedSwap) {
1373
- const swap = storedSwap.response;
1374
- const fullPubKey = storedSwap.publicKey;
1375
- const userPubKey = fullPubKey.length === 66 ? fullPubKey.slice(2) : fullPubKey;
1376
- let lendaswapPubKey;
1377
- let arkadeServerPubKey;
1378
- let vhtlcAddress;
1379
- let refundLocktime;
1380
- let unilateralClaimDelay;
1381
- let unilateralRefundDelay;
1382
- let unilateralRefundWithoutReceiverDelay;
1383
- let network;
1384
- if (swap.direction === "btc_to_arkade") {
1385
- const s = swap;
1386
- lendaswapPubKey = s.server_vhtlc_pk;
1387
- arkadeServerPubKey = s.arkade_server_pk;
1388
- vhtlcAddress = s.arkade_vhtlc_address;
1389
- refundLocktime = s.vhtlc_refund_locktime;
1390
- unilateralClaimDelay = s.unilateral_claim_delay;
1391
- unilateralRefundDelay = s.unilateral_refund_delay;
1392
- unilateralRefundWithoutReceiverDelay =
1393
- s.unilateral_refund_without_receiver_delay;
1394
- network = s.network;
1395
- }
1396
- else if (swap.direction === "evm_to_arkade") {
1397
- const s = swap;
1398
- lendaswapPubKey = s.sender_pk;
1399
- arkadeServerPubKey = s.arkade_server_pk;
1400
- vhtlcAddress = s.btc_vhtlc_address;
1401
- refundLocktime = s.vhtlc_refund_locktime;
1402
- unilateralClaimDelay = s.unilateral_claim_delay;
1403
- unilateralRefundDelay = s.unilateral_refund_delay;
1404
- unilateralRefundWithoutReceiverDelay =
1405
- s.unilateral_refund_without_receiver_delay;
1406
- network = s.network;
1407
- }
1408
- else if (swap.direction === "lightning_to_arkade") {
1409
- lendaswapPubKey = swap.sender_pk;
1410
- arkadeServerPubKey = swap.arkade_server_pk;
1411
- vhtlcAddress = swap.arkade_vhtlc_address;
1412
- refundLocktime = swap.vhtlc_refund_locktime;
1413
- unilateralClaimDelay = swap.unilateral_claim_delay;
1414
- unilateralRefundDelay = swap.unilateral_refund_delay;
1415
- unilateralRefundWithoutReceiverDelay =
1416
- swap.unilateral_refund_without_receiver_delay;
1417
- network = swap.network;
1418
- }
1419
- else {
1420
- throw Error(`Unsupported direction for Arkade claim: ${swap.direction}`);
1421
- }
1422
- return {
1423
- userSecretKey: storedSwap.secretKey,
1424
- userPubKey,
1425
- lendaswapPubKey,
1426
- arkadeServerPubKey,
1427
- vhtlcAddress,
1428
- refundLocktime,
1429
- unilateralClaimDelay,
1430
- unilateralRefundDelay,
1431
- unilateralRefundWithoutReceiverDelay,
1432
- network,
1433
- preimage: storedSwap.preimage,
1434
- preimageHash: storedSwap.preimageHash,
1435
- };
1436
- }
1437
- /**
1438
- * Claims via the offchain submitTx/finalizeTx path (spendable VTXOs only).
1439
- * @internal
1440
- */
1441
- async #claimArkadeOffchain(params, options) {
1442
- try {
1443
- const result = await buildArkadeClaim({
1444
- ...params,
1445
- destinationAddress: options.destinationAddress,
1446
- arkadeServerUrl: options.arkadeServerUrl ?? this.#config.arkadeServerUrl,
1447
- logger: this.#config.logger,
1448
- logLevel: this.#config.logLevel,
1449
- });
1450
- return {
1451
- success: true,
1452
- message: "Arkade claim executed successfully via offchain spend!",
1453
- txId: result.txId,
1454
- claimAmount: result.claimAmount,
1455
- };
1456
- }
1457
- catch (error) {
1458
- const message = error instanceof Error ? error.message : String(error);
1459
- return {
1460
- success: false,
1461
- message: `Failed to execute offchain Arkade claim: ${message}`,
1462
- };
1463
- }
1464
- }
1465
- /**
1466
- * Claims via the delegated settlement path (works for all VTXO states).
1467
- * @internal
1468
- */
1469
- async #claimArkadeDelegate(swapId, params, options) {
1470
- try {
1471
- const result = await delegateClaim({
1472
- ...params,
1473
- destinationAddress: options.destinationAddress,
1474
- lendaswapApiUrl: this.#config.baseUrl,
1475
- arkadeServerUrl: options.arkadeServerUrl ?? this.#config.arkadeServerUrl,
1476
- swapId,
1477
- logger: this.#config.logger,
1478
- logLevel: this.#config.logLevel,
1479
- });
1480
- return {
1481
- success: true,
1482
- message: "Arkade claim executed successfully via delegated settlement!",
1483
- txId: result.commitmentTxid,
1484
- };
1485
- }
1486
- catch (error) {
1487
- const message = error instanceof Error ? error.message : String(error);
1488
- return {
1489
- success: false,
1490
- message: `Failed to execute delegated Arkade claim: ${message}`,
1491
- };
1492
- }
1493
- }
1494
- // =========================================================================
1495
- // Refund
1496
- // =========================================================================
1497
- /**
1498
- * Attempts to refund a swap.
1499
- *
1500
- * Refund behavior depends on the swap type:
1501
- * - **Lightning to EVM**: Cannot refund - Lightning swaps auto-expire if not completed.
1502
- * The invoice will simply expire and no funds are locked.
1503
- * - **Arkade to EVM**: Off-chain refund via Arkade server
1504
- * - **Bitcoin (on-chain) to EVM**: Builds a signed refund transaction that the user
1505
- * must broadcast to reclaim their funds after the locktime.
1506
- *
1507
- * @param id - The UUID of the swap to refund.
1508
- * @param options - Options for on-chain refunds (required for btc_onchain swaps).
1509
- * @returns A RefundResult with the transaction details (for on-chain) or status message.
1510
- * @throws Error if the swap cannot be found, storage is not configured, or params are invalid.
1511
- *
1512
- * @example
1513
- * ```ts
1514
- * // For on-chain swaps
1515
- * const result = await client.refundSwap(swapId, {
1516
- * destinationAddress: "bc1q...",
1517
- * feeRateSatPerVb: 5,
1518
- * });
1519
- * if (result.success) {
1520
- * console.log("Broadcast this transaction:", result.txHex);
1521
- * console.log("Transaction ID:", result.txId);
1522
- * }
1523
- * ```
1524
- */
1525
- async refundSwap(id, options) {
1526
- // Get the swap to determine its type
1527
- const storedSwap = await this.getStoredSwap(id);
1528
- if (!storedSwap) {
1529
- throw Error("Swap not found");
1530
- }
1531
- const swap = storedSwap.response;
1532
- // Use direction to determine refund method (source_token may be a TokenSummary object)
1533
- const direction = swap.direction;
1534
- // Arkade swaps require off-chain refund
1535
- if (direction === "arkade_to_evm") {
1536
- return this.#buildArkadeRefund(id, swap, options);
1537
- }
1538
- // Bitcoin on-chain swaps require on-chain refund transaction
1539
- if (direction === "bitcoin_to_evm" || direction === "btc_to_arkade") {
1540
- return this.#buildOnchainRefund(id, swap, options);
1541
- }
1542
- // EVM-sourced swaps: collaborative refund or timelock-based refund
1543
- if (direction === "evm_to_arkade" ||
1544
- direction === "evm_to_bitcoin" ||
1545
- direction === "evm_to_lightning") {
1546
- const evmOptions = options;
1547
- const settlement = evmOptions?.mode ?? "swap-back";
1548
- if (evmOptions?.collaborative) {
1549
- return this.#collabRefundEvm(id, settlement);
1550
- }
1551
- if (direction === "evm_to_arkade") {
1552
- return this.#buildEvmToArkadeRefund(id, swap, settlement);
1553
- }
1554
- if (direction === "evm_to_bitcoin") {
1555
- return this.#buildEvmToBitcoinRefund(id, swap, settlement);
1556
- }
1557
- return this.#buildEvmToLightningRefund(id, swap, settlement);
1558
- }
1559
- // Arkade-to-Lightning: collaborative refund or locktime refund
1560
- if (direction === "arkade_to_lightning") {
1561
- return this.#buildArkadeToLightningRefund(id, swap, options);
1562
- }
1563
- return {
1564
- success: false,
1565
- message: `Refund not supported for direction: ${direction}.`,
1566
- };
1567
- }
1568
- /**
1569
- * Claims BTC from an on-chain Taproot HTLC for an EVM-to-Bitcoin swap.
1570
- *
1571
- * The user reveals the preimage to spend from the hashlock script path.
1572
- * @internal
1573
- */
1574
- async #claimOnchainBtc(id, options) {
1575
- if (!this.#swapStorage) {
1576
- return {
1577
- success: false,
1578
- message: "Swap storage is not configured. Cannot retrieve preimage and keys needed for claim.",
1579
- };
1580
- }
1581
- const storedSwap = await this.#swapStorage.get(id);
1582
- if (!storedSwap) {
1583
- return {
1584
- success: false,
1585
- message: `Swap ${id} not found in local storage.`,
1586
- };
1587
- }
1588
- // Fetch the latest swap state from API
1589
- const swap = (await this.getSwap(id, {
1590
- updateStorage: false,
1591
- }));
1592
- if (swap.direction !== "evm_to_bitcoin") {
1593
- return {
1594
- success: false,
1595
- message: `Expected evm_to_bitcoin swap, got ${swap.direction}`,
1596
- };
1597
- }
1598
- // Extract BTC HTLC parameters
1599
- const btcHtlcAddress = swap.btc_htlc_address;
1600
- const btcHashLock = swap.btc_hash_lock;
1601
- const btcRefundLocktime = swap.btc_refund_locktime;
1602
- const networkStr = swap.network;
1603
- // Get server refund pk (needed to reconstruct the Taproot tree)
1604
- const serverRefundPkRaw = swap
1605
- .btc_server_refund_pk;
1606
- if (!serverRefundPkRaw) {
1607
- return {
1608
- success: false,
1609
- message: "Server refund public key not available. The API response may need to be updated.",
1610
- };
1611
- }
1612
- // Map network string
1613
- const networkMap = {
1614
- bitcoin: "mainnet",
1615
- mainnet: "mainnet",
1616
- testnet: "testnet",
1617
- signet: "signet",
1618
- regtest: "regtest",
1619
- };
1620
- const network = networkMap[networkStr];
1621
- if (!network) {
1622
- return {
1623
- success: false,
1624
- message: `Unknown Bitcoin network: ${networkStr}`,
1625
- };
1626
- }
1627
- // Get user's x-only public key (32 bytes from 33-byte compressed)
1628
- const fullPubKey = storedSwap.publicKey;
1629
- const userClaimPk = fullPubKey.length === 66 ? fullPubKey.slice(2) : fullPubKey;
1630
- // Strip compressed key prefix if present
1631
- const serverRefundPk = serverRefundPkRaw.length === 66
1632
- ? serverRefundPkRaw.slice(2)
1633
- : serverRefundPkRaw;
1634
- // Verify HTLC address matches our reconstruction
1635
- const addressMatches = verifyHtlcAddress(btcHtlcAddress, btcHashLock, userClaimPk, // claimer = user (goes in hashlock position)
1636
- serverRefundPk, // refunder = server (goes in timelock position)
1637
- btcRefundLocktime, network);
1638
- if (!addressMatches) {
1639
- return {
1640
- success: false,
1641
- message: `HTLC address mismatch. Computed address does not match server's (${btcHtlcAddress}). ` +
1642
- `Parameters: hashLock='${btcHashLock}', userPk='${userClaimPk}', ` +
1643
- `serverPk='${serverRefundPk}', locktime='${btcRefundLocktime}', network='${network}'`,
1644
- };
1645
- }
1646
- // Get the HTLC output info - prefer API data over Esplora lookup
1647
- const esploraUrl = this.#config.esploraUrl ?? DEFAULT_ESPLORA_URLS[network];
1648
- if (!esploraUrl) {
1649
- return {
1650
- success: false,
1651
- message: `No Esplora URL configured for network ${network}.`,
1652
- };
1653
- }
1654
- // Try to use funding info from the API response (faster, works before confirmation)
1655
- const btcFundTxid = swap.btc_fund_txid;
1656
- const btcFundVout = swap.btc_fund_vout;
1657
- let htlcOutput = null;
1658
- if (btcFundTxid && btcFundVout !== undefined) {
1659
- // We have the funding info from the API, but we need to get the amount
1660
- // Query the transaction to get the output amount
1661
- try {
1662
- const txResponse = await fetch(`${esploraUrl}/tx/${btcFundTxid}`);
1663
- if (txResponse.ok) {
1664
- const txData = (await txResponse.json());
1665
- if (txData.vout?.[btcFundVout]) {
1666
- htlcOutput = {
1667
- txid: btcFundTxid,
1668
- vout: btcFundVout,
1669
- amount: BigInt(txData.vout[btcFundVout].value),
1670
- };
1671
- }
1672
- }
1673
- }
1674
- catch {
1675
- // Fall through to Esplora lookup
1676
- }
1677
- }
1678
- // Fallback: query Esplora for UTXOs at the address (requires confirmation)
1679
- if (!htlcOutput) {
1680
- htlcOutput = await findOutputByAddress(esploraUrl, btcHtlcAddress);
1681
- }
1682
- if (!htlcOutput) {
1683
- return {
1684
- success: false,
1685
- message: `Could not find UTXO at HTLC address ${btcHtlcAddress}. The server may not have funded the HTLC yet.`,
1686
- };
1687
- }
1688
- // Determine destination address: prefer explicit option, fall back to stored response
1689
- const destinationAddress = options?.destinationAddress ??
1690
- swap.target_btc_address;
1691
- if (!destinationAddress) {
1692
- return {
1693
- success: false,
1694
- message: "Destination address is required to claim BTC. " +
1695
- 'Provide it via options: { destinationAddress: "bc1p..." }',
1696
- };
1697
- }
1698
- try {
1699
- const result = buildOnchainClaimTransaction({
1700
- fundingTxId: htlcOutput.txid,
1701
- fundingVout: htlcOutput.vout,
1702
- htlcAmount: htlcOutput.amount,
1703
- hashLock: btcHashLock,
1704
- userClaimPubKey: userClaimPk,
1705
- serverRefundPubKey: serverRefundPk,
1706
- userSecretKey: storedSwap.secretKey,
1707
- preimage: storedSwap.preimage,
1708
- refundLocktime: btcRefundLocktime,
1709
- destinationAddress,
1710
- feeRateSatPerVb: options?.feeRateSatPerVb ?? 2,
1711
- network,
1712
- });
1713
- // Broadcast
1714
- try {
1715
- await broadcastTransaction(esploraUrl, result.txHex);
1716
- return {
1717
- success: true,
1718
- message: "BTC claim transaction broadcast successfully!",
1719
- txHash: result.txId,
1720
- // chain: "bitcoin" — not in ClaimChain type
1721
- };
1722
- }
1723
- catch (broadcastError) {
1724
- const msg = broadcastError instanceof Error
1725
- ? broadcastError.message
1726
- : String(broadcastError);
1727
- return {
1728
- success: true,
1729
- message: `Claim transaction built but broadcast failed: ${msg}. TxHex: ${result.txHex}`,
1730
- txHash: result.txId,
1731
- // chain: "bitcoin" — not in ClaimChain type
1732
- };
1733
- }
1734
- }
1735
- catch (error) {
1736
- const msg = error instanceof Error ? error.message : String(error);
1737
- return {
1738
- success: false,
1739
- message: `Failed to build claim transaction: ${msg}`,
1740
- };
1741
- }
1742
- }
1743
- /**
1744
- * Builds an on-chain Bitcoin refund transaction.
1745
- * @internal
1746
- */
1747
- async #buildOnchainRefund(id, swap, options) {
1748
- // Validate options
1749
- if (!options?.destinationAddress) {
1750
- return {
1751
- success: false,
1752
- message: "Destination address is required for on-chain refunds. " +
1753
- 'Provide it via the options parameter: { destinationAddress: "bc1q..." }',
1754
- };
1755
- }
1756
- // Check swap storage is configured
1757
- if (!this.#swapStorage) {
1758
- return {
1759
- success: false,
1760
- message: "Swap storage is not configured. Cannot retrieve the secret key needed for refund.",
1761
- };
1762
- }
1763
- // Get stored swap data (contains secret key)
1764
- const storedSwap = await this.#swapStorage.get(id);
1765
- if (!storedSwap) {
1766
- return {
1767
- success: false,
1768
- message: `Swap ${id} not found in local storage. The secret key is required to sign the refund transaction.`,
1769
- };
1770
- }
1771
- // Ensure we have an on-chain funded swap
1772
- if (swap.direction !== "bitcoin_to_evm" &&
1773
- swap.direction !== "btc_to_arkade") {
1774
- return {
1775
- success: false,
1776
- message: `Expected bitcoin_to_evm or btc_to_arkade swap, got ${swap.direction}`,
1777
- };
1778
- }
1779
- // Extract on-chain HTLC fields based on direction
1780
- // Both directions have the same on-chain HTLC but fields are named differently
1781
- let btcHtlcAddress;
1782
- let btcRefundLocktime;
1783
- let hashLock;
1784
- let serverPubKeyFull;
1785
- let networkStr;
1786
- if (swap.direction === "btc_to_arkade") {
1787
- const arkadeSwap = swap;
1788
- btcHtlcAddress = arkadeSwap.btc_htlc_address;
1789
- btcRefundLocktime = arkadeSwap.btc_refund_locktime;
1790
- hashLock = arkadeSwap.hash_lock;
1791
- serverPubKeyFull = arkadeSwap.server_vhtlc_pk;
1792
- networkStr = arkadeSwap.network;
1793
- }
1794
- else {
1795
- // OnchainToEvmSwapResponse (on-chain Bitcoin to EVM)
1796
- const onchainSwap = swap;
1797
- btcHtlcAddress = onchainSwap.btc_htlc_address;
1798
- btcRefundLocktime = onchainSwap.btc_refund_locktime;
1799
- hashLock = onchainSwap.btc_hash_lock;
1800
- serverPubKeyFull = onchainSwap.btc_server_pk;
1801
- networkStr = onchainSwap.network;
1802
- }
1803
- // Check refund locktime
1804
- const now = Math.floor(Date.now() / 1000);
1805
- if (now < btcRefundLocktime) {
1806
- const remainingSeconds = btcRefundLocktime - now;
1807
- const remainingMinutes = Math.ceil(remainingSeconds / 60);
1808
- return {
1809
- success: false,
1810
- message: `Refund is not yet available. The locktime expires in ${remainingMinutes} minutes ` +
1811
- `(at ${new Date(btcRefundLocktime * 1000).toISOString()}).`,
1812
- };
1813
- }
1814
- // Map network string to BitcoinNetwork type
1815
- const networkMap = {
1816
- bitcoin: "mainnet",
1817
- mainnet: "mainnet",
1818
- testnet: "testnet",
1819
- signet: "signet",
1820
- regtest: "regtest",
1821
- };
1822
- const network = networkMap[networkStr];
1823
- if (!network) {
1824
- return {
1825
- success: false,
1826
- message: `Unknown Bitcoin network: ${networkStr}`,
1827
- };
1828
- }
1829
- // Get user's x-only public key (32 bytes) from stored swap
1830
- // The stored publicKey is the full compressed pubkey (33 bytes)
1831
- // We need to extract the x-only portion (drop the first byte prefix)
1832
- const fullPubKey = storedSwap.publicKey;
1833
- const userPubKey = fullPubKey.length === 66 ? fullPubKey.slice(2) : fullPubKey;
1834
- // Strip compressed key prefix if present (33-byte → 32-byte x-only)
1835
- const serverXOnlyPubKey = serverPubKeyFull.length === 66
1836
- ? serverPubKeyFull.slice(2)
1837
- : serverPubKeyFull;
1838
- // Verify that our computed HTLC address matches the server's address
1839
- const addressMatches = verifyHtlcAddress(btcHtlcAddress, hashLock, serverXOnlyPubKey, userPubKey, btcRefundLocktime, network);
1840
- if (!addressMatches) {
1841
- return {
1842
- success: false,
1843
- message: `HTLC address mismatch. The computed address does not match the server's address (${btcHtlcAddress}). ` +
1844
- `This could indicate different script construction. ` +
1845
- `Parameters: \nhashLock='${hashLock}', \nserverPk='${serverPubKeyFull}', ` +
1846
- `\nuserPk='${userPubKey}', \nlocktime='${btcRefundLocktime}',` +
1847
- `\nnetwork='${network}'`,
1848
- };
1849
- }
1850
- // Get the HTLC output info - prefer API data over Esplora lookup
1851
- const esploraUrl = this.#config.esploraUrl ?? DEFAULT_ESPLORA_URLS[network];
1852
- if (!esploraUrl) {
1853
- return {
1854
- success: false,
1855
- message: `No Esplora URL configured for network ${network}. Cannot look up funding transaction.`,
1856
- };
1857
- }
1858
- // Try to use funding info from the API response (faster, works before confirmation)
1859
- const btcFundTxid = swap.btc_fund_txid;
1860
- const btcFundVout = swap.btc_fund_vout;
1861
- let htlcOutput = null;
1862
- if (btcFundTxid && btcFundVout !== undefined) {
1863
- // We have the funding info from the API, get the amount from the transaction
1864
- try {
1865
- const txResponse = await fetch(`${esploraUrl}/tx/${btcFundTxid}`);
1866
- if (txResponse.ok) {
1867
- const txData = (await txResponse.json());
1868
- if (txData.vout?.[btcFundVout]) {
1869
- htlcOutput = {
1870
- txid: btcFundTxid,
1871
- vout: btcFundVout,
1872
- amount: BigInt(txData.vout[btcFundVout].value),
1873
- };
1874
- }
1875
- }
1876
- }
1877
- catch {
1878
- // Fall through to Esplora lookup
1879
- }
1880
- }
1881
- // Fallback: query Esplora for UTXOs at the address (requires confirmation)
1882
- if (!htlcOutput) {
1883
- htlcOutput = await findOutputByAddress(esploraUrl, btcHtlcAddress);
1884
- }
1885
- if (!htlcOutput) {
1886
- return {
1887
- success: false,
1888
- message: `Could not find UTXO at HTLC address ${btcHtlcAddress}. ` +
1889
- `The address may not have been funded yet.`,
1890
- };
1891
- }
1892
- try {
1893
- // Build the refund transaction
1894
- const result = buildOnchainRefundTransaction({
1895
- fundingTxId: htlcOutput.txid,
1896
- fundingVout: htlcOutput.vout,
1897
- htlcAmount: htlcOutput.amount,
1898
- hashLock,
1899
- serverPubKey: serverXOnlyPubKey,
1900
- userPubKey,
1901
- userSecretKey: storedSwap.secretKey,
1902
- refundLocktime: btcRefundLocktime,
1903
- destinationAddress: options.destinationAddress,
1904
- feeRateSatPerVb: options.feeRateSatPerVb ?? 2,
1905
- network,
1906
- });
1907
- // If dry run, just return the transaction without broadcasting
1908
- if (options.dryRun) {
1909
- return {
1910
- success: true,
1911
- message: "Refund transaction built successfully (dry run - not broadcast).",
1912
- txHex: result.txHex,
1913
- txId: result.txId,
1914
- refundAmount: result.refundAmount,
1915
- fee: result.fee,
1916
- broadcast: false,
1917
- htlcAddress: result.htlcAddress,
1918
- serverHtlcAddress: btcHtlcAddress,
1919
- };
1920
- }
1921
- // Broadcast the transaction
1922
- const broadcastEsploraUrl = this.#config.esploraUrl ?? DEFAULT_ESPLORA_URLS[network];
1923
- if (!broadcastEsploraUrl) {
1924
- return {
1925
- success: true,
1926
- message: "Refund transaction built successfully. No Esplora URL configured for broadcast. " +
1927
- "Broadcast the txHex manually to the Bitcoin network.",
1928
- txHex: result.txHex,
1929
- txId: result.txId,
1930
- refundAmount: result.refundAmount,
1931
- fee: result.fee,
1932
- broadcast: false,
1933
- htlcAddress: result.htlcAddress,
1934
- serverHtlcAddress: btcHtlcAddress,
1935
- };
1936
- }
1937
- try {
1938
- await broadcastTransaction(broadcastEsploraUrl, result.txHex);
1939
- return {
1940
- success: true,
1941
- message: "Refund transaction broadcast successfully!",
1942
- txHex: result.txHex,
1943
- txId: result.txId,
1944
- refundAmount: result.refundAmount,
1945
- fee: result.fee,
1946
- broadcast: true,
1947
- htlcAddress: result.htlcAddress,
1948
- serverHtlcAddress: btcHtlcAddress,
1949
- };
1950
- }
1951
- catch (broadcastError) {
1952
- const broadcastMessage = broadcastError instanceof Error
1953
- ? broadcastError.message
1954
- : String(broadcastError);
1955
- return {
1956
- success: true,
1957
- message: `Transaction built but broadcast failed: ${broadcastMessage}. ` +
1958
- "You can broadcast the txHex manually.",
1959
- txHex: result.txHex,
1960
- txId: result.txId,
1961
- refundAmount: result.refundAmount,
1962
- fee: result.fee,
1963
- broadcast: false,
1964
- htlcAddress: result.htlcAddress,
1965
- serverHtlcAddress: btcHtlcAddress,
1966
- };
1967
- }
1968
- }
1969
- catch (error) {
1970
- const message = error instanceof Error ? error.message : String(error);
1971
- return {
1972
- success: false,
1973
- message: `Failed to build refund transaction: ${message}`,
1974
- };
1975
- }
1976
- }
1977
- /**
1978
- * Builds and executes an Arkade (off-chain) VHTLC refund.
1979
- *
1980
- * Automatically selects the best refund method based on VTXO status:
1981
- * - **spendable** VTXOs → offchain spend (submitTx/finalizeTx)
1982
- * - **recoverable** or **mixed** VTXOs → delegated settlement via backend
1983
- *
1984
- * @internal
1985
- */
1986
- async #buildArkadeRefund(id, swap, options) {
1987
- // Validate options
1988
- if (!options?.destinationAddress) {
1989
- return {
1990
- success: false,
1991
- message: "Destination address is required for Arkade refunds. " +
1992
- 'Provide it via the options parameter: { destinationAddress: "ark1..." }',
1993
- };
1994
- }
1995
- // Check swap storage is configured
1996
- if (!this.#swapStorage) {
1997
- return {
1998
- success: false,
1999
- message: "Swap storage is not configured. Cannot retrieve the secret key needed for refund.",
2000
- };
2001
- }
2002
- // Get stored swap data (contains secret key)
2003
- const storedSwap = await this.#swapStorage.get(id);
2004
- if (!storedSwap) {
2005
- return {
2006
- success: false,
2007
- message: `Swap ${id} not found in local storage. The secret key is required to sign the refund transaction.`,
2008
- };
2009
- }
2010
- // Ensure we have an arkade_to_evm swap
2011
- if (swap.direction !== "arkade_to_evm") {
2012
- return {
2013
- success: false,
2014
- message: `Expected arkade_to_evm swap, got ${swap.direction}`,
2015
- };
2016
- }
2017
- const s = swap;
2018
- const fullPubKey = storedSwap.publicKey;
2019
- const userPubKey = fullPubKey.length === 66 ? fullPubKey.slice(2) : fullPubKey;
2020
- const hashLock = s.hash_lock.startsWith("0x")
2021
- ? s.hash_lock.slice(2)
2022
- : s.hash_lock;
2023
- // Query VTXO status to determine refund method
2024
- const amounts = await this.amountsForSwap(id);
2025
- const vtxoStatus = amounts.vtxoStatus;
2026
- if (vtxoStatus === "not_funded" || vtxoStatus === "spent") {
2027
- return {
2028
- success: false,
2029
- message: vtxoStatus === "not_funded"
2030
- ? "No VTXOs found at the VHTLC address."
2031
- : "All VTXOs have already been spent.",
2032
- };
2033
- }
2034
- const refundParams = {
2035
- userSecretKey: storedSwap.secretKey,
2036
- userPubKey,
2037
- lendaswapPubKey: s.receiver_pk,
2038
- arkadeServerPubKey: s.arkade_server_pk,
2039
- hashLock,
2040
- vhtlcAddress: s.btc_vhtlc_address,
2041
- refundLocktime: s.vhtlc_refund_locktime,
2042
- unilateralClaimDelay: s.unilateral_claim_delay,
2043
- unilateralRefundDelay: s.unilateral_refund_delay,
2044
- unilateralRefundWithoutReceiverDelay: s.unilateral_refund_without_receiver_delay,
2045
- destinationAddress: options.destinationAddress,
2046
- network: s.network,
2047
- };
2048
- // Try collaborative refund first (instant, no locktime wait).
2049
- // Falls back to non-collab refund if the server rejects (e.g. unsafe state).
2050
- const collabParams = {
2051
- ...refundParams,
2052
- swapId: id,
2053
- apiClient: this.#apiClient,
2054
- arkadeServerUrl: options.arkadeServerUrl ?? this.#config.arkadeServerUrl,
2055
- logger: this.#config.logger,
2056
- logLevel: this.#config.logLevel,
2057
- };
2058
- try {
2059
- if (vtxoStatus === "spendable") {
2060
- const result = await collabRefundArkadeToEvmOffchain(collabParams);
2061
- return {
2062
- success: true,
2063
- message: "Arkade refund executed successfully via collaborative offchain spend!",
2064
- txId: result.txId,
2065
- refundAmount: result.refundAmount,
2066
- broadcast: true,
2067
- };
2068
- }
2069
- // recoverable or mixed → collab delegate
2070
- const result = await collabRefundArkadeToEvmDelegate(collabParams);
2071
- return {
2072
- success: true,
2073
- message: "Arkade refund executed successfully via collaborative delegated settlement!",
2074
- txId: result.commitmentTxid,
2075
- broadcast: true,
2076
- };
2077
- }
2078
- catch (collabError) {
2079
- const collabMsg = collabError instanceof Error
2080
- ? collabError.message
2081
- : String(collabError);
2082
- this.#logger.warn({
2083
- event: "arkade.refund.collab_failed",
2084
- message: "Collaborative refund failed, falling back to non-collaborative refund",
2085
- swapId: id,
2086
- data: { reason: collabMsg },
2087
- error: collabError,
2088
- });
2089
- }
2090
- // Fallback: non-collaborative refund (requires locktime to have expired)
2091
- const now = Math.floor(Date.now() / 1000);
2092
- if (now < s.vhtlc_refund_locktime) {
2093
- const remainingSeconds = s.vhtlc_refund_locktime - now;
2094
- const remainingMinutes = Math.ceil(remainingSeconds / 60);
2095
- return {
2096
- success: false,
2097
- message: `Collaborative refund was rejected by the server and non-collaborative refund ` +
2098
- `is not yet available. The VHTLC locktime expires in ${remainingMinutes} minutes ` +
2099
- `(at ${new Date(s.vhtlc_refund_locktime * 1000).toISOString()}).`,
2100
- };
2101
- }
2102
- if (vtxoStatus === "spendable") {
2103
- return this.#refundArkadeOffchain(refundParams, options);
2104
- }
2105
- // recoverable or mixed → delegate
2106
- return this.#refundArkadeDelegate(refundParams, options);
2107
- }
2108
- /**
2109
- * Refund an Arkade-to-Lightning swap.
2110
- *
2111
- * Two paths:
2112
- * 1. **Collaborative refund** (instant) — available when status is `serverwontfund` or `clientinvalidfunded`
2113
- * 2. **Locktime refund** (after CLTV expiry) — fallback when the receiver doesn't cooperate
2114
- *
2115
- * For retrying with a new invoice, use {@link retryArkadeToLightningSwap} instead.
2116
- * @internal
2117
- */
2118
- async #buildArkadeToLightningRefund(id, swap, options) {
2119
- if (!options?.destinationAddress) {
2120
- return {
2121
- success: false,
2122
- message: "Destination address is required for Arkade refunds. " +
2123
- 'Provide it via the options parameter: { destinationAddress: "ark1..." }',
2124
- };
2125
- }
2126
- if (!this.#swapStorage) {
2127
- return {
2128
- success: false,
2129
- message: "Swap storage is not configured. Cannot retrieve the secret key needed for refund.",
2130
- };
2131
- }
2132
- const storedSwap = await this.#swapStorage.get(id);
2133
- if (!storedSwap) {
2134
- return {
2135
- success: false,
2136
- message: `Swap ${id} not found in local storage. The secret key is required to sign the refund transaction.`,
2137
- };
2138
- }
2139
- if (swap.direction !== "arkade_to_lightning") {
2140
- return {
2141
- success: false,
2142
- message: `Expected arkade_to_lightning swap, got ${swap.direction}`,
2143
- };
2144
- }
2145
- const s = swap;
2146
- const fullPubKey = storedSwap.publicKey;
2147
- const userPubKey = fullPubKey.length === 66 ? fullPubKey.slice(2) : fullPubKey;
2148
- const hashLock = s.hash_lock.startsWith("0x")
2149
- ? s.hash_lock.slice(2)
2150
- : s.hash_lock;
2151
- // Try collaborative refund first (instant)
2152
- try {
2153
- // TODO: Add collabRefundArkadeToLightningDelegate
2154
- const result = await collabRefundArkadeToLightningOffchain({
2155
- userSecretKey: storedSwap.secretKey,
2156
- userPubKey,
2157
- receiverPubKey: s.receiver_pk,
2158
- arkadeServerPubKey: s.arkade_server_pk,
2159
- hashLock,
2160
- vhtlcAddress: s.arkade_vhtlc_address,
2161
- refundLocktime: s.vhtlc_refund_locktime,
2162
- unilateralClaimDelay: s.unilateral_claim_delay,
2163
- unilateralRefundDelay: s.unilateral_refund_delay,
2164
- unilateralRefundWithoutReceiverDelay: s.unilateral_refund_without_receiver_delay,
2165
- destinationAddress: options.destinationAddress,
2166
- network: s.network,
2167
- arkadeServerUrl: options.arkadeServerUrl ?? this.#config.arkadeServerUrl,
2168
- swapId: id,
2169
- apiClient: this.#apiClient,
2170
- logger: this.#config.logger,
2171
- logLevel: this.#config.logLevel,
2172
- });
2173
- return {
2174
- success: true,
2175
- message: "Arkade-to-Lightning refund executed via collaborative refund!",
2176
- txId: result.txId,
2177
- refundAmount: result.refundAmount,
2178
- broadcast: true,
2179
- };
2180
- }
2181
- catch (collabError) {
2182
- const collabMsg = collabError instanceof Error
2183
- ? collabError.message
2184
- : String(collabError);
2185
- this.#logger.warn({
2186
- event: "arkade_to_lightning.refund.collab_failed",
2187
- message: "Collaborative refund failed, checking locktime fallback",
2188
- swapId: id,
2189
- data: { reason: collabMsg },
2190
- error: collabError,
2191
- });
2192
- }
2193
- // Fallback: non-collaborative refund (requires locktime to have expired)
2194
- // TODO: Should use Bitcoin's MTP.
2195
- const now = Math.floor(Date.now() / 1000);
2196
- if (now < s.vhtlc_refund_locktime) {
2197
- const remainingSeconds = s.vhtlc_refund_locktime - now;
2198
- const remainingMinutes = Math.ceil(remainingSeconds / 60);
2199
- return {
2200
- success: false,
2201
- message: `collaborative refund failed and non-collaborative refund ` +
2202
- `is not yet available. The VHTLC locktime expires in ${remainingMinutes} minutes ` +
2203
- `(at ${new Date(s.vhtlc_refund_locktime * 1000).toISOString()}). ` +
2204
- `Try again after the locktime expires, or use retryArkadeToLightningSwap() to ` +
2205
- `retry with a new Lightning invoice.`,
2206
- };
2207
- }
2208
- // Locktime expired — use refund_without_receiver path (2-of-2: sender + server)
2209
- // This reuses the existing Arkade refund logic — lendaswapPubKey is set to
2210
- // the receiver key from this swap's VHTLC script.
2211
- // TODO: Not yet e2e-tested!
2212
- const refundParams = {
2213
- userSecretKey: storedSwap.secretKey,
2214
- userPubKey,
2215
- lendaswapPubKey: s.receiver_pk,
2216
- arkadeServerPubKey: s.arkade_server_pk,
2217
- hashLock,
2218
- vhtlcAddress: s.arkade_vhtlc_address,
2219
- refundLocktime: s.vhtlc_refund_locktime,
2220
- unilateralClaimDelay: s.unilateral_claim_delay,
2221
- unilateralRefundDelay: s.unilateral_refund_delay,
2222
- unilateralRefundWithoutReceiverDelay: s.unilateral_refund_without_receiver_delay,
2223
- destinationAddress: options.destinationAddress,
2224
- network: s.network,
2225
- };
2226
- // Query VTXO status to determine refund method
2227
- const amounts = await this.amountsForSwap(id);
2228
- const vtxoStatus = amounts.vtxoStatus;
2229
- if (vtxoStatus === "not_funded" || vtxoStatus === "spent") {
2230
- return {
2231
- success: false,
2232
- message: vtxoStatus === "not_funded"
2233
- ? "No VTXOs found at the VHTLC address."
2234
- : "All VTXOs have already been spent.",
2235
- };
2236
- }
2237
- if (vtxoStatus === "spendable") {
2238
- return this.#refundArkadeOffchain(refundParams, options);
2239
- }
2240
- return this.#refundArkadeDelegate(refundParams, options);
2241
- }
2242
- /**
2243
- * Refunds via the offchain submitTx/finalizeTx path (spendable VTXOs only).
2244
- * @internal
2245
- */
2246
- async #refundArkadeOffchain(params, options) {
2247
- try {
2248
- const result = await buildArkadeRefund({
2249
- ...params,
2250
- arkadeServerUrl: options.arkadeServerUrl ?? this.#config.arkadeServerUrl,
2251
- logger: this.#config.logger,
2252
- logLevel: this.#config.logLevel,
2253
- });
2254
- return {
2255
- success: true,
2256
- message: "Arkade refund executed successfully via offchain spend!",
2257
- txId: result.txId,
2258
- refundAmount: result.refundAmount,
2259
- broadcast: true,
2260
- };
2261
- }
2262
- catch (error) {
2263
- const message = error instanceof Error ? error.message : String(error);
2264
- return {
2265
- success: false,
2266
- message: `Failed to execute offchain Arkade refund: ${message}`,
2267
- };
2268
- }
2269
- }
2270
- /**
2271
- * Refunds via the delegated settlement path (works for all VTXO states).
2272
- * @internal
2273
- */
2274
- async #refundArkadeDelegate(params, options) {
2275
- try {
2276
- const result = await delegateRefund({
2277
- ...params,
2278
- lendaswapApiUrl: this.#config.baseUrl,
2279
- arkadeServerUrl: options.arkadeServerUrl ?? this.#config.arkadeServerUrl,
2280
- logger: this.#config.logger,
2281
- logLevel: this.#config.logLevel,
2282
- });
2283
- return {
2284
- success: true,
2285
- message: "Arkade refund executed successfully via delegated settlement!",
2286
- txId: result.commitmentTxid,
2287
- broadcast: true,
2288
- };
2289
- }
2290
- catch (error) {
2291
- const message = error instanceof Error ? error.message : String(error);
2292
- return {
2293
- success: false,
2294
- message: `Failed to execute delegated Arkade refund: ${message}`,
2295
- };
2296
- }
2297
- }
2298
- /**
2299
- * Builds refund data for an EVM-to-Arkade swap via the coordinator.
2300
- *
2301
- * Calls the server's refund-calldata endpoint which builds coordinator
2302
- * calldata for `refundAndExecute` (swap WBTC back to source token) or
2303
- * `refundTo` (return WBTC directly).
2304
- *
2305
- * @internal
2306
- */
2307
- async #buildEvmToArkadeRefund(id, swap, mode = "swap-back") {
2308
- const evmSwap = swap;
2309
- const timelock = evmSwap.evm_refund_locktime;
2310
- const now = Math.floor(Date.now() / 1000);
2311
- const timelockExpired = now >= timelock;
2312
- // Check if source token is BTC-pegged (WBTC/tBTC) - if so, use direct HTLCErc20 refund
2313
- const isWbtcSource = evmSwap.source_token
2314
- ? isBtcPegged(evmSwap.source_token)
2315
- : false;
2316
- if (isWbtcSource) {
2317
- // Direct HTLCErc20 refund - no DEX swap needed
2318
- const htlcAddress = evmSwap.evm_htlc_address;
2319
- const hashLock = evmSwap.hash_lock;
2320
- const refundData = encodeHtlcErc20RefundCallData(htlcAddress, {
2321
- preimageHash: hashLock,
2322
- amount: BigInt(evmSwap.source_amount),
2323
- token: evmSwap.source_token.token_id,
2324
- claimAddress: evmSwap.server_evm_address, // The server would have been the claimer
2325
- timelock: timelock,
2326
- });
2327
- return {
2328
- success: true,
2329
- message: timelockExpired
2330
- ? "EVM refund calldata ready. Submit this transaction with your EVM wallet."
2331
- : `Timelock has not expired yet. Refund will be available at ${new Date(timelock * 1000).toISOString()}.`,
2332
- evmRefundData: {
2333
- to: refundData.to,
2334
- data: refundData.data,
2335
- timelockExpired,
2336
- timelockExpiry: timelock,
2337
- },
2338
- };
2339
- }
2340
- // Non-WBTC source: fetch coordinator refund calldata from server
2341
- // - "swap-back": swap WBTC back to original token via DEX (default)
2342
- // - "direct": return WBTC directly (useful when DEX calldata is stale)
2343
- const response = await this.#apiClient.GET("/swap/{id}/refund-and-swap-calldata", {
2344
- params: {
2345
- path: { id },
2346
- query: { mode },
2347
- },
2348
- });
2349
- if (response.error) {
2350
- return {
2351
- success: false,
2352
- message: `Failed to fetch refund calldata: ${response.error.error || "Unknown error"}`,
2353
- };
2354
- }
2355
- const { coordinator_address, calldata } = response.data;
2356
- return {
2357
- success: true,
2358
- message: timelockExpired
2359
- ? "EVM refund calldata ready. Submit this transaction with your EVM wallet."
2360
- : `Timelock has not expired yet. Refund will be available at ${new Date(timelock * 1000).toISOString()}.`,
2361
- evmRefundData: {
2362
- to: coordinator_address,
2363
- data: calldata,
2364
- timelockExpired,
2365
- timelockExpiry: timelock,
2366
- },
2367
- };
2368
- }
2369
- /**
2370
- * Builds refund data for an EVM-to-Bitcoin swap via the coordinator.
2371
- * Same pattern as EVM-to-Arkade: uses the coordinator refund-and-swap-calldata endpoint.
2372
- * @internal
2373
- */
2374
- async #buildEvmToBitcoinRefund(id, swap, mode = "swap-back") {
2375
- const evmSwap = swap;
2376
- const timelock = evmSwap.evm_refund_locktime;
2377
- const now = Math.floor(Date.now() / 1000);
2378
- const timelockExpired = now >= timelock;
2379
- // Check if source token is BTC-pegged (WBTC/tBTC) - if so, use direct HTLCErc20 refund
2380
- const isWbtcSource = evmSwap.source_token
2381
- ? isBtcPegged(evmSwap.source_token)
2382
- : false;
2383
- if (isWbtcSource) {
2384
- // Direct HTLCErc20 refund - no DEX swap needed
2385
- const htlcAddress = evmSwap.evm_htlc_address;
2386
- const hashLock = evmSwap.evm_hash_lock;
2387
- const refundData = encodeHtlcErc20RefundCallData(htlcAddress, {
2388
- preimageHash: hashLock,
2389
- amount: BigInt(evmSwap.source_amount),
2390
- token: evmSwap.source_token.token_id,
2391
- claimAddress: evmSwap.server_evm_address, // The server would have been the claimer
2392
- timelock: timelock,
2393
- });
2394
- return {
2395
- success: true,
2396
- message: timelockExpired
2397
- ? "EVM refund calldata ready. Submit this transaction with your EVM wallet."
2398
- : `Timelock has not expired yet. Refund will be available at ${new Date(timelock * 1000).toISOString()}.`,
2399
- evmRefundData: {
2400
- to: refundData.to,
2401
- data: refundData.data,
2402
- timelockExpired,
2403
- timelockExpiry: timelock,
2404
- },
2405
- };
2406
- }
2407
- // Non-WBTC source: use coordinator refund
2408
- // - "swap-back": swap WBTC back to original token via DEX (default)
2409
- // - "direct": return WBTC directly (useful when DEX calldata is stale)
2410
- const response = await this.#apiClient.GET("/swap/{id}/refund-and-swap-calldata", {
2411
- params: {
2412
- path: { id },
2413
- query: { mode },
2414
- },
2415
- });
2416
- if (response.error) {
2417
- return {
2418
- success: false,
2419
- message: `Failed to fetch refund calldata: ${response.error.error || "Unknown error"}`,
2420
- };
2421
- }
2422
- const { coordinator_address, calldata } = response.data;
2423
- return {
2424
- success: true,
2425
- message: timelockExpired
2426
- ? "EVM refund calldata ready. Submit this transaction with your EVM wallet."
2427
- : `Timelock has not expired yet. Refund will be available at ${new Date(timelock * 1000).toISOString()}.`,
2428
- evmRefundData: {
2429
- to: coordinator_address,
2430
- data: calldata,
2431
- timelockExpired,
2432
- timelockExpiry: timelock,
2433
- },
2434
- };
2435
- }
2436
- /**
2437
- * Builds refund data for an EVM-to-Lightning swap via the coordinator.
2438
- *
2439
- * Like EVM-to-Arkade, the coordinator atomically swapped the source token to WBTC
2440
- * before locking in the HTLC. For refunds:
2441
- * - If source was WBTC: direct HTLCErc20 refund
2442
- * - Otherwise: use coordinator refund endpoint (swap-back or direct mode)
2443
- *
2444
- * @internal
2445
- */
2446
- async #buildEvmToLightningRefund(id, swap, mode = "swap-back") {
2447
- const evmSwap = swap;
2448
- const timelock = evmSwap.evm_refund_locktime;
2449
- const now = Math.floor(Date.now() / 1000);
2450
- const timelockExpired = now >= timelock;
2451
- // Check if source token is BTC-pegged (WBTC/tBTC) - if so, use direct HTLCErc20 refund
2452
- const isWbtcSource = evmSwap.source_token
2453
- ? isBtcPegged(evmSwap.source_token)
2454
- : false;
2455
- if (isWbtcSource) {
2456
- // Direct HTLCErc20 refund - no DEX swap needed
2457
- const htlcAddress = evmSwap.evm_htlc_address;
2458
- const hashLock = evmSwap.hash_lock;
2459
- const refundData = encodeHtlcErc20RefundCallData(htlcAddress, {
2460
- preimageHash: hashLock,
2461
- amount: BigInt(evmSwap.source_amount),
2462
- token: evmSwap.source_token.token_id,
2463
- claimAddress: evmSwap.server_evm_address,
2464
- timelock: timelock,
2465
- });
2466
- return {
2467
- success: true,
2468
- message: timelockExpired
2469
- ? "EVM refund calldata ready. Submit this transaction with your EVM wallet."
2470
- : `Timelock has not expired yet. Refund will be available at ${new Date(timelock * 1000).toISOString()}.`,
2471
- evmRefundData: {
2472
- to: refundData.to,
2473
- data: refundData.data,
2474
- timelockExpired,
2475
- timelockExpiry: timelock,
2476
- },
2477
- };
2478
- }
2479
- // Non-WBTC source: fetch coordinator refund calldata from server
2480
- // - "swap-back": swap WBTC back to original token via DEX (default)
2481
- // - "direct": return WBTC directly (useful when DEX calldata is stale)
2482
- const response = await this.#apiClient.GET("/swap/{id}/refund-and-swap-calldata", {
2483
- params: {
2484
- path: { id },
2485
- query: { mode },
2486
- },
2487
- });
2488
- if (response.error) {
2489
- return {
2490
- success: false,
2491
- message: `Failed to fetch refund calldata: ${response.error.error || "Unknown error"}`,
2492
- };
2493
- }
2494
- const { coordinator_address, calldata } = response.data;
2495
- return {
2496
- success: true,
2497
- message: timelockExpired
2498
- ? "EVM refund calldata ready. Submit this transaction with your EVM wallet."
2499
- : `Timelock has not expired yet. Refund will be available at ${new Date(timelock * 1000).toISOString()}.`,
2500
- evmRefundData: {
2501
- to: coordinator_address,
2502
- data: calldata,
2503
- timelockExpired,
2504
- timelockExpiry: timelock,
2505
- },
2506
- };
2507
- }
2508
- // =========================================================================
2509
- // Collaborative EVM Refund
2510
- // =========================================================================
2511
- /**
2512
- * Fetches the EIP-712 parameters for collaborative EVM HTLC refund.
2513
- *
2514
- * Returns the addresses and values needed to build the `CollabRefund`
2515
- * EIP-712 typed data that the depositor signs.
2516
- *
2517
- * @param swapId - Swap ID
2518
- * @returns CollabRefund parameters
2519
- */
2520
- async getCollabRefundEvmParams(swapId, settlement = "direct") {
2521
- const response = await this.#apiClient.GET("/api/swap/{id}/collab-refund-evm/params", {
2522
- params: {
2523
- path: { id: swapId },
2524
- query: { mode: settlement },
2525
- },
2526
- });
2527
- if (response.error) {
2528
- throw new Error(`Failed to fetch collab refund params: ${response.error.error || "Unknown error"}`);
2529
- }
2530
- const d = response.data;
2531
- return {
2532
- coordinatorAddress: d.coordinator_address,
2533
- serverSignerAddress: d.server_signer_address,
2534
- preimageHash: d.preimage_hash,
2535
- amount: d.amount,
2536
- token: d.token,
2537
- claimAddress: d.claim_address,
2538
- timelock: d.timelock,
2539
- chainId: d.chain_id,
2540
- mode: d.mode,
2541
- sweepToken: d.sweep_token,
2542
- minAmountOut: d.min_amount_out,
2543
- callsHash: d.calls_hash,
2544
- sourceTokenAddress: d.source_token_address ?? undefined,
2545
- dexCalldata: d.dex_calldata
2546
- ? {
2547
- to: d.dex_calldata.to,
2548
- data: d.dex_calldata.data,
2549
- value: d.dex_calldata.value,
2550
- }
2551
- : undefined,
2552
- };
2553
- }
2554
- /**
2555
- * Builds the EIP-712 typed data for collaborative EVM refund.
2556
- *
2557
- * The depositor signs this with their wallet (via `eth_signTypedData_v4`
2558
- * or the SDK's `signEvmDigest`) to authorize the server to submit the
2559
- * refund on-chain.
2560
- *
2561
- * @param swapId - Swap ID
2562
- * @param settlement - Settlement mode: "direct" (WBTC) or "swap-back" (original token via DEX)
2563
- * @returns Typed data and digest for signing
2564
- */
2565
- async buildCollabRefundEvmTypedData(swapId, settlement = "direct") {
2566
- const params = await this.getCollabRefundEvmParams(swapId, settlement);
2567
- const digestParams = {
2568
- coordinatorAddress: params.coordinatorAddress,
2569
- chainId: params.chainId,
2570
- preimageHash: params.preimageHash,
2571
- amount: BigInt(params.amount),
2572
- token: params.token,
2573
- claimAddress: params.claimAddress,
2574
- timelock: params.timelock,
2575
- caller: params.serverSignerAddress,
2576
- sweepToken: params.sweepToken,
2577
- minAmountOut: BigInt(params.minAmountOut),
2578
- callsHash: params.callsHash,
2579
- };
2580
- const typedData = buildCollabRefundEvmTypedData(digestParams);
2581
- const digest = buildCollabRefundEvmDigest(digestParams);
2582
- return { typedData, digest, params };
2583
- }
2584
- /**
2585
- * Performs a collaborative EVM refund using the SDK's embedded wallet.
2586
- *
2587
- * The SDK signs the EIP-712 `CollabRefund` digest with the depositor's
2588
- * derived EVM key and POSTs to the server, which cosigns and submits
2589
- * the transaction on-chain. Gasless for the client — no timelock wait.
2590
- *
2591
- * @param swapId - Swap ID
2592
- * @param settlement - Settlement mode: "direct" (WBTC) or "swap-back" (original token via DEX)
2593
- * @returns Refund result with transaction hash
2594
- */
2595
- async collabRefundEvmSwap(swapId, settlement = "direct") {
2596
- const { params, digest } = await this.buildCollabRefundEvmTypedData(swapId, settlement);
2597
- // Sign using the EVM secret key for this swap
2598
- const storedSwap = await this.getStoredSwap(swapId);
2599
- if (!storedSwap?.secretKey) {
2600
- throw new Error("No secret key found for this swap. Cannot sign collab refund.");
2601
- }
2602
- const evmKey = storedSwap.secretKey;
2603
- const sig = signEvmDigest(evmKey, digest);
2604
- // Derive the on-chain depositor address from the per-swap EVM signing key
2605
- const depositorAddress = deriveEvmAddress(evmKey);
2606
- // POST to the server
2607
- const response = await this.#apiClient.POST("/api/swap/{id}/collab-refund-evm", {
2608
- params: { path: { id: swapId } },
2609
- body: {
2610
- v: sig.v,
2611
- r: sig.r,
2612
- s: sig.s,
2613
- depositor_address: depositorAddress,
2614
- mode: settlement,
2615
- sweep_token: params.sweepToken,
2616
- min_amount_out: params.minAmountOut,
2617
- },
2618
- });
2619
- if (response.error) {
2620
- throw new Error(`Collaborative EVM refund failed: ${response.error.error || "Unknown error"}`);
2621
- }
2622
- return {
2623
- id: response.data.id,
2624
- txHash: response.data.tx_hash,
2625
- message: response.data.message,
2626
- };
2627
- }
2628
- /**
2629
- * Submits a pre-signed collaborative EVM refund.
2630
- *
2631
- * Use this when an external wallet (e.g. MetaMask) signs the EIP-712
2632
- * `CollabRefund` digest instead of the SDK's embedded key.
2633
- * Call {@link buildCollabRefundEvmTypedData} first to obtain the typed data
2634
- * for the wallet to sign, then pass the resulting signature here.
2635
- *
2636
- * @param swapId - Swap ID
2637
- * @param body - Signed refund request (signature + refund parameters)
2638
- * @returns Refund result with transaction hash
2639
- */
2640
- async submitCollabRefundEvm(swapId, body) {
2641
- const response = await this.#apiClient.POST("/api/swap/{id}/collab-refund-evm", {
2642
- params: { path: { id: swapId } },
2643
- body,
2644
- });
2645
- if (response.error) {
2646
- throw new Error(`Collaborative EVM refund failed: ${response.error.error || "Unknown error"}`);
2647
- }
2648
- return {
2649
- id: response.data.id,
2650
- txHash: response.data.tx_hash,
2651
- message: response.data.message,
2652
- };
2653
- }
2654
- /**
2655
- * Collaborative EVM refund — internal method called by refundSwap.
2656
- * @internal
2657
- */
2658
- async #collabRefundEvm(id, settlement = "direct") {
2659
- try {
2660
- const result = await this.collabRefundEvmSwap(id, settlement);
2661
- return {
2662
- success: true,
2663
- message: `${result.message} (tx: ${result.txHash})`,
2664
- };
2665
- }
2666
- catch (error) {
2667
- return {
2668
- success: false,
2669
- message: `Collaborative EVM refund failed: ${error instanceof Error ? error.message : String(error)}`,
2670
- };
2671
- }
2672
- }
2673
- // =========================================================================
2674
- // Swap Creation - BTC to EVM
2675
- // =========================================================================
2676
- /**
2677
- * Gets the context object for swap creation functions.
2678
- * @internal
2679
- */
2680
- #getCreateContext() {
2681
- return {
2682
- apiClient: this.#apiClient,
2683
- baseUrl: this.#config.baseUrl,
2684
- deriveSwapParams: () => this.deriveSwapParams(),
2685
- evmAddress: this.getEvmAddress(),
2686
- logger: this.#config.logger,
2687
- logLevel: this.#config.logLevel,
2688
- skipKeyIndices: async (n) => {
2689
- if (this.#signerStorage) {
2690
- const current = await this.#signerStorage.getKeyIndex();
2691
- await this.#signerStorage.setKeyIndex(current + n);
2692
- }
2693
- },
2694
- storeSwap: (swapId, swapParams, response) => this.#storeSwap(swapId, swapParams, response),
2695
- };
2696
- }
2697
- /**
2698
- * Stores a swap in the configured swap storage.
2699
- * @internal
2700
- */
2701
- async #storeSwap(swapId, swapParams, response, targetAddress) {
2702
- if (!this.#swapStorage)
2703
- return;
2704
- const storedSwap = {
2705
- version: SWAP_STORAGE_VERSION,
2706
- swapId,
2707
- keyIndex: swapParams.keyIndex,
2708
- response: response,
2709
- publicKey: bytesToHex(swapParams.publicKey),
2710
- preimage: bytesToHex(swapParams.preimage),
2711
- preimageHash: bytesToHex(swapParams.preimageHash),
2712
- secretKey: bytesToHex(swapParams.secretKey),
2713
- storedAt: Date.now(),
2714
- updatedAt: Date.now(),
2715
- targetAddress,
2716
- };
2717
- await this.#swapStorage.store(storedSwap);
2718
- }
2719
- /**
2720
- * Creates a swap by routing to the correct direction-specific method
2721
- * based on `sourceAsset.chain` and `targetAsset.chain`.
2722
- *
2723
- * Supported directions:
2724
- * - Arkade → EVM
2725
- * - Lightning → EVM
2726
- * - Bitcoin (on-chain) → EVM
2727
- * - Bitcoin (on-chain) → Arkade
2728
- * - EVM → Arkade
2729
- * - EVM → Bitcoin (on-chain)
2730
- * - EVM → Lightning
2731
- *
2732
- * @param options - The swap options including source/target assets, amounts, and addresses.
2733
- * @returns The swap result (response + swapParams).
2734
- * @throws Error if the swap direction is unsupported or required fields are missing.
2735
- */
2736
- async createSwap(options) {
2737
- // Apply config-level referralCode (set via withReferralCode /
2738
- // withOrgCode) as a fallback when the caller didn't pass one. Per-call
2739
- // value always wins.
2740
- options = {
2741
- ...options,
2742
- referralCode: options.referralCode ?? this.#config.referralCode,
2743
- };
2744
- // Resolve source/target from either the new Asset form or the legacy TokenInfo form
2745
- const src = options.source ?? options.sourceAsset;
2746
- const tgt = options.target ?? options.targetAsset;
2747
- if (!src || !tgt) {
2748
- throw new Error("source and target are required (use Asset constants or { chain, tokenId })");
2749
- }
2750
- // Normalize to a common shape with .chain and .token_id
2751
- const sourceAsset = "token_id" in src
2752
- ? { chain: src.chain, token_id: src.token_id }
2753
- : { chain: src.chain, token_id: src.tokenId };
2754
- const targetAsset = "token_id" in tgt
2755
- ? { chain: tgt.chain, token_id: tgt.token_id }
2756
- : { chain: tgt.chain, token_id: tgt.tokenId };
2757
- let sourceChain = sourceAsset.chain;
2758
- let sourceTokenId = sourceAsset.token_id;
2759
- let targetChain = targetAsset.chain;
2760
- let tokenAddress = targetAsset.token_id;
2761
- // If the target is a bridge-only chain (e.g. USDC on Base), automatically
2762
- // remap to the token on Arbitrum for the DEX swap and populate bridgeParams
2763
- // so the backend knows to bridge after. This keeps the remapping logic
2764
- // in one place — SDK consumers just pass their desired target.
2765
- let bridgeParams = options.bridgeParams;
2766
- if (!bridgeParams && isBridgeOnlyChain(targetChain)) {
2767
- const chainName = toChainName(targetChain);
2768
- if (chainName) {
2769
- const isUsdt0 = Object.values(USDT0_ADDRESSES).some((addr) => addr.toLowerCase() === tokenAddress.toLowerCase());
2770
- bridgeParams = {
2771
- targetChain: chainName,
2772
- targetTokenAddress: isUsdt0
2773
- ? USDT0_ADDRESSES[chainName]
2774
- : USDC_ADDRESSES[chainName],
2775
- recipientSetup: options.bridgeRecipientSetup,
2776
- };
2777
- targetChain = "42161"; // Arbitrum
2778
- tokenAddress = isUsdt0
2779
- ? USDT0_ADDRESSES.Arbitrum
2780
- : USDC_ADDRESSES.Arbitrum;
2781
- }
2782
- }
2783
- else if (bridgeParams &&
2784
- options.bridgeRecipientSetup !== undefined &&
2785
- bridgeParams.recipientSetup === undefined) {
2786
- // Caller supplied `bridgeParams` without a `recipientSetup` but
2787
- // also passed the top-level hint — fold the hint in.
2788
- bridgeParams = {
2789
- ...bridgeParams,
2790
- recipientSetup: options.bridgeRecipientSetup,
2791
- };
2792
- }
2793
- // Mirror of the outbound remap for CCTP-inbound sources: when the user
2794
- // provides USDC on a chain the backend doesn't accept as a direct swap
2795
- // source (Optimism, Base, Linea, …), rewrite to Arbitrum USDC and
2796
- // populate `inboundBridgeParams` so the backend accounts for the
2797
- // CCTPv2 fast-transfer fee at quote + UserOp-calldata time.
2798
- let inboundBridgeParams = options.inboundBridgeParams;
2799
- const parsedSourceChainId = Number.parseInt(sourceChain, 10);
2800
- if (!inboundBridgeParams &&
2801
- !Number.isNaN(parsedSourceChainId) &&
2802
- isCctpOnlySource(parsedSourceChainId)) {
2803
- const source = cctpMetaForChainId(parsedSourceChainId);
2804
- if (sourceTokenId.toLowerCase() !== source.usdc.toLowerCase()) {
2805
- throw new Error(`createSwap on ${source.name} requires native USDC (${source.usdc}); got ${sourceTokenId}. Only USDC is bridgeable via CCTP.`);
2806
- }
2807
- inboundBridgeParams = {
2808
- sourceChain: source.name,
2809
- sourceTokenAddress: source.usdc,
2810
- };
2811
- sourceChain = "42161";
2812
- sourceTokenId = USDC_ADDRESSES.Arbitrum;
2813
- }
2814
- // Arkade → EVM
2815
- if (isArkade(sourceAsset) && isEvmToken(targetChain)) {
2816
- return this.createArkadeToEvmSwapGeneric({
2817
- targetAddress: options.targetAddress,
2818
- tokenAddress,
2819
- evmChainId: Number(targetChain),
2820
- sourceAmount: options.sourceAmount
2821
- ? BigInt(options.sourceAmount)
2822
- : undefined,
2823
- targetAmount: options.targetAmount
2824
- ? BigInt(options.targetAmount)
2825
- : undefined,
2826
- referralCode: options.referralCode,
2827
- extraFees: options.extraFees,
2828
- bridgeParams,
2829
- });
2830
- }
2831
- // Lightning → EVM
2832
- if (isLightning(sourceAsset) && isEvmToken(targetChain)) {
2833
- return this.createLightningToEvmSwapGeneric({
2834
- targetAddress: options.targetAddress,
2835
- tokenAddress,
2836
- evmChainId: Number(targetChain),
2837
- amountIn: options.sourceAmount,
2838
- amountOut: options.targetAmount,
2839
- referralCode: options.referralCode,
2840
- extraFees: options.extraFees,
2841
- bridgeParams,
2842
- });
2843
- }
2844
- // Arkade → Lightning
2845
- if (isArkade(sourceAsset) && isLightning(targetAsset)) {
2846
- // Detect whether targetAddress is a Lightning address (user@domain),
2847
- // an LNURL (lnurl1...), or a BOLT11 invoice (starts with ln...).
2848
- const isAddress = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(options.targetAddress);
2849
- const isLnurlStr = /^lnurl1[a-z0-9]+$/i.test(options.targetAddress);
2850
- if (isAddress || isLnurlStr) {
2851
- if (options.targetAmount == null ||
2852
- !Number.isFinite(options.targetAmount) ||
2853
- options.targetAmount <= 0) {
2854
- throw new Error("targetAmount (in sats) is required when using a Lightning address or LNURL");
2855
- }
2856
- return this.createArkadeToLightningSwap({
2857
- ...(isAddress
2858
- ? { lightningAddress: options.targetAddress }
2859
- : { lnurl: options.targetAddress }),
2860
- amountSats: options.targetAmount,
2861
- referralCode: options.referralCode,
2862
- extraFees: options.extraFees,
2863
- });
2864
- }
2865
- return this.createArkadeToLightningSwap({
2866
- lightningInvoice: options.targetAddress,
2867
- referralCode: options.referralCode,
2868
- extraFees: options.extraFees,
2869
- });
2870
- }
2871
- // Lightning → Arkade
2872
- if (isLightning(sourceAsset) && isArkade(targetAsset)) {
2873
- if (options.targetAmount == null) {
2874
- throw new Error("targetAmount (sats to receive on Arkade) is required for Lightning → Arkade swaps");
2875
- }
2876
- return this.createLightningToArkadeSwap({
2877
- satsReceive: options.targetAmount,
2878
- targetAddress: options.targetAddress,
2879
- referralCode: options.referralCode,
2880
- extraFees: options.extraFees,
2881
- });
2882
- }
2883
- // Bitcoin (on-chain) → EVM
2884
- if (isBtcOnchain(sourceAsset) && isEvmToken(targetChain)) {
2885
- return this.createBitcoinToEvmSwap({
2886
- targetAddress: options.targetAddress,
2887
- tokenAddress,
2888
- evmChainId: Number(targetChain),
2889
- sourceAmount: options.sourceAmount,
2890
- targetAmount: options.targetAmount,
2891
- referralCode: options.referralCode,
2892
- extraFees: options.extraFees,
2893
- bridgeParams,
2894
- });
2895
- }
2896
- // Bitcoin (on-chain) → Arkade
2897
- if (isBtcOnchain(sourceAsset) && isArkade(targetAsset)) {
2898
- if (options.targetAmount == null) {
2899
- throw new Error("targetAmount (sats to receive on Arkade) is required for Bitcoin → Arkade swaps");
2900
- }
2901
- return this.createBitcoinToArkadeSwap({
2902
- satsReceive: options.targetAmount,
2903
- targetAddress: options.targetAddress,
2904
- referralCode: options.referralCode,
2905
- extraFees: options.extraFees,
2906
- });
2907
- }
2908
- // EVM → Arkade
2909
- if (isSourceEvmChain(sourceChain) && isArkade(targetAsset)) {
2910
- if (!options.userAddress && !options.gasless) {
2911
- throw new Error("userAddress is required for EVM → Arkade swaps (unless gasless)");
2912
- }
2913
- return this.createEvmToArkadeSwapGeneric({
2914
- targetAddress: options.targetAddress,
2915
- tokenAddress: sourceTokenId,
2916
- evmChainId: Number(sourceChain),
2917
- userAddress: options.userAddress ?? "",
2918
- sourceAmount: options.sourceAmount
2919
- ? BigInt(options.sourceAmount)
2920
- : undefined,
2921
- targetAmount: options.targetAmount,
2922
- referralCode: options.referralCode,
2923
- extraFees: options.extraFees,
2924
- gasless: options.gasless,
2925
- inboundBridgeParams,
2926
- });
2927
- }
2928
- // EVM → Bitcoin (on-chain)
2929
- if (isSourceEvmChain(sourceChain) && isBtcOnchain(targetAsset)) {
2930
- if (!options.userAddress && !options.gasless) {
2931
- throw new Error("userAddress is required for EVM → Bitcoin swaps (unless gasless)");
2932
- }
2933
- return this.createEvmToBitcoinSwap({
2934
- tokenAddress: sourceTokenId,
2935
- evmChainId: Number(sourceChain),
2936
- userAddress: options.userAddress ?? "",
2937
- targetAddress: options.targetAddress,
2938
- sourceAmount: options.sourceAmount
2939
- ? BigInt(options.sourceAmount)
2940
- : undefined,
2941
- targetAmount: options.targetAmount,
2942
- referralCode: options.referralCode,
2943
- extraFees: options.extraFees,
2944
- gasless: options.gasless,
2945
- inboundBridgeParams,
2946
- });
2947
- }
2948
- // EVM → Lightning
2949
- if (isSourceEvmChain(sourceChain) && isLightning(targetAsset)) {
2950
- if (!options.userAddress && !options.gasless) {
2951
- throw new Error("userAddress is required for EVM → Lightning swaps (unless gasless)");
2952
- }
2953
- // Detect whether targetAddress is a Lightning address (user@domain),
2954
- // an LNURL (lnurl1...), or a BOLT11 invoice (starts with ln...).
2955
- const isAddress = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(options.targetAddress);
2956
- const isLnurlStr = /^lnurl1[a-z0-9]+$/i.test(options.targetAddress);
2957
- if (isAddress || isLnurlStr) {
2958
- if (options.targetAmount == null ||
2959
- !Number.isFinite(options.targetAmount) ||
2960
- options.targetAmount <= 0) {
2961
- throw new Error("targetAmount (in sats) is required when using a Lightning address or LNURL");
2962
- }
2963
- return this.createEvmToLightningSwapGeneric({
2964
- ...(isAddress
2965
- ? { lightningAddress: options.targetAddress }
2966
- : { lnurl: options.targetAddress }),
2967
- amountSats: options.targetAmount,
2968
- evmChainId: Number(sourceChain),
2969
- tokenAddress: sourceTokenId,
2970
- userAddress: options.userAddress ?? "",
2971
- referralCode: options.referralCode,
2972
- extraFees: options.extraFees,
2973
- gasless: options.gasless,
2974
- inboundBridgeParams,
2975
- });
2976
- }
2977
- if (isArkade(targetAsset) && isLightning(sourceAsset)) {
2978
- if (!options.targetAmount) {
2979
- throw new Error("Target amount must be set");
2980
- }
2981
- return this.createLightningToArkadeSwap({
2982
- targetAddress: options.targetAddress,
2983
- referralCode: options.referralCode,
2984
- extraFees: options.extraFees,
2985
- satsReceive: options.targetAmount,
2986
- });
2987
- }
2988
- return this.createEvmToLightningSwapGeneric({
2989
- lightningInvoice: options.targetAddress,
2990
- evmChainId: Number(sourceChain),
2991
- tokenAddress: sourceTokenId,
2992
- userAddress: options.userAddress ?? "",
2993
- referralCode: options.referralCode,
2994
- extraFees: options.extraFees,
2995
- gasless: options.gasless,
2996
- inboundBridgeParams,
2997
- });
2998
- }
2999
- throw new Error(`Unsupported swap direction: ${sourceChain} → ${targetChain}`);
3000
- }
3001
- /**
3002
- * Creates a new Arkade-to-EVM swap via the generic chain-agnostic endpoint.
3003
- *
3004
- * Uses the `/swap/arkade/evm` endpoint which supports any ERC-20 token
3005
- * reachable through 1inch aggregation. Returns coordinator address and
3006
- * optional 1inch calldata for the redeem-and-swap flow.
3007
- *
3008
- * @param options - The swap options.
3009
- * @returns The swap response and parameters for storage.
3010
- * @throws Error if the swap creation fails.
3011
- *
3012
- * @example
3013
- * ```ts
3014
- * const result = await client.createArkadeToEvmSwapGeneric({
3015
- * targetAddress: "0x1234...",
3016
- * tokenAddress: "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359", // USDC on Polygon
3017
- * evmChainId: 137,
3018
- * sourceAmount: 100000, // 100k sats
3019
- * });
3020
- * console.log("Fund:", result.response.btc_vhtlc_address);
3021
- * console.log("Coordinator:", result.response.evm_coordinator_address);
3022
- * ```
3023
- */
3024
- async createArkadeToEvmSwapGeneric(options) {
3025
- return createArkadeToEvmSwapGeneric(options, this.#getCreateContext());
3026
- }
3027
- /**
3028
- * Creates a new Lightning to EVM swap using the generic chain-agnostic endpoint.
3029
- *
3030
- * @param options - The swap options including evmChainId and tokenAddress.
3031
- * @returns The swap response and parameters for storage.
3032
- */
3033
- async createLightningToEvmSwapGeneric(options) {
3034
- return createLightningToEvmSwapGeneric(options, this.#getCreateContext());
3035
- }
3036
- /**
3037
- * Creates a new Bitcoin (on-chain) to EVM swap.
3038
- *
3039
- * Automatically derives swap parameters and increments the key index.
3040
- *
3041
- * @param options - The swap options.
3042
- * @returns The swap response and parameters for storage.
3043
- * @throws Error if the swap creation fails.
3044
- *
3045
- * @example
3046
- * ```ts
3047
- * const result = await client.createBitcoinToEvmSwap({
3048
- * targetAddress: "0x1234...",
3049
- * tokenAddress: "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359", // USDC on Polygon
3050
- * evmChainId: 137,
3051
- * sourceAmount: 100000, // 100k sats
3052
- * });
3053
- * console.log("Send BTC to:", result.response.btc_htlc_address);
3054
- * ```
3055
- */
3056
- async createBitcoinToEvmSwap(options) {
3057
- return createBitcoinToEvmSwap(options, this.#getCreateContext());
3058
- }
3059
- // =========================================================================
3060
- // Swap Creation - Bitcoin (on-chain) to Arkade
3061
- // =========================================================================
3062
- /**
3063
- * Creates a new Bitcoin (on-chain) to Arkade swap.
3064
- *
3065
- * The user sends on-chain BTC to a Taproot HTLC address and receives
3066
- * Arkade VTXOs after the server funds the Arkade VHTLC.
3067
- *
3068
- * Automatically derives swap parameters and increments the key index.
3069
- *
3070
- * @param options - The swap options.
3071
- * @returns The swap response and parameters for storage.
3072
- * @throws Error if the swap creation fails.
3073
- *
3074
- * @example
3075
- * ```ts
3076
- * const result = await client.createBitcoinToArkadeSwap({
3077
- * satsReceive: 100000, // 100k sats to receive on Arkade
3078
- * targetAddress: "ark1q...", // Arkade address
3079
- * });
3080
- * console.log("Send BTC to:", result.response.btc_htlc_address);
3081
- * console.log("Amount to send:", result.response.source_amount, "sats");
3082
- * ```
3083
- */
3084
- async createBitcoinToArkadeSwap(options) {
3085
- return createBitcoinToArkadeSwap(options, this.#getCreateContext());
3086
- }
3087
- // =========================================================================
3088
- // Swap Creation - Lightning to Arkade
3089
- // =========================================================================
3090
- /**
3091
- * Creates a new Lightning to Arkade swap.
3092
- *
3093
- * The user pays a Lightning invoice and receives Arkade VTXOs
3094
- * after the server funds the Arkade VHTLC.
3095
- *
3096
- * @param options - The swap options.
3097
- * @returns The swap response and parameters for storage.
3098
- * @throws Error if the swap creation fails.
3099
- *
3100
- * @example
3101
- * ```ts
3102
- * const result = await client.createLightningToArkadeSwap({
3103
- * satsReceive: 100000, // 100k sats to receive on Arkade
3104
- * targetAddress: "ark1q...", // Arkade address
3105
- * });
3106
- * console.log("Pay this invoice:", result.response.bolt11_invoice);
3107
- * ```
3108
- */
3109
- async createLightningToArkadeSwap(options) {
3110
- return createLightningToArkadeSwap(options, this.#getCreateContext());
3111
- }
3112
- // =========================================================================
3113
- // Swap Creation - Arkade to Lightning
3114
- // =========================================================================
3115
- /**
3116
- * Creates a new Arkade to Lightning swap.
3117
- *
3118
- * The user sends Arkade VTXOs and a Lightning invoice gets paid
3119
- * via the Lendaswap server.
3120
- *
3121
- * @param options - The swap options.
3122
- * @returns The swap response and parameters for storage.
3123
- * @throws Error if the swap creation fails.
3124
- *
3125
- * @example
3126
- * ```ts
3127
- * const result = await client.createArkadeToLightningSwap({
3128
- * lightningInvoice: "lnbc100u1p...",
3129
- * });
3130
- * console.log("Fund:", result.response.arkade_vhtlc_address);
3131
- * console.log("Amount:", result.response.source_amount, "sats");
3132
- * ```
3133
- */
3134
- async createArkadeToLightningSwap(options) {
3135
- return createArkadeToLightningSwap(options, this.#getCreateContext());
3136
- }
3137
- // =========================================================================
3138
- // Arkade-to-Lightning: Fee Estimation & Retry
3139
- // =========================================================================
3140
- /**
3141
- * Calculate the correct Lightning invoice amount for an Arkade→Lightning swap.
3142
- *
3143
- * Given the source amount in sats (what will be locked in the VHTLC), returns
3144
- * the target amount that the Lightning invoice should be for (after fees
3145
- * are deducted).
3146
- *
3147
- * Useful for:
3148
- * - Knowing what invoice amount to generate before creating a swap
3149
- * - Retrying a failed swap: the user's funds are locked in the old VHTLC at
3150
- * `sourceAmountSats`, and the new invoice must match the expected target amount
3151
- *
3152
- * @param sourceAmountSats - Amount in sats that will fund the VHTLC
3153
- * @returns Quote with target amount, fee breakdown, and exchange rate
3154
- * @throws Error if the quote request fails or the amount is out of range
3155
- *
3156
- * @example
3157
- * ```ts
3158
- * const quote = await client.getArkadeToLightningQuote(100000);
3159
- * console.log(`Invoice should be for ${quote.net_target_amount} sats`);
3160
- * console.log(`Fees: ${quote.fee} sats`);
3161
- * ```
3162
- */
3163
- async getArkadeToLightningQuote(sourceAmountSats) {
3164
- const { data, error } = await this.#apiClient.GET("/quote", {
3165
- params: {
3166
- query: {
3167
- source_chain: "Arkade",
3168
- source_token: "btc",
3169
- target_chain: "Lightning",
3170
- target_token: "btc",
3171
- source_amount: sourceAmountSats,
3172
- },
3173
- },
3174
- });
3175
- if (error) {
3176
- throw new Error(`Failed to get Arkade→Lightning quote: ${typeof error === "string" ? error : JSON.stringify(error)}`);
3177
- }
3178
- if (!data) {
3179
- throw new Error("No quote data returned");
3180
- }
3181
- return data;
3182
- }
3183
- /**
3184
- * Retry a failed Arkade→Lightning swap with a new Lightning invoice or LNURL.
3185
- *
3186
- * When an Arkade→Lightning swap fails (status `serverwontfund` or `clientinvalidfunded`),
3187
- * this method:
3188
- * 1. Creates a new Arkade→Lightning swap with the new invoice/LNURL
3189
- * 2. Collaboratively refunds the old VHTLC into the new swap's VHTLC
3190
- *
3191
- * The refund uses the collaborative `refund` script leaf (3-of-3: sender + receiver + Arkade),
3192
- * which is instant (no locktime wait). The receiver cooperates because the swap is in
3193
- * `invoice.failedToPay` state.
3194
- *
3195
- * **Invoice amount**: The new invoice must match the expected target amount for the
3196
- * source amount locked in the old VHTLC. Use {@link getArkadeToLightningQuote} to
3197
- * calculate the correct invoice amount, or use `lightningAddress` (LNURL) which
3198
- * handles amount negotiation automatically.
3199
- *
3200
- * @param swapId - ID of the failed swap (must be in `serverwontfund` or `clientinvalidfunded` status)
3201
- * @param options - New invoice or Lightning address for the retry
3202
- * @returns The new swap response and the refund transaction ID
3203
- * @throws Error if the swap is not in the right state, amount mismatch, or server refuses
3204
- *
3205
- * @example
3206
- * ```ts
3207
- * // With LNURL (recommended — handles amount automatically):
3208
- * const result = await client.retryArkadeToLightningSwap(swapId, {
3209
- * lightningAddress: "user@speed.app",
3210
- * });
3211
- *
3212
- * // With invoice (must match expected amount):
3213
- * const quote = await client.getArkadeToLightningQuote(oldSwap.boltz_amount_sats);
3214
- * // Generate invoice for quote.net_target_amount sats, then:
3215
- * const result = await client.retryArkadeToLightningSwap(swapId, {
3216
- * lightningInvoice: "lnbc...",
3217
- * });
3218
- * ```
3219
- */
3220
- async retryArkadeToLightningSwap(swapId, options) {
3221
- if (!this.#swapStorage) {
3222
- throw new Error("Swap storage not configured. Cannot retrieve keys needed for refund.");
3223
- }
3224
- if (!options.lightningInvoice && !options.lightningAddress) {
3225
- throw new Error("Provide either lightningInvoice or lightningAddress for retry");
3226
- }
3227
- if (options.lightningInvoice && options.lightningAddress) {
3228
- throw new Error("Provide either lightningInvoice or lightningAddress, not both");
3229
- }
3230
- // 1. Validate the old swap is in a retryable state
3231
- const oldSwap = await this.getSwap(swapId, {
3232
- updateStorage: true,
3233
- });
3234
- if (oldSwap.direction !== "arkade_to_lightning") {
3235
- throw new Error(`Expected arkade_to_lightning swap, got ${oldSwap.direction}`);
3236
- }
3237
- const retryableStatuses = ["serverwontfund", "clientinvalidfunded"];
3238
- if (!retryableStatuses.includes(oldSwap.status)) {
3239
- throw new Error(`Swap must be in serverwontfund or clientinvalidfunded status to retry ` +
3240
- `(current: ${oldSwap.status}). ` +
3241
- (oldSwap.status === "serverredeemed"
3242
- ? "This swap completed successfully — no retry needed."
3243
- : oldSwap.status === "clientrefunded"
3244
- ? "This swap was already refunded."
3245
- : "The swap may still be in progress."));
3246
- }
3247
- // 2. Get the source amount locked in the old VHTLC
3248
- const sourceAmountSats = oldSwap.boltz_amount_sats;
3249
- // 3. Build create-swap options
3250
- const createOptions = {};
3251
- if (options.lightningAddress) {
3252
- // LNURL: use the quote to determine the right amount
3253
- const quote = await this.getArkadeToLightningQuote(sourceAmountSats);
3254
- createOptions.lightningAddress = options.lightningAddress;
3255
- createOptions.amountSats = Number(quote.net_target_amount);
3256
- }
3257
- else if (options.lightningInvoice) {
3258
- createOptions.lightningInvoice = options.lightningInvoice;
3259
- }
3260
- // 4. Create the new swap
3261
- const newSwapResult = await this.createArkadeToLightningSwap(createOptions);
3262
- const newSwap = newSwapResult.response;
3263
- // 5. Verify the new swap's expected funding amount matches our old VHTLC
3264
- const newExpectedAmount = newSwap.boltz_amount_sats;
3265
- if (newExpectedAmount !== sourceAmountSats) {
3266
- const mismatchHint = newExpectedAmount > sourceAmountSats
3267
- ? "The invoice amount may be too high"
3268
- : "The invoice amount may be too low";
3269
- throw new Error(`Amount mismatch: new swap expects ${newExpectedAmount} sats in VHTLC ` +
3270
- `but old VHTLC has ${sourceAmountSats} sats. ` +
3271
- `${mismatchHint} — use getArkadeToLightningQuote(${sourceAmountSats}) ` +
3272
- `to calculate the correct invoice amount before retrying.`);
3273
- }
3274
- const storedSwap = await this.#swapStorage.get(swapId);
3275
- if (!storedSwap) {
3276
- throw new Error(`Swap ${swapId} not found in local storage. ` +
3277
- `The secret key is required to sign the collaborative refund.`);
3278
- }
3279
- const fullPubKey = storedSwap.publicKey;
3280
- const userPubKey = fullPubKey.length === 66 ? fullPubKey.slice(2) : fullPubKey;
3281
- const hashLock = oldSwap.hash_lock.startsWith("0x")
3282
- ? oldSwap.hash_lock.slice(2)
3283
- : oldSwap.hash_lock;
3284
- // Collaborative refund: old VHTLC → new VHTLC address
3285
- this.#logger.info({
3286
- event: "arkade_to_lightning.retry.collab_refund_start",
3287
- message: "Starting collaborative refund before retry",
3288
- swapId,
3289
- data: {
3290
- oldSwapId: swapId,
3291
- newSwapId: newSwap.id,
3292
- sourceAmount: sourceAmountSats,
3293
- network: oldSwap.network,
3294
- },
3295
- });
3296
- const refundResult = await collabRefundArkadeToLightningOffchain({
3297
- userSecretKey: storedSwap.secretKey,
3298
- userPubKey,
3299
- receiverPubKey: oldSwap.receiver_pk,
3300
- arkadeServerPubKey: oldSwap.arkade_server_pk,
3301
- hashLock,
3302
- vhtlcAddress: oldSwap.arkade_vhtlc_address,
3303
- refundLocktime: oldSwap.vhtlc_refund_locktime,
3304
- unilateralClaimDelay: oldSwap.unilateral_claim_delay,
3305
- unilateralRefundDelay: oldSwap.unilateral_refund_delay,
3306
- unilateralRefundWithoutReceiverDelay: oldSwap.unilateral_refund_without_receiver_delay,
3307
- destinationAddress: newSwap.arkade_vhtlc_address,
3308
- network: oldSwap.network,
3309
- arkadeServerUrl: this.#config.arkadeServerUrl,
3310
- swapId,
3311
- apiClient: this.#apiClient,
3312
- logger: this.#config.logger,
3313
- logLevel: this.#config.logLevel,
3314
- });
3315
- return {
3316
- newSwap,
3317
- refundTxId: refundResult.txId,
3318
- refundAmount: refundResult.refundAmount,
3319
- };
3320
- }
3321
- // =========================================================================
3322
- // Swap Creation - EVM to Arkade
3323
- // =========================================================================
3324
- /**
3325
- * Creates a new EVM-to-Arkade swap via the generic endpoint.
3326
- *
3327
- * Uses the chain-agnostic `/swap/evm/arkade` endpoint which supports any
3328
- * ERC-20 token reachable through 1inch aggregation.
3329
- *
3330
- * @param options - The swap options.
3331
- * @returns The swap response and parameters for storage.
3332
- * @throws Error if the swap creation fails.
3333
- *
3334
- * @example
3335
- * ```ts
3336
- * const result = await client.createEvmToArkadeSwapGeneric({
3337
- * targetAddress: "ark1q...",
3338
- * tokenAddress: "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359", // USDC on Polygon
3339
- * evmChainId: 137,
3340
- * userAddress: "0x1234...",
3341
- * sourceAmount: 100000000, // 100 USDC (6 decimals)
3342
- * });
3343
- * console.log("HTLC:", result.response.evm_htlc_address);
3344
- * ```
3345
- */
3346
- async createEvmToArkadeSwapGeneric(options) {
3347
- return createEvmToArkadeSwapGeneric(options, this.#getCreateContext());
3348
- }
3349
- /**
3350
- * Creates a new EVM-to-Bitcoin (on-chain) swap.
3351
- *
3352
- * Uses the chain-agnostic `/swap/evm/bitcoin` endpoint which supports any
3353
- * ERC-20 token reachable through 1inch aggregation. The user locks tokens
3354
- * in an EVM HTLC and receives BTC to an on-chain Taproot HTLC.
3355
- *
3356
- * @param options - The swap options.
3357
- * @returns The swap response and parameters for storage.
3358
- * @throws Error if the swap creation fails.
3359
- *
3360
- * @example
3361
- * ```ts
3362
- * const result = await client.createEvmToBitcoinSwap({
3363
- * tokenAddress: "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359", // USDC on Polygon
3364
- * evmChainId: 137,
3365
- * userAddress: "0x1234...",
3366
- * sourceAmount: 100000000n, // 100 USDC (6 decimals)
3367
- * });
3368
- * console.log("EVM HTLC:", result.response.evm_htlc_address);
3369
- * console.log("BTC HTLC:", result.response.btc_htlc_address);
3370
- * ```
3371
- */
3372
- async createEvmToBitcoinSwap(options) {
3373
- return createEvmToBitcoinSwap(options, this.#getCreateContext());
3374
- }
3375
- /**
3376
- * Creates a new EVM to Lightning swap using the chain-agnostic generic endpoint.
3377
- *
3378
- * This allows users to swap any ERC-20 token from any supported EVM chain
3379
- * to pay a Lightning invoice.
3380
- *
3381
- * @param options - The swap options including Lightning invoice, chain ID, and token address.
3382
- * @returns The swap response and parameters for storage.
3383
- * @throws Error if the swap creation fails.
3384
- *
3385
- * @example
3386
- * ```ts
3387
- * const result = await client.createEvmToLightningSwapGeneric({
3388
- * lightningInvoice: "lnbc...",
3389
- * evmChainId: 137, // Polygon
3390
- * tokenAddress: "0x1BFD67037B42Cf73acF2047067bd4F2C47D9BfD6", // WBTC
3391
- * userAddress: "0x1234...",
3392
- * });
3393
- * console.log("HTLC contract:", result.response.evm_htlc_address);
3394
- * console.log("Swap ID:", result.response.id);
3395
- * ```
3396
- */
3397
- async createEvmToLightningSwapGeneric(options) {
3398
- return createEvmToLightningSwapGeneric(options, this.#getCreateContext());
3399
- }
3400
- // =========================================================================
3401
- // Coordinator Funding (EVM-to-BTC via DEX + HTLC)
3402
- // =========================================================================
3403
- /**
3404
- * Gets Permit2-based call data to fund an EVM-to-BTC swap via the HTLCCoordinator.
3405
- *
3406
- * Uses Permit2 for gasless token approval: the user signs an off-chain EIP-712
3407
- * message instead of submitting a separate `approve` tx to the source token.
3408
- *
3409
- * The returned `approve` is a one-time max approval of the source token to the
3410
- * Permit2 contract (can be skipped if already approved). The `executeAndCreate`
3411
- * calldata includes the Permit2 signature and can be submitted by anyone (relayer).
3412
- *
3413
- * @param swapId - The UUID of the swap
3414
- * @param chainId - The EVM chain ID (e.g. 137 for Polygon, 1 for Ethereum)
3415
- * @returns The approve (token → Permit2) and executeAndCreateWithPermit2 call data
3416
- *
3417
- * @example
3418
- * ```ts
3419
- * const funding = await client.getCoordinatorFundingCallDataPermit2(swap.response.id, 137);
3420
- *
3421
- * // Step 1: One-time approve source token to Permit2 (can skip if already done)
3422
- * await wallet.sendTransaction({ to: funding.approve.to, data: funding.approve.data });
3423
- *
3424
- * // Step 2: Execute swap + create HTLC (signed via Permit2)
3425
- * await wallet.sendTransaction({ to: funding.executeAndCreate.to, data: funding.executeAndCreate.data });
3426
- * ```
3427
- */
3428
- async getCoordinatorFundingCallDataPermit2(swapId, chainId) {
3429
- // 1. Look up stored swap to get the secret key for signing
3430
- const storedSwap = await this.getStoredSwap(swapId);
3431
- if (!storedSwap) {
3432
- throw new Error(`Swap ${swapId} not found in local storage. Cannot sign Permit2 message without the secret key.`);
3433
- }
3434
- const swap = await this.getSwap(swapId);
3435
- if (swap.direction !== "evm_to_arkade" &&
3436
- swap.direction !== "evm_to_bitcoin" &&
3437
- swap.direction !== "evm_to_lightning") {
3438
- throw new Error(`Expected evm_to_arkade/evm_to_bitcoin/evm_to_lightning swap, got ${swap.direction}. Permit2 fund method is for EVM-sourced swaps.`);
3439
- }
3440
- // 2. Fetch Permit2 funding data from server
3441
- const baseUrl = this.#config.baseUrl.replace(/\/$/, "");
3442
- const url = `${baseUrl}/swap/${swapId}/swap-and-lock-calldata-permit2`;
3443
- const resp = await fetch(url);
3444
- if (!resp.ok) {
3445
- const body = await resp.text();
3446
- throw new Error(`Failed to get Permit2 funding data: ${resp.status} ${body}`);
3447
- }
3448
- const serverData = (await resp.json());
3449
- // 3. Generate random Permit2 nonce and deadline
3450
- const nonceBytes = new Uint8Array(32);
3451
- crypto.getRandomValues(nonceBytes);
3452
- const nonce = BigInt(`0x${Array.from(nonceBytes)
3453
- .map((b) => b.toString(16).padStart(2, "0"))
3454
- .join("")}`);
3455
- const deadline = BigInt(Math.floor(Date.now() / 1000) + 30 * 60); // 30 minutes
3456
- const sourceAmount = BigInt(serverData.source_amount);
3457
- // 4. Build EIP-712 digest
3458
- // refundAddress = coordinator address for overload 1 (depositor tracking)
3459
- const digest = buildPermit2FundingDigest({
3460
- chainId,
3461
- coordinatorAddress: serverData.coordinator_address,
3462
- sourceToken: serverData.source_token_address,
3463
- sourceAmount,
3464
- preimageHash: serverData.preimage_hash,
3465
- lockToken: serverData.lock_token_address,
3466
- claimAddress: serverData.claim_address,
3467
- refundAddress: serverData.coordinator_address,
3468
- timelock: serverData.timelock,
3469
- callsHash: serverData.calls_hash,
3470
- nonce,
3471
- deadline,
3472
- });
3473
- // 5. Sign with the EVM key (deterministic for new swaps, per-swap for legacy)
3474
- const evmKey = this.#getEvmSigningKey();
3475
- const sig = signEvmDigest(evmKey, digest);
3476
- // Compact signature: r (32 bytes) || s (32 bytes) || v (1 byte)
3477
- const rClean = sig.r.replace(/^0x/, "");
3478
- const sClean = sig.s.replace(/^0x/, "");
3479
- const vHex = sig.v.toString(16).padStart(2, "0");
3480
- const compactSignature = `0x${rClean}${sClean}${vHex}`;
3481
- // 6. Build calls array for the coordinator
3482
- const calls = serverData.calls.map((c) => ({
3483
- target: c.target,
3484
- value: BigInt(c.value),
3485
- data: c.call_data,
3486
- }));
3487
- // Derive depositor address from the EVM signing key
3488
- const depositorAddress = deriveEvmAddress(evmKey);
3489
- // 7. Encode executeAndCreateWithPermit2 calldata
3490
- const encoded = encodeExecuteAndCreateWithPermit2(serverData.coordinator_address, {
3491
- calls,
3492
- preimageHash: serverData.preimage_hash,
3493
- token: serverData.lock_token_address,
3494
- claimAddress: serverData.claim_address,
3495
- timelock: serverData.timelock,
3496
- depositor: depositorAddress,
3497
- sourceToken: serverData.source_token_address,
3498
- sourceAmount,
3499
- nonce,
3500
- deadline,
3501
- signature: compactSignature,
3502
- });
3503
- // 8. Build approve: source token → Permit2 (max uint256, one-time)
3504
- const maxUint256 = BigInt("0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff");
3505
- const approve = encodeApproveCallData(serverData.source_token_address, PERMIT2_ADDRESS, maxUint256);
3506
- return {
3507
- approve: {
3508
- to: approve.to,
3509
- data: approve.data,
3510
- },
3511
- executeAndCreate: {
3512
- to: encoded.to,
3513
- data: encoded.data,
3514
- },
3515
- };
3516
- }
3517
- /**
3518
- * Get unsigned Permit2 funding parameters for the sovereign flow.
3519
- *
3520
- * Returns the EIP-712 typed data structure and all parameters needed
3521
- * for the user's browser wallet to sign via `signTypedData` and then
3522
- * encode + submit the `executeAndCreateWithPermit2` transaction.
3523
- *
3524
- * Unlike `getCoordinatorFundingCallDataPermit2`, this method does NOT
3525
- * sign the Permit2 message — the caller is responsible for obtaining
3526
- * the signature from the user's wallet.
3527
- *
3528
- * @param swapId - The UUID of the swap
3529
- * @param chainId - The EVM chain ID
3530
- * @returns Unsigned Permit2 funding data including typed data for signing
3531
- *
3532
- * @example
3533
- * ```ts
3534
- * const params = await client.getPermit2FundingParamsUnsigned(swapId, chainId);
3535
- *
3536
- * // In the browser with wagmi/viem:
3537
- * const signature = await walletClient.signTypedData(params.typedData);
3538
- *
3539
- * // Encode the final calldata
3540
- * const calldata = encodeExecuteAndCreateWithPermit2(
3541
- * params.coordinatorAddress,
3542
- * {
3543
- * calls: params.calls,
3544
- * preimageHash: params.preimageHash,
3545
- * token: params.lockTokenAddress,
3546
- * claimAddress: params.claimAddress,
3547
- * timelock: params.timelock,
3548
- * depositor: userWalletAddress,
3549
- * sourceToken: params.sourceTokenAddress,
3550
- * sourceAmount: params.sourceAmount,
3551
- * nonce: params.nonce,
3552
- * deadline: params.deadline,
3553
- * signature,
3554
- * },
3555
- * );
3556
- * ```
3557
- */
3558
- async getPermit2FundingParamsUnsigned(swapId, chainId) {
3559
- const swap = await this.getSwap(swapId);
3560
- if (swap.direction !== "evm_to_arkade" &&
3561
- swap.direction !== "evm_to_bitcoin" &&
3562
- swap.direction !== "evm_to_lightning") {
3563
- throw new Error(`Expected evm_to_arkade/evm_to_bitcoin/evm_to_lightning swap, got ${swap.direction}. Permit2 fund method is for EVM-sourced swaps.`);
3564
- }
3565
- // Fetch Permit2 funding data from server
3566
- const baseUrl = this.#config.baseUrl.replace(/\/$/, "");
3567
- const url = `${baseUrl}/swap/${swapId}/swap-and-lock-calldata-permit2`;
3568
- const resp = await fetch(url);
3569
- if (!resp.ok) {
3570
- const body = await resp.text();
3571
- throw new Error(`Failed to get Permit2 funding data: ${resp.status} ${body}`);
3572
- }
3573
- const serverData = (await resp.json());
3574
- // Generate random Permit2 nonce and deadline
3575
- const nonceBytes = new Uint8Array(32);
3576
- crypto.getRandomValues(nonceBytes);
3577
- const nonce = BigInt(`0x${Array.from(nonceBytes)
3578
- .map((b) => b.toString(16).padStart(2, "0"))
3579
- .join("")}`);
3580
- const deadline = BigInt(Math.floor(Date.now() / 1000) + 30 * 60); // 30 minutes
3581
- const sourceAmount = BigInt(serverData.source_amount);
3582
- const calls = serverData.calls.map((c) => ({
3583
- target: c.target,
3584
- value: BigInt(c.value),
3585
- data: c.call_data,
3586
- }));
3587
- // Build EIP-712 typed data for wallet signing
3588
- const fundingParams = {
3589
- chainId,
3590
- coordinatorAddress: serverData.coordinator_address,
3591
- sourceToken: serverData.source_token_address,
3592
- sourceAmount,
3593
- preimageHash: serverData.preimage_hash,
3594
- lockToken: serverData.lock_token_address,
3595
- claimAddress: serverData.claim_address,
3596
- refundAddress: serverData.coordinator_address, // overload 1: depositor tracking
3597
- timelock: serverData.timelock,
3598
- callsHash: serverData.calls_hash,
3599
- nonce,
3600
- deadline,
3601
- };
3602
- const typedData = buildPermit2TypedData(fundingParams);
3603
- return {
3604
- coordinatorAddress: serverData.coordinator_address,
3605
- sourceTokenAddress: serverData.source_token_address,
3606
- sourceAmount,
3607
- lockTokenAddress: serverData.lock_token_address,
3608
- preimageHash: serverData.preimage_hash,
3609
- claimAddress: serverData.claim_address,
3610
- timelock: serverData.timelock,
3611
- calls,
3612
- callsHash: serverData.calls_hash,
3613
- nonce,
3614
- deadline,
3615
- typedData,
3616
- };
3617
- }
3618
- /**
3619
- * Fund an EVM-sourced swap using an external wallet (e.g. MetaMask).
3620
- *
3621
- * Handles the full flow:
3622
- * 1. Check ERC-20 allowance to Permit2 — approve if insufficient
3623
- * 2. Sign the Permit2 EIP-712 typed data
3624
- * 3. Encode and send the `executeAndCreateWithPermit2` transaction
3625
- * 4. Wait for the transaction receipt
3626
- *
3627
- * @param swapId - The UUID of the swap
3628
- * @param signer - An {@link EvmSigner} wrapping the user's wallet
3629
- * @param options - Optional CCTP-path tuning: `maxFee` caps the
3630
- * CCTP fast-transfer fee (defaults to the
3631
- * IRIS-quoted value); `onProgress` receives
3632
- * per-phase updates during the CCTP flow;
3633
- * `signal` aborts the attestation wait. All
3634
- * three are ignored when the swap takes the
3635
- * direct-Permit2 path.
3636
- * @returns The funding transaction hash, plus an optional `cctp`
3637
- * object with `burnTxHash`, `userOpHash`, and
3638
- * `smartAccountAddress` when the CCTP path ran.
3639
- *
3640
- * @example
3641
- * ```ts
3642
- * // Wrap wagmi/viem into an EvmSigner
3643
- * const signer: EvmSigner = {
3644
- * address: walletClient.account.address,
3645
- * chainId: walletClient.chain.id,
3646
- * signTypedData: (td) => walletClient.signTypedData({ ...td, account: walletClient.account }),
3647
- * sendTransaction: (tx) => walletClient.sendTransaction({ to: tx.to, data: tx.data, chain, gas: tx.gas }),
3648
- * waitForReceipt: (hash) => publicClient.waitForReceipt({ hash }),
3649
- * getTransaction: (hash) => publicClient.getTransaction({ hash }),
3650
- * call: (tx) => publicClient.call(tx),
3651
- * };
3652
- *
3653
- * const { txHash } = await client.fundSwap(swapId, signer);
3654
- * ```
3655
- */
3656
- async fundSwap(swapId, signer, options) {
3657
- // Dispatch the CCTP-inbound path only for chains the backend does
3658
- // NOT accept as a direct swap source (Optimism, Base, Linea, …).
3659
- // Ethereum / Polygon / Arbitrum fund via Permit2 on the source
3660
- // chain even though they're CCTP-supported. Source asset must be
3661
- // native USDC for the chain — otherwise CCTP isn't viable and we
3662
- // surface a clear error instead of silently burning the wrong
3663
- // token through TokenMessenger.
3664
- if (isCctpOnlySource(signer.chainId)) {
3665
- const stored = await this.#swapStorage?.get(swapId);
3666
- const swapResponse = stored?.response ??
3667
- (await this.getSwap(swapId, { updateStorage: true }));
3668
- const source = cctpMetaForChainId(signer.chainId);
3669
- // CCTP-inbound swaps are stored against post-hop Arbitrum USDC
3670
- // (`source_token` reports the Arbitrum address), so we validate
3671
- // against the `bridge_source_*` fields the backend sets when the
3672
- // swap was created through the CCTP path. If those are missing
3673
- // the swap wasn't created for a CCTP-inbound flow — reject rather
3674
- // than silently burn USDC through the TokenMessenger.
3675
- const bridgeSourceChain = swapResponse.bridge_source_chain;
3676
- const bridgeSourceToken = swapResponse.bridge_source_token_address;
3677
- if (!bridgeSourceChain || !bridgeSourceToken) {
3678
- throw new Error(`fundSwap on chain ${signer.chainId} expects a CCTP-inbound swap but the swap has no bridge_source_chain set. ` +
3679
- `Was the swap created against native USDC on a CCTP-only chain?`);
3680
- }
3681
- if (bridgeSourceChain !== source.name) {
3682
- throw new Error(`fundSwap signer is on ${source.name} but the swap was created for bridge source ${bridgeSourceChain}.`);
3683
- }
3684
- if (bridgeSourceToken.toLowerCase() !== source.usdc.toLowerCase()) {
3685
- throw new Error(`fundSwap on ${source.name} requires native USDC (${source.usdc}), but swap's bridge_source_token_address is ${bridgeSourceToken}. ` +
3686
- `Only native USDC is bridgeable via CCTP.`);
3687
- }
3688
- const rawAmount = swapResponse
3689
- .source_amount;
3690
- if (rawAmount === undefined) {
3691
- throw new Error(`Swap ${swapId} has no source_amount — cannot route CCTP fund.`);
3692
- }
3693
- const amount = BigInt(rawAmount);
3694
- const destination = cctpMetaForChainId(42161);
3695
- const maxFee = options?.maxFee ??
3696
- computeCctpFastFee(await getCachedCctpFee({
3697
- sourceDomain: source.domain,
3698
- destinationDomain: destination.domain,
3699
- }), amount);
3700
- const cctp = await this.cctpInbound.fundSwap({
3701
- swapId,
3702
- signer,
3703
- amount,
3704
- maxFee,
3705
- onProgress: options?.onProgress,
3706
- signal: options?.signal,
3707
- });
3708
- return { txHash: cctp.transactionHash ?? cctp.userOpHash, cctp };
3709
- }
3710
- // Direct Permit2 path — Ethereum, Polygon, Arbitrum, or any other
3711
- // chain `signer.chainId` specifies. Unchanged from the original
3712
- // behaviour.
3713
- // 1. Fetch Permit2 funding params
3714
- const funding = await this.getPermit2FundingParamsUnsigned(swapId, signer.chainId);
3715
- const tokenAddress = funding.sourceTokenAddress;
3716
- const permit2 = PERMIT2_ADDRESS;
3717
- // 2. Check balance — fail early before burning gas on approve/execute
3718
- const balanceCall = encodeBalanceOfCall(tokenAddress, signer.address);
3719
- const balanceResult = await signer.call({
3720
- to: balanceCall.to,
3721
- data: balanceCall.data,
3722
- });
3723
- const balance = decodeUint256(balanceResult);
3724
- if (balance < funding.sourceAmount) {
3725
- throw new Error(`Insufficient token balance: have ${balance}, need ${funding.sourceAmount}`);
3726
- }
3727
- // 3. Check allowance to Permit2 — approve if insufficient
3728
- const allowanceCall = encodeAllowanceCall(tokenAddress, signer.address, permit2);
3729
- const allowanceResult = await signer.call({
3730
- to: allowanceCall.to,
3731
- data: allowanceCall.data,
3732
- });
3733
- const allowance = decodeUint256(allowanceResult);
3734
- if (allowance < funding.sourceAmount) {
3735
- // Send approve(Permit2, max)
3736
- const approveData = encodeMaxApproveData(tokenAddress, permit2);
3737
- const approveTxHash = await signer.sendTransaction({
3738
- to: approveData.to,
3739
- data: approveData.data,
3740
- gas: 100000n,
3741
- });
3742
- const approveReceipt = await signer.waitForReceipt(approveTxHash);
3743
- if (approveReceipt.status !== "success") {
3744
- const reason = await getRevertReason(signer, approveTxHash, approveReceipt.blockNumber);
3745
- throw new Error(`Token approval failed: ${reason}`);
3746
- }
3747
- }
3748
- // 4. Re-fetch fresh funding params (1inch quotes expire quickly)
3749
- const freshFunding = await this.getPermit2FundingParamsUnsigned(swapId, signer.chainId);
3750
- // 5. Sign the Permit2 EIP-712 typed data
3751
- const signature = await signer.signTypedData(freshFunding.typedData);
3752
- // 6. Encode executeAndCreateWithPermit2 calldata
3753
- const encoded = encodeExecuteAndCreateWithPermit2(freshFunding.coordinatorAddress, {
3754
- calls: freshFunding.calls,
3755
- preimageHash: freshFunding.preimageHash,
3756
- token: freshFunding.lockTokenAddress,
3757
- claimAddress: freshFunding.claimAddress,
3758
- timelock: freshFunding.timelock,
3759
- depositor: signer.address,
3760
- sourceToken: freshFunding.sourceTokenAddress,
3761
- sourceAmount: freshFunding.sourceAmount,
3762
- nonce: freshFunding.nonce,
3763
- deadline: freshFunding.deadline,
3764
- signature,
3765
- });
3766
- // 7. Simulate before sending to catch reverts without burning gas
3767
- await simulateTransaction(signer, encoded, "Funding transaction");
3768
- // 8. Send the funding transaction
3769
- const txHash = await signer.sendTransaction({
3770
- to: encoded.to,
3771
- data: encoded.data,
3772
- gas: 500000n,
3773
- });
3774
- // 9. Wait for receipt
3775
- const receipt = await signer.waitForReceipt(txHash);
3776
- if (receipt.status !== "success") {
3777
- const reason = await getRevertReason(signer, txHash, receipt.blockNumber);
3778
- throw new Error(`Funding transaction failed: ${reason}`);
3779
- }
3780
- return { txHash };
3781
- }
3782
- /**
3783
- * Refund an EVM-sourced swap using an external wallet.
3784
- *
3785
- * Fetches refund calldata from the server, sends the transaction via the
3786
- * provided signer, and waits for the receipt.
3787
- *
3788
- * Use this for manual (timelock-based) refunds where the user pays gas.
3789
- * For collaborative (gasless) refunds, use {@link collabRefundEvmWithSigner}.
3790
- *
3791
- * @param swapId - The UUID of the swap
3792
- * @param signer - An {@link EvmSigner} wrapping the user's wallet
3793
- * @param mode - "swap-back" (refund as original token via DEX) or "direct" (refund as WBTC)
3794
- * @returns The refund transaction hash
3795
- */
3796
- async refundEvmWithSigner(swapId, signer, mode = "swap-back") {
3797
- const result = await this.refundSwap(swapId, { mode });
3798
- if (!result.evmRefundData) {
3799
- throw new Error(`Unable to get EVM refund data for: ${swapId}. ${result.message}`);
3800
- }
3801
- if (!result.evmRefundData.timelockExpired) {
3802
- const expiryDate = new Date(result.evmRefundData.timelockExpiry * 1000).toISOString();
3803
- throw new Error(`Refund timelock has not expired yet (expires ${expiryDate}). Use collabRefundEvmWithSigner for instant refunds.`);
3804
- }
3805
- // Simulate before sending to catch reverts without burning gas
3806
- await simulateTransaction(signer, result.evmRefundData, "Refund transaction");
3807
- const txHash = await signer.sendTransaction({
3808
- to: result.evmRefundData.to,
3809
- data: result.evmRefundData.data,
3810
- gas: 500000n,
3811
- });
3812
- const receipt = await signer.waitForReceipt(txHash);
3813
- if (receipt.status !== "success") {
3814
- const reason = await getRevertReason(signer, txHash, receipt.blockNumber);
3815
- throw new Error(`Refund transaction reverted: ${reason}`);
3816
- }
3817
- return { txHash };
3818
- }
3819
- /**
3820
- * Collaborative refund of an EVM-sourced swap using an external wallet.
3821
- *
3822
- * Signs the EIP-712 `CollabRefund` digest with the provided signer and
3823
- * submits it to the server, which cosigns and executes the refund on-chain.
3824
- * Gasless for the user — no timelock wait required.
3825
- *
3826
- * For gasless (Permit2) swaps where the SDK's embedded key is the depositor,
3827
- * use {@link collabRefundEvmSwap} instead (no external signer needed).
3828
- *
3829
- * @param swapId - The UUID of the swap
3830
- * @param signer - An {@link EvmSigner} wrapping the user's wallet
3831
- * @param settlement - "swap-back" (original token via DEX) or "direct" (WBTC)
3832
- * @returns The refund transaction hash
3833
- */
3834
- async collabRefundEvmWithSigner(swapId, signer, settlement = "direct") {
3835
- const { typedData, params } = await this.buildCollabRefundEvmTypedData(swapId, settlement);
3836
- const signature = await signer.signTypedData(typedData);
3837
- const { v, r, s } = parseSignature(signature);
3838
- const result = await this.submitCollabRefundEvm(swapId, {
3839
- v,
3840
- r,
3841
- s,
3842
- depositor_address: signer.address,
3843
- mode: settlement,
3844
- sweep_token: params.sweepToken,
3845
- min_amount_out: params.minAmountOut,
3846
- });
3847
- return { txHash: result.txHash };
3848
- }
3849
- /**
3850
- * Fund an EVM-sourced swap via the gasless relay.
3851
- *
3852
- * Signs the Permit2 authorization and (optionally) an EIP-2612 permit
3853
- * off-chain, then POSTs them to the server which submits the on-chain
3854
- * transactions on behalf of the user. No wallet or ETH required.
3855
- *
3856
- * @param swapId - The UUID of the swap (must have been created with gasless=true)
3857
- * @returns The relay transaction hash
3858
- *
3859
- * @example
3860
- * ```ts
3861
- * const result = await client.fundSwapGasless(swap.response.id);
3862
- * console.log("Funded via relay:", result.txHash);
3863
- * ```
3864
- */
3865
- async fundSwapGasless(swapId) {
3866
- // 1. Look up stored swap to get the secret key
3867
- const storedSwap = await this.getStoredSwap(swapId);
3868
- if (!storedSwap) {
3869
- throw new Error(`Swap ${swapId} not found in local storage. Cannot sign without the secret key.`);
3870
- }
3871
- const swap = await this.getSwap(swapId);
3872
- if (swap.direction !== "evm_to_arkade" &&
3873
- swap.direction !== "evm_to_bitcoin" &&
3874
- swap.direction !== "evm_to_lightning") {
3875
- throw new Error(`Expected evm_to_arkade/evm_to_bitcoin/evm_to_lightning swap, got ${swap.direction}. Gasless fund is for EVM-sourced swaps.`);
3876
- }
3877
- const chainId = swap.evm_chain_id;
3878
- // 2. Fetch Permit2 funding data (includes fee transfer in calls + EIP-2612 data)
3879
- const baseUrl = this.#config.baseUrl.replace(/\/$/, "");
3880
- const url = `${baseUrl}/swap/${swapId}/swap-and-lock-calldata-permit2`;
3881
- const resp = await fetch(url);
3882
- if (!resp.ok) {
3883
- const body = await resp.text();
3884
- throw new Error(`Failed to get Permit2 funding data: ${resp.status} ${body}`);
3885
- }
3886
- const serverData = (await resp.json());
3887
- // 3. Generate random Permit2 nonce and deadline
3888
- const nonceBytes = new Uint8Array(32);
3889
- crypto.getRandomValues(nonceBytes);
3890
- const nonce = BigInt(`0x${Array.from(nonceBytes)
3891
- .map((b) => b.toString(16).padStart(2, "0"))
3892
- .join("")}`);
3893
- const deadline = BigInt(Math.floor(Date.now() / 1000) + 30 * 60); // 30 minutes
3894
- const sourceAmount = BigInt(serverData.source_amount);
3895
- // 4. Build and sign Permit2 EIP-712 digest
3896
- const digest = buildPermit2FundingDigest({
3897
- chainId,
3898
- coordinatorAddress: serverData.coordinator_address,
3899
- sourceToken: serverData.source_token_address,
3900
- sourceAmount,
3901
- preimageHash: serverData.preimage_hash,
3902
- lockToken: serverData.lock_token_address,
3903
- claimAddress: serverData.claim_address,
3904
- refundAddress: serverData.coordinator_address,
3905
- timelock: serverData.timelock,
3906
- callsHash: serverData.calls_hash,
3907
- nonce,
3908
- deadline,
3909
- });
3910
- // 4b. Use the EVM key (deterministic for new swaps, per-swap for legacy)
3911
- const evmKey = this.#getEvmSigningKey();
3912
- const permit2Sig = signEvmDigest(evmKey, digest);
3913
- const rClean = permit2Sig.r.replace(/^0x/, "");
3914
- const sClean = permit2Sig.s.replace(/^0x/, "");
3915
- const vHex = permit2Sig.v.toString(16).padStart(2, "0");
3916
- const compactSignature = `0x${rClean}${sClean}${vHex}`;
3917
- // 5. Derive depositor address from EVM key
3918
- const depositorAddress = deriveEvmAddress(evmKey);
3919
- // 6. If EIP-2612 needed (token supports it and not yet approved to Permit2)
3920
- let eip2612Permit;
3921
- if (serverData.eip2612?.supported && !serverData.eip2612.already_approved) {
3922
- const maxUint256 = BigInt("0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff");
3923
- const eip2612Digest = buildEip2612PermitDigest({
3924
- domainSeparator: serverData.eip2612.domain_separator,
3925
- owner: depositorAddress,
3926
- spender: PERMIT2_ADDRESS,
3927
- value: maxUint256,
3928
- nonce: serverData.eip2612.nonce,
3929
- deadline,
3930
- });
3931
- const sig = signEvmDigest(evmKey, eip2612Digest);
3932
- eip2612Permit = {
3933
- v: sig.v,
3934
- r: sig.r.replace(/^0x/, ""),
3935
- s: sig.s.replace(/^0x/, ""),
3936
- value: maxUint256.toString(),
3937
- deadline: Number(deadline),
3938
- nonce: serverData.eip2612.nonce,
3939
- };
3940
- }
3941
- // 7. POST to fund-gasless relay endpoint
3942
- const fundUrl = `${baseUrl}/swap/${swapId}/fund-gasless`;
3943
- const fundResp = await fetch(fundUrl, {
3944
- method: "POST",
3945
- headers: {
3946
- "Content-Type": "application/json",
3947
- },
3948
- body: JSON.stringify({
3949
- permit2_nonce: nonce.toString(),
3950
- permit2_deadline: Number(deadline),
3951
- permit2_signature: compactSignature,
3952
- calls: serverData.calls,
3953
- eip2612_permit: eip2612Permit,
3954
- }),
3955
- });
3956
- if (!fundResp.ok) {
3957
- const body = await fundResp.text();
3958
- throw new Error(`Gasless fund relay failed: ${fundResp.status} ${body}`);
3959
- }
3960
- const result = (await fundResp.json());
3961
- return { txHash: result.tx_hash };
3962
- }
3963
- /**
3964
- * Get the ephemeral depositor key for a gasless swap funded swap.
3965
- *
3966
- * Returns the private key and derived EVM address of the ephemeral
3967
- * depositor used in a gasless swap. Use this to build a wallet/signer
3968
- * for recovering stuck funds via {@link recoverGaslessFunds}.
3969
- *
3970
- * The key is deterministically derived from the user's mnemonic and
3971
- * the swap's key index, so it can be re-derived even if local storage
3972
- * is rebuilt from the mnemonic.
3973
- *
3974
- * @param swapId - The UUID of the swap
3975
- * @returns The depositor's private key (hex) and EVM address
3976
- *
3977
- * @example
3978
- * ```ts
3979
- * const { privateKey, address } = await client.getSwapDepositorKey(swapId);
3980
- * // Build a viem wallet from the key:
3981
- * const account = privateKeyToAccount(privateKey);
3982
- * ```
3983
- */
3984
- async getSwapDepositorKey(swapId) {
3985
- const storedSwap = await this.getStoredSwap(swapId);
3986
- if (!storedSwap) {
3987
- throw new Error(`Swap ${swapId} not found in local storage. Cannot recover without the secret key.`);
3988
- }
3989
- const evmKey = this.#getEvmSigningKey();
3990
- const privateKey = evmKey.startsWith("0x") ? evmKey : `0x${evmKey}`;
3991
- const address = deriveEvmAddress(evmKey);
3992
- return { privateKey, address };
3993
- }
3994
- /**
3995
- * Wallet-level variant of {@link getSwapDepositorKey} — returns the
3996
- * deterministic EVM signing key for this wallet without requiring a
3997
- * specific swap to be stored locally.
3998
- *
3999
- * Same key, same address as {@link getSwapDepositorKey} (both derive
4000
- * from `m/44'/60'/0'/0/0`). Intended for standalone recovery tools
4001
- * where the user supplies the burn details manually and a per-swap
4002
- * record may no longer exist (e.g. fresh device, cleared site data).
4003
- */
4004
- getEvmDepositorKey() {
4005
- const evmKey = this.#getEvmSigningKey();
4006
- const privateKey = evmKey.startsWith("0x") ? evmKey : `0x${evmKey}`;
4007
- const address = deriveEvmAddress(evmKey);
4008
- return { privateKey, address };
4009
- }
4010
- /**
4011
- * Recover ERC-20 tokens stuck in a gasless swap's ephemeral depositor address.
4012
- *
4013
- * When a gasless swap fails after the user transfers tokens to the
4014
- * SDK-derived depositor address but before the Permit2 tx is published,
4015
- * this method sweeps the tokens back to a destination address.
4016
- *
4017
- * Requires an {@link EvmSigner} backed by the depositor's private key
4018
- * (obtainable via {@link getSwapDepositorKey}). The depositor needs
4019
- * a small amount of ETH/POL for gas — the caller must fund it beforehand.
4020
- *
4021
- * @param swapId - The UUID of the swap whose depositor holds stuck tokens
4022
- * @param depositorSigner - An {@link EvmSigner} backed by the depositor's private key
4023
- * @param destination - EVM address to send recovered tokens to
4024
- * @returns The recovery transaction hash and recovered amount
4025
- *
4026
- * @example
4027
- * ```ts
4028
- * // 1. Get the depositor key
4029
- * const { privateKey } = await client.getSwapDepositorKey(swapId);
4030
- *
4031
- * // 2. Build a signer from the depositor key (e.g. with viem)
4032
- * const depositorAccount = privateKeyToAccount(privateKey);
4033
- * const depositorWallet = createWalletClient({ account: depositorAccount, ... });
4034
- * const depositorSigner = buildEvmSigner(depositorWallet, publicClient);
4035
- *
4036
- * // 3. Fund the depositor with ETH for gas, then recover
4037
- * const result = await client.recoverGaslessFunds(swapId, depositorSigner, myAddress);
4038
- * console.log(`Recovered ${result.amount} tokens, tx: ${result.txHash}`);
4039
- * ```
4040
- */
4041
- async recoverGaslessFunds(swapId, depositorSigner, destination) {
4042
- const storedSwap = await this.getStoredSwap(swapId);
4043
- if (!storedSwap) {
4044
- throw new Error(`Swap ${swapId} not found in local storage. Cannot recover without the secret key.`);
4045
- }
4046
- const swap = await this.getSwap(swapId);
4047
- if (swap.direction !== "evm_to_arkade" &&
4048
- swap.direction !== "evm_to_bitcoin" &&
4049
- swap.direction !== "evm_to_lightning") {
4050
- throw new Error(`Expected EVM-sourced swap, got ${swap.direction}. Recovery is only for gasless EVM swaps.`);
4051
- }
4052
- const tokenAddress = swap
4053
- .source_token.token_id;
4054
- // Check the depositor's token balance
4055
- const { encodeBalanceOfCall, encodeTransferCall, decodeUint256 } = await import("./evm/wallet.js");
4056
- const balanceCall = encodeBalanceOfCall(tokenAddress, depositorSigner.address);
4057
- const balanceResult = await depositorSigner.call(balanceCall);
4058
- const balance = decodeUint256(balanceResult || "0x0");
4059
- if (balance === 0n) {
4060
- throw new Error(`No tokens found at depositor address ${depositorSigner.address}. Nothing to recover.`);
4061
- }
4062
- // Transfer all tokens to destination
4063
- const transferCall = encodeTransferCall(tokenAddress, destination, balance);
4064
- const txHash = await depositorSigner.sendTransaction(transferCall);
4065
- const receipt = await depositorSigner.waitForReceipt(txHash);
4066
- if (receipt.status !== "success") {
4067
- throw new Error(`Recovery transaction reverted: ${txHash}`);
4068
- }
4069
- return { txHash, amount: balance.toString() };
4070
- }
4071
- /**
4072
- * Check if a swap's VTXO has been received on Arkade.
4073
- *
4074
- * For Arkade-destination swaps (EVM/Bitcoin/Lightning → Arkade), queries the
4075
- * Arkade indexer for VTXOs at the `target_arkade_address` and checks if any
4076
- * VTXO's txid matches the Arkade txid that paid the target address.
4077
- *
4078
- * @param swapId - The UUID of the swap
4079
- * @returns `true` if the received Arkade VTXO was found
4080
- */
4081
- async hasReceivedVtxo(swapId) {
4082
- const swap = await this.getSwap(swapId);
4083
- // Extract target_arkade_address and the Arkade-side txid that pays it.
4084
- let targetArkadeAddress;
4085
- let receiveArkadeTxid;
4086
- let network;
4087
- if (swap.direction === "evm_to_arkade") {
4088
- const s = swap;
4089
- targetArkadeAddress = s.target_arkade_address;
4090
- receiveArkadeTxid = s.btc_claim_txid;
4091
- network = s.network;
4092
- }
4093
- else if (swap.direction === "btc_to_arkade") {
4094
- const s = swap;
4095
- targetArkadeAddress = s.target_arkade_address;
4096
- receiveArkadeTxid = s.arkade_claim_txid;
4097
- network = s.network;
4098
- }
4099
- else if (swap.direction === "lightning_to_arkade") {
4100
- const s = swap;
4101
- targetArkadeAddress = s.target_arkade_address;
4102
- receiveArkadeTxid = s.arkade_claim_txid;
4103
- network = s.network;
4104
- }
4105
- else {
4106
- throw new Error(`hasReceivedVtxo only works for Arkade-destination swaps, got ${swap.direction}`);
4107
- }
4108
- if (!receiveArkadeTxid) {
4109
- return false;
4110
- }
4111
- const { ArkAddress, RestIndexerProvider } = await import("@arkade-os/sdk");
4112
- const { hex } = await import("@scure/base");
4113
- const serverUrl = this.#config.arkadeServerUrl;
4114
- if (!serverUrl) {
4115
- throw new Error(`No Arkade server URL for network: ${network}`);
4116
- }
4117
- const decoded = ArkAddress.decode(targetArkadeAddress);
4118
- const pkScript = hex.encode(decoded.pkScript);
4119
- const indexer = new RestIndexerProvider(serverUrl);
4120
- const { vtxos } = await indexer.getVtxos({
4121
- scripts: [pkScript],
4122
- });
4123
- return vtxos.some((v) => v.txid === receiveArkadeTxid);
4124
- }
4125
- }
4126
- //# sourceMappingURL=client.js.map