@lendasat/lendaswap-sdk-pure 0.2.24 → 0.2.26

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 (153) hide show
  1. package/dist/arkade-network.d.ts +8 -0
  2. package/dist/arkade-network.d.ts.map +1 -0
  3. package/dist/arkade-network.js +40 -0
  4. package/dist/arkade-network.js.map +1 -0
  5. package/dist/arkade.d.ts.map +1 -1
  6. package/dist/arkade.js +2 -9
  7. package/dist/arkade.js.map +1 -1
  8. package/dist/cctp/constants.d.ts.map +1 -1
  9. package/dist/cctp/constants.js +4 -0
  10. package/dist/cctp/constants.js.map +1 -1
  11. package/dist/cctp/fee.d.ts +49 -0
  12. package/dist/cctp/fee.d.ts.map +1 -0
  13. package/dist/cctp/fee.js +70 -0
  14. package/dist/cctp/fee.js.map +1 -0
  15. package/dist/cctp/index.d.ts +2 -1
  16. package/dist/cctp/index.d.ts.map +1 -1
  17. package/dist/cctp/index.js +2 -1
  18. package/dist/cctp/index.js.map +1 -1
  19. package/dist/cctp/utils.d.ts +15 -0
  20. package/dist/cctp/utils.d.ts.map +1 -1
  21. package/dist/cctp/utils.js +41 -0
  22. package/dist/cctp/utils.js.map +1 -1
  23. package/dist/cctp-bridge/bridge.d.ts +62 -0
  24. package/dist/cctp-bridge/bridge.d.ts.map +1 -0
  25. package/dist/cctp-bridge/bridge.js +74 -0
  26. package/dist/cctp-bridge/bridge.js.map +1 -0
  27. package/dist/cctp-bridge/index.d.ts +17 -0
  28. package/dist/cctp-bridge/index.d.ts.map +1 -0
  29. package/dist/cctp-bridge/index.js +16 -0
  30. package/dist/cctp-bridge/index.js.map +1 -0
  31. package/dist/cctp-inbound/approveAndBurn.d.ts +68 -0
  32. package/dist/cctp-inbound/approveAndBurn.d.ts.map +1 -0
  33. package/dist/cctp-inbound/approveAndBurn.js +95 -0
  34. package/dist/cctp-inbound/approveAndBurn.js.map +1 -0
  35. package/dist/cctp-inbound/burn.d.ts +51 -0
  36. package/dist/cctp-inbound/burn.d.ts.map +1 -0
  37. package/dist/cctp-inbound/burn.js +66 -0
  38. package/dist/cctp-inbound/burn.js.map +1 -0
  39. package/dist/cctp-inbound/chainMap.d.ts +48 -0
  40. package/dist/cctp-inbound/chainMap.d.ts.map +1 -0
  41. package/dist/cctp-inbound/chainMap.js +103 -0
  42. package/dist/cctp-inbound/chainMap.js.map +1 -0
  43. package/dist/cctp-inbound/client.d.ts +1573 -0
  44. package/dist/cctp-inbound/client.d.ts.map +1 -0
  45. package/dist/cctp-inbound/client.js +117 -0
  46. package/dist/cctp-inbound/client.js.map +1 -0
  47. package/dist/cctp-inbound/eip2612.d.ts +77 -0
  48. package/dist/cctp-inbound/eip2612.d.ts.map +1 -0
  49. package/dist/cctp-inbound/eip2612.js +95 -0
  50. package/dist/cctp-inbound/eip2612.js.map +1 -0
  51. package/dist/cctp-inbound/evmSignerAdapter.d.ts +25 -0
  52. package/dist/cctp-inbound/evmSignerAdapter.d.ts.map +1 -0
  53. package/dist/cctp-inbound/evmSignerAdapter.js +50 -0
  54. package/dist/cctp-inbound/evmSignerAdapter.js.map +1 -0
  55. package/dist/cctp-inbound/fundSwap.d.ts +94 -0
  56. package/dist/cctp-inbound/fundSwap.d.ts.map +1 -0
  57. package/dist/cctp-inbound/fundSwap.js +90 -0
  58. package/dist/cctp-inbound/fundSwap.js.map +1 -0
  59. package/dist/cctp-inbound/index.d.ts +35 -0
  60. package/dist/cctp-inbound/index.d.ts.map +1 -0
  61. package/dist/cctp-inbound/index.js +33 -0
  62. package/dist/cctp-inbound/index.js.map +1 -0
  63. package/dist/cctp-inbound/permit2.d.ts +33 -0
  64. package/dist/cctp-inbound/permit2.d.ts.map +1 -0
  65. package/dist/cctp-inbound/permit2.js +32 -0
  66. package/dist/cctp-inbound/permit2.js.map +1 -0
  67. package/dist/cctp-inbound/preflight.d.ts +36 -0
  68. package/dist/cctp-inbound/preflight.d.ts.map +1 -0
  69. package/dist/cctp-inbound/preflight.js +67 -0
  70. package/dist/cctp-inbound/preflight.js.map +1 -0
  71. package/dist/cctp-inbound/smartAccount.d.ts +1518 -0
  72. package/dist/cctp-inbound/smartAccount.d.ts.map +1 -0
  73. package/dist/cctp-inbound/smartAccount.js +117 -0
  74. package/dist/cctp-inbound/smartAccount.js.map +1 -0
  75. package/dist/cctp-inbound/submit.d.ts +75 -0
  76. package/dist/cctp-inbound/submit.d.ts.map +1 -0
  77. package/dist/cctp-inbound/submit.js +126 -0
  78. package/dist/cctp-inbound/submit.js.map +1 -0
  79. package/dist/cctp-inbound/types.d.ts +30 -0
  80. package/dist/cctp-inbound/types.d.ts.map +1 -0
  81. package/dist/cctp-inbound/types.js +5 -0
  82. package/dist/cctp-inbound/types.js.map +1 -0
  83. package/dist/cctp-inbound/userOp.d.ts +93 -0
  84. package/dist/cctp-inbound/userOp.d.ts.map +1 -0
  85. package/dist/cctp-inbound/userOp.js +151 -0
  86. package/dist/cctp-inbound/userOp.js.map +1 -0
  87. package/dist/client.d.ts +166 -6
  88. package/dist/client.d.ts.map +1 -1
  89. package/dist/client.js +565 -32
  90. package/dist/client.js.map +1 -1
  91. package/dist/create/arkade.d.ts.map +1 -1
  92. package/dist/create/arkade.js +21 -19
  93. package/dist/create/arkade.js.map +1 -1
  94. package/dist/create/bitcoin.d.ts.map +1 -1
  95. package/dist/create/bitcoin.js +17 -15
  96. package/dist/create/bitcoin.js.map +1 -1
  97. package/dist/create/evm-to-arkade.d.ts.map +1 -1
  98. package/dist/create/evm-to-arkade.js +2 -0
  99. package/dist/create/evm-to-arkade.js.map +1 -1
  100. package/dist/create/evm-to-bitcoin.d.ts.map +1 -1
  101. package/dist/create/evm-to-bitcoin.js +2 -0
  102. package/dist/create/evm-to-bitcoin.js.map +1 -1
  103. package/dist/create/evm-to-lightning.d.ts.map +1 -1
  104. package/dist/create/evm-to-lightning.js +6 -3
  105. package/dist/create/evm-to-lightning.js.map +1 -1
  106. package/dist/create/lightning.d.ts.map +1 -1
  107. package/dist/create/lightning.js +17 -15
  108. package/dist/create/lightning.js.map +1 -1
  109. package/dist/create/types.d.ts +40 -0
  110. package/dist/create/types.d.ts.map +1 -1
  111. package/dist/delegate.d.ts.map +1 -1
  112. package/dist/delegate.js +2 -31
  113. package/dist/delegate.js.map +1 -1
  114. package/dist/evm/wallet.d.ts +16 -0
  115. package/dist/evm/wallet.d.ts.map +1 -1
  116. package/dist/evm/wallet.js.map +1 -1
  117. package/dist/generated/api.d.ts +408 -2
  118. package/dist/generated/api.d.ts.map +1 -1
  119. package/dist/index.d.ts +3 -2
  120. package/dist/index.d.ts.map +1 -1
  121. package/dist/index.js +4 -2
  122. package/dist/index.js.map +1 -1
  123. package/dist/redeem/arkade.d.ts.map +1 -1
  124. package/dist/redeem/arkade.js +4 -43
  125. package/dist/redeem/arkade.js.map +1 -1
  126. package/dist/redeem/gasless.d.ts +16 -0
  127. package/dist/redeem/gasless.d.ts.map +1 -1
  128. package/dist/redeem/gasless.js +3 -1
  129. package/dist/redeem/gasless.js.map +1 -1
  130. package/dist/refund/arkade.d.ts.map +1 -1
  131. package/dist/refund/arkade.js +3 -39
  132. package/dist/refund/arkade.js.map +1 -1
  133. package/dist/refund/{collab.d.ts → collab-arkade-evm.d.ts} +6 -6
  134. package/dist/refund/collab-arkade-evm.d.ts.map +1 -0
  135. package/dist/refund/{collab.js → collab-arkade-evm.js} +6 -38
  136. package/dist/refund/collab-arkade-evm.js.map +1 -0
  137. package/dist/refund/collab-arkade-lightning.d.ts +59 -0
  138. package/dist/refund/collab-arkade-lightning.d.ts.map +1 -0
  139. package/dist/refund/collab-arkade-lightning.js +208 -0
  140. package/dist/refund/collab-arkade-lightning.js.map +1 -0
  141. package/dist/refund/index.d.ts +3 -2
  142. package/dist/refund/index.d.ts.map +1 -1
  143. package/dist/refund/index.js +3 -2
  144. package/dist/refund/index.js.map +1 -1
  145. package/dist/tokens.d.ts +18 -0
  146. package/dist/tokens.d.ts.map +1 -1
  147. package/dist/tokens.js +43 -7
  148. package/dist/tokens.js.map +1 -1
  149. package/dist/version.d.ts +2 -2
  150. package/dist/version.js +1 -1
  151. package/package.json +27 -2
  152. package/dist/refund/collab.d.ts.map +0 -1
  153. package/dist/refund/collab.js.map +0 -1
