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