@arkade-os/boltz-swap 0.3.22 → 0.3.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -0
- package/dist/{arkade-swaps-DEvf-2g2.d.ts → arkade-swaps-CZF9XoFR.d.cts} +101 -16
- package/dist/{arkade-swaps-CvA3RP7_.d.cts → arkade-swaps-pfAgQUMP.d.ts} +101 -16
- package/dist/{background-EHOJZDKT.js → background-GCBCKLGY.js} +2 -2
- package/dist/{chunk-EQK2BJQC.js → chunk-FEXQELYZ.js} +1 -1
- package/dist/{chunk-K3QEFL7D.js → chunk-XC2ARJZO.js} +390 -70
- package/dist/expo/index.cjs +408 -73
- package/dist/expo/index.d.cts +8 -9
- package/dist/expo/index.d.ts +8 -9
- package/dist/expo/index.js +16 -4
- package/dist/index.cjs +515 -79
- package/dist/index.d.cts +46 -14
- package/dist/index.d.ts +46 -14
- package/dist/index.js +126 -10
- package/dist/repositories/realm/index.d.cts +1 -1
- package/dist/repositories/realm/index.d.ts +1 -1
- package/dist/repositories/sqlite/index.d.cts +1 -1
- package/dist/repositories/sqlite/index.d.ts +1 -1
- package/dist/{types-OyAdK824.d.cts → types-LCXS1AVA.d.cts} +63 -1
- package/dist/{types-OyAdK824.d.ts → types-LCXS1AVA.d.ts} +63 -1
- package/package.json +2 -2
package/dist/expo/index.cjs
CHANGED
|
@@ -118,7 +118,7 @@ var init_errors = __esm({
|
|
|
118
118
|
});
|
|
119
119
|
|
|
120
120
|
// src/boltz-swap-provider.ts
|
|
121
|
-
var import_sdk, import_base, isSubmarineFinalStatus, isSubmarineRefundableStatus, isReverseFinalStatus, isReverseClaimableStatus, isChainClaimableStatus, isChainFinalStatus, isChainRefundableStatus, isChainSignableStatus, isPendingReverseSwap, isPendingSubmarineSwap, isPendingChainSwap, isSubmarineSwapRefundable, isTimeoutBlockHeights, isGetReverseSwapTxIdResponse, isGetSwapStatusResponse, isGetSubmarinePairsResponse, isGetReversePairsResponse, isCreateSubmarineSwapResponse, isGetSwapPreimageResponse, isCreateReverseSwapResponse, isRefundSubmarineSwapResponse, isRefundChainSwapResponse, isGetChainPairsResponse, isSwapTree, isChainSwapDetailsResponse, isCreateChainSwapResponse, isGetChainClaimDetailsResponse, isPostChainClaimDetailsResponse, isGetChainQuoteResponse, isPostChainQuoteResponse, isPostBtcTransactionResponse, isLeaf, isTree, isDetails, isRestoredChainSwap, isRestoredSubmarineSwap, isRestoredReverseSwap, isCreateSwapsRestoreResponse, BASE_URLS, BoltzSwapProvider;
|
|
121
|
+
var import_sdk, import_base, isSubmarineFinalStatus, isSubmarineRefundableStatus, isSubmarineSuccessStatus, isReverseFinalStatus, isReverseClaimableStatus, isChainClaimableStatus, isChainFinalStatus, isChainRefundableStatus, isChainSignableStatus, isPendingReverseSwap, isPendingSubmarineSwap, isPendingChainSwap, isSubmarineSwapRefundable, isTimeoutBlockHeights, isGetReverseSwapTxIdResponse, isGetSwapStatusResponse, isGetSubmarinePairsResponse, isGetReversePairsResponse, isCreateSubmarineSwapResponse, isGetSwapPreimageResponse, isCreateReverseSwapResponse, isRefundSubmarineSwapResponse, isRefundChainSwapResponse, isGetChainPairsResponse, isSwapTree, isChainSwapDetailsResponse, isCreateChainSwapResponse, isGetChainClaimDetailsResponse, isPostChainClaimDetailsResponse, isGetChainQuoteResponse, isPostChainQuoteResponse, isPostBtcTransactionResponse, isLeaf, isTree, isDetails, isRestoredChainSwap, isRestoredSubmarineSwap, isRestoredReverseSwap, isCreateSwapsRestoreResponse, BASE_URLS, BoltzSwapProvider;
|
|
122
122
|
var init_boltz_swap_provider = __esm({
|
|
123
123
|
"src/boltz-swap-provider.ts"() {
|
|
124
124
|
"use strict";
|
|
@@ -139,6 +139,9 @@ var init_boltz_swap_provider = __esm({
|
|
|
139
139
|
"swap.expired"
|
|
140
140
|
].includes(status);
|
|
141
141
|
};
|
|
142
|
+
isSubmarineSuccessStatus = (status) => {
|
|
143
|
+
return status === "transaction.claimed";
|
|
144
|
+
};
|
|
142
145
|
isReverseFinalStatus = (status) => {
|
|
143
146
|
return [
|
|
144
147
|
"transaction.refunded",
|
|
@@ -2672,7 +2675,7 @@ var init_batch = __esm({
|
|
|
2672
2675
|
function scriptFromTapLeafScript(leaf) {
|
|
2673
2676
|
return leaf[1].subarray(0, leaf[1].length - 1);
|
|
2674
2677
|
}
|
|
2675
|
-
var import_sdk7, import_base8, import_legacy, import_btc_signer4, import_payment3, createVHTLCScript, joinBatch, claimVHTLCwithOffchainTx, refundVHTLCwithOffchainTx;
|
|
2678
|
+
var import_sdk7, import_base8, import_legacy, import_btc_signer4, import_payment3, toBip68RelativeTimelock, createVHTLCScript, joinBatch, claimVHTLCwithOffchainTx, refundVHTLCwithOffchainTx;
|
|
2676
2679
|
var init_vhtlc = __esm({
|
|
2677
2680
|
"src/utils/vhtlc.ts"() {
|
|
2678
2681
|
"use strict";
|
|
@@ -2685,6 +2688,10 @@ var init_vhtlc = __esm({
|
|
|
2685
2688
|
import_btc_signer4 = require("@scure/btc-signer");
|
|
2686
2689
|
import_payment3 = require("@scure/btc-signer/payment.js");
|
|
2687
2690
|
init_signatures();
|
|
2691
|
+
toBip68RelativeTimelock = (value) => ({
|
|
2692
|
+
type: value < 512 ? "blocks" : "seconds",
|
|
2693
|
+
value: BigInt(value)
|
|
2694
|
+
});
|
|
2688
2695
|
createVHTLCScript = (args) => {
|
|
2689
2696
|
const {
|
|
2690
2697
|
network,
|
|
@@ -2692,7 +2699,7 @@ var init_vhtlc = __esm({
|
|
|
2692
2699
|
receiverPubkey,
|
|
2693
2700
|
senderPubkey,
|
|
2694
2701
|
serverPubkey,
|
|
2695
|
-
timeoutBlockHeights
|
|
2702
|
+
timeoutBlockHeights: vhtlcTimeouts
|
|
2696
2703
|
} = args;
|
|
2697
2704
|
const receiverXOnlyPublicKey = normalizeToXOnlyKey(
|
|
2698
2705
|
import_base8.hex.decode(receiverPubkey),
|
|
@@ -2706,27 +2713,21 @@ var init_vhtlc = __esm({
|
|
|
2706
2713
|
import_base8.hex.decode(serverPubkey),
|
|
2707
2714
|
"server"
|
|
2708
2715
|
);
|
|
2709
|
-
const delayType = (num) => num < 512 ? "blocks" : "seconds";
|
|
2710
2716
|
const vhtlcScript = new import_sdk7.VHTLC.Script({
|
|
2711
2717
|
preimageHash: (0, import_legacy.ripemd160)(preimageHash),
|
|
2712
2718
|
sender: senderXOnlyPublicKey,
|
|
2713
2719
|
receiver: receiverXOnlyPublicKey,
|
|
2714
2720
|
server: serverXOnlyPublicKey,
|
|
2715
|
-
refundLocktime: BigInt(
|
|
2716
|
-
unilateralClaimDelay:
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
type: delayType(
|
|
2726
|
-
timeoutBlockHeights.unilateralRefundWithoutReceiver
|
|
2727
|
-
),
|
|
2728
|
-
value: BigInt(timeoutBlockHeights.unilateralRefundWithoutReceiver)
|
|
2729
|
-
}
|
|
2721
|
+
refundLocktime: BigInt(vhtlcTimeouts.refund),
|
|
2722
|
+
unilateralClaimDelay: toBip68RelativeTimelock(
|
|
2723
|
+
vhtlcTimeouts.unilateralClaim
|
|
2724
|
+
),
|
|
2725
|
+
unilateralRefundDelay: toBip68RelativeTimelock(
|
|
2726
|
+
vhtlcTimeouts.unilateralRefund
|
|
2727
|
+
),
|
|
2728
|
+
unilateralRefundWithoutReceiverDelay: toBip68RelativeTimelock(
|
|
2729
|
+
vhtlcTimeouts.unilateralRefundWithoutReceiver
|
|
2730
|
+
)
|
|
2730
2731
|
});
|
|
2731
2732
|
if (!vhtlcScript.claimScript)
|
|
2732
2733
|
throw new Error("Failed to create VHTLC script");
|
|
@@ -2964,7 +2965,7 @@ var init_vhtlc = __esm({
|
|
|
2964
2965
|
});
|
|
2965
2966
|
|
|
2966
2967
|
// src/arkade-swaps.ts
|
|
2967
|
-
var import_sdk8, import_sha23, import_base9, import_secp256k13, import_utils3, import_btc_signer5, import_utils4, CLAIM_VTXO_RETRY_ATTEMPTS, CLAIM_VTXO_RETRY_DELAY_MS, ArkadeSwaps;
|
|
2968
|
+
var import_sdk8, import_sha23, import_base9, import_secp256k13, import_utils3, import_btc_signer5, import_utils4, dedupeVtxos, hasNonEmptyString, canRecoverViaBoltz3of3, isSubmarineRefundLocktimeReached, CLAIM_VTXO_RETRY_ATTEMPTS, CLAIM_VTXO_RETRY_DELAY_MS, ArkadeSwaps;
|
|
2968
2969
|
var init_arkade_swaps = __esm({
|
|
2969
2970
|
"src/arkade-swaps.ts"() {
|
|
2970
2971
|
"use strict";
|
|
@@ -2988,6 +2989,20 @@ var init_arkade_swaps = __esm({
|
|
|
2988
2989
|
init_swap_repository();
|
|
2989
2990
|
init_identity();
|
|
2990
2991
|
init_vhtlc();
|
|
2992
|
+
dedupeVtxos = (vtxos) => [
|
|
2993
|
+
...new Map(
|
|
2994
|
+
vtxos.map((vtxo) => [`${vtxo.txid}:${vtxo.vout}`, vtxo])
|
|
2995
|
+
).values()
|
|
2996
|
+
];
|
|
2997
|
+
hasNonEmptyString = (value) => typeof value === "string" && value.length > 0;
|
|
2998
|
+
canRecoverViaBoltz3of3 = (refundableVtxos, swap) => {
|
|
2999
|
+
const hasRequiredSwapMetadata = hasNonEmptyString(swap.id) && hasNonEmptyString(swap.request.refundPublicKey) && hasNonEmptyString(swap.response.address) && hasNonEmptyString(swap.response.claimPublicKey) && !!swap.response.timeoutBlockHeights;
|
|
3000
|
+
if (!hasRequiredSwapMetadata) return false;
|
|
3001
|
+
return refundableVtxos.some(
|
|
3002
|
+
(vtxo) => !vtxo.isSpent && !(0, import_sdk8.isRecoverable)(vtxo)
|
|
3003
|
+
);
|
|
3004
|
+
};
|
|
3005
|
+
isSubmarineRefundLocktimeReached = (refundTimestamp) => Math.floor(Date.now() / 1e3) >= refundTimestamp;
|
|
2991
3006
|
CLAIM_VTXO_RETRY_ATTEMPTS = 3;
|
|
2992
3007
|
CLAIM_VTXO_RETRY_DELAY_MS = 500;
|
|
2993
3008
|
ArkadeSwaps = class _ArkadeSwaps {
|
|
@@ -3255,8 +3270,12 @@ var init_arkade_swaps = __esm({
|
|
|
3255
3270
|
throw new Error(
|
|
3256
3271
|
`Swap ${pendingSwap.id}: preimage is required to claim VHTLC`
|
|
3257
3272
|
);
|
|
3258
|
-
const {
|
|
3259
|
-
|
|
3273
|
+
const {
|
|
3274
|
+
refundPublicKey,
|
|
3275
|
+
lockupAddress,
|
|
3276
|
+
timeoutBlockHeights: vhtlcTimeouts
|
|
3277
|
+
} = pendingSwap.response;
|
|
3278
|
+
if (!refundPublicKey || !lockupAddress || !vhtlcTimeouts)
|
|
3260
3279
|
throw new Error(
|
|
3261
3280
|
`Swap ${pendingSwap.id}: incomplete reverse swap response`
|
|
3262
3281
|
);
|
|
@@ -3284,7 +3303,7 @@ var init_arkade_swaps = __esm({
|
|
|
3284
3303
|
receiverPubkey: import_base9.hex.encode(receiverXOnly),
|
|
3285
3304
|
senderPubkey: import_base9.hex.encode(senderXOnly),
|
|
3286
3305
|
serverPubkey: import_base9.hex.encode(serverXOnly),
|
|
3287
|
-
timeoutBlockHeights
|
|
3306
|
+
timeoutBlockHeights: vhtlcTimeouts
|
|
3288
3307
|
});
|
|
3289
3308
|
if (!vhtlcScript.claimScript)
|
|
3290
3309
|
throw new Error(
|
|
@@ -3512,84 +3531,184 @@ var init_arkade_swaps = __esm({
|
|
|
3512
3531
|
return pendingSwap;
|
|
3513
3532
|
}
|
|
3514
3533
|
/**
|
|
3515
|
-
*
|
|
3516
|
-
*
|
|
3517
|
-
*
|
|
3518
|
-
*
|
|
3534
|
+
* Reconstruct a submarine swap's VHTLC script from stored data. This does
|
|
3535
|
+
* not query the indexer, so bulk scans can build every script first and
|
|
3536
|
+
* then use batched VTXO lookups.
|
|
3537
|
+
*
|
|
3538
|
+
* @throws {Error} If preimage hash is unavailable, the swap response is
|
|
3539
|
+
* incomplete, the script can't be built, or the reconstructed address
|
|
3540
|
+
* doesn't match the one Boltz returned.
|
|
3519
3541
|
*/
|
|
3520
|
-
async
|
|
3521
|
-
const preimageHash =
|
|
3542
|
+
async buildSubmarineVHTLCContext(swap, arkInfo) {
|
|
3543
|
+
const preimageHash = swap.request.invoice ? getInvoicePaymentHash(swap.request.invoice) : swap.preimageHash;
|
|
3522
3544
|
if (!preimageHash)
|
|
3523
3545
|
throw new Error(
|
|
3524
|
-
`Swap ${
|
|
3546
|
+
`Swap ${swap.id}: preimage hash is required to refund VHTLC`
|
|
3525
3547
|
);
|
|
3526
|
-
const
|
|
3527
|
-
const address = await this.wallet.getAddress();
|
|
3528
|
-
if (!address) throw new Error("Failed to get ark address from wallet");
|
|
3548
|
+
const resolvedArkInfo = arkInfo ?? await this.arkProvider.getInfo();
|
|
3529
3549
|
const ourXOnlyPublicKey = normalizeToXOnlyKey(
|
|
3530
3550
|
await this.wallet.identity.xOnlyPublicKey(),
|
|
3531
3551
|
"our",
|
|
3532
|
-
|
|
3552
|
+
swap.id
|
|
3533
3553
|
);
|
|
3534
3554
|
const serverXOnlyPublicKey = normalizeToXOnlyKey(
|
|
3535
|
-
import_base9.hex.decode(
|
|
3555
|
+
import_base9.hex.decode(resolvedArkInfo.signerPubkey),
|
|
3536
3556
|
"server",
|
|
3537
|
-
|
|
3557
|
+
swap.id
|
|
3538
3558
|
);
|
|
3539
|
-
const { claimPublicKey, timeoutBlockHeights } =
|
|
3540
|
-
if (!claimPublicKey || !
|
|
3559
|
+
const { claimPublicKey, timeoutBlockHeights: vhtlcTimeouts } = swap.response;
|
|
3560
|
+
if (!claimPublicKey || !vhtlcTimeouts)
|
|
3541
3561
|
throw new Error(
|
|
3542
|
-
`Swap ${
|
|
3562
|
+
`Swap ${swap.id}: incomplete submarine swap response`
|
|
3543
3563
|
);
|
|
3544
3564
|
const boltzXOnlyPublicKey = normalizeToXOnlyKey(
|
|
3545
3565
|
import_base9.hex.decode(claimPublicKey),
|
|
3546
3566
|
"boltz",
|
|
3547
|
-
|
|
3567
|
+
swap.id
|
|
3548
3568
|
);
|
|
3549
3569
|
const { vhtlcScript, vhtlcAddress } = this.createVHTLCScript({
|
|
3550
|
-
network:
|
|
3570
|
+
network: resolvedArkInfo.network,
|
|
3551
3571
|
preimageHash: import_base9.hex.decode(preimageHash),
|
|
3552
3572
|
receiverPubkey: import_base9.hex.encode(boltzXOnlyPublicKey),
|
|
3553
3573
|
senderPubkey: import_base9.hex.encode(ourXOnlyPublicKey),
|
|
3554
3574
|
serverPubkey: import_base9.hex.encode(serverXOnlyPublicKey),
|
|
3555
|
-
timeoutBlockHeights
|
|
3575
|
+
timeoutBlockHeights: vhtlcTimeouts
|
|
3556
3576
|
});
|
|
3557
3577
|
if (!vhtlcScript.claimScript)
|
|
3558
3578
|
throw new Error(
|
|
3559
|
-
`Swap ${
|
|
3579
|
+
`Swap ${swap.id}: failed to create VHTLC script for submarine swap`
|
|
3560
3580
|
);
|
|
3561
|
-
if (vhtlcAddress !==
|
|
3581
|
+
if (vhtlcAddress !== swap.response.address)
|
|
3562
3582
|
throw new Error(
|
|
3563
|
-
`VHTLC address mismatch for swap ${
|
|
3583
|
+
`VHTLC address mismatch for swap ${swap.id}: expected ${swap.response.address}, got ${vhtlcAddress}`
|
|
3564
3584
|
);
|
|
3565
3585
|
const vhtlcPkScriptHex = import_base9.hex.encode(vhtlcScript.pkScript);
|
|
3586
|
+
return {
|
|
3587
|
+
arkInfo: resolvedArkInfo,
|
|
3588
|
+
vhtlcScript,
|
|
3589
|
+
vhtlcAddress,
|
|
3590
|
+
vhtlcPkScriptHex,
|
|
3591
|
+
vhtlcTimeouts,
|
|
3592
|
+
ourXOnlyPublicKey,
|
|
3593
|
+
serverXOnlyPublicKey,
|
|
3594
|
+
boltzXOnlyPublicKey
|
|
3595
|
+
};
|
|
3596
|
+
}
|
|
3597
|
+
/**
|
|
3598
|
+
* Reconstruct a submarine swap's VHTLC script from stored data and look
|
|
3599
|
+
* up its VTXOs at the indexer. Side-effect free; shared by `refundVHTLC`
|
|
3600
|
+
* (spending path) and `inspectSubmarineRecovery` (diagnostic path).
|
|
3601
|
+
*
|
|
3602
|
+
* `refundableVtxos` merges spendable + recoverable indexer queries
|
|
3603
|
+
* (deduped by outpoint). When that set is empty, a third query
|
|
3604
|
+
* populates `diagnostic` so callers can distinguish "never funded",
|
|
3605
|
+
* "already spent", and "preconfirmed-only".
|
|
3606
|
+
*/
|
|
3607
|
+
async lookupSubmarineVHTLC(swap, arkInfo) {
|
|
3608
|
+
const context = await this.buildSubmarineVHTLCContext(swap, arkInfo);
|
|
3566
3609
|
const [spendableResult, recoverableResult] = await Promise.all([
|
|
3567
3610
|
this.indexerProvider.getVtxos({
|
|
3568
|
-
scripts: [vhtlcPkScriptHex],
|
|
3611
|
+
scripts: [context.vhtlcPkScriptHex],
|
|
3569
3612
|
spendableOnly: true
|
|
3570
3613
|
}),
|
|
3571
3614
|
this.indexerProvider.getVtxos({
|
|
3572
|
-
scripts: [vhtlcPkScriptHex],
|
|
3615
|
+
scripts: [context.vhtlcPkScriptHex],
|
|
3573
3616
|
recoverableOnly: true
|
|
3574
3617
|
})
|
|
3575
3618
|
]);
|
|
3576
|
-
const refundableVtxos = [
|
|
3577
|
-
...
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
-
).values()
|
|
3582
|
-
];
|
|
3619
|
+
const refundableVtxos = dedupeVtxos([
|
|
3620
|
+
...spendableResult.vtxos,
|
|
3621
|
+
...recoverableResult.vtxos
|
|
3622
|
+
]);
|
|
3623
|
+
let diagnostic;
|
|
3583
3624
|
if (refundableVtxos.length === 0) {
|
|
3584
3625
|
const { vtxos: allVtxos } = await this.indexerProvider.getVtxos({
|
|
3585
|
-
scripts: [vhtlcPkScriptHex]
|
|
3626
|
+
scripts: [context.vhtlcPkScriptHex]
|
|
3586
3627
|
});
|
|
3587
|
-
|
|
3628
|
+
diagnostic = {
|
|
3629
|
+
totalVtxoCount: allVtxos.length,
|
|
3630
|
+
allSpent: allVtxos.length > 0 && allVtxos.every((vtxo) => vtxo.isSpent)
|
|
3631
|
+
};
|
|
3632
|
+
}
|
|
3633
|
+
return {
|
|
3634
|
+
...context,
|
|
3635
|
+
refundableVtxos,
|
|
3636
|
+
diagnostic
|
|
3637
|
+
};
|
|
3638
|
+
}
|
|
3639
|
+
submarineRecoveryInfoFromLookup(swap, lookup) {
|
|
3640
|
+
const { refundableVtxos, diagnostic, vhtlcTimeouts } = lookup;
|
|
3641
|
+
if (refundableVtxos.length > 0) {
|
|
3642
|
+
const cltvSatisfied = isSubmarineRefundLocktimeReached(
|
|
3643
|
+
vhtlcTimeouts.refund
|
|
3644
|
+
);
|
|
3645
|
+
const amountSats = refundableVtxos.reduce(
|
|
3646
|
+
(sum, vtxo) => sum + Number(vtxo.value),
|
|
3647
|
+
0
|
|
3648
|
+
);
|
|
3649
|
+
const isRecoverable2 = cltvSatisfied || canRecoverViaBoltz3of3(refundableVtxos, swap);
|
|
3650
|
+
return {
|
|
3651
|
+
swap,
|
|
3652
|
+
status: isRecoverable2 ? "recoverable" : "pre_cltv",
|
|
3653
|
+
vtxoCount: refundableVtxos.length,
|
|
3654
|
+
amountSats,
|
|
3655
|
+
refundLocktime: vhtlcTimeouts.refund
|
|
3656
|
+
};
|
|
3657
|
+
}
|
|
3658
|
+
if (!diagnostic || diagnostic.totalVtxoCount === 0) {
|
|
3659
|
+
return {
|
|
3660
|
+
swap,
|
|
3661
|
+
status: "none",
|
|
3662
|
+
vtxoCount: 0,
|
|
3663
|
+
amountSats: 0,
|
|
3664
|
+
refundLocktime: vhtlcTimeouts.refund
|
|
3665
|
+
};
|
|
3666
|
+
}
|
|
3667
|
+
if (diagnostic.allSpent) {
|
|
3668
|
+
return {
|
|
3669
|
+
swap,
|
|
3670
|
+
status: "already_spent",
|
|
3671
|
+
vtxoCount: 0,
|
|
3672
|
+
amountSats: 0,
|
|
3673
|
+
refundLocktime: vhtlcTimeouts.refund
|
|
3674
|
+
};
|
|
3675
|
+
}
|
|
3676
|
+
return {
|
|
3677
|
+
swap,
|
|
3678
|
+
status: "none",
|
|
3679
|
+
vtxoCount: 0,
|
|
3680
|
+
amountSats: 0,
|
|
3681
|
+
refundLocktime: vhtlcTimeouts.refund
|
|
3682
|
+
};
|
|
3683
|
+
}
|
|
3684
|
+
/**
|
|
3685
|
+
* Refunds the VHTLC for a failed submarine swap, returning locked funds to the wallet.
|
|
3686
|
+
* Uses multi-party signatures (user + Boltz + server) for non-recoverable VTXOs.
|
|
3687
|
+
* @param pendingSwap - The submarine swap to refund.
|
|
3688
|
+
* @returns Counts of VTXOs swept vs. deferred. A return value of `{ swept: 0, skipped: N }`
|
|
3689
|
+
* means the call was a no-op — callers should not treat it as a successful refund.
|
|
3690
|
+
* @throws {Error} If preimage hash is unavailable, VHTLC not found, or already spent.
|
|
3691
|
+
*/
|
|
3692
|
+
async refundVHTLC(pendingSwap, cachedArkInfo) {
|
|
3693
|
+
const address = await this.wallet.getAddress();
|
|
3694
|
+
if (!address) throw new Error("Failed to get ark address from wallet");
|
|
3695
|
+
const {
|
|
3696
|
+
arkInfo,
|
|
3697
|
+
vhtlcScript,
|
|
3698
|
+
vhtlcTimeouts,
|
|
3699
|
+
ourXOnlyPublicKey,
|
|
3700
|
+
serverXOnlyPublicKey,
|
|
3701
|
+
boltzXOnlyPublicKey,
|
|
3702
|
+
refundableVtxos,
|
|
3703
|
+
diagnostic
|
|
3704
|
+
} = await this.lookupSubmarineVHTLC(pendingSwap, cachedArkInfo);
|
|
3705
|
+
if (refundableVtxos.length === 0) {
|
|
3706
|
+
if (!diagnostic || diagnostic.totalVtxoCount === 0) {
|
|
3588
3707
|
throw new Error(
|
|
3589
3708
|
`Swap ${pendingSwap.id}: VHTLC not found for address ${pendingSwap.response.address}`
|
|
3590
3709
|
);
|
|
3591
3710
|
}
|
|
3592
|
-
if (
|
|
3711
|
+
if (diagnostic.allSpent) {
|
|
3593
3712
|
throw new Error(
|
|
3594
3713
|
`Swap ${pendingSwap.id}: VHTLC is already spent`
|
|
3595
3714
|
);
|
|
@@ -3600,10 +3719,11 @@ var init_arkade_swaps = __esm({
|
|
|
3600
3719
|
}
|
|
3601
3720
|
const outputScript = import_sdk8.ArkAddress.decode(address).pkScript;
|
|
3602
3721
|
const refundWithoutReceiverLeaf = vhtlcScript.refundWithoutReceiver();
|
|
3603
|
-
const
|
|
3604
|
-
|
|
3605
|
-
|
|
3722
|
+
const cltvSatisfied = isSubmarineRefundLocktimeReached(
|
|
3723
|
+
vhtlcTimeouts.refund
|
|
3724
|
+
);
|
|
3606
3725
|
let boltzCallCount = 0;
|
|
3726
|
+
let sweptCount = 0;
|
|
3607
3727
|
let skippedCount = 0;
|
|
3608
3728
|
for (const vtxo of refundableVtxos) {
|
|
3609
3729
|
const isRecoverableVtxo = (0, import_sdk8.isRecoverable)(vtxo);
|
|
@@ -3624,11 +3744,12 @@ var init_arkade_swaps = __esm({
|
|
|
3624
3744
|
arkInfo,
|
|
3625
3745
|
isRecoverableVtxo
|
|
3626
3746
|
);
|
|
3747
|
+
sweptCount++;
|
|
3627
3748
|
continue;
|
|
3628
3749
|
}
|
|
3629
3750
|
if (isRecoverableVtxo) {
|
|
3630
3751
|
logger.error(
|
|
3631
|
-
`Swap ${pendingSwap.id}: recoverable VTXO ${vtxo.txid}:${vtxo.vout} cannot be refunded yet \u2014 refundWithoutReceiver locktime has not passed (refundLocktime=${
|
|
3752
|
+
`Swap ${pendingSwap.id}: recoverable VTXO ${vtxo.txid}:${vtxo.vout} cannot be refunded yet \u2014 refundWithoutReceiver locktime has not passed (refundLocktime=${vhtlcTimeouts.refund}, currentTimestamp=${Math.floor(Date.now() / 1e3)}). Refund will be retried after locktime.`
|
|
3632
3753
|
);
|
|
3633
3754
|
skippedCount++;
|
|
3634
3755
|
continue;
|
|
@@ -3657,14 +3778,14 @@ var init_arkade_swaps = __esm({
|
|
|
3657
3778
|
)
|
|
3658
3779
|
);
|
|
3659
3780
|
boltzCallCount++;
|
|
3781
|
+
sweptCount++;
|
|
3660
3782
|
} catch (error) {
|
|
3661
3783
|
if (!(error instanceof BoltzRefundError)) {
|
|
3662
3784
|
throw error;
|
|
3663
3785
|
}
|
|
3664
|
-
|
|
3665
|
-
if (BigInt(tipNow) < refundLocktime) {
|
|
3786
|
+
if (!isSubmarineRefundLocktimeReached(vhtlcTimeouts.refund)) {
|
|
3666
3787
|
logger.error(
|
|
3667
|
-
`Swap ${pendingSwap.id}: Boltz rejected VTXO outpoint and refundWithoutReceiver locktime has not passed yet (
|
|
3788
|
+
`Swap ${pendingSwap.id}: Boltz rejected VTXO outpoint and refundWithoutReceiver locktime has not passed yet (currentTimestamp=${Math.floor(Date.now() / 1e3)}, locktime=${vhtlcTimeouts.refund}). Refund will be retried after locktime.`
|
|
3668
3789
|
);
|
|
3669
3790
|
skippedCount++;
|
|
3670
3791
|
continue;
|
|
@@ -3684,16 +3805,218 @@ var init_arkade_swaps = __esm({
|
|
|
3684
3805
|
arkInfo,
|
|
3685
3806
|
false
|
|
3686
3807
|
);
|
|
3808
|
+
sweptCount++;
|
|
3687
3809
|
}
|
|
3688
3810
|
}
|
|
3689
|
-
|
|
3690
|
-
|
|
3691
|
-
|
|
3692
|
-
|
|
3693
|
-
|
|
3694
|
-
|
|
3695
|
-
|
|
3811
|
+
if (!isSubmarineSuccessStatus(pendingSwap.status)) {
|
|
3812
|
+
const fullyRefunded = skippedCount === 0;
|
|
3813
|
+
await updateSubmarineSwapStatus(
|
|
3814
|
+
pendingSwap,
|
|
3815
|
+
pendingSwap.status,
|
|
3816
|
+
// Keep current status
|
|
3817
|
+
this.savePendingSubmarineSwap.bind(this),
|
|
3818
|
+
{ refundable: true, refunded: fullyRefunded }
|
|
3819
|
+
);
|
|
3820
|
+
}
|
|
3821
|
+
return { swept: sweptCount, skipped: skippedCount };
|
|
3822
|
+
}
|
|
3823
|
+
/**
|
|
3824
|
+
* Inspect a submarine swap's lockup address for recoverable funds.
|
|
3825
|
+
*
|
|
3826
|
+
* Side-effect free. Returns a structured snapshot the UI can use to
|
|
3827
|
+
* decide whether to offer the user a recovery action — it will not
|
|
3828
|
+
* trigger any signing or persistence.
|
|
3829
|
+
*
|
|
3830
|
+
* Only `transaction.claimed` (success with possible stranded extras)
|
|
3831
|
+
* and refundable failure statuses are recovery candidates. Pending
|
|
3832
|
+
* statuses (`invoice.set`, `transaction.mempool`, …) are returned as
|
|
3833
|
+
* `invalid_swap`; this API is for recovery, not a generic VTXO probe.
|
|
3834
|
+
*
|
|
3835
|
+
* @param swap - The submarine swap to inspect.
|
|
3836
|
+
*/
|
|
3837
|
+
async inspectSubmarineRecovery(swap) {
|
|
3838
|
+
if (!isSubmarineSuccessStatus(swap.status) && !isSubmarineRefundableStatus(swap.status)) {
|
|
3839
|
+
return {
|
|
3840
|
+
swap,
|
|
3841
|
+
status: "invalid_swap",
|
|
3842
|
+
vtxoCount: 0,
|
|
3843
|
+
amountSats: 0,
|
|
3844
|
+
refundLocktime: swap.response.timeoutBlockHeights?.refund,
|
|
3845
|
+
error: `Swap status ${swap.status} is not a recovery candidate`
|
|
3846
|
+
};
|
|
3847
|
+
}
|
|
3848
|
+
let lookup;
|
|
3849
|
+
try {
|
|
3850
|
+
lookup = await this.lookupSubmarineVHTLC(swap);
|
|
3851
|
+
} catch (err) {
|
|
3852
|
+
return {
|
|
3853
|
+
swap,
|
|
3854
|
+
status: "invalid_swap",
|
|
3855
|
+
vtxoCount: 0,
|
|
3856
|
+
amountSats: 0,
|
|
3857
|
+
refundLocktime: swap.response.timeoutBlockHeights?.refund,
|
|
3858
|
+
error: err instanceof Error ? err.message : String(err)
|
|
3859
|
+
};
|
|
3860
|
+
}
|
|
3861
|
+
return this.submarineRecoveryInfoFromLookup(swap, lookup);
|
|
3862
|
+
}
|
|
3863
|
+
/**
|
|
3864
|
+
* Scan all locally-known submarine swaps for recoverable VHTLC funds.
|
|
3865
|
+
*
|
|
3866
|
+
* Loads submarine swaps from the repository, filters to recovery
|
|
3867
|
+
* candidates (`transaction.claimed` plus refundable failure
|
|
3868
|
+
* statuses), reconstructs their scripts, and performs one batched
|
|
3869
|
+
* spendable query plus one batched recoverable query. Pending swaps are
|
|
3870
|
+
* skipped entirely — they appear in the local repository but cannot
|
|
3871
|
+
* be in a recovery state yet.
|
|
3872
|
+
*
|
|
3873
|
+
* Side-effect free: does not mutate the repository, does not sign,
|
|
3874
|
+
* and does not query Boltz swap status.
|
|
3875
|
+
*/
|
|
3876
|
+
async scanRecoverableSubmarineSwaps() {
|
|
3877
|
+
const submarineSwaps = await this.swapRepository.getAllSwaps({
|
|
3878
|
+
type: "submarine"
|
|
3879
|
+
});
|
|
3880
|
+
const candidates = submarineSwaps.filter(
|
|
3881
|
+
(swap) => isSubmarineSuccessStatus(swap.status) || isSubmarineRefundableStatus(swap.status)
|
|
3882
|
+
);
|
|
3883
|
+
let arkInfo;
|
|
3884
|
+
let arkInfoError;
|
|
3885
|
+
if (candidates.length > 0) {
|
|
3886
|
+
try {
|
|
3887
|
+
arkInfo = await this.arkProvider.getInfo();
|
|
3888
|
+
} catch (err) {
|
|
3889
|
+
arkInfoError = err instanceof Error ? err.message : String(err);
|
|
3890
|
+
}
|
|
3891
|
+
}
|
|
3892
|
+
const prepared = await Promise.all(
|
|
3893
|
+
candidates.map(async (swap) => {
|
|
3894
|
+
if (arkInfoError) {
|
|
3895
|
+
return {
|
|
3896
|
+
swap,
|
|
3897
|
+
error: arkInfoError
|
|
3898
|
+
};
|
|
3899
|
+
}
|
|
3900
|
+
try {
|
|
3901
|
+
return {
|
|
3902
|
+
swap,
|
|
3903
|
+
context: await this.buildSubmarineVHTLCContext(
|
|
3904
|
+
swap,
|
|
3905
|
+
arkInfo
|
|
3906
|
+
)
|
|
3907
|
+
};
|
|
3908
|
+
} catch (err) {
|
|
3909
|
+
return {
|
|
3910
|
+
swap,
|
|
3911
|
+
error: err instanceof Error ? err.message : String(err)
|
|
3912
|
+
};
|
|
3913
|
+
}
|
|
3914
|
+
})
|
|
3915
|
+
);
|
|
3916
|
+
const valid = prepared.filter(
|
|
3917
|
+
(item) => "context" in item
|
|
3696
3918
|
);
|
|
3919
|
+
const scripts = [
|
|
3920
|
+
...new Set(valid.map(({ context }) => context.vhtlcPkScriptHex))
|
|
3921
|
+
];
|
|
3922
|
+
const refundableByScript = /* @__PURE__ */ new Map();
|
|
3923
|
+
if (scripts.length > 0) {
|
|
3924
|
+
const [spendableResult, recoverableResult] = await Promise.all([
|
|
3925
|
+
this.indexerProvider.getVtxos({
|
|
3926
|
+
scripts,
|
|
3927
|
+
spendableOnly: true
|
|
3928
|
+
}),
|
|
3929
|
+
this.indexerProvider.getVtxos({
|
|
3930
|
+
scripts,
|
|
3931
|
+
recoverableOnly: true
|
|
3932
|
+
})
|
|
3933
|
+
]);
|
|
3934
|
+
for (const vtxo of dedupeVtxos([
|
|
3935
|
+
...spendableResult.vtxos,
|
|
3936
|
+
...recoverableResult.vtxos
|
|
3937
|
+
])) {
|
|
3938
|
+
const script = vtxo.script?.toLowerCase();
|
|
3939
|
+
if (!script) continue;
|
|
3940
|
+
const existing = refundableByScript.get(script) ?? [];
|
|
3941
|
+
existing.push(vtxo);
|
|
3942
|
+
refundableByScript.set(script, existing);
|
|
3943
|
+
}
|
|
3944
|
+
}
|
|
3945
|
+
return prepared.map((item) => {
|
|
3946
|
+
if ("error" in item) {
|
|
3947
|
+
return {
|
|
3948
|
+
swap: item.swap,
|
|
3949
|
+
status: "invalid_swap",
|
|
3950
|
+
vtxoCount: 0,
|
|
3951
|
+
amountSats: 0,
|
|
3952
|
+
refundLocktime: item.swap.response.timeoutBlockHeights?.refund,
|
|
3953
|
+
error: item.error
|
|
3954
|
+
};
|
|
3955
|
+
}
|
|
3956
|
+
const refundableVtxos = refundableByScript.get(
|
|
3957
|
+
item.context.vhtlcPkScriptHex.toLowerCase()
|
|
3958
|
+
) ?? [];
|
|
3959
|
+
return this.submarineRecoveryInfoFromLookup(item.swap, {
|
|
3960
|
+
...item.context,
|
|
3961
|
+
refundableVtxos
|
|
3962
|
+
});
|
|
3963
|
+
});
|
|
3964
|
+
}
|
|
3965
|
+
/**
|
|
3966
|
+
* Recover funds locked at a single submarine swap's VHTLC address.
|
|
3967
|
+
*
|
|
3968
|
+
* Thin wrapper around `refundVHTLC` for callers that have already
|
|
3969
|
+
* confirmed (e.g. via `inspectSubmarineRecovery`) that funds are
|
|
3970
|
+
* present. Centralises the spending logic in one place — flag-write
|
|
3971
|
+
* behavior matches `refundVHTLC` (no-op for `transaction.claimed`,
|
|
3972
|
+
* normal flag updates for failure statuses).
|
|
3973
|
+
*/
|
|
3974
|
+
async recoverSubmarineFunds(swap, arkInfo) {
|
|
3975
|
+
return this.refundVHTLC(swap, arkInfo);
|
|
3976
|
+
}
|
|
3977
|
+
/**
|
|
3978
|
+
* Recover funds for a batch of submarine swaps.
|
|
3979
|
+
*
|
|
3980
|
+
* Each swap's recovery is independent — a failure on one swap does
|
|
3981
|
+
* not abort the rest, and the caller receives a per-swap result so
|
|
3982
|
+
* they can present partial outcomes in the UI. Recovery runs
|
|
3983
|
+
* sequentially to avoid hammering Boltz / the indexer with parallel
|
|
3984
|
+
* batch joins.
|
|
3985
|
+
*/
|
|
3986
|
+
async recoverAllSubmarineFunds(swaps) {
|
|
3987
|
+
const results = [];
|
|
3988
|
+
let arkInfo;
|
|
3989
|
+
try {
|
|
3990
|
+
if (swaps.length > 0) {
|
|
3991
|
+
arkInfo = await this.arkProvider.getInfo();
|
|
3992
|
+
}
|
|
3993
|
+
} catch (err) {
|
|
3994
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
3995
|
+
return swaps.map((swap) => ({
|
|
3996
|
+
swapId: swap.id,
|
|
3997
|
+
recovered: false,
|
|
3998
|
+
skipped: false,
|
|
3999
|
+
error
|
|
4000
|
+
}));
|
|
4001
|
+
}
|
|
4002
|
+
for (const swap of swaps) {
|
|
4003
|
+
try {
|
|
4004
|
+
const outcome = await this.recoverSubmarineFunds(swap, arkInfo);
|
|
4005
|
+
results.push({
|
|
4006
|
+
swapId: swap.id,
|
|
4007
|
+
recovered: outcome.swept > 0,
|
|
4008
|
+
skipped: outcome.skipped > 0
|
|
4009
|
+
});
|
|
4010
|
+
} catch (err) {
|
|
4011
|
+
results.push({
|
|
4012
|
+
swapId: swap.id,
|
|
4013
|
+
recovered: false,
|
|
4014
|
+
skipped: false,
|
|
4015
|
+
error: err instanceof Error ? err.message : String(err)
|
|
4016
|
+
});
|
|
4017
|
+
}
|
|
4018
|
+
}
|
|
4019
|
+
return results;
|
|
3697
4020
|
}
|
|
3698
4021
|
/**
|
|
3699
4022
|
* Waits for a submarine swap's Lightning payment to settle.
|
|
@@ -4475,14 +4798,14 @@ var init_arkade_swaps = __esm({
|
|
|
4475
4798
|
const serverPubkey = import_base9.hex.encode(
|
|
4476
4799
|
normalizeToXOnlyKey(arkInfo.signerPubkey, "server")
|
|
4477
4800
|
);
|
|
4478
|
-
const
|
|
4801
|
+
const vhtlcTimeouts = to === "ARK" ? swap.response.claimDetails.timeouts : swap.response.lockupDetails.timeouts;
|
|
4479
4802
|
const { vhtlcAddress } = this.createVHTLCScript({
|
|
4480
4803
|
network: arkInfo.network,
|
|
4481
4804
|
preimageHash: import_base9.hex.decode(swap.request.preimageHash),
|
|
4482
4805
|
receiverPubkey,
|
|
4483
4806
|
senderPubkey,
|
|
4484
4807
|
serverPubkey,
|
|
4485
|
-
timeoutBlockHeights
|
|
4808
|
+
timeoutBlockHeights: vhtlcTimeouts
|
|
4486
4809
|
});
|
|
4487
4810
|
if (lockupAddress !== vhtlcAddress) {
|
|
4488
4811
|
throw new SwapError({
|
|
@@ -5262,6 +5585,18 @@ var ExpoArkadeSwaps = class _ExpoArkadeSwaps {
|
|
|
5262
5585
|
refundVHTLC(pendingSwap) {
|
|
5263
5586
|
return this.inner.refundVHTLC(pendingSwap);
|
|
5264
5587
|
}
|
|
5588
|
+
inspectSubmarineRecovery(swap) {
|
|
5589
|
+
return this.inner.inspectSubmarineRecovery(swap);
|
|
5590
|
+
}
|
|
5591
|
+
scanRecoverableSubmarineSwaps() {
|
|
5592
|
+
return this.inner.scanRecoverableSubmarineSwaps();
|
|
5593
|
+
}
|
|
5594
|
+
recoverSubmarineFunds(swap) {
|
|
5595
|
+
return this.inner.recoverSubmarineFunds(swap);
|
|
5596
|
+
}
|
|
5597
|
+
recoverAllSubmarineFunds(swaps) {
|
|
5598
|
+
return this.inner.recoverAllSubmarineFunds(swaps);
|
|
5599
|
+
}
|
|
5265
5600
|
waitAndClaim(pendingSwap) {
|
|
5266
5601
|
return this.inner.waitAndClaim(pendingSwap);
|
|
5267
5602
|
}
|