package/dist/client.js CHANGED
@@ -1,13 +1,16 @@
1
1
  import { createApiClient, } from "./api/client.js";
2
2
  import { getVhtlcAmounts } from "./arkade.js";
3
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";
4
7
  import { createArkadeToEvmSwapGeneric, createArkadeToLightningSwap, createBitcoinToArkadeSwap, createBitcoinToEvmSwap, createEvmToArkadeSwapGeneric, createEvmToBitcoinSwap, createEvmToLightningSwapGeneric, createLightningToArkadeSwap, createLightningToEvmSwapGeneric, } from "./create/index.js";
5
8
  import { delegateClaim, delegateRefund } from "./delegate.js";
6
9
  import { broadcastTransaction, findOutputByAddress } from "./esplora.js";
7
10
  import { buildCollabRefundEvmDigest, buildCollabRefundEvmTypedData, buildEip2612PermitDigest, buildPermit2FundingDigest, buildPermit2TypedData, deriveEvmAddress, encodeApproveCallData, encodeExecuteAndCreateWithPermit2, encodeHtlcErc20RefundCallData, PERMIT2_ADDRESS, signEvmDigest, } from "./evm/index.js";
8
11
  import { decodeUint256, encodeAllowanceCall, encodeBalanceOfCall, encodeMaxApproveData, getRevertReason, parseSignature, simulateTransaction, } from "./evm/wallet.js";
9
12
  import { buildArkadeClaim, continueArkadeClaim, claimViaGasless as gaslessClaim, claim as redeemClaim, } from "./redeem/index.js";
10
- import { buildArkadeRefund, buildOnchainClaimTransaction, buildOnchainRefundTransaction, collabRefundDelegate, collabRefundOffchain, verifyHtlcAddress, } from "./refund/index.js";
13
+ import { buildArkadeRefund, buildOnchainClaimTransaction, buildOnchainRefundTransaction, collabRefundArkadeToEvmDelegate, collabRefundArkadeToEvmOffchain, collabRefundArkadeToLightningOffchain, verifyHtlcAddress, } from "./refund/index.js";
11
14
  import { bytesToHex, hexToBytes, Signer, } from "./signer/index.js";
12
15
  import { SWAP_STORAGE_VERSION, } from "./storage/index.js";
13
16
  import { isArkade, isBridgeOnlyChain, isBtcOnchain, isBtcPegged, isEvmToken, isLightning, isSourceEvmChain, toChainName, } from "./tokens.js";
