@atomiqlabs/lp-lib 16.1.1 → 16.2.0
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/swaps/spv_vault_swap/SpvVaultSwapHandler.js +2 -2
- package/dist/utils/Utils.d.ts +3 -0
- package/dist/utils/Utils.js +41 -1
- package/dist/wallets/IBitcoinWallet.d.ts +107 -32
- package/dist/wallets/IBitcoinWallet.js +95 -0
- package/dist/wallets/ILightningWallet.d.ts +19 -0
- package/package.json +1 -1
- package/src/swaps/spv_vault_swap/SpvVaultSwapHandler.ts +9 -3
- package/src/utils/Utils.ts +46 -0
- package/src/wallets/IBitcoinWallet.ts +194 -33
|
@@ -420,7 +420,7 @@ class SpvVaultSwapHandler extends SwapHandler_1.SwapHandler {
|
|
|
420
420
|
const { spvVaultContract } = this.getChain(swap.chainIdentifier);
|
|
421
421
|
let data;
|
|
422
422
|
try {
|
|
423
|
-
data = await spvVaultContract.getWithdrawalData(
|
|
423
|
+
data = await spvVaultContract.getWithdrawalData((0, Utils_1.parsePsbt)(transaction));
|
|
424
424
|
}
|
|
425
425
|
catch (e) {
|
|
426
426
|
this.swapLogger.error(swap, "REST: /postQuote: failed to parse PSBT to withdrawal tx data: ", e);
|
|
@@ -463,7 +463,7 @@ class SpvVaultSwapHandler extends SwapHandler_1.SwapHandler {
|
|
|
463
463
|
code: 20513,
|
|
464
464
|
msg: "One or more PSBT inputs not finalized!"
|
|
465
465
|
};
|
|
466
|
-
const effectiveFeeRate = await this.bitcoinRpc.getEffectiveFeeRate(
|
|
466
|
+
const effectiveFeeRate = await this.bitcoinRpc.getEffectiveFeeRate((0, Utils_1.parsePsbt)(signedTx));
|
|
467
467
|
if (effectiveFeeRate.feeRate < 1 || Math.round(effectiveFeeRate.feeRate) < swap.btcFeeRate)
|
|
468
468
|
throw {
|
|
469
469
|
code: 20511,
|
package/dist/utils/Utils.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { Request, Response } from "express";
|
|
2
2
|
import { ServerParamEncoder } from "./paramcoders/server/ServerParamEncoder";
|
|
3
|
+
import { Transaction } from "@scure/btc-signer";
|
|
4
|
+
import { BtcTx } from "@atomiqlabs/base";
|
|
3
5
|
export type LoggerType = {
|
|
4
6
|
debug: (msg: string, ...args: any[]) => void;
|
|
5
7
|
info: (msg: string, ...args: any[]) => void;
|
|
@@ -27,3 +29,4 @@ export declare function bigIntSorter(a: bigint, b: bigint): -1 | 0 | 1;
|
|
|
27
29
|
* @param responseStream
|
|
28
30
|
*/
|
|
29
31
|
export declare function getAbortController(responseStream: ServerParamEncoder): AbortController;
|
|
32
|
+
export declare function parsePsbt(btcTx: Transaction): BtcTx;
|
package/dist/utils/Utils.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.getAbortController = exports.bigIntSorter = exports.deserializeBN = exports.serializeBN = exports.HEX_REGEX = exports.expressHandlerWrapper = exports.isDefinedRuntimeError = exports.getLogger = void 0;
|
|
3
|
+
exports.parsePsbt = exports.getAbortController = exports.bigIntSorter = exports.deserializeBN = exports.serializeBN = exports.HEX_REGEX = exports.expressHandlerWrapper = exports.isDefinedRuntimeError = exports.getLogger = void 0;
|
|
4
|
+
const crypto_1 = require("crypto");
|
|
5
|
+
const btc_signer_1 = require("@scure/btc-signer");
|
|
4
6
|
function getLogger(prefix) {
|
|
5
7
|
return {
|
|
6
8
|
debug: (msg, ...args) => global.atomiqLogLevel >= 3 && console.debug((typeof (prefix) === "function" ? prefix() : prefix) + msg, ...args),
|
|
@@ -87,3 +89,41 @@ function getAbortController(responseStream) {
|
|
|
87
89
|
return abortController;
|
|
88
90
|
}
|
|
89
91
|
exports.getAbortController = getAbortController;
|
|
92
|
+
function parsePsbt(btcTx) {
|
|
93
|
+
const txWithoutWitness = btcTx.toBytes(true, false);
|
|
94
|
+
return {
|
|
95
|
+
locktime: btcTx.lockTime,
|
|
96
|
+
version: btcTx.version,
|
|
97
|
+
blockhash: null,
|
|
98
|
+
confirmations: 0,
|
|
99
|
+
txid: (0, crypto_1.createHash)("sha256").update((0, crypto_1.createHash)("sha256").update(txWithoutWitness).digest()).digest().reverse().toString("hex"),
|
|
100
|
+
hex: Buffer.from(txWithoutWitness).toString("hex"),
|
|
101
|
+
raw: Buffer.from(btcTx.toBytes(true, true)).toString("hex"),
|
|
102
|
+
vsize: btcTx.isFinal ? btcTx.vsize : null,
|
|
103
|
+
outs: Array.from({ length: btcTx.outputsLength }, (_, i) => i).map((index) => {
|
|
104
|
+
const output = btcTx.getOutput(index);
|
|
105
|
+
return {
|
|
106
|
+
value: Number(output.amount),
|
|
107
|
+
n: index,
|
|
108
|
+
scriptPubKey: {
|
|
109
|
+
asm: btc_signer_1.Script.decode(output.script).map(val => typeof (val) === "object" ? Buffer.from(val).toString("hex") : val.toString()).join(" "),
|
|
110
|
+
hex: Buffer.from(output.script).toString("hex")
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
}),
|
|
114
|
+
ins: Array.from({ length: btcTx.inputsLength }, (_, i) => i).map(index => {
|
|
115
|
+
const input = btcTx.getInput(index);
|
|
116
|
+
return {
|
|
117
|
+
txid: Buffer.from(input.txid).toString("hex"),
|
|
118
|
+
vout: input.index,
|
|
119
|
+
scriptSig: {
|
|
120
|
+
asm: btc_signer_1.Script.decode(input.finalScriptSig).map(val => typeof (val) === "object" ? Buffer.from(val).toString("hex") : val.toString()).join(" "),
|
|
121
|
+
hex: Buffer.from(input.finalScriptSig).toString("hex")
|
|
122
|
+
},
|
|
123
|
+
sequence: input.sequence,
|
|
124
|
+
txinwitness: input.finalScriptWitness == null ? [] : input.finalScriptWitness.map(witness => Buffer.from(witness).toString("hex"))
|
|
125
|
+
};
|
|
126
|
+
})
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
exports.parsePsbt = parsePsbt;
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { BtcTx } from "@atomiqlabs/base";
|
|
3
3
|
import { Command } from "@atomiqlabs/server-base";
|
|
4
4
|
import { Transaction } from "@scure/btc-signer";
|
|
5
|
+
import { BTC_NETWORK } from "@scure/btc-signer/utils";
|
|
5
6
|
export type BitcoinUtxo = {
|
|
6
7
|
address: string;
|
|
7
8
|
type: "p2wpkh" | "p2sh-p2wpkh" | "p2tr";
|
|
@@ -18,55 +19,129 @@ export type SignPsbtResponse = {
|
|
|
18
19
|
txId: string;
|
|
19
20
|
networkFee: number;
|
|
20
21
|
};
|
|
21
|
-
export
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
getStatus(): string;
|
|
25
|
-
getStatusInfo(): Promise<Record<string, string>>;
|
|
26
|
-
getCommands(): Command<any>[];
|
|
22
|
+
export declare abstract class IBitcoinWallet {
|
|
23
|
+
readonly network: BTC_NETWORK;
|
|
24
|
+
protected constructor(network: BTC_NETWORK);
|
|
27
25
|
toOutputScript(address: string): Buffer;
|
|
28
|
-
|
|
26
|
+
getSignedTransaction(destination: string, amount: number, feeRate?: number, nonce?: bigint, maxAllowedFeeRate?: number): Promise<SignPsbtResponse>;
|
|
27
|
+
getSignedMultiTransaction(destinations: {
|
|
28
|
+
address: string;
|
|
29
|
+
amount: number;
|
|
30
|
+
}[], feeRate?: number, nonce?: bigint, maxAllowedFeeRate?: number): Promise<SignPsbtResponse>;
|
|
31
|
+
estimateFee(destination: string, amount: number, feeRate?: number, feeRateMultiplier?: number): Promise<{
|
|
32
|
+
satsPerVbyte: number;
|
|
33
|
+
networkFee: number;
|
|
34
|
+
}>;
|
|
35
|
+
drainAll(destination: string | Buffer, inputs: Omit<BitcoinUtxo, "address">[], feeRate?: number): Promise<SignPsbtResponse>;
|
|
36
|
+
burnAll(inputs: Omit<BitcoinUtxo, "address">[]): Promise<SignPsbtResponse>;
|
|
37
|
+
/**
|
|
38
|
+
* Initializes the wallet, called before any actions on the wallet
|
|
39
|
+
*/
|
|
40
|
+
abstract init(): Promise<void>;
|
|
41
|
+
/**
|
|
42
|
+
* Returns whether the wallet is ready
|
|
43
|
+
*/
|
|
44
|
+
abstract isReady(): boolean;
|
|
45
|
+
/**
|
|
46
|
+
* Returns the status defined string to be displayed in the status message
|
|
47
|
+
*/
|
|
48
|
+
abstract getStatus(): string;
|
|
49
|
+
/**
|
|
50
|
+
* Additional status information to be displayed in the status message
|
|
51
|
+
*/
|
|
52
|
+
abstract getStatusInfo(): Promise<Record<string, string>>;
|
|
53
|
+
/**
|
|
54
|
+
* Returns the commands that will be exposed
|
|
55
|
+
*/
|
|
56
|
+
abstract getCommands(): Command<any>[];
|
|
57
|
+
/**
|
|
58
|
+
* Returns the address type of the wallet
|
|
59
|
+
*/
|
|
60
|
+
abstract getAddressType(): "p2wpkh" | "p2sh-p2wpkh" | "p2tr";
|
|
29
61
|
/**
|
|
30
62
|
* Returns an unused address suitable for receiving
|
|
31
63
|
*/
|
|
32
|
-
getAddress(): Promise<string>;
|
|
64
|
+
abstract getAddress(): Promise<string>;
|
|
33
65
|
/**
|
|
34
66
|
* Adds previously returned address (with getAddress call), to the pool of unused addresses
|
|
35
67
|
* @param address
|
|
36
68
|
*/
|
|
37
|
-
addUnusedAddress(address: string): Promise<void>;
|
|
38
|
-
|
|
39
|
-
|
|
69
|
+
abstract addUnusedAddress(address: string): Promise<void>;
|
|
70
|
+
/**
|
|
71
|
+
* Returns the wallet balance, separated between confirmed and unconfirmed balance (both in sats)
|
|
72
|
+
*/
|
|
73
|
+
abstract getBalance(): Promise<{
|
|
40
74
|
confirmed: number;
|
|
41
75
|
unconfirmed: number;
|
|
42
76
|
}>;
|
|
43
77
|
/**
|
|
44
|
-
* Returns
|
|
78
|
+
* Returns the total spendable wallet balance in sats
|
|
45
79
|
*/
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
80
|
+
abstract getSpendableBalance(): Promise<number>;
|
|
81
|
+
/**
|
|
82
|
+
* Returns all wallet transactions confirmed after the specified blockheight (includes also unconfirmed
|
|
83
|
+
* wallet transaction!!)
|
|
84
|
+
*
|
|
85
|
+
* @param startHeight
|
|
86
|
+
*/
|
|
87
|
+
abstract getWalletTransactions(startHeight?: number): Promise<BtcTx[]>;
|
|
88
|
+
/**
|
|
89
|
+
* Returns the in-wallet transaction as identified by its transaction ID
|
|
90
|
+
*
|
|
91
|
+
* @param txId
|
|
92
|
+
*/
|
|
93
|
+
abstract getWalletTransaction(txId: string): Promise<BtcTx | null>;
|
|
94
|
+
/**
|
|
95
|
+
* Subscribes to wallet transactions, should fire when transaction enters mempool, and then also
|
|
96
|
+
* for the first confirmation of the transactions
|
|
97
|
+
*
|
|
98
|
+
* @param callback
|
|
99
|
+
* @param abortSignal
|
|
100
|
+
*/
|
|
101
|
+
abstract subscribeToWalletTransactions(callback: (tx: BtcTx) => void, abortSignal?: AbortSignal): void;
|
|
102
|
+
/**
|
|
103
|
+
* Estimates a network fee (in sats), for sending a specific PSBT, the provided PSBT might not contain
|
|
104
|
+
* any inputs, hence the fee returned should also reflect the transaction size increase by adding
|
|
105
|
+
* wallet UTXOs as inputs
|
|
106
|
+
*
|
|
107
|
+
* @param psbt
|
|
108
|
+
* @param feeRate
|
|
109
|
+
*/
|
|
110
|
+
abstract estimatePsbtFee(psbt: Transaction, feeRate?: number): Promise<{
|
|
59
111
|
satsPerVbyte: number;
|
|
60
112
|
networkFee: number;
|
|
61
113
|
}>;
|
|
62
|
-
drainAll(destination: string | Buffer, inputs: Omit<BitcoinUtxo, "address">[], feeRate?: number): Promise<SignPsbtResponse>;
|
|
63
|
-
burnAll(inputs: Omit<BitcoinUtxo, "address">[]): Promise<SignPsbtResponse>;
|
|
64
|
-
parsePsbt(psbt: Transaction): Promise<BtcTx>;
|
|
65
|
-
getBlockheight(): Promise<number>;
|
|
66
|
-
getFeeRate(): Promise<number>;
|
|
67
114
|
/**
|
|
68
|
-
*
|
|
69
|
-
*
|
|
115
|
+
* Funds the provided PSBT (adds wallet UTXOs)
|
|
116
|
+
*
|
|
117
|
+
* @param psbt
|
|
118
|
+
* @param feeRate
|
|
119
|
+
* @param maxAllowedFeeRate
|
|
120
|
+
*/
|
|
121
|
+
abstract fundPsbt(psbt: Transaction, feeRate?: number, maxAllowedFeeRate?: number): Promise<Transaction>;
|
|
122
|
+
/**
|
|
123
|
+
* Signs the provided PSBT
|
|
124
|
+
*
|
|
125
|
+
* @param psbt
|
|
126
|
+
*/
|
|
127
|
+
abstract signPsbt(psbt: Transaction): Promise<SignPsbtResponse>;
|
|
128
|
+
/**
|
|
129
|
+
* Broadcasts a raw bitcoin hex encoded transaction
|
|
130
|
+
*
|
|
131
|
+
* @param tx
|
|
132
|
+
*/
|
|
133
|
+
abstract sendRawTransaction(tx: string): Promise<void>;
|
|
134
|
+
/**
|
|
135
|
+
* Returns bitcoin network fee in sats/vB
|
|
136
|
+
*/
|
|
137
|
+
abstract getFeeRate(): Promise<number>;
|
|
138
|
+
/**
|
|
139
|
+
* Returns the blockheight of the bitcoin chain
|
|
140
|
+
*/
|
|
141
|
+
abstract getBlockheight(): Promise<number>;
|
|
142
|
+
/**
|
|
143
|
+
* Post a task to be executed on the sequential thread of the wallet, in case wallets requires
|
|
144
|
+
* the UTXOs staying consistent during operation, it is recommended to implement this function
|
|
70
145
|
*
|
|
71
146
|
* @param executor
|
|
72
147
|
*/
|
|
@@ -1,2 +1,97 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.IBitcoinWallet = void 0;
|
|
4
|
+
const btc_signer_1 = require("@scure/btc-signer");
|
|
5
|
+
class IBitcoinWallet {
|
|
6
|
+
constructor(network) {
|
|
7
|
+
this.network = network;
|
|
8
|
+
}
|
|
9
|
+
toOutputScript(address) {
|
|
10
|
+
const outputScript = (0, btc_signer_1.Address)(this.network).decode(address);
|
|
11
|
+
switch (outputScript.type) {
|
|
12
|
+
case "pkh":
|
|
13
|
+
case "sh":
|
|
14
|
+
case "wpkh":
|
|
15
|
+
case "wsh":
|
|
16
|
+
return Buffer.from(btc_signer_1.OutScript.encode({
|
|
17
|
+
type: outputScript.type,
|
|
18
|
+
hash: outputScript.hash
|
|
19
|
+
}));
|
|
20
|
+
case "tr":
|
|
21
|
+
return Buffer.from(btc_signer_1.OutScript.encode({
|
|
22
|
+
type: "tr",
|
|
23
|
+
pubkey: outputScript.pubkey
|
|
24
|
+
}));
|
|
25
|
+
}
|
|
26
|
+
throw new Error("Unrecognized address type");
|
|
27
|
+
}
|
|
28
|
+
getSignedTransaction(destination, amount, feeRate, nonce, maxAllowedFeeRate) {
|
|
29
|
+
return this.getSignedMultiTransaction([{ address: destination, amount }], feeRate, nonce, maxAllowedFeeRate);
|
|
30
|
+
}
|
|
31
|
+
async getSignedMultiTransaction(destinations, feeRate, nonce, maxAllowedFeeRate) {
|
|
32
|
+
let locktime = 0;
|
|
33
|
+
let sequence = 0xFFFFFFFD;
|
|
34
|
+
//Apply nonce
|
|
35
|
+
if (nonce != null) {
|
|
36
|
+
const locktimeBN = nonce >> 24n;
|
|
37
|
+
locktime = Number(locktimeBN) + 500000000;
|
|
38
|
+
if (locktime > (Date.now() / 1000 - 24 * 60 * 60))
|
|
39
|
+
throw new Error("Invalid escrow nonce (locktime)!");
|
|
40
|
+
const sequenceBN = nonce & 0xffffffn;
|
|
41
|
+
sequence = 0xFE000000 + Number(sequenceBN);
|
|
42
|
+
}
|
|
43
|
+
let psbt = new btc_signer_1.Transaction({ lockTime: locktime });
|
|
44
|
+
destinations.forEach(dst => psbt.addOutput({
|
|
45
|
+
script: this.toOutputScript(dst.address),
|
|
46
|
+
amount: BigInt(dst.amount)
|
|
47
|
+
}));
|
|
48
|
+
await this.fundPsbt(psbt, feeRate, maxAllowedFeeRate);
|
|
49
|
+
//Apply nonce
|
|
50
|
+
for (let i = 0; i < psbt.inputsLength; i++) {
|
|
51
|
+
psbt.updateInput(i, { sequence });
|
|
52
|
+
}
|
|
53
|
+
return await this.signPsbt(psbt);
|
|
54
|
+
}
|
|
55
|
+
async estimateFee(destination, amount, feeRate, feeRateMultiplier) {
|
|
56
|
+
feeRate ?? (feeRate = await this.getFeeRate());
|
|
57
|
+
if (feeRateMultiplier != null)
|
|
58
|
+
feeRate = feeRate * feeRateMultiplier;
|
|
59
|
+
let psbt = new btc_signer_1.Transaction();
|
|
60
|
+
psbt.addOutput({
|
|
61
|
+
script: this.toOutputScript(destination),
|
|
62
|
+
amount: BigInt(amount)
|
|
63
|
+
});
|
|
64
|
+
return await this.estimatePsbtFee(psbt, feeRate);
|
|
65
|
+
}
|
|
66
|
+
drainAll(destination, inputs, feeRate) {
|
|
67
|
+
throw new Error("Not implemented");
|
|
68
|
+
}
|
|
69
|
+
burnAll(inputs) {
|
|
70
|
+
let psbt = new btc_signer_1.Transaction();
|
|
71
|
+
inputs.forEach(input => psbt.addInput({
|
|
72
|
+
txid: input.txId,
|
|
73
|
+
index: input.vout,
|
|
74
|
+
witnessUtxo: {
|
|
75
|
+
script: input.outputScript,
|
|
76
|
+
amount: BigInt(input.value)
|
|
77
|
+
},
|
|
78
|
+
sighashType: 0x01,
|
|
79
|
+
sequence: 0
|
|
80
|
+
}));
|
|
81
|
+
psbt.addOutput({
|
|
82
|
+
script: Buffer.concat([Buffer.from([0x6a, 20]), Buffer.from("BURN, BABY, BURN! AQ", "ascii")]),
|
|
83
|
+
amount: 0n
|
|
84
|
+
});
|
|
85
|
+
return this.signPsbt(psbt);
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Post a task to be executed on the sequential thread of the wallet, in case wallets requires
|
|
89
|
+
* the UTXOs staying consistent during operation, it is recommended to implement this function
|
|
90
|
+
*
|
|
91
|
+
* @param executor
|
|
92
|
+
*/
|
|
93
|
+
execute(executor) {
|
|
94
|
+
return executor();
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
exports.IBitcoinWallet = IBitcoinWallet;
|
|
@@ -30,6 +30,7 @@ export type LightningNetworkChannel = {
|
|
|
30
30
|
id: string;
|
|
31
31
|
capacity: bigint;
|
|
32
32
|
isActive: boolean;
|
|
33
|
+
peerPublicKey: string;
|
|
33
34
|
localBalance: bigint;
|
|
34
35
|
localReserve: bigint;
|
|
35
36
|
remoteBalance: bigint;
|
|
@@ -38,6 +39,21 @@ export type LightningNetworkChannel = {
|
|
|
38
39
|
transactionId: string;
|
|
39
40
|
transactionVout: number;
|
|
40
41
|
};
|
|
42
|
+
export type OpenChannelRequest = {
|
|
43
|
+
amountSats: bigint;
|
|
44
|
+
peerPublicKey: string;
|
|
45
|
+
peerAddress?: string;
|
|
46
|
+
feeRate?: number;
|
|
47
|
+
channelFees?: {
|
|
48
|
+
feeRatePPM?: bigint;
|
|
49
|
+
baseFeeMsat?: bigint;
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
export type CloseChannelRequest = {
|
|
53
|
+
channelId: string;
|
|
54
|
+
feeRate?: number;
|
|
55
|
+
forceClose?: boolean;
|
|
56
|
+
};
|
|
41
57
|
export type InvoiceInit = {
|
|
42
58
|
mtokens: bigint;
|
|
43
59
|
descriptionHash?: string;
|
|
@@ -112,6 +128,9 @@ export interface ILightningWallet {
|
|
|
112
128
|
parsePaymentRequest(request: string): Promise<ParsedPaymentRequest>;
|
|
113
129
|
getBlockheight(): Promise<number>;
|
|
114
130
|
getChannels(activeOnly?: boolean): Promise<LightningNetworkChannel[]>;
|
|
131
|
+
getPendingChannels(): Promise<LightningNetworkChannel[]>;
|
|
132
|
+
openChannel(req: OpenChannelRequest): Promise<LightningNetworkChannel>;
|
|
133
|
+
closeChannel(req: CloseChannelRequest): Promise<string>;
|
|
115
134
|
getLightningBalance(): Promise<LightningBalanceResponse>;
|
|
116
135
|
getIdentityPublicKey(): Promise<string>;
|
|
117
136
|
}
|
package/package.json
CHANGED
|
@@ -21,7 +21,13 @@ import {ISpvVaultSigner} from "../../wallets/ISpvVaultSigner";
|
|
|
21
21
|
import {PluginManager} from "../../plugins/PluginManager";
|
|
22
22
|
import {SpvVault} from "./SpvVault";
|
|
23
23
|
import {serverParamDecoder} from "../../utils/paramcoders/server/ServerParamDecoder";
|
|
24
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
expressHandlerWrapper,
|
|
26
|
+
getAbortController,
|
|
27
|
+
HEX_REGEX,
|
|
28
|
+
isDefinedRuntimeError,
|
|
29
|
+
parsePsbt
|
|
30
|
+
} from "../../utils/Utils";
|
|
25
31
|
import {IParamReader} from "../../utils/paramcoders/IParamReader";
|
|
26
32
|
import {ServerParamEncoder} from "../../utils/paramcoders/server/ServerParamEncoder";
|
|
27
33
|
import {FieldTypeEnum} from "../../utils/paramcoders/SchemaVerifier";
|
|
@@ -558,7 +564,7 @@ export class SpvVaultSwapHandler extends SwapHandler<SpvVaultSwap, SpvVaultSwapS
|
|
|
558
564
|
|
|
559
565
|
let data: SpvWithdrawalTransactionData;
|
|
560
566
|
try {
|
|
561
|
-
data = await spvVaultContract.getWithdrawalData(
|
|
567
|
+
data = await spvVaultContract.getWithdrawalData(parsePsbt(transaction));
|
|
562
568
|
} catch (e) {
|
|
563
569
|
this.swapLogger.error(swap, "REST: /postQuote: failed to parse PSBT to withdrawal tx data: ", e);
|
|
564
570
|
throw {
|
|
@@ -606,7 +612,7 @@ export class SpvVaultSwapHandler extends SwapHandler<SpvVaultSwap, SpvVaultSwapS
|
|
|
606
612
|
msg: "One or more PSBT inputs not finalized!"
|
|
607
613
|
};
|
|
608
614
|
|
|
609
|
-
const effectiveFeeRate = await this.bitcoinRpc.getEffectiveFeeRate(
|
|
615
|
+
const effectiveFeeRate = await this.bitcoinRpc.getEffectiveFeeRate(parsePsbt(signedTx));
|
|
610
616
|
if(effectiveFeeRate.feeRate < 1 || Math.round(effectiveFeeRate.feeRate) < swap.btcFeeRate) throw {
|
|
611
617
|
code: 20511,
|
|
612
618
|
msg: "Bitcoin transaction fee too low, expected minimum: "+swap.btcFeeRate+" adjusted effective fee rate: "+effectiveFeeRate.feeRate
|
package/src/utils/Utils.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import {Request, Response} from "express";
|
|
2
2
|
import {ServerParamEncoder} from "./paramcoders/server/ServerParamEncoder";
|
|
3
|
+
import {createHash} from "crypto";
|
|
4
|
+
import {Script, Transaction} from "@scure/btc-signer";
|
|
5
|
+
import {BtcTx} from "@atomiqlabs/base";
|
|
3
6
|
|
|
4
7
|
export type LoggerType = {
|
|
5
8
|
debug: (msg: string, ...args: any[]) => void,
|
|
@@ -102,3 +105,46 @@ export function getAbortController(responseStream: ServerParamEncoder): AbortCon
|
|
|
102
105
|
responseStreamAbortController.addEventListener("abort", () => abortController.abort(responseStreamAbortController.reason));
|
|
103
106
|
return abortController;
|
|
104
107
|
}
|
|
108
|
+
|
|
109
|
+
export function parsePsbt(btcTx: Transaction): BtcTx {
|
|
110
|
+
const txWithoutWitness = btcTx.toBytes(true, false);
|
|
111
|
+
return {
|
|
112
|
+
locktime: btcTx.lockTime,
|
|
113
|
+
version: btcTx.version,
|
|
114
|
+
blockhash: null,
|
|
115
|
+
confirmations: 0,
|
|
116
|
+
txid: createHash("sha256").update(
|
|
117
|
+
createHash("sha256").update(
|
|
118
|
+
txWithoutWitness
|
|
119
|
+
).digest()
|
|
120
|
+
).digest().reverse().toString("hex"),
|
|
121
|
+
hex: Buffer.from(txWithoutWitness).toString("hex"),
|
|
122
|
+
raw: Buffer.from(btcTx.toBytes(true, true)).toString("hex"),
|
|
123
|
+
vsize: btcTx.isFinal ? btcTx.vsize : null,
|
|
124
|
+
|
|
125
|
+
outs: Array.from({length: btcTx.outputsLength}, (_, i) => i).map((index) => {
|
|
126
|
+
const output = btcTx.getOutput(index);
|
|
127
|
+
return {
|
|
128
|
+
value: Number(output.amount),
|
|
129
|
+
n: index,
|
|
130
|
+
scriptPubKey: {
|
|
131
|
+
asm: Script.decode(output.script).map(val => typeof(val)==="object" ? Buffer.from(val).toString("hex") : val.toString()).join(" "),
|
|
132
|
+
hex: Buffer.from(output.script).toString("hex")
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}),
|
|
136
|
+
ins: Array.from({length: btcTx.inputsLength}, (_, i) => i).map(index => {
|
|
137
|
+
const input = btcTx.getInput(index);
|
|
138
|
+
return {
|
|
139
|
+
txid: Buffer.from(input.txid).toString("hex"),
|
|
140
|
+
vout: input.index,
|
|
141
|
+
scriptSig: {
|
|
142
|
+
asm: Script.decode(input.finalScriptSig).map(val => typeof(val)==="object" ? Buffer.from(val).toString("hex") : val.toString()).join(" "),
|
|
143
|
+
hex: Buffer.from(input.finalScriptSig).toString("hex")
|
|
144
|
+
},
|
|
145
|
+
sequence: input.sequence,
|
|
146
|
+
txinwitness: input.finalScriptWitness==null ? [] : input.finalScriptWitness.map(witness => Buffer.from(witness).toString("hex"))
|
|
147
|
+
}
|
|
148
|
+
})
|
|
149
|
+
};
|
|
150
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {BtcTx} from "@atomiqlabs/base";
|
|
2
2
|
import {Command} from "@atomiqlabs/server-base";
|
|
3
|
-
import {Transaction} from "@scure/btc-signer";
|
|
3
|
+
import {Address, OutScript, Transaction} from "@scure/btc-signer";
|
|
4
|
+
import {BTC_NETWORK} from "@scure/btc-signer/utils";
|
|
4
5
|
|
|
5
6
|
export type BitcoinUtxo = {
|
|
6
7
|
address: string,
|
|
@@ -20,58 +21,218 @@ export type SignPsbtResponse = {
|
|
|
20
21
|
networkFee: number
|
|
21
22
|
};
|
|
22
23
|
|
|
23
|
-
export
|
|
24
|
+
export abstract class IBitcoinWallet {
|
|
24
25
|
|
|
25
|
-
|
|
26
|
+
readonly network: BTC_NETWORK;
|
|
26
27
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
getCommands(): Command<any>[];
|
|
28
|
+
protected constructor(network: BTC_NETWORK) {
|
|
29
|
+
this.network = network;
|
|
30
|
+
}
|
|
31
31
|
|
|
32
|
-
toOutputScript(address: string): Buffer
|
|
32
|
+
toOutputScript(address: string): Buffer {
|
|
33
|
+
const outputScript = Address(this.network).decode(address);
|
|
34
|
+
switch(outputScript.type) {
|
|
35
|
+
case "pkh":
|
|
36
|
+
case "sh":
|
|
37
|
+
case "wpkh":
|
|
38
|
+
case "wsh":
|
|
39
|
+
return Buffer.from(OutScript.encode({
|
|
40
|
+
type: outputScript.type,
|
|
41
|
+
hash: outputScript.hash
|
|
42
|
+
}));
|
|
43
|
+
case "tr":
|
|
44
|
+
return Buffer.from(OutScript.encode({
|
|
45
|
+
type: "tr",
|
|
46
|
+
pubkey: outputScript.pubkey
|
|
47
|
+
}));
|
|
48
|
+
}
|
|
49
|
+
throw new Error("Unrecognized address type");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
getSignedTransaction(destination: string, amount: number, feeRate?: number, nonce?: bigint, maxAllowedFeeRate?: number): Promise<SignPsbtResponse> {
|
|
53
|
+
return this.getSignedMultiTransaction([{address: destination, amount}], feeRate, nonce, maxAllowedFeeRate);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async getSignedMultiTransaction(
|
|
57
|
+
destinations: {address: string, amount: number}[], feeRate?: number, nonce?: bigint, maxAllowedFeeRate?: number
|
|
58
|
+
): Promise<SignPsbtResponse> {
|
|
59
|
+
let locktime = 0;
|
|
60
|
+
let sequence = 0xFFFFFFFD;
|
|
61
|
+
//Apply nonce
|
|
62
|
+
if(nonce!=null) {
|
|
63
|
+
const locktimeBN = nonce >> 24n;
|
|
64
|
+
locktime = Number(locktimeBN) + 500000000;
|
|
65
|
+
if(locktime > (Date.now()/1000 - 24*60*60)) throw new Error("Invalid escrow nonce (locktime)!");
|
|
66
|
+
|
|
67
|
+
const sequenceBN = nonce & 0xFFFFFFn;
|
|
68
|
+
sequence = 0xFE000000 + Number(sequenceBN);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
let psbt = new Transaction({lockTime: locktime});
|
|
72
|
+
destinations.forEach(dst => psbt.addOutput({
|
|
73
|
+
script: this.toOutputScript(dst.address),
|
|
74
|
+
amount: BigInt(dst.amount)
|
|
75
|
+
}));
|
|
76
|
+
|
|
77
|
+
await this.fundPsbt(psbt, feeRate, maxAllowedFeeRate);
|
|
78
|
+
|
|
79
|
+
//Apply nonce
|
|
80
|
+
for(let i=0;i<psbt.inputsLength;i++) {
|
|
81
|
+
psbt.updateInput(i, {sequence});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return await this.signPsbt(psbt);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async estimateFee(destination: string, amount: number, feeRate?: number, feeRateMultiplier?: number): Promise<{satsPerVbyte: number, networkFee: number}> {
|
|
88
|
+
feeRate ??= await this.getFeeRate();
|
|
89
|
+
if(feeRateMultiplier!=null) feeRate = feeRate * feeRateMultiplier;
|
|
90
|
+
|
|
91
|
+
let psbt = new Transaction();
|
|
92
|
+
psbt.addOutput({
|
|
93
|
+
script: this.toOutputScript(destination),
|
|
94
|
+
amount: BigInt(amount)
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
return await this.estimatePsbtFee(psbt, feeRate);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
drainAll(destination: string | Buffer, inputs: Omit<BitcoinUtxo, "address">[], feeRate?: number): Promise<SignPsbtResponse> {
|
|
101
|
+
throw new Error("Not implemented");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
burnAll(inputs: Omit<BitcoinUtxo, "address">[]): Promise<SignPsbtResponse> {
|
|
105
|
+
let psbt = new Transaction();
|
|
106
|
+
inputs.forEach(input => psbt.addInput({
|
|
107
|
+
txid: input.txId,
|
|
108
|
+
index: input.vout,
|
|
109
|
+
witnessUtxo: {
|
|
110
|
+
script: input.outputScript,
|
|
111
|
+
amount: BigInt(input.value)
|
|
112
|
+
},
|
|
113
|
+
sighashType: 0x01,
|
|
114
|
+
sequence: 0
|
|
115
|
+
}));
|
|
116
|
+
psbt.addOutput({
|
|
117
|
+
script: Buffer.concat([Buffer.from([0x6a, 20]), Buffer.from("BURN, BABY, BURN! AQ", "ascii")]),
|
|
118
|
+
amount: 0n
|
|
119
|
+
});
|
|
120
|
+
return this.signPsbt(psbt);
|
|
121
|
+
}
|
|
33
122
|
|
|
34
|
-
|
|
123
|
+
/**
|
|
124
|
+
* Initializes the wallet, called before any actions on the wallet
|
|
125
|
+
*/
|
|
126
|
+
abstract init(): Promise<void>;
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Returns whether the wallet is ready
|
|
130
|
+
*/
|
|
131
|
+
abstract isReady(): boolean;
|
|
132
|
+
/**
|
|
133
|
+
* Returns the status defined string to be displayed in the status message
|
|
134
|
+
*/
|
|
135
|
+
abstract getStatus(): string;
|
|
136
|
+
/**
|
|
137
|
+
* Additional status information to be displayed in the status message
|
|
138
|
+
*/
|
|
139
|
+
abstract getStatusInfo(): Promise<Record<string, string>>;
|
|
140
|
+
/**
|
|
141
|
+
* Returns the commands that will be exposed
|
|
142
|
+
*/
|
|
143
|
+
abstract getCommands(): Command<any>[];
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Returns the address type of the wallet
|
|
147
|
+
*/
|
|
148
|
+
abstract getAddressType(): "p2wpkh" | "p2sh-p2wpkh" | "p2tr";
|
|
35
149
|
/**
|
|
36
150
|
* Returns an unused address suitable for receiving
|
|
37
151
|
*/
|
|
38
|
-
getAddress(): Promise<string>;
|
|
152
|
+
abstract getAddress(): Promise<string>;
|
|
39
153
|
/**
|
|
40
154
|
* Adds previously returned address (with getAddress call), to the pool of unused addresses
|
|
41
155
|
* @param address
|
|
42
156
|
*/
|
|
43
|
-
addUnusedAddress(address: string): Promise<void>;
|
|
44
|
-
|
|
45
|
-
getUtxos(): Promise<BitcoinUtxo[]>;
|
|
46
|
-
getBalance(): Promise<{confirmed: number, unconfirmed: number}>;
|
|
157
|
+
abstract addUnusedAddress(address: string): Promise<void>;
|
|
47
158
|
/**
|
|
48
|
-
* Returns
|
|
159
|
+
* Returns the wallet balance, separated between confirmed and unconfirmed balance (both in sats)
|
|
49
160
|
*/
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
161
|
+
abstract getBalance(): Promise<{confirmed: number, unconfirmed: number}>;
|
|
162
|
+
/**
|
|
163
|
+
* Returns the total spendable wallet balance in sats
|
|
164
|
+
*/
|
|
165
|
+
abstract getSpendableBalance(): Promise<number>;
|
|
54
166
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
167
|
+
/**
|
|
168
|
+
* Returns all wallet transactions confirmed after the specified blockheight (includes also unconfirmed
|
|
169
|
+
* wallet transaction!!)
|
|
170
|
+
*
|
|
171
|
+
* @param startHeight
|
|
172
|
+
*/
|
|
173
|
+
abstract getWalletTransactions(startHeight?: number): Promise<BtcTx[]>;
|
|
174
|
+
/**
|
|
175
|
+
* Returns the in-wallet transaction as identified by its transaction ID
|
|
176
|
+
*
|
|
177
|
+
* @param txId
|
|
178
|
+
*/
|
|
179
|
+
abstract getWalletTransaction(txId: string): Promise<BtcTx | null>;
|
|
180
|
+
/**
|
|
181
|
+
* Subscribes to wallet transactions, should fire when transaction enters mempool, and then also
|
|
182
|
+
* for the first confirmation of the transactions
|
|
183
|
+
*
|
|
184
|
+
* @param callback
|
|
185
|
+
* @param abortSignal
|
|
186
|
+
*/
|
|
187
|
+
abstract subscribeToWalletTransactions(callback: (tx: BtcTx) => void, abortSignal?: AbortSignal): void;
|
|
63
188
|
|
|
64
|
-
|
|
189
|
+
/**
|
|
190
|
+
* Estimates a network fee (in sats), for sending a specific PSBT, the provided PSBT might not contain
|
|
191
|
+
* any inputs, hence the fee returned should also reflect the transaction size increase by adding
|
|
192
|
+
* wallet UTXOs as inputs
|
|
193
|
+
*
|
|
194
|
+
* @param psbt
|
|
195
|
+
* @param feeRate
|
|
196
|
+
*/
|
|
197
|
+
abstract estimatePsbtFee(psbt: Transaction, feeRate?: number): Promise<{satsPerVbyte: number, networkFee: number}>;
|
|
198
|
+
/**
|
|
199
|
+
* Funds the provided PSBT (adds wallet UTXOs)
|
|
200
|
+
*
|
|
201
|
+
* @param psbt
|
|
202
|
+
* @param feeRate
|
|
203
|
+
* @param maxAllowedFeeRate
|
|
204
|
+
*/
|
|
205
|
+
abstract fundPsbt(psbt: Transaction, feeRate?: number, maxAllowedFeeRate?: number): Promise<Transaction>;
|
|
206
|
+
/**
|
|
207
|
+
* Signs the provided PSBT
|
|
208
|
+
*
|
|
209
|
+
* @param psbt
|
|
210
|
+
*/
|
|
211
|
+
abstract signPsbt(psbt: Transaction): Promise<SignPsbtResponse>;
|
|
212
|
+
/**
|
|
213
|
+
* Broadcasts a raw bitcoin hex encoded transaction
|
|
214
|
+
*
|
|
215
|
+
* @param tx
|
|
216
|
+
*/
|
|
217
|
+
abstract sendRawTransaction(tx: string): Promise<void>;
|
|
65
218
|
|
|
66
|
-
|
|
67
|
-
|
|
219
|
+
/**
|
|
220
|
+
* Returns bitcoin network fee in sats/vB
|
|
221
|
+
*/
|
|
222
|
+
abstract getFeeRate(): Promise<number>;
|
|
223
|
+
/**
|
|
224
|
+
* Returns the blockheight of the bitcoin chain
|
|
225
|
+
*/
|
|
226
|
+
abstract getBlockheight(): Promise<number>;
|
|
68
227
|
|
|
69
228
|
/**
|
|
70
|
-
* Post a task to be executed on the sequential thread of the wallet,
|
|
71
|
-
* operation, it is recommended to
|
|
229
|
+
* Post a task to be executed on the sequential thread of the wallet, in case wallets requires
|
|
230
|
+
* the UTXOs staying consistent during operation, it is recommended to implement this function
|
|
72
231
|
*
|
|
73
232
|
* @param executor
|
|
74
233
|
*/
|
|
75
|
-
execute(executor: () => Promise<void>): Promise<void
|
|
234
|
+
execute(executor: () => Promise<void>): Promise<void> {
|
|
235
|
+
return executor();
|
|
236
|
+
}
|
|
76
237
|
|
|
77
238
|
}
|