@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
|
@@ -2702,6 +2702,10 @@ function createForfeitTx(input, forfeitOutputScript, connector) {
|
|
|
2702
2702
|
import { ripemd160 } from "@noble/hashes/legacy.js";
|
|
2703
2703
|
import { Address, OutScript } from "@scure/btc-signer";
|
|
2704
2704
|
import { tapLeafHash as tapLeafHash3 } from "@scure/btc-signer/payment.js";
|
|
2705
|
+
var toBip68RelativeTimelock = (value) => ({
|
|
2706
|
+
type: value < 512 ? "blocks" : "seconds",
|
|
2707
|
+
value: BigInt(value)
|
|
2708
|
+
});
|
|
2705
2709
|
var createVHTLCScript = (args) => {
|
|
2706
2710
|
const {
|
|
2707
2711
|
network,
|
|
@@ -2709,7 +2713,7 @@ var createVHTLCScript = (args) => {
|
|
|
2709
2713
|
receiverPubkey,
|
|
2710
2714
|
senderPubkey,
|
|
2711
2715
|
serverPubkey,
|
|
2712
|
-
timeoutBlockHeights
|
|
2716
|
+
timeoutBlockHeights: vhtlcTimeouts
|
|
2713
2717
|
} = args;
|
|
2714
2718
|
const receiverXOnlyPublicKey = normalizeToXOnlyKey(
|
|
2715
2719
|
hex7.decode(receiverPubkey),
|
|
@@ -2723,27 +2727,21 @@ var createVHTLCScript = (args) => {
|
|
|
2723
2727
|
hex7.decode(serverPubkey),
|
|
2724
2728
|
"server"
|
|
2725
2729
|
);
|
|
2726
|
-
const delayType = (num) => num < 512 ? "blocks" : "seconds";
|
|
2727
2730
|
const vhtlcScript = new VHTLC.Script({
|
|
2728
2731
|
preimageHash: ripemd160(preimageHash),
|
|
2729
2732
|
sender: senderXOnlyPublicKey,
|
|
2730
2733
|
receiver: receiverXOnlyPublicKey,
|
|
2731
2734
|
server: serverXOnlyPublicKey,
|
|
2732
|
-
refundLocktime: BigInt(
|
|
2733
|
-
unilateralClaimDelay:
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
type: delayType(
|
|
2743
|
-
timeoutBlockHeights.unilateralRefundWithoutReceiver
|
|
2744
|
-
),
|
|
2745
|
-
value: BigInt(timeoutBlockHeights.unilateralRefundWithoutReceiver)
|
|
2746
|
-
}
|
|
2735
|
+
refundLocktime: BigInt(vhtlcTimeouts.refund),
|
|
2736
|
+
unilateralClaimDelay: toBip68RelativeTimelock(
|
|
2737
|
+
vhtlcTimeouts.unilateralClaim
|
|
2738
|
+
),
|
|
2739
|
+
unilateralRefundDelay: toBip68RelativeTimelock(
|
|
2740
|
+
vhtlcTimeouts.unilateralRefund
|
|
2741
|
+
),
|
|
2742
|
+
unilateralRefundWithoutReceiverDelay: toBip68RelativeTimelock(
|
|
2743
|
+
vhtlcTimeouts.unilateralRefundWithoutReceiver
|
|
2744
|
+
)
|
|
2747
2745
|
});
|
|
2748
2746
|
if (!vhtlcScript.claimScript)
|
|
2749
2747
|
throw new Error("Failed to create VHTLC script");
|
|
@@ -2982,6 +2980,20 @@ function scriptFromTapLeafScript(leaf) {
|
|
|
2982
2980
|
}
|
|
2983
2981
|
|
|
2984
2982
|
// src/arkade-swaps.ts
|
|
2983
|
+
var dedupeVtxos = (vtxos) => [
|
|
2984
|
+
...new Map(
|
|
2985
|
+
vtxos.map((vtxo) => [`${vtxo.txid}:${vtxo.vout}`, vtxo])
|
|
2986
|
+
).values()
|
|
2987
|
+
];
|
|
2988
|
+
var hasNonEmptyString = (value) => typeof value === "string" && value.length > 0;
|
|
2989
|
+
var canRecoverViaBoltz3of3 = (refundableVtxos, swap) => {
|
|
2990
|
+
const hasRequiredSwapMetadata = hasNonEmptyString(swap.id) && hasNonEmptyString(swap.request.refundPublicKey) && hasNonEmptyString(swap.response.address) && hasNonEmptyString(swap.response.claimPublicKey) && !!swap.response.timeoutBlockHeights;
|
|
2991
|
+
if (!hasRequiredSwapMetadata) return false;
|
|
2992
|
+
return refundableVtxos.some(
|
|
2993
|
+
(vtxo) => !vtxo.isSpent && !isRecoverable(vtxo)
|
|
2994
|
+
);
|
|
2995
|
+
};
|
|
2996
|
+
var isSubmarineRefundLocktimeReached = (refundTimestamp) => Math.floor(Date.now() / 1e3) >= refundTimestamp;
|
|
2985
2997
|
var CLAIM_VTXO_RETRY_ATTEMPTS = 3;
|
|
2986
2998
|
var CLAIM_VTXO_RETRY_DELAY_MS = 500;
|
|
2987
2999
|
var ArkadeSwaps = class _ArkadeSwaps {
|
|
@@ -3249,8 +3261,12 @@ var ArkadeSwaps = class _ArkadeSwaps {
|
|
|
3249
3261
|
throw new Error(
|
|
3250
3262
|
`Swap ${pendingSwap.id}: preimage is required to claim VHTLC`
|
|
3251
3263
|
);
|
|
3252
|
-
const {
|
|
3253
|
-
|
|
3264
|
+
const {
|
|
3265
|
+
refundPublicKey,
|
|
3266
|
+
lockupAddress,
|
|
3267
|
+
timeoutBlockHeights: vhtlcTimeouts
|
|
3268
|
+
} = pendingSwap.response;
|
|
3269
|
+
if (!refundPublicKey || !lockupAddress || !vhtlcTimeouts)
|
|
3254
3270
|
throw new Error(
|
|
3255
3271
|
`Swap ${pendingSwap.id}: incomplete reverse swap response`
|
|
3256
3272
|
);
|
|
@@ -3278,7 +3294,7 @@ var ArkadeSwaps = class _ArkadeSwaps {
|
|
|
3278
3294
|
receiverPubkey: hex8.encode(receiverXOnly),
|
|
3279
3295
|
senderPubkey: hex8.encode(senderXOnly),
|
|
3280
3296
|
serverPubkey: hex8.encode(serverXOnly),
|
|
3281
|
-
timeoutBlockHeights
|
|
3297
|
+
timeoutBlockHeights: vhtlcTimeouts
|
|
3282
3298
|
});
|
|
3283
3299
|
if (!vhtlcScript.claimScript)
|
|
3284
3300
|
throw new Error(
|
|
@@ -3506,84 +3522,184 @@ var ArkadeSwaps = class _ArkadeSwaps {
|
|
|
3506
3522
|
return pendingSwap;
|
|
3507
3523
|
}
|
|
3508
3524
|
/**
|
|
3509
|
-
*
|
|
3510
|
-
*
|
|
3511
|
-
*
|
|
3512
|
-
*
|
|
3525
|
+
* Reconstruct a submarine swap's VHTLC script from stored data. This does
|
|
3526
|
+
* not query the indexer, so bulk scans can build every script first and
|
|
3527
|
+
* then use batched VTXO lookups.
|
|
3528
|
+
*
|
|
3529
|
+
* @throws {Error} If preimage hash is unavailable, the swap response is
|
|
3530
|
+
* incomplete, the script can't be built, or the reconstructed address
|
|
3531
|
+
* doesn't match the one Boltz returned.
|
|
3513
3532
|
*/
|
|
3514
|
-
async
|
|
3515
|
-
const preimageHash =
|
|
3533
|
+
async buildSubmarineVHTLCContext(swap, arkInfo) {
|
|
3534
|
+
const preimageHash = swap.request.invoice ? getInvoicePaymentHash(swap.request.invoice) : swap.preimageHash;
|
|
3516
3535
|
if (!preimageHash)
|
|
3517
3536
|
throw new Error(
|
|
3518
|
-
`Swap ${
|
|
3537
|
+
`Swap ${swap.id}: preimage hash is required to refund VHTLC`
|
|
3519
3538
|
);
|
|
3520
|
-
const
|
|
3521
|
-
const address = await this.wallet.getAddress();
|
|
3522
|
-
if (!address) throw new Error("Failed to get ark address from wallet");
|
|
3539
|
+
const resolvedArkInfo = arkInfo ?? await this.arkProvider.getInfo();
|
|
3523
3540
|
const ourXOnlyPublicKey = normalizeToXOnlyKey(
|
|
3524
3541
|
await this.wallet.identity.xOnlyPublicKey(),
|
|
3525
3542
|
"our",
|
|
3526
|
-
|
|
3543
|
+
swap.id
|
|
3527
3544
|
);
|
|
3528
3545
|
const serverXOnlyPublicKey = normalizeToXOnlyKey(
|
|
3529
|
-
hex8.decode(
|
|
3546
|
+
hex8.decode(resolvedArkInfo.signerPubkey),
|
|
3530
3547
|
"server",
|
|
3531
|
-
|
|
3548
|
+
swap.id
|
|
3532
3549
|
);
|
|
3533
|
-
const { claimPublicKey, timeoutBlockHeights } =
|
|
3534
|
-
if (!claimPublicKey || !
|
|
3550
|
+
const { claimPublicKey, timeoutBlockHeights: vhtlcTimeouts } = swap.response;
|
|
3551
|
+
if (!claimPublicKey || !vhtlcTimeouts)
|
|
3535
3552
|
throw new Error(
|
|
3536
|
-
`Swap ${
|
|
3553
|
+
`Swap ${swap.id}: incomplete submarine swap response`
|
|
3537
3554
|
);
|
|
3538
3555
|
const boltzXOnlyPublicKey = normalizeToXOnlyKey(
|
|
3539
3556
|
hex8.decode(claimPublicKey),
|
|
3540
3557
|
"boltz",
|
|
3541
|
-
|
|
3558
|
+
swap.id
|
|
3542
3559
|
);
|
|
3543
3560
|
const { vhtlcScript, vhtlcAddress } = this.createVHTLCScript({
|
|
3544
|
-
network:
|
|
3561
|
+
network: resolvedArkInfo.network,
|
|
3545
3562
|
preimageHash: hex8.decode(preimageHash),
|
|
3546
3563
|
receiverPubkey: hex8.encode(boltzXOnlyPublicKey),
|
|
3547
3564
|
senderPubkey: hex8.encode(ourXOnlyPublicKey),
|
|
3548
3565
|
serverPubkey: hex8.encode(serverXOnlyPublicKey),
|
|
3549
|
-
timeoutBlockHeights
|
|
3566
|
+
timeoutBlockHeights: vhtlcTimeouts
|
|
3550
3567
|
});
|
|
3551
3568
|
if (!vhtlcScript.claimScript)
|
|
3552
3569
|
throw new Error(
|
|
3553
|
-
`Swap ${
|
|
3570
|
+
`Swap ${swap.id}: failed to create VHTLC script for submarine swap`
|
|
3554
3571
|
);
|
|
3555
|
-
if (vhtlcAddress !==
|
|
3572
|
+
if (vhtlcAddress !== swap.response.address)
|
|
3556
3573
|
throw new Error(
|
|
3557
|
-
`VHTLC address mismatch for swap ${
|
|
3574
|
+
`VHTLC address mismatch for swap ${swap.id}: expected ${swap.response.address}, got ${vhtlcAddress}`
|
|
3558
3575
|
);
|
|
3559
3576
|
const vhtlcPkScriptHex = hex8.encode(vhtlcScript.pkScript);
|
|
3577
|
+
return {
|
|
3578
|
+
arkInfo: resolvedArkInfo,
|
|
3579
|
+
vhtlcScript,
|
|
3580
|
+
vhtlcAddress,
|
|
3581
|
+
vhtlcPkScriptHex,
|
|
3582
|
+
vhtlcTimeouts,
|
|
3583
|
+
ourXOnlyPublicKey,
|
|
3584
|
+
serverXOnlyPublicKey,
|
|
3585
|
+
boltzXOnlyPublicKey
|
|
3586
|
+
};
|
|
3587
|
+
}
|
|
3588
|
+
/**
|
|
3589
|
+
* Reconstruct a submarine swap's VHTLC script from stored data and look
|
|
3590
|
+
* up its VTXOs at the indexer. Side-effect free; shared by `refundVHTLC`
|
|
3591
|
+
* (spending path) and `inspectSubmarineRecovery` (diagnostic path).
|
|
3592
|
+
*
|
|
3593
|
+
* `refundableVtxos` merges spendable + recoverable indexer queries
|
|
3594
|
+
* (deduped by outpoint). When that set is empty, a third query
|
|
3595
|
+
* populates `diagnostic` so callers can distinguish "never funded",
|
|
3596
|
+
* "already spent", and "preconfirmed-only".
|
|
3597
|
+
*/
|
|
3598
|
+
async lookupSubmarineVHTLC(swap, arkInfo) {
|
|
3599
|
+
const context = await this.buildSubmarineVHTLCContext(swap, arkInfo);
|
|
3560
3600
|
const [spendableResult, recoverableResult] = await Promise.all([
|
|
3561
3601
|
this.indexerProvider.getVtxos({
|
|
3562
|
-
scripts: [vhtlcPkScriptHex],
|
|
3602
|
+
scripts: [context.vhtlcPkScriptHex],
|
|
3563
3603
|
spendableOnly: true
|
|
3564
3604
|
}),
|
|
3565
3605
|
this.indexerProvider.getVtxos({
|
|
3566
|
-
scripts: [vhtlcPkScriptHex],
|
|
3606
|
+
scripts: [context.vhtlcPkScriptHex],
|
|
3567
3607
|
recoverableOnly: true
|
|
3568
3608
|
})
|
|
3569
3609
|
]);
|
|
3570
|
-
const refundableVtxos = [
|
|
3571
|
-
...
|
|
3572
|
-
|
|
3573
|
-
|
|
3574
|
-
|
|
3575
|
-
).values()
|
|
3576
|
-
];
|
|
3610
|
+
const refundableVtxos = dedupeVtxos([
|
|
3611
|
+
...spendableResult.vtxos,
|
|
3612
|
+
...recoverableResult.vtxos
|
|
3613
|
+
]);
|
|
3614
|
+
let diagnostic;
|
|
3577
3615
|
if (refundableVtxos.length === 0) {
|
|
3578
3616
|
const { vtxos: allVtxos } = await this.indexerProvider.getVtxos({
|
|
3579
|
-
scripts: [vhtlcPkScriptHex]
|
|
3617
|
+
scripts: [context.vhtlcPkScriptHex]
|
|
3580
3618
|
});
|
|
3581
|
-
|
|
3619
|
+
diagnostic = {
|
|
3620
|
+
totalVtxoCount: allVtxos.length,
|
|
3621
|
+
allSpent: allVtxos.length > 0 && allVtxos.every((vtxo) => vtxo.isSpent)
|
|
3622
|
+
};
|
|
3623
|
+
}
|
|
3624
|
+
return {
|
|
3625
|
+
...context,
|
|
3626
|
+
refundableVtxos,
|
|
3627
|
+
diagnostic
|
|
3628
|
+
};
|
|
3629
|
+
}
|
|
3630
|
+
submarineRecoveryInfoFromLookup(swap, lookup) {
|
|
3631
|
+
const { refundableVtxos, diagnostic, vhtlcTimeouts } = lookup;
|
|
3632
|
+
if (refundableVtxos.length > 0) {
|
|
3633
|
+
const cltvSatisfied = isSubmarineRefundLocktimeReached(
|
|
3634
|
+
vhtlcTimeouts.refund
|
|
3635
|
+
);
|
|
3636
|
+
const amountSats = refundableVtxos.reduce(
|
|
3637
|
+
(sum, vtxo) => sum + Number(vtxo.value),
|
|
3638
|
+
0
|
|
3639
|
+
);
|
|
3640
|
+
const isRecoverable2 = cltvSatisfied || canRecoverViaBoltz3of3(refundableVtxos, swap);
|
|
3641
|
+
return {
|
|
3642
|
+
swap,
|
|
3643
|
+
status: isRecoverable2 ? "recoverable" : "pre_cltv",
|
|
3644
|
+
vtxoCount: refundableVtxos.length,
|
|
3645
|
+
amountSats,
|
|
3646
|
+
refundLocktime: vhtlcTimeouts.refund
|
|
3647
|
+
};
|
|
3648
|
+
}
|
|
3649
|
+
if (!diagnostic || diagnostic.totalVtxoCount === 0) {
|
|
3650
|
+
return {
|
|
3651
|
+
swap,
|
|
3652
|
+
status: "none",
|
|
3653
|
+
vtxoCount: 0,
|
|
3654
|
+
amountSats: 0,
|
|
3655
|
+
refundLocktime: vhtlcTimeouts.refund
|
|
3656
|
+
};
|
|
3657
|
+
}
|
|
3658
|
+
if (diagnostic.allSpent) {
|
|
3659
|
+
return {
|
|
3660
|
+
swap,
|
|
3661
|
+
status: "already_spent",
|
|
3662
|
+
vtxoCount: 0,
|
|
3663
|
+
amountSats: 0,
|
|
3664
|
+
refundLocktime: vhtlcTimeouts.refund
|
|
3665
|
+
};
|
|
3666
|
+
}
|
|
3667
|
+
return {
|
|
3668
|
+
swap,
|
|
3669
|
+
status: "none",
|
|
3670
|
+
vtxoCount: 0,
|
|
3671
|
+
amountSats: 0,
|
|
3672
|
+
refundLocktime: vhtlcTimeouts.refund
|
|
3673
|
+
};
|
|
3674
|
+
}
|
|
3675
|
+
/**
|
|
3676
|
+
* Refunds the VHTLC for a failed submarine swap, returning locked funds to the wallet.
|
|
3677
|
+
* Uses multi-party signatures (user + Boltz + server) for non-recoverable VTXOs.
|
|
3678
|
+
* @param pendingSwap - The submarine swap to refund.
|
|
3679
|
+
* @returns Counts of VTXOs swept vs. deferred. A return value of `{ swept: 0, skipped: N }`
|
|
3680
|
+
* means the call was a no-op — callers should not treat it as a successful refund.
|
|
3681
|
+
* @throws {Error} If preimage hash is unavailable, VHTLC not found, or already spent.
|
|
3682
|
+
*/
|
|
3683
|
+
async refundVHTLC(pendingSwap, cachedArkInfo) {
|
|
3684
|
+
const address = await this.wallet.getAddress();
|
|
3685
|
+
if (!address) throw new Error("Failed to get ark address from wallet");
|
|
3686
|
+
const {
|
|
3687
|
+
arkInfo,
|
|
3688
|
+
vhtlcScript,
|
|
3689
|
+
vhtlcTimeouts,
|
|
3690
|
+
ourXOnlyPublicKey,
|
|
3691
|
+
serverXOnlyPublicKey,
|
|
3692
|
+
boltzXOnlyPublicKey,
|
|
3693
|
+
refundableVtxos,
|
|
3694
|
+
diagnostic
|
|
3695
|
+
} = await this.lookupSubmarineVHTLC(pendingSwap, cachedArkInfo);
|
|
3696
|
+
if (refundableVtxos.length === 0) {
|
|
3697
|
+
if (!diagnostic || diagnostic.totalVtxoCount === 0) {
|
|
3582
3698
|
throw new Error(
|
|
3583
3699
|
`Swap ${pendingSwap.id}: VHTLC not found for address ${pendingSwap.response.address}`
|
|
3584
3700
|
);
|
|
3585
3701
|
}
|
|
3586
|
-
if (
|
|
3702
|
+
if (diagnostic.allSpent) {
|
|
3587
3703
|
throw new Error(
|
|
3588
3704
|
`Swap ${pendingSwap.id}: VHTLC is already spent`
|
|
3589
3705
|
);
|
|
@@ -3594,10 +3710,11 @@ var ArkadeSwaps = class _ArkadeSwaps {
|
|
|
3594
3710
|
}
|
|
3595
3711
|
const outputScript = ArkAddress2.decode(address).pkScript;
|
|
3596
3712
|
const refundWithoutReceiverLeaf = vhtlcScript.refundWithoutReceiver();
|
|
3597
|
-
const
|
|
3598
|
-
|
|
3599
|
-
|
|
3713
|
+
const cltvSatisfied = isSubmarineRefundLocktimeReached(
|
|
3714
|
+
vhtlcTimeouts.refund
|
|
3715
|
+
);
|
|
3600
3716
|
let boltzCallCount = 0;
|
|
3717
|
+
let sweptCount = 0;
|
|
3601
3718
|
let skippedCount = 0;
|
|
3602
3719
|
for (const vtxo of refundableVtxos) {
|
|
3603
3720
|
const isRecoverableVtxo = isRecoverable(vtxo);
|
|
@@ -3618,11 +3735,12 @@ var ArkadeSwaps = class _ArkadeSwaps {
|
|
|
3618
3735
|
arkInfo,
|
|
3619
3736
|
isRecoverableVtxo
|
|
3620
3737
|
);
|
|
3738
|
+
sweptCount++;
|
|
3621
3739
|
continue;
|
|
3622
3740
|
}
|
|
3623
3741
|
if (isRecoverableVtxo) {
|
|
3624
3742
|
logger.error(
|
|
3625
|
-
`Swap ${pendingSwap.id}: recoverable VTXO ${vtxo.txid}:${vtxo.vout} cannot be refunded yet \u2014 refundWithoutReceiver locktime has not passed (refundLocktime=${
|
|
3743
|
+
`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.`
|
|
3626
3744
|
);
|
|
3627
3745
|
skippedCount++;
|
|
3628
3746
|
continue;
|
|
@@ -3651,14 +3769,14 @@ var ArkadeSwaps = class _ArkadeSwaps {
|
|
|
3651
3769
|
)
|
|
3652
3770
|
);
|
|
3653
3771
|
boltzCallCount++;
|
|
3772
|
+
sweptCount++;
|
|
3654
3773
|
} catch (error) {
|
|
3655
3774
|
if (!(error instanceof BoltzRefundError)) {
|
|
3656
3775
|
throw error;
|
|
3657
3776
|
}
|
|
3658
|
-
|
|
3659
|
-
if (BigInt(tipNow) < refundLocktime) {
|
|
3777
|
+
if (!isSubmarineRefundLocktimeReached(vhtlcTimeouts.refund)) {
|
|
3660
3778
|
logger.error(
|
|
3661
|
-
`Swap ${pendingSwap.id}: Boltz rejected VTXO outpoint and refundWithoutReceiver locktime has not passed yet (
|
|
3779
|
+
`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.`
|
|
3662
3780
|
);
|
|
3663
3781
|
skippedCount++;
|
|
3664
3782
|
continue;
|
|
@@ -3678,16 +3796,218 @@ var ArkadeSwaps = class _ArkadeSwaps {
|
|
|
3678
3796
|
arkInfo,
|
|
3679
3797
|
false
|
|
3680
3798
|
);
|
|
3799
|
+
sweptCount++;
|
|
3681
3800
|
}
|
|
3682
3801
|
}
|
|
3683
|
-
|
|
3684
|
-
|
|
3685
|
-
|
|
3686
|
-
|
|
3687
|
-
|
|
3688
|
-
|
|
3689
|
-
|
|
3802
|
+
if (!isSubmarineSuccessStatus(pendingSwap.status)) {
|
|
3803
|
+
const fullyRefunded = skippedCount === 0;
|
|
3804
|
+
await updateSubmarineSwapStatus(
|
|
3805
|
+
pendingSwap,
|
|
3806
|
+
pendingSwap.status,
|
|
3807
|
+
// Keep current status
|
|
3808
|
+
this.savePendingSubmarineSwap.bind(this),
|
|
3809
|
+
{ refundable: true, refunded: fullyRefunded }
|
|
3810
|
+
);
|
|
3811
|
+
}
|
|
3812
|
+
return { swept: sweptCount, skipped: skippedCount };
|
|
3813
|
+
}
|
|
3814
|
+
/**
|
|
3815
|
+
* Inspect a submarine swap's lockup address for recoverable funds.
|
|
3816
|
+
*
|
|
3817
|
+
* Side-effect free. Returns a structured snapshot the UI can use to
|
|
3818
|
+
* decide whether to offer the user a recovery action — it will not
|
|
3819
|
+
* trigger any signing or persistence.
|
|
3820
|
+
*
|
|
3821
|
+
* Only `transaction.claimed` (success with possible stranded extras)
|
|
3822
|
+
* and refundable failure statuses are recovery candidates. Pending
|
|
3823
|
+
* statuses (`invoice.set`, `transaction.mempool`, …) are returned as
|
|
3824
|
+
* `invalid_swap`; this API is for recovery, not a generic VTXO probe.
|
|
3825
|
+
*
|
|
3826
|
+
* @param swap - The submarine swap to inspect.
|
|
3827
|
+
*/
|
|
3828
|
+
async inspectSubmarineRecovery(swap) {
|
|
3829
|
+
if (!isSubmarineSuccessStatus(swap.status) && !isSubmarineRefundableStatus(swap.status)) {
|
|
3830
|
+
return {
|
|
3831
|
+
swap,
|
|
3832
|
+
status: "invalid_swap",
|
|
3833
|
+
vtxoCount: 0,
|
|
3834
|
+
amountSats: 0,
|
|
3835
|
+
refundLocktime: swap.response.timeoutBlockHeights?.refund,
|
|
3836
|
+
error: `Swap status ${swap.status} is not a recovery candidate`
|
|
3837
|
+
};
|
|
3838
|
+
}
|
|
3839
|
+
let lookup;
|
|
3840
|
+
try {
|
|
3841
|
+
lookup = await this.lookupSubmarineVHTLC(swap);
|
|
3842
|
+
} catch (err) {
|
|
3843
|
+
return {
|
|
3844
|
+
swap,
|
|
3845
|
+
status: "invalid_swap",
|
|
3846
|
+
vtxoCount: 0,
|
|
3847
|
+
amountSats: 0,
|
|
3848
|
+
refundLocktime: swap.response.timeoutBlockHeights?.refund,
|
|
3849
|
+
error: err instanceof Error ? err.message : String(err)
|
|
3850
|
+
};
|
|
3851
|
+
}
|
|
3852
|
+
return this.submarineRecoveryInfoFromLookup(swap, lookup);
|
|
3853
|
+
}
|
|
3854
|
+
/**
|
|
3855
|
+
* Scan all locally-known submarine swaps for recoverable VHTLC funds.
|
|
3856
|
+
*
|
|
3857
|
+
* Loads submarine swaps from the repository, filters to recovery
|
|
3858
|
+
* candidates (`transaction.claimed` plus refundable failure
|
|
3859
|
+
* statuses), reconstructs their scripts, and performs one batched
|
|
3860
|
+
* spendable query plus one batched recoverable query. Pending swaps are
|
|
3861
|
+
* skipped entirely — they appear in the local repository but cannot
|
|
3862
|
+
* be in a recovery state yet.
|
|
3863
|
+
*
|
|
3864
|
+
* Side-effect free: does not mutate the repository, does not sign,
|
|
3865
|
+
* and does not query Boltz swap status.
|
|
3866
|
+
*/
|
|
3867
|
+
async scanRecoverableSubmarineSwaps() {
|
|
3868
|
+
const submarineSwaps = await this.swapRepository.getAllSwaps({
|
|
3869
|
+
type: "submarine"
|
|
3870
|
+
});
|
|
3871
|
+
const candidates = submarineSwaps.filter(
|
|
3872
|
+
(swap) => isSubmarineSuccessStatus(swap.status) || isSubmarineRefundableStatus(swap.status)
|
|
3690
3873
|
);
|
|
3874
|
+
let arkInfo;
|
|
3875
|
+
let arkInfoError;
|
|
3876
|
+
if (candidates.length > 0) {
|
|
3877
|
+
try {
|
|
3878
|
+
arkInfo = await this.arkProvider.getInfo();
|
|
3879
|
+
} catch (err) {
|
|
3880
|
+
arkInfoError = err instanceof Error ? err.message : String(err);
|
|
3881
|
+
}
|
|
3882
|
+
}
|
|
3883
|
+
const prepared = await Promise.all(
|
|
3884
|
+
candidates.map(async (swap) => {
|
|
3885
|
+
if (arkInfoError) {
|
|
3886
|
+
return {
|
|
3887
|
+
swap,
|
|
3888
|
+
error: arkInfoError
|
|
3889
|
+
};
|
|
3890
|
+
}
|
|
3891
|
+
try {
|
|
3892
|
+
return {
|
|
3893
|
+
swap,
|
|
3894
|
+
context: await this.buildSubmarineVHTLCContext(
|
|
3895
|
+
swap,
|
|
3896
|
+
arkInfo
|
|
3897
|
+
)
|
|
3898
|
+
};
|
|
3899
|
+
} catch (err) {
|
|
3900
|
+
return {
|
|
3901
|
+
swap,
|
|
3902
|
+
error: err instanceof Error ? err.message : String(err)
|
|
3903
|
+
};
|
|
3904
|
+
}
|
|
3905
|
+
})
|
|
3906
|
+
);
|
|
3907
|
+
const valid = prepared.filter(
|
|
3908
|
+
(item) => "context" in item
|
|
3909
|
+
);
|
|
3910
|
+
const scripts = [
|
|
3911
|
+
...new Set(valid.map(({ context }) => context.vhtlcPkScriptHex))
|
|
3912
|
+
];
|
|
3913
|
+
const refundableByScript = /* @__PURE__ */ new Map();
|
|
3914
|
+
if (scripts.length > 0) {
|
|
3915
|
+
const [spendableResult, recoverableResult] = await Promise.all([
|
|
3916
|
+
this.indexerProvider.getVtxos({
|
|
3917
|
+
scripts,
|
|
3918
|
+
spendableOnly: true
|
|
3919
|
+
}),
|
|
3920
|
+
this.indexerProvider.getVtxos({
|
|
3921
|
+
scripts,
|
|
3922
|
+
recoverableOnly: true
|
|
3923
|
+
})
|
|
3924
|
+
]);
|
|
3925
|
+
for (const vtxo of dedupeVtxos([
|
|
3926
|
+
...spendableResult.vtxos,
|
|
3927
|
+
...recoverableResult.vtxos
|
|
3928
|
+
])) {
|
|
3929
|
+
const script = vtxo.script?.toLowerCase();
|
|
3930
|
+
if (!script) continue;
|
|
3931
|
+
const existing = refundableByScript.get(script) ?? [];
|
|
3932
|
+
existing.push(vtxo);
|
|
3933
|
+
refundableByScript.set(script, existing);
|
|
3934
|
+
}
|
|
3935
|
+
}
|
|
3936
|
+
return prepared.map((item) => {
|
|
3937
|
+
if ("error" in item) {
|
|
3938
|
+
return {
|
|
3939
|
+
swap: item.swap,
|
|
3940
|
+
status: "invalid_swap",
|
|
3941
|
+
vtxoCount: 0,
|
|
3942
|
+
amountSats: 0,
|
|
3943
|
+
refundLocktime: item.swap.response.timeoutBlockHeights?.refund,
|
|
3944
|
+
error: item.error
|
|
3945
|
+
};
|
|
3946
|
+
}
|
|
3947
|
+
const refundableVtxos = refundableByScript.get(
|
|
3948
|
+
item.context.vhtlcPkScriptHex.toLowerCase()
|
|
3949
|
+
) ?? [];
|
|
3950
|
+
return this.submarineRecoveryInfoFromLookup(item.swap, {
|
|
3951
|
+
...item.context,
|
|
3952
|
+
refundableVtxos
|
|
3953
|
+
});
|
|
3954
|
+
});
|
|
3955
|
+
}
|
|
3956
|
+
/**
|
|
3957
|
+
* Recover funds locked at a single submarine swap's VHTLC address.
|
|
3958
|
+
*
|
|
3959
|
+
* Thin wrapper around `refundVHTLC` for callers that have already
|
|
3960
|
+
* confirmed (e.g. via `inspectSubmarineRecovery`) that funds are
|
|
3961
|
+
* present. Centralises the spending logic in one place — flag-write
|
|
3962
|
+
* behavior matches `refundVHTLC` (no-op for `transaction.claimed`,
|
|
3963
|
+
* normal flag updates for failure statuses).
|
|
3964
|
+
*/
|
|
3965
|
+
async recoverSubmarineFunds(swap, arkInfo) {
|
|
3966
|
+
return this.refundVHTLC(swap, arkInfo);
|
|
3967
|
+
}
|
|
3968
|
+
/**
|
|
3969
|
+
* Recover funds for a batch of submarine swaps.
|
|
3970
|
+
*
|
|
3971
|
+
* Each swap's recovery is independent — a failure on one swap does
|
|
3972
|
+
* not abort the rest, and the caller receives a per-swap result so
|
|
3973
|
+
* they can present partial outcomes in the UI. Recovery runs
|
|
3974
|
+
* sequentially to avoid hammering Boltz / the indexer with parallel
|
|
3975
|
+
* batch joins.
|
|
3976
|
+
*/
|
|
3977
|
+
async recoverAllSubmarineFunds(swaps) {
|
|
3978
|
+
const results = [];
|
|
3979
|
+
let arkInfo;
|
|
3980
|
+
try {
|
|
3981
|
+
if (swaps.length > 0) {
|
|
3982
|
+
arkInfo = await this.arkProvider.getInfo();
|
|
3983
|
+
}
|
|
3984
|
+
} catch (err) {
|
|
3985
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
3986
|
+
return swaps.map((swap) => ({
|
|
3987
|
+
swapId: swap.id,
|
|
3988
|
+
recovered: false,
|
|
3989
|
+
skipped: false,
|
|
3990
|
+
error
|
|
3991
|
+
}));
|
|
3992
|
+
}
|
|
3993
|
+
for (const swap of swaps) {
|
|
3994
|
+
try {
|
|
3995
|
+
const outcome = await this.recoverSubmarineFunds(swap, arkInfo);
|
|
3996
|
+
results.push({
|
|
3997
|
+
swapId: swap.id,
|
|
3998
|
+
recovered: outcome.swept > 0,
|
|
3999
|
+
skipped: outcome.skipped > 0
|
|
4000
|
+
});
|
|
4001
|
+
} catch (err) {
|
|
4002
|
+
results.push({
|
|
4003
|
+
swapId: swap.id,
|
|
4004
|
+
recovered: false,
|
|
4005
|
+
skipped: false,
|
|
4006
|
+
error: err instanceof Error ? err.message : String(err)
|
|
4007
|
+
});
|
|
4008
|
+
}
|
|
4009
|
+
}
|
|
4010
|
+
return results;
|
|
3691
4011
|
}
|
|
3692
4012
|
/**
|
|
3693
4013
|
* Waits for a submarine swap's Lightning payment to settle.
|
|
@@ -4469,14 +4789,14 @@ var ArkadeSwaps = class _ArkadeSwaps {
|
|
|
4469
4789
|
const serverPubkey = hex8.encode(
|
|
4470
4790
|
normalizeToXOnlyKey(arkInfo.signerPubkey, "server")
|
|
4471
4791
|
);
|
|
4472
|
-
const
|
|
4792
|
+
const vhtlcTimeouts = to === "ARK" ? swap.response.claimDetails.timeouts : swap.response.lockupDetails.timeouts;
|
|
4473
4793
|
const { vhtlcAddress } = this.createVHTLCScript({
|
|
4474
4794
|
network: arkInfo.network,
|
|
4475
4795
|
preimageHash: hex8.decode(swap.request.preimageHash),
|
|
4476
4796
|
receiverPubkey,
|
|
4477
4797
|
senderPubkey,
|
|
4478
4798
|
serverPubkey,
|
|
4479
|
-
timeoutBlockHeights
|
|
4799
|
+
timeoutBlockHeights: vhtlcTimeouts
|
|
4480
4800
|
});
|
|
4481
4801
|
if (lockupAddress !== vhtlcAddress) {
|
|
4482
4802
|
throw new SwapError({
|