@@ -60,6 +63,7 @@ export class ClientBuilder {
60
63
  #swapStorage;
61
64
  #mnemonic;
62
65
  #xprv;
66
+ #aa;
63
67
  /**
64
68
  * Sets the base URL for the API.
65
69
  * @param baseUrl - The base URL of the Lendaswap API.
@@ -176,6 +180,23 @@ export class ClientBuilder {
176
180
  this.#xprv = xprv;
177
181
  return this;
178
182
  }
183
+ /**
184
+ * Sets the account-abstraction config (bundler + Gas Manager policy).
185
+ *
186
+ * Required when using the CCTP-inbound swap flow (any non-Arbitrum
187
+ * EVM chain as the source). The settlement UserOp — `receiveMessage`
188
+ * + `USDC.approve(Permit2)` + `executeAndCreateWithPermit2` — is
189
+ * submitted via the Kernel smart account owned by the consumer's
190
+ * connected wallet, sponsored by the Gas Manager so users need no
191
+ * ETH on Arbitrum.
192
+ *
193
+ * @param aa - Bundler URL + paymaster policy id.
194
+ * @returns The builder instance for chaining.
195
+ */
196
+ withAa(aa) {
197
+ this.#aa = aa;
198
+ return this;
199
+ }
179
200
  /**
180
201
  * Builds and returns a fully initialized Client instance.
181
202
  *
@@ -231,6 +252,7 @@ export class ClientBuilder {
231
252
  defaultHeaders: this.#defaultHeaders,
232
253
  esploraUrl: this.#esploraUrl?.replace(/\/+$/, ""),
233
254
  arkadeServerUrl: this.#arkadeServerUrl?.replace(/\/+$/, ""),
255
+ aa: this.#aa,
234
256
  }, signer, this.#signerStorage, this.#swapStorage);
235
257
  }
236
258
  }
@@ -265,6 +287,7 @@ export class Client {
265
287
  #signerStorage;
266
288
  #swapStorage;
267
289
  #statusWatcher = null;
290
+ #cctpInbound = null;
268
291
  /**
269
292
  * Creates a new Client instance.
270
293
  *
@@ -326,6 +349,27 @@ export class Client {
326
349
  get baseUrl() {
327
350
  return this.#config.baseUrl;
328
351
  }
352
+ /**
353
+ * Namespace for CCTP-inbound swap primitives (source-chain burn,
354
+ * IRIS attestation, settlement UserOp). Requires `withAa(...)` on
355
+ * the builder — throws with a clear error otherwise.
356
+ *
357
+ * For simple integrations prefer `Client.fundSwap(swapId, signer)`
358
+ * which auto-dispatches to the CCTP path when needed; drop down to
359
+ * `client.cctpInbound.*` when you need step-by-step progress control.
360
+ */
361
+ get cctpInbound() {
362
+ if (!this.#cctpInbound) {
363
+ if (!this.#config.aa) {
364
+ throw new Error("CCTP-inbound flow requires AA config. Call `.withAa({ bundlerUrl, paymasterPolicyId })` on the ClientBuilder before `.build()`.");
365
+ }
366
+ this.#cctpInbound = new CctpInboundClient({
367
+ apiClient: this.#apiClient,
368
+ aa: this.#config.aa,
369
+ });
370
+ }
371
+ return this.#cctpInbound;
372
+ }
329
373
  /** The swap storage, if configured. */
330
374
  get swapStorage() {
331
375
  return this.#swapStorage;
@@ -573,19 +617,42 @@ export class Client {
573
617
  ? USDT0_ADDRESSES.Arbitrum
574
618
  : USDC_ADDRESSES.Arbitrum;
575
619
  }
620
+ // CCTP-only source (USDC on Optimism / Base / Linea / …): the quote
621
+ // endpoint expects the source chain/token the DEX actually runs on
622
+ // (Arbitrum + native USDC), plus `bridge_source_chain` + address so
623
+ // the backend can apply the CCTPv2 fast-transfer fee. All gross-vs-net
624
+ // math happens server-side.
625
+ let sourceChain = params.sourceChain;
626
+ let sourceToken = params.sourceToken;
627
+ let bridgeSourceChain;
628
+ let bridgeSourceTokenAddress;
629
+ const parsedSourceChainId = Number.parseInt(params.sourceChain, 10);
630
+ if (!Number.isNaN(parsedSourceChainId) &&
631
+ isCctpOnlySource(parsedSourceChainId)) {
632
+ const source = cctpMetaForChainId(parsedSourceChainId);
633
+ if (params.sourceToken.toLowerCase() !== source.usdc.toLowerCase()) {
634
+ throw new Error(`Quote on ${source.name} requires native USDC (${source.usdc}); got ${params.sourceToken}. Only USDC is bridgeable via CCTP.`);
635
+ }
636
+ bridgeSourceChain = source.name;
637
+ bridgeSourceTokenAddress = source.usdc;
638
+ sourceChain = "42161";
639
+ sourceToken = USDC_ADDRESSES.Arbitrum;
640
+ }
641
+ const query = {
642
+ source_chain: sourceChain,
643
+ source_token: sourceToken,
644
+ target_chain: targetChain,
645
+ target_token: targetToken,
646
+ source_amount: params.sourceAmount,
647
+ target_amount: params.targetAmount,
648
+ bridge_target_chain: bridgeTargetChain,
649
+ bridge_source_chain: bridgeSourceChain,
650
+ bridge_source_token_address: bridgeSourceTokenAddress,
651
+ bridge_recipient_setup: params.bridgeRecipientSetup,
652
+ ref: params.referralCode,
653
+ };
576
654
  const { data, error } = await this.#apiClient.GET("/quote", {
577
- params: {
578
- query: {
579
- source_chain: params.sourceChain,
580
- source_token: params.sourceToken,
581
- target_chain: targetChain,
582
- target_token: targetToken,
583
- source_amount: params.sourceAmount,
584
- target_amount: params.targetAmount,
585
- bridge_target_chain: bridgeTargetChain,
586
- ref: params.referralCode,
587
- },
588
- },
655
+ params: { query },
589
656
  });
