@atomiqlabs/lp-lib 16.1.2 → 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.
@@ -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(await this.bitcoin.parsePsbt(transaction));
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(await this.bitcoin.parsePsbt(signedTx));
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,
@@ -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;
@@ -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 interface IBitcoinWallet {
22
- init(): Promise<void>;
23
- isReady(): boolean;
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
- getAddressType(): "p2wpkh" | "p2sh-p2wpkh" | "p2tr";
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
- getUtxos(): Promise<BitcoinUtxo[]>;
39
- getBalance(): Promise<{
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 required reserve amount that needs to be kept in the wallet (for e.g. lightning anchor channels)
78
+ * Returns the total spendable wallet balance in sats
45
79
  */
46
- getRequiredReserve(): Promise<number>;
47
- getWalletTransactions(startHeight?: number): Promise<BtcTx[]>;
48
- getWalletTransaction(txId: string): Promise<BtcTx | null>;
49
- subscribeToWalletTransactions(callback: (tx: BtcTx) => void, abortSignal?: AbortSignal): void;
50
- fundPsbt(psbt: Transaction, feeRate?: number): Promise<Transaction>;
51
- signPsbt(psbt: Transaction): Promise<SignPsbtResponse>;
52
- sendRawTransaction(tx: string): Promise<void>;
53
- getSignedTransaction(destination: string, amount: number, feeRate?: number, nonce?: bigint, maxAllowedFeeRate?: number): Promise<SignPsbtResponse>;
54
- getSignedMultiTransaction(destinations: {
55
- address: string;
56
- amount: number;
57
- }[], feeRate?: number, nonce?: bigint, maxAllowedFeeRate?: number): Promise<SignPsbtResponse>;
58
- estimateFee(destination: string, amount: number, feeRate?: number, feeRateMultiplier?: number): Promise<{
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
- * Post a task to be executed on the sequential thread of the wallet, this makes sure the UTXOs stay consistent during
69
- * operation, it is recommended to use this approach when spending wallet UTXOs
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atomiqlabs/lp-lib",
3
- "version": "16.1.2",
3
+ "version": "16.2.0",
4
4
  "description": "Main functionality implementation for atomiq LP node",
5
5
  "main": "./dist/index.js",
6
6
  "types:": "./dist/index.d.ts",
@@ -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 {expressHandlerWrapper, getAbortController, HEX_REGEX, isDefinedRuntimeError} from "../../utils/Utils";
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(await this.bitcoin.parsePsbt(transaction));
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(await this.bitcoin.parsePsbt(signedTx));
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
@@ -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 interface IBitcoinWallet {
24
+ export abstract class IBitcoinWallet {
24
25
 
25
- init(): Promise<void>;
26
+ readonly network: BTC_NETWORK;
26
27
 
27
- isReady(): boolean;
28
- getStatus(): string;
29
- getStatusInfo(): Promise<Record<string, string>>;
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
- getAddressType(): "p2wpkh" | "p2sh-p2wpkh" | "p2tr";
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 required reserve amount that needs to be kept in the wallet (for e.g. lightning anchor channels)
159
+ * Returns the wallet balance, separated between confirmed and unconfirmed balance (both in sats)
49
160
  */
50
- getRequiredReserve(): Promise<number>;
51
- getWalletTransactions(startHeight?: number): Promise<BtcTx[]>;
52
- getWalletTransaction(txId: string): Promise<BtcTx | null>;
53
- subscribeToWalletTransactions(callback: (tx: BtcTx) => void, abortSignal?: AbortSignal): void;
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
- fundPsbt(psbt: Transaction, feeRate?: number): Promise<Transaction>;
56
- signPsbt(psbt: Transaction): Promise<SignPsbtResponse>;
57
- sendRawTransaction(tx: string): Promise<void>;
58
- getSignedTransaction(destination: string, amount: number, feeRate?: number, nonce?: bigint, maxAllowedFeeRate?: number): Promise<SignPsbtResponse>;
59
- getSignedMultiTransaction(destinations: {address: string, amount: number}[], feeRate?: number, nonce?: bigint, maxAllowedFeeRate?: number): Promise<SignPsbtResponse>;
60
- estimateFee(destination: string, amount: number, feeRate?: number, feeRateMultiplier?: number): Promise<{satsPerVbyte: number, networkFee: number}>;
61
- drainAll(destination: string | Buffer, inputs: Omit<BitcoinUtxo, "address">[], feeRate?: number): Promise<SignPsbtResponse>;
62
- burnAll(inputs: Omit<BitcoinUtxo, "address">[]): Promise<SignPsbtResponse>;
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
- parsePsbt(psbt: Transaction): Promise<BtcTx>;
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
- getBlockheight(): Promise<number>;
67
- getFeeRate(): Promise<number>;
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, this makes sure the UTXOs stay consistent during
71
- * operation, it is recommended to use this approach when spending wallet UTXOs
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
  }