@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.
- package/dist/arkade-network.d.ts +8 -0
- package/dist/arkade-network.d.ts.map +1 -0
- package/dist/arkade-network.js +40 -0
- package/dist/arkade-network.js.map +1 -0
- package/dist/arkade.d.ts.map +1 -1
- package/dist/arkade.js +2 -9
- package/dist/arkade.js.map +1 -1
- package/dist/cctp/constants.d.ts.map +1 -1
- package/dist/cctp/constants.js +4 -0
- package/dist/cctp/constants.js.map +1 -1
- package/dist/cctp/fee.d.ts +49 -0
- package/dist/cctp/fee.d.ts.map +1 -0
- package/dist/cctp/fee.js +70 -0
- package/dist/cctp/fee.js.map +1 -0
- package/dist/cctp/index.d.ts +2 -1
- package/dist/cctp/index.d.ts.map +1 -1
- package/dist/cctp/index.js +2 -1
- package/dist/cctp/index.js.map +1 -1
- package/dist/cctp/utils.d.ts +15 -0
- package/dist/cctp/utils.d.ts.map +1 -1
- package/dist/cctp/utils.js +41 -0
- package/dist/cctp/utils.js.map +1 -1
- package/dist/cctp-bridge/bridge.d.ts +62 -0
- package/dist/cctp-bridge/bridge.d.ts.map +1 -0
- package/dist/cctp-bridge/bridge.js +74 -0
- package/dist/cctp-bridge/bridge.js.map +1 -0
- package/dist/cctp-bridge/index.d.ts +17 -0
- package/dist/cctp-bridge/index.d.ts.map +1 -0
- package/dist/cctp-bridge/index.js +16 -0
- package/dist/cctp-bridge/index.js.map +1 -0
- package/dist/cctp-inbound/approveAndBurn.d.ts +68 -0
- package/dist/cctp-inbound/approveAndBurn.d.ts.map +1 -0
- package/dist/cctp-inbound/approveAndBurn.js +95 -0
- package/dist/cctp-inbound/approveAndBurn.js.map +1 -0
- package/dist/cctp-inbound/burn.d.ts +51 -0
- package/dist/cctp-inbound/burn.d.ts.map +1 -0
- package/dist/cctp-inbound/burn.js +66 -0
- package/dist/cctp-inbound/burn.js.map +1 -0
- package/dist/cctp-inbound/chainMap.d.ts +48 -0
- package/dist/cctp-inbound/chainMap.d.ts.map +1 -0
- package/dist/cctp-inbound/chainMap.js +103 -0
- package/dist/cctp-inbound/chainMap.js.map +1 -0
- package/dist/cctp-inbound/client.d.ts +1573 -0
- package/dist/cctp-inbound/client.d.ts.map +1 -0
- package/dist/cctp-inbound/client.js +117 -0
- package/dist/cctp-inbound/client.js.map +1 -0
- package/dist/cctp-inbound/eip2612.d.ts +77 -0
- package/dist/cctp-inbound/eip2612.d.ts.map +1 -0
- package/dist/cctp-inbound/eip2612.js +95 -0
- package/dist/cctp-inbound/eip2612.js.map +1 -0
- package/dist/cctp-inbound/evmSignerAdapter.d.ts +25 -0
- package/dist/cctp-inbound/evmSignerAdapter.d.ts.map +1 -0
- package/dist/cctp-inbound/evmSignerAdapter.js +50 -0
- package/dist/cctp-inbound/evmSignerAdapter.js.map +1 -0
- package/dist/cctp-inbound/fundSwap.d.ts +94 -0
- package/dist/cctp-inbound/fundSwap.d.ts.map +1 -0
- package/dist/cctp-inbound/fundSwap.js +90 -0
- package/dist/cctp-inbound/fundSwap.js.map +1 -0
- package/dist/cctp-inbound/index.d.ts +35 -0
- package/dist/cctp-inbound/index.d.ts.map +1 -0
- package/dist/cctp-inbound/index.js +33 -0
- package/dist/cctp-inbound/index.js.map +1 -0
- package/dist/cctp-inbound/permit2.d.ts +33 -0
- package/dist/cctp-inbound/permit2.d.ts.map +1 -0
- package/dist/cctp-inbound/permit2.js +32 -0
- package/dist/cctp-inbound/permit2.js.map +1 -0
- package/dist/cctp-inbound/preflight.d.ts +36 -0
- package/dist/cctp-inbound/preflight.d.ts.map +1 -0
- package/dist/cctp-inbound/preflight.js +67 -0
- package/dist/cctp-inbound/preflight.js.map +1 -0
- package/dist/cctp-inbound/smartAccount.d.ts +1518 -0
- package/dist/cctp-inbound/smartAccount.d.ts.map +1 -0
- package/dist/cctp-inbound/smartAccount.js +117 -0
- package/dist/cctp-inbound/smartAccount.js.map +1 -0
- package/dist/cctp-inbound/submit.d.ts +75 -0
- package/dist/cctp-inbound/submit.d.ts.map +1 -0
- package/dist/cctp-inbound/submit.js +126 -0
- package/dist/cctp-inbound/submit.js.map +1 -0
- package/dist/cctp-inbound/types.d.ts +30 -0
- package/dist/cctp-inbound/types.d.ts.map +1 -0
- package/dist/cctp-inbound/types.js +5 -0
- package/dist/cctp-inbound/types.js.map +1 -0
- package/dist/cctp-inbound/userOp.d.ts +93 -0
- package/dist/cctp-inbound/userOp.d.ts.map +1 -0
- package/dist/cctp-inbound/userOp.js +151 -0
- package/dist/cctp-inbound/userOp.js.map +1 -0
- package/dist/client.d.ts +166 -6
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +565 -32
- package/dist/client.js.map +1 -1
- package/dist/create/arkade.d.ts.map +1 -1
- package/dist/create/arkade.js +21 -19
- package/dist/create/arkade.js.map +1 -1
- package/dist/create/bitcoin.d.ts.map +1 -1
- package/dist/create/bitcoin.js +17 -15
- package/dist/create/bitcoin.js.map +1 -1
- package/dist/create/evm-to-arkade.d.ts.map +1 -1
- package/dist/create/evm-to-arkade.js +2 -0
- package/dist/create/evm-to-arkade.js.map +1 -1
- package/dist/create/evm-to-bitcoin.d.ts.map +1 -1
- package/dist/create/evm-to-bitcoin.js +2 -0
- package/dist/create/evm-to-bitcoin.js.map +1 -1
- package/dist/create/evm-to-lightning.d.ts.map +1 -1
- package/dist/create/evm-to-lightning.js +6 -3
- package/dist/create/evm-to-lightning.js.map +1 -1
- package/dist/create/lightning.d.ts.map +1 -1
- package/dist/create/lightning.js +17 -15
- package/dist/create/lightning.js.map +1 -1
- package/dist/create/types.d.ts +40 -0
- package/dist/create/types.d.ts.map +1 -1
- package/dist/delegate.d.ts.map +1 -1
- package/dist/delegate.js +2 -31
- package/dist/delegate.js.map +1 -1
- package/dist/evm/wallet.d.ts +16 -0
- package/dist/evm/wallet.d.ts.map +1 -1
- package/dist/evm/wallet.js.map +1 -1
- package/dist/generated/api.d.ts +408 -2
- package/dist/generated/api.d.ts.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/dist/redeem/arkade.d.ts.map +1 -1
- package/dist/redeem/arkade.js +4 -43
- package/dist/redeem/arkade.js.map +1 -1
- package/dist/redeem/gasless.d.ts +16 -0
- package/dist/redeem/gasless.d.ts.map +1 -1
- package/dist/redeem/gasless.js +3 -1
- package/dist/redeem/gasless.js.map +1 -1
- package/dist/refund/arkade.d.ts.map +1 -1
- package/dist/refund/arkade.js +3 -39
- package/dist/refund/arkade.js.map +1 -1
- package/dist/refund/{collab.d.ts → collab-arkade-evm.d.ts} +6 -6
- package/dist/refund/collab-arkade-evm.d.ts.map +1 -0
- package/dist/refund/{collab.js → collab-arkade-evm.js} +6 -38
- package/dist/refund/collab-arkade-evm.js.map +1 -0
- package/dist/refund/collab-arkade-lightning.d.ts +59 -0
- package/dist/refund/collab-arkade-lightning.d.ts.map +1 -0
- package/dist/refund/collab-arkade-lightning.js +208 -0
- package/dist/refund/collab-arkade-lightning.js.map +1 -0
- package/dist/refund/index.d.ts +3 -2
- package/dist/refund/index.d.ts.map +1 -1
- package/dist/refund/index.js +3 -2
- package/dist/refund/index.js.map +1 -1
- package/dist/tokens.d.ts +18 -0
- package/dist/tokens.d.ts.map +1 -1
- package/dist/tokens.js +43 -7
- package/dist/tokens.js.map +1 -1
- package/dist/version.d.ts +2 -2
- package/dist/version.js +1 -1
- package/package.json +27 -2
- package/dist/refund/collab.d.ts.map +0 -1
- 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,
|
|
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
|
-
//
|
|
819
|
-
|
|
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
|
-
|
|
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: {
|
|
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
|
-
|
|
1016
|
-
const 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
|
|
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
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
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
|
|
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
|
-
* @
|
|
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;
|