590
657
  if (error) {
591
658
  throw new Error(`Failed to get quote: ${JSON.stringify(error)}`);
@@ -815,8 +882,17 @@ export class Client {
815
882
  swap.direction === "lightning_to_evm" ||
816
883
  swap.direction === "bitcoin_to_evm") {
817
884
  const evmSwap = swap;
818
- // Use the stored target address - this was set when the swap was created
819
- const destination = evmSwap.target_evm_address ?? evmSwap.client_evm_address;
885
+ // For Solana bridge targets the stored `target_evm_address` actually
886
+ // holds a base58 SPL pubkey, which can't satisfy the EIP-712
887
+ // `address destination` field. Fall back to `client_evm_address`
888
+ // (the user's EVM-side wallet at swap creation, used for refunds);
889
+ // the actual Solana destination rides on `bridgeRecipient`.
890
+ const bridgeTargetChain = evmSwap
891
+ .bridge_target_chain;
892
+ const isSolanaBridge = bridgeTargetChain === "Solana";
893
+ const destination = isSolanaBridge
894
+ ? evmSwap.client_evm_address
895
+ : (evmSwap.target_evm_address ?? evmSwap.client_evm_address);
820
896
  if (!destination) {
821
897
  return {
822
898
  success: false,
@@ -824,7 +900,17 @@ export class Client {
824
900
  "This swap may have been created before target address storage was implemented.",
825
901
  };
826
902
  }
827
- const gaslessResult = await this.claimViaGasless(id, destination);
903
+ if (isSolanaBridge && !options?.bridgeRecipient) {
904
+ return {
905
+ success: false,
906
+ message: "Solana bridge claim requires `bridgeRecipient` (the recipient's USDC ATA) " +
907
+ "in claim options. Derive it via `deriveSolanaUsdcAta` before calling claim.",
908
+ };
909
+ }
910
+ const gaslessResult = await this.claimViaGasless(id, destination, {
911
+ bridgeRecipient: options?.bridgeRecipient,
912
+ bridgeRecipientWallet: options?.bridgeRecipientWallet,
913
+ });
828
914
  return {
829
915
  success: true,
830
916
  message: gaslessResult.message,
@@ -898,7 +984,7 @@ export class Client {
898
984
  * console.log("Claimed! TX:", result.txHash);
899
985
  * ```
900
986
  */
901
- async claimViaGasless(id, destination, { slippage = 1.0 } = {}) {
987
+ async claimViaGasless(id, destination, { slippage = 1.0, bridgeRecipient, bridgeRecipientWallet, } = {}) {
902
988
  if (!this.#swapStorage) {
903
989
  throw new Error("Swap storage is not configured. Cannot retrieve preimage needed for gasless claim.");
904
990
  }
@@ -917,10 +1003,20 @@ export class Client {
917
1003
  }
918
1004
  // Always fetch redeem calldata from the server to get calls_hash.
919
1005
  // For non-WBTC targets this also returns DEX calldata.
1006
+ // bridge_recipient is forwarded for non-EVM bridge targets (e.g.
1007
+ // Solana) so the server's calls_hash matches what claim-gasless
1008
+ // will rebuild below.
920
1009
  const calldataResponse = await this.#apiClient.GET("/swap/{id}/redeem-and-swap-calldata", {
921
1010
  params: {
922
1011
  path: { id },
923
- query: { destination, slippage },
1012
+ query: {
1013
+ destination,
1014
+ slippage,
1015
+ ...(bridgeRecipient ? { bridge_recipient: bridgeRecipient } : {}),
1016
+ ...(bridgeRecipientWallet
1017
+ ? { bridge_recipient_wallet: bridgeRecipientWallet }
1018
+ : {}),
1019
+ },
924
1020
  },
925
1021
  });
926
1022
  if (calldataResponse.error) {
@@ -948,6 +1044,8 @@ export class Client {
948
1044
  destination,
949
1045
  dexCalldata,
950
1046
  callsHash,
1047
+ bridgeRecipient,
1048
+ bridgeRecipientWallet,
951
1049
  });
952
1050
  }
953
1051
  /**
@@ -1011,9 +1109,9 @@ export class Client {
1011
1109
  }
1012
1110
  // Extract common VHTLC parameters
1013
1111
  const claimParams = this.#extractArkadeClaimParams(id, storedSwap);
1014
- // Query VTXO status to determine claim method
1015
- const amounts = await this.amountsForSwap(id);
1016
- const vtxoStatus = amounts.vtxoStatus;
1112
+ // Query VTXO status to determine claim method, polling briefly while
1113
+ // the Arkade indexer catches up if the funding tx hasn't been seen yet.
1114
+ const vtxoStatus = await this.#waitForVtxoStatus(id, options.waitForVtxoMs ?? 30_000);
1017
1115
  if (vtxoStatus === "not_funded" || vtxoStatus === "spent") {
1018
1116
  return {
1019
1117
  success: false,
@@ -1031,6 +1129,32 @@ export class Client {
1031
1129
  // recoverable or mixed → delegate
1032
1130
  return this.#claimArkadeDelegate(id, claimParams, options);
1033
1131
  }
1132
+ /**
1133
+ * Poll `amountsForSwap` until the VTXO status leaves `not_funded` or
1134
+ * the timeout elapses. Returns the most recent status observed.
1135
+ *
1136
+ * Uses an exponential-ish backoff capped at 2s between probes so the
1137
+ * usual case (indexer catches up within a few seconds) feels snappy
1138
+ * without hammering the server.
1139
+ */
1140
+ async #waitForVtxoStatus(id, timeoutMs) {
1141
+ const deadline = Date.now() + Math.max(0, timeoutMs);
1142
+ let delayMs = 500;
1143
+ // First probe is immediate; subsequent probes back off.
1144
+ while (true) {
1145
+ const amounts = await this.amountsForSwap(id);
1146
+ if (amounts.vtxoStatus !== "not_funded")
1147
+ return amounts.vtxoStatus;
1148
+ if (Date.now() >= deadline)
1149
+ return amounts.vtxoStatus;
1150
+ const remaining = deadline - Date.now();
1151
+ const sleepMs = Math.min(delayMs, remaining);
1152
+ if (sleepMs <= 0)
1153
+ return amounts.vtxoStatus;
1154
+ await new Promise((resolve) => setTimeout(resolve, sleepMs));
1155
+ delayMs = Math.min(delayMs * 2, 2_000);
1156
+ }
1157
+ }
1034
1158
  /**
1035
1159
  * Continue (finalize) a pending Arkade claim.
1036
1160
  *
@@ -1273,6 +1397,10 @@ export class Client {
1273
1397
  }
1274
1398
  return this.#buildEvmToLightningRefund(id, swap, settlement);
1275
1399
  }
1400
+ // Arkade-to-Lightning: collaborative refund or locktime refund
1401
+ if (direction === "arkade_to_lightning") {
1402
+ return this.#buildArkadeToLightningRefund(id, swap, options);
1403
+ }
1276
1404
  return {
1277
1405
  success: false,
1278
1406
  message: `Refund not supported for direction: ${direction}.`,
@@ -1768,7 +1896,7 @@ export class Client {
1768
1896
  };
1769
1897
  try {
1770
1898
  if (vtxoStatus === "spendable") {
1771
- const result = await collabRefundOffchain(collabParams);
1899
+ const result = await collabRefundArkadeToEvmOffchain(collabParams);
1772
1900
  return {
1773
1901
  success: true,
1774
1902
  message: "Arkade refund executed successfully via collaborative offchain spend!",
@@ -1778,7 +1906,7 @@ export class Client {
1778
1906
  };
1779
1907
  }
1780
1908
  // recoverable or mixed → collab delegate
1781
- const result = await collabRefundDelegate(collabParams);
1909
+ const result = await collabRefundArkadeToEvmDelegate(collabParams);
1782
1910
  return {
1783
1911
  success: true,
1784
1912
  message: "Arkade refund executed successfully via collaborative delegated settlement!",
@@ -1810,6 +1938,132 @@ export class Client {
1810
1938
  // recoverable or mixed → delegate
1811
1939
  return this.#refundArkadeDelegate(refundParams, options);
1812
1940
  }
1941
+ /**
1942
+ * Refund an Arkade-to-Lightning swap.
1943
+ *
1944
+ * Two paths:
1945
+ * 1. **Collaborative refund** (instant) — available when status is `serverwontfund` or `clientinvalidfunded`
1946
+ * 2. **Locktime refund** (after CLTV expiry) — fallback when the receiver doesn't cooperate
1947
+ *
1948
+ * For retrying with a new invoice, use {@link retryArkadeToLightningSwap} instead.
1949
+ * @internal
1950
+ */
1951
+ async #buildArkadeToLightningRefund(id, swap, options) {
1952
+ if (!options?.destinationAddress) {
1953
+ return {
1954
+ success: false,
1955
+ message: "Destination address is required for Arkade refunds. " +
1956
+ 'Provide it via the options parameter: { destinationAddress: "ark1..." }',
1957
+ };
1958
+ }
1959
+ if (!this.#swapStorage) {
1960
+ return {
1961
+ success: false,
1962
+ message: "Swap storage is not configured. Cannot retrieve the secret key needed for refund.",
1963
+ };
1964
+ }
1965
+ const storedSwap = await this.#swapStorage.get(id);
1966
+ if (!storedSwap) {
1967
+ return {
1968
+ success: false,
1969
+ message: `Swap ${id} not found in local storage. The secret key is required to sign the refund transaction.`,
1970
+ };
1971
+ }
1972
+ if (swap.direction !== "arkade_to_lightning") {
1973
+ return {
1974
+ success: false,
1975
+ message: `Expected arkade_to_lightning swap, got ${swap.direction}`,
1976
+ };
1977
+ }
1978
+ const s = swap;
1979
+ const fullPubKey = storedSwap.publicKey;
1980
+ const userPubKey = fullPubKey.length === 66 ? fullPubKey.slice(2) : fullPubKey;
1981
+ const hashLock = s.hash_lock.startsWith("0x")
1982
+ ? s.hash_lock.slice(2)
1983
+ : s.hash_lock;
1984
+ // Try collaborative refund first (instant)
1985
+ try {
1986
+ // TODO: Add collabRefundArkadeToLightningDelegate
1987
+ const result = await collabRefundArkadeToLightningOffchain({
1988
+ userSecretKey: storedSwap.secretKey,
1989
+ userPubKey,
1990
+ receiverPubKey: s.receiver_pk,
1991
+ arkadeServerPubKey: s.arkade_server_pk,
1992
+ hashLock,
1993
+ vhtlcAddress: s.arkade_vhtlc_address,
1994
+ refundLocktime: s.vhtlc_refund_locktime,
1995
+ unilateralClaimDelay: s.unilateral_claim_delay,
1996
+ unilateralRefundDelay: s.unilateral_refund_delay,
1997
+ unilateralRefundWithoutReceiverDelay: s.unilateral_refund_without_receiver_delay,
1998
+ destinationAddress: options.destinationAddress,
1999
+ network: s.network,
2000
+ arkadeServerUrl: options.arkadeServerUrl ?? this.#config.arkadeServerUrl,
2001
+ swapId: id,
2002
+ apiClient: this.#apiClient,
2003
+ });
2004
+ return {
2005
+ success: true,
2006
+ message: "Arkade-to-Lightning refund executed via collaborative refund!",
2007
+ txId: result.txId,
2008
+ refundAmount: result.refundAmount,
2009
+ broadcast: true,
2010
+ };
2011
+ }
2012
+ catch (collabError) {
2013
+ const collabMsg = collabError instanceof Error
2014
+ ? collabError.message
2015
+ : String(collabError);
2016
+ console.warn(`collaborative refund failed (${collabMsg}), checking locktime fallback`);
2017
+ }
2018
+ // Fallback: non-collaborative refund (requires locktime to have expired)
2019
+ // TODO: Should use Bitcoin's MTP.
2020
+ const now = Math.floor(Date.now() / 1000);
2021
+ if (now < s.vhtlc_refund_locktime) {
2022
+ const remainingSeconds = s.vhtlc_refund_locktime - now;
2023
+ const remainingMinutes = Math.ceil(remainingSeconds / 60);
2024
+ return {
2025
+ success: false,
2026
+ message: `collaborative refund failed and non-collaborative refund ` +
2027
+ `is not yet available. The VHTLC locktime expires in ${remainingMinutes} minutes ` +
2028
+ `(at ${new Date(s.vhtlc_refund_locktime * 1000).toISOString()}). ` +
2029
+ `Try again after the locktime expires, or use retryArkadeToLightningSwap() to ` +
2030
+ `retry with a new Lightning invoice.`,
2031
+ };
2032
+ }
2033
+ // Locktime expired — use refund_without_receiver path (2-of-2: sender + server)
2034
+ // This reuses the existing Arkade refund logic — lendaswapPubKey is set to
2035
+ // the receiver key from this swap's VHTLC script.
2036
+ // TODO: Not yet e2e-tested!
2037
+ const refundParams = {
2038
+ userSecretKey: storedSwap.secretKey,
2039
+ userPubKey,
2040
+ lendaswapPubKey: s.receiver_pk,
2041
+ arkadeServerPubKey: s.arkade_server_pk,
2042
+ hashLock,
2043
+ vhtlcAddress: s.arkade_vhtlc_address,
2044
+ refundLocktime: s.vhtlc_refund_locktime,
2045
+ unilateralClaimDelay: s.unilateral_claim_delay,
2046
+ unilateralRefundDelay: s.unilateral_refund_delay,
2047
+ unilateralRefundWithoutReceiverDelay: s.unilateral_refund_without_receiver_delay,
2048
+ destinationAddress: options.destinationAddress,
2049
+ network: s.network,
2050
+ };
2051
+ // Query VTXO status to determine refund method
2052
+ const amounts = await this.amountsForSwap(id);
2053
+ const vtxoStatus = amounts.vtxoStatus;
2054
+ if (vtxoStatus === "not_funded" || vtxoStatus === "spent") {
2055
+ return {
2056
+ success: false,
2057
+ message: vtxoStatus === "not_funded"
2058
+ ? "No VTXOs found at the VHTLC address."
2059
+ : "All VTXOs have already been spent.",
2060
+ };
2061
+ }
2062
+ if (vtxoStatus === "spendable") {
2063
+ return this.#refundArkadeOffchain(refundParams, options);
2064
+ }
2065
+ return this.#refundArkadeDelegate(refundParams, options);
2066
+ }
1813
2067
  /**
1814
2068
  * Refunds via the offchain submitTx/finalizeTx path (spendable VTXOs only).
1815
2069
  * @internal
@@ -2312,7 +2566,8 @@ export class Client {
2312
2566
  const targetAsset = "token_id" in tgt
2313
2567
  ? { chain: tgt.chain, token_id: tgt.token_id }
2314
2568
  : { chain: tgt.chain, token_id: tgt.tokenId };
2315
- const sourceChain = sourceAsset.chain;
2569
+ let sourceChain = sourceAsset.chain;
2570
+ let sourceTokenId = sourceAsset.token_id;
2316
2571
  let targetChain = targetAsset.chain;
2317
2572
  let tokenAddress = targetAsset.token_id;
2318
2573
  // If the target is a bridge-only chain (e.g. USDC on Base), automatically
@@ -2329,6 +2584,7 @@ export class Client {
2329
2584
  targetTokenAddress: isUsdt0
2330
2585
  ? USDT0_ADDRESSES[chainName]
2331
2586
  : USDC_ADDRESSES[chainName],
2587
+ recipientSetup: options.bridgeRecipientSetup,
2332
2588
  };
2333
2589
  targetChain = "42161"; // Arbitrum
2334
2590
  tokenAddress = isUsdt0
@@ -2336,6 +2592,37 @@ export class Client {
2336
2592
  : USDC_ADDRESSES.Arbitrum;
2337
2593
  }
2338
2594
  }
2595
+ else if (bridgeParams &&
2596
+ options.bridgeRecipientSetup !== undefined &&
2597
+ bridgeParams.recipientSetup === undefined) {
2598
+ // Caller supplied `bridgeParams` without a `recipientSetup` but
2599
+ // also passed the top-level hint — fold the hint in.
2600
+ bridgeParams = {
2601
+ ...bridgeParams,
2602
+ recipientSetup: options.bridgeRecipientSetup,
2603
+ };
2604
+ }
2605
+ // Mirror of the outbound remap for CCTP-inbound sources: when the user
2606
+ // provides USDC on a chain the backend doesn't accept as a direct swap
2607
+ // source (Optimism, Base, Linea, …), rewrite to Arbitrum USDC and
2608
+ // populate `inboundBridgeParams` so the backend accounts for the
2609
+ // CCTPv2 fast-transfer fee at quote + UserOp-calldata time.
2610
+ let inboundBridgeParams = options.inboundBridgeParams;
2611
+ const parsedSourceChainId = Number.parseInt(sourceChain, 10);
2612
+ if (!inboundBridgeParams &&
2613
+ !Number.isNaN(parsedSourceChainId) &&
2614
+ isCctpOnlySource(parsedSourceChainId)) {
2615
+ const source = cctpMetaForChainId(parsedSourceChainId);
2616
+ if (sourceTokenId.toLowerCase() !== source.usdc.toLowerCase()) {
2617
+ throw new Error(`createSwap on ${source.name} requires native USDC (${source.usdc}); got ${sourceTokenId}. Only USDC is bridgeable via CCTP.`);
2618
+ }
2619
+ inboundBridgeParams = {
2620
+ sourceChain: source.name,
2621
+ sourceTokenAddress: source.usdc,
2622
+ };
2623
+ sourceChain = "42161";
2624
+ sourceTokenId = USDC_ADDRESSES.Arbitrum;
2625
+ }
2339
2626
  // Arkade → EVM
2340
2627
  if (isArkade(sourceAsset) && isEvmToken(targetChain)) {
2341
2628
  return this.createArkadeToEvmSwapGeneric({
@@ -2430,7 +2717,7 @@ export class Client {
2430
2717
  }
2431
2718
  return this.createEvmToArkadeSwapGeneric({
2432
2719
  targetAddress: options.targetAddress,
2433
- tokenAddress: sourceAsset.token_id,
2720
+ tokenAddress: sourceTokenId,
2434
2721
  evmChainId: Number(sourceChain),
2435
2722
  userAddress: options.userAddress ?? "",
2436
2723
  sourceAmount: options.sourceAmount
@@ -2439,6 +2726,7 @@ export class Client {
2439
2726
  targetAmount: options.targetAmount,
2440
2727
  referralCode: options.referralCode,
2441
2728
  gasless: options.gasless,
2729
+ inboundBridgeParams,
2442
2730
  });
2443
2731
  }
2444
2732
  // EVM → Bitcoin (on-chain)
@@ -2447,7 +2735,7 @@ export class Client {
2447
2735
  throw new Error("userAddress is required for EVM → Bitcoin swaps (unless gasless)");
2448
2736
  }
2449
2737
  return this.createEvmToBitcoinSwap({
2450
- tokenAddress: sourceAsset.token_id,
2738
+ tokenAddress: sourceTokenId,
2451
2739
  evmChainId: Number(sourceChain),
2452
2740
  userAddress: options.userAddress ?? "",
2453
2741
  targetAddress: options.targetAddress,
@@ -2457,6 +2745,7 @@ export class Client {
2457
2745
  targetAmount: options.targetAmount,
2458
2746
  referralCode: options.referralCode,
2459
2747
  gasless: options.gasless,
2748
+ inboundBridgeParams,
2460
2749
  });
2461
2750
  }
2462
2751
  // EVM → Lightning
@@ -2480,10 +2769,11 @@ export class Client {
2480
2769
  : { lnurl: options.targetAddress }),
2481
2770
  amountSats: options.targetAmount,
2482
2771
  evmChainId: Number(sourceChain),
2483
- tokenAddress: sourceAsset.token_id,
2772
+ tokenAddress: sourceTokenId,
2484
2773
  userAddress: options.userAddress ?? "",
2485
2774
  referralCode: options.referralCode,
2486
2775
  gasless: options.gasless,
2776
+ inboundBridgeParams,
2487
2777
  });
2488
2778
  }
2489
2779
  if (isArkade(targetAsset) && isLightning(sourceAsset)) {
@@ -2499,10 +2789,11 @@ export class Client {
2499
2789
  return this.createEvmToLightningSwapGeneric({
2500
2790
  lightningInvoice: options.targetAddress,
2501
2791
  evmChainId: Number(sourceChain),
2502
- tokenAddress: sourceAsset.token_id,
2792
+ tokenAddress: sourceTokenId,
2503
2793
  userAddress: options.userAddress ?? "",
2504
2794
  referralCode: options.referralCode,
2505
2795
  gasless: options.gasless,
2796
+ inboundBridgeParams,
2506
2797
  });
2507
2798
  }
2508
2799
  throw new Error(`Unsupported swap direction: ${sourceChain} → ${targetChain}`);
@@ -2600,7 +2891,7 @@ export class Client {
2600
2891
  * Creates a new Lightning to Arkade swap.
2601
2892
  *
2602
2893
  * The user pays a Lightning invoice and receives Arkade VTXOs
2603
- * after Boltz funds the Arkade VHTLC.
2894
+ * after the server funds the Arkade VHTLC.
2604
2895
  *
2605
2896
  * @param options - The swap options.
2606
2897
  * @returns The swap response and parameters for storage.
@@ -2625,7 +2916,7 @@ export class Client {
2625
2916
  * Creates a new Arkade to Lightning swap.
2626
2917
  *
2627
2918
  * The user sends Arkade VTXOs and a Lightning invoice gets paid
2628
- * via a Boltz submarine swap.
2919
+ * via the Lendaswap server.
2629
2920
  *
2630
2921
  * @param options - The swap options.
2631
2922
  * @returns The swap response and parameters for storage.
@@ -2644,6 +2935,183 @@ export class Client {
2644
2935
  return createArkadeToLightningSwap(options, this.#getCreateContext());
2645
2936
  }
2646
2937
  // =========================================================================
2938
+ // Arkade-to-Lightning: Fee Estimation & Retry
2939
+ // =========================================================================
2940
+ /**
2941
+ * Calculate the correct Lightning invoice amount for an Arkade→Lightning swap.
2942
+ *
2943
+ * Given the source amount in sats (what will be locked in the VHTLC), returns
2944
+ * the target amount that the Lightning invoice should be for (after fees
2945
+ * are deducted).
2946
+ *
2947
+ * Useful for:
2948
+ * - Knowing what invoice amount to generate before creating a swap
2949
+ * - Retrying a failed swap: the user's funds are locked in the old VHTLC at
2950
+ * `sourceAmountSats`, and the new invoice must match the expected target amount
2951
+ *
2952
+ * @param sourceAmountSats - Amount in sats that will fund the VHTLC
2953
+ * @returns Quote with target amount, fee breakdown, and exchange rate
2954
+ * @throws Error if the quote request fails or the amount is out of range
2955
+ *
2956
+ * @example
2957
+ * ```ts
2958
+ * const quote = await client.getArkadeToLightningQuote(100000);
2959
+ * console.log(`Invoice should be for ${quote.target_amount} sats`);
2960
+ * console.log(`Fees: ${quote.fee} sats`);
2961
+ * ```
2962
+ */
2963
+ async getArkadeToLightningQuote(sourceAmountSats) {
2964
+ const { data, error } = await this.#apiClient.GET("/quote", {
2965
+ params: {
2966
+ query: {
2967
+ source_chain: "Arkade",
2968
+ source_token: "btc",
2969
+ target_chain: "Lightning",
2970
+ target_token: "btc",
2971
+ source_amount: sourceAmountSats,
2972
+ },
2973
+ },
2974
+ });
2975
+ if (error) {
2976
+ throw new Error(`Failed to get Arkade→Lightning quote: ${typeof error === "string" ? error : JSON.stringify(error)}`);
2977
+ }
2978
+ if (!data) {
2979
+ throw new Error("No quote data returned");
2980
+ }
2981
+ return data;
2982
+ }
2983
+ /**
2984
+ * Retry a failed Arkade→Lightning swap with a new Lightning invoice or LNURL.
2985
+ *
2986
+ * When an Arkade→Lightning swap fails (status `serverwontfund` or `clientinvalidfunded`),
2987
+ * this method:
2988
+ * 1. Creates a new Arkade→Lightning swap with the new invoice/LNURL
2989
+ * 2. Collaboratively refunds the old VHTLC into the new swap's VHTLC
2990
+ *
2991
+ * The refund uses the collaborative `refund` script leaf (3-of-3: sender + receiver + Arkade),
2992
+ * which is instant (no locktime wait). The receiver cooperates because the swap is in
2993
+ * `invoice.failedToPay` state.
2994
+ *
2995
+ * **Invoice amount**: The new invoice must match the expected target amount for the
2996
+ * source amount locked in the old VHTLC. Use {@link getArkadeToLightningQuote} to
2997
+ * calculate the correct invoice amount, or use `lightningAddress` (LNURL) which
2998
+ * handles amount negotiation automatically.
2999
+ *
3000
+ * @param swapId - ID of the failed swap (must be in `serverwontfund` or `clientinvalidfunded` status)
3001
+ * @param options - New invoice or Lightning address for the retry
3002
+ * @returns The new swap response and the refund transaction ID
3003
+ * @throws Error if the swap is not in the right state, amount mismatch, or server refuses
3004
+ *
3005
+ * @example
3006
+ * ```ts
3007
+ * // With LNURL (recommended — handles amount automatically):
3008
+ * const result = await client.retryArkadeToLightningSwap(swapId, {
3009
+ * lightningAddress: "user@speed.app",
3010
+ * });
3011
+ *
3012
+ * // With invoice (must match expected amount):
3013
+ * const quote = await client.getArkadeToLightningQuote(oldSwap.boltz_amount_sats);
3014
+ * // Generate invoice for quote.target_amount sats, then:
3015
+ * const result = await client.retryArkadeToLightningSwap(swapId, {
3016
+ * lightningInvoice: "lnbc...",
3017
+ * });
3018
+ * ```
3019
+ */
3020
+ async retryArkadeToLightningSwap(swapId, options) {
3021
+ if (!this.#swapStorage) {
3022
+ throw new Error("Swap storage not configured. Cannot retrieve keys needed for refund.");
3023
+ }
3024
+ if (!options.lightningInvoice && !options.lightningAddress) {
3025
+ throw new Error("Provide either lightningInvoice or lightningAddress for retry");
3026
+ }
3027
+ if (options.lightningInvoice && options.lightningAddress) {
3028
+ throw new Error("Provide either lightningInvoice or lightningAddress, not both");
3029
+ }
3030
+ // 1. Validate the old swap is in a retryable state
3031
+ const oldSwap = await this.getSwap(swapId, {
3032
+ updateStorage: true,
3033
+ });
3034
+ if (oldSwap.direction !== "arkade_to_lightning") {
3035
+ throw new Error(`Expected arkade_to_lightning swap, got ${oldSwap.direction}`);
3036
+ }
3037
+ const retryableStatuses = ["serverwontfund", "clientinvalidfunded"];
3038
+ if (!retryableStatuses.includes(oldSwap.status)) {
3039
+ throw new Error(`Swap must be in serverwontfund or clientinvalidfunded status to retry ` +
3040
+ `(current: ${oldSwap.status}). ` +
3041
+ (oldSwap.status === "serverredeemed"
3042
+ ? "This swap completed successfully — no retry needed."
3043
+ : oldSwap.status === "clientrefunded"
3044
+ ? "This swap was already refunded."
3045
+ : "The swap may still be in progress."));
3046
+ }
3047
+ // 2. Get the source amount locked in the old VHTLC
3048
+ const sourceAmountSats = oldSwap.boltz_amount_sats;
3049
+ // 3. Build create-swap options
3050
+ const createOptions = {};
3051
+ if (options.lightningAddress) {
3052
+ // LNURL: use the quote to determine the right amount
3053
+ const quote = await this.getArkadeToLightningQuote(sourceAmountSats);
3054
+ createOptions.lightningAddress = options.lightningAddress;
3055
+ createOptions.amountSats = Number(quote.target_amount);
3056
+ }
3057
+ else if (options.lightningInvoice) {
3058
+ createOptions.lightningInvoice = options.lightningInvoice;
3059
+ }
3060
+ // 4. Create the new swap
3061
+ const newSwapResult = await this.createArkadeToLightningSwap(createOptions);
3062
+ const newSwap = newSwapResult.response;
3063
+ // 5. Verify the new swap's expected funding amount matches our old VHTLC
3064
+ const newExpectedAmount = newSwap.boltz_amount_sats;
3065
+ if (newExpectedAmount !== sourceAmountSats) {
3066
+ const mismatchHint = newExpectedAmount > sourceAmountSats
3067
+ ? "The invoice amount may be too high"
3068
+ : "The invoice amount may be too low";
3069
+ throw new Error(`Amount mismatch: new swap expects ${newExpectedAmount} sats in VHTLC ` +
3070
+ `but old VHTLC has ${sourceAmountSats} sats. ` +
3071
+ `${mismatchHint} — use getArkadeToLightningQuote(${sourceAmountSats}) ` +
3072
+ `to calculate the correct invoice amount before retrying.`);
3073
+ }
3074
+ const storedSwap = await this.#swapStorage.get(swapId);
3075
+ if (!storedSwap) {
3076
+ throw new Error(`Swap ${swapId} not found in local storage. ` +
3077
+ `The secret key is required to sign the collaborative refund.`);
3078
+ }
3079
+ const fullPubKey = storedSwap.publicKey;
3080
+ const userPubKey = fullPubKey.length === 66 ? fullPubKey.slice(2) : fullPubKey;
3081
+ const hashLock = oldSwap.hash_lock.startsWith("0x")
3082
+ ? oldSwap.hash_lock.slice(2)
3083
+ : oldSwap.hash_lock;
3084
+ // Collaborative refund: old VHTLC → new VHTLC address
3085
+ console.log("[retry] Starting collaborative refund", {
3086
+ oldSwapId: swapId,
3087
+ newSwapId: newSwap.id,
3088
+ sourceAmount: sourceAmountSats,
3089
+ network: oldSwap.network,
3090
+ });
3091
+ const refundResult = await collabRefundArkadeToLightningOffchain({
3092
+ userSecretKey: storedSwap.secretKey,
3093
+ userPubKey,
3094
+ receiverPubKey: oldSwap.receiver_pk,
3095
+ arkadeServerPubKey: oldSwap.arkade_server_pk,
3096
+ hashLock,
3097
+ vhtlcAddress: oldSwap.arkade_vhtlc_address,
3098
+ refundLocktime: oldSwap.vhtlc_refund_locktime,
3099
+ unilateralClaimDelay: oldSwap.unilateral_claim_delay,
3100
+ unilateralRefundDelay: oldSwap.unilateral_refund_delay,
3101
+ unilateralRefundWithoutReceiverDelay: oldSwap.unilateral_refund_without_receiver_delay,
3102
+ destinationAddress: newSwap.arkade_vhtlc_address,
3103
+ network: oldSwap.network,
3104
+ arkadeServerUrl: this.#config.arkadeServerUrl,
3105
+ swapId,
3106
+ apiClient: this.#apiClient,
3107
+ });
3108
+ return {
3109
+ newSwap,
3110
+ refundTxId: refundResult.txId,
3111
+ refundAmount: refundResult.refundAmount,
3112
+ };
3113
+ }
3114
+ // =========================================================================
2647
3115
  // Swap Creation - EVM to Arkade
2648
3116
  // =========================================================================
2649
3117
  /**
@@ -2959,7 +3427,16 @@ export class Client {
2959
3427
  *
2960
3428
  * @param swapId - The UUID of the swap
2961
3429
  * @param signer - An {@link EvmSigner} wrapping the user's wallet
2962
- * @returns The funding transaction hash
3430
+ * @param options - Optional CCTP-path tuning: `maxFee` caps the
3431
+ * CCTP fast-transfer fee (defaults to the
3432
+ * IRIS-quoted value); `onProgress` receives
3433
+ * per-phase updates during the CCTP flow;
3434
+ * `signal` aborts the attestation wait. All
3435
+ * three are ignored when the swap takes the
3436
+ * direct-Permit2 path.
3437
+ * @returns The funding transaction hash, plus an optional `cctp`
3438
+ * object with `burnTxHash`, `userOpHash`, and
3439
+ * `smartAccountAddress` when the CCTP path ran.
2963
3440
  *
2964
3441
  * @example
2965
3442
  * ```ts
@@ -2977,7 +3454,63 @@ export class Client {
2977
3454
  * const { txHash } = await client.fundSwap(swapId, signer);
2978
3455
  * ```
2979
3456
  */
2980
- async fundSwap(swapId, signer) {
3457
+ async fundSwap(swapId, signer, options) {
3458
+ // Dispatch the CCTP-inbound path only for chains the backend does
3459
+ // NOT accept as a direct swap source (Optimism, Base, Linea, …).
3460
+ // Ethereum / Polygon / Arbitrum fund via Permit2 on the source
3461
+ // chain even though they're CCTP-supported. Source asset must be
3462
+ // native USDC for the chain — otherwise CCTP isn't viable and we
3463
+ // surface a clear error instead of silently burning the wrong
3464
+ // token through TokenMessenger.
3465
+ if (isCctpOnlySource(signer.chainId)) {
3466
+ const stored = await this.#swapStorage?.get(swapId);
3467
+ const swapResponse = stored?.response ??
3468
+ (await this.getSwap(swapId, { updateStorage: true }));
3469
+ const source = cctpMetaForChainId(signer.chainId);
3470
+ // CCTP-inbound swaps are stored against post-hop Arbitrum USDC
3471
+ // (`source_token` reports the Arbitrum address), so we validate
3472
+ // against the `bridge_source_*` fields the backend sets when the
3473
+ // swap was created through the CCTP path. If those are missing
3474
+ // the swap wasn't created for a CCTP-inbound flow — reject rather
3475
+ // than silently burn USDC through the TokenMessenger.
3476
+ const bridgeSourceChain = swapResponse.bridge_source_chain;
3477
+ const bridgeSourceToken = swapResponse.bridge_source_token_address;
3478
+ if (!bridgeSourceChain || !bridgeSourceToken) {
3479
+ throw new Error(`fundSwap on chain ${signer.chainId} expects a CCTP-inbound swap but the swap has no bridge_source_chain set. ` +
3480
+ `Was the swap created against native USDC on a CCTP-only chain?`);
3481
+ }
3482
+ if (bridgeSourceChain !== source.name) {
3483
+ throw new Error(`fundSwap signer is on ${source.name} but the swap was created for bridge source ${bridgeSourceChain}.`);
3484
+ }
3485
+ if (bridgeSourceToken.toLowerCase() !== source.usdc.toLowerCase()) {
3486
+ throw new Error(`fundSwap on ${source.name} requires native USDC (${source.usdc}), but swap's bridge_source_token_address is ${bridgeSourceToken}. ` +
3487
+ `Only native USDC is bridgeable via CCTP.`);
3488
+ }
3489
+ const rawAmount = swapResponse
3490
+ .source_amount;
3491
+ if (rawAmount === undefined) {
3492
+ throw new Error(`Swap ${swapId} has no source_amount — cannot route CCTP fund.`);
3493
+ }
3494
+ const amount = BigInt(rawAmount);
3495
+ const destination = cctpMetaForChainId(42161);
3496
+ const maxFee = options?.maxFee ??
3497
+ computeCctpFastFee(await getCachedCctpFee({
3498
+ sourceDomain: source.domain,
3499
+ destinationDomain: destination.domain,
3500
+ }), amount);
3501
+ const cctp = await this.cctpInbound.fundSwap({
3502
+ swapId,
3503
+ signer,
3504
+ amount,
3505
+ maxFee,
3506
+ onProgress: options?.onProgress,
3507
+ signal: options?.signal,
3508
+ });
3509
+ return { txHash: cctp.transactionHash ?? cctp.userOpHash, cctp };
3510
+ }
3511
+ // Direct Permit2 path — Ethereum, Polygon, Arbitrum, or any other
3512
+ // chain `signer.chainId` specifies. Unchanged from the original
3513
+ // behaviour.
2981
3514
  // 1. Fetch Permit2 funding params
2982
3515
  const funding = await this.getPermit2FundingParamsUnsigned(swapId, signer.chainId);
2983
3516
  const tokenAddress = funding.sourceTokenAddress;