beignet 0.2.0 → 0.4.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/README.md +306 -1
- package/dist/cli/auth.js +238 -0
- package/dist/cli/auth.js.map +1 -0
- package/dist/cli/beignet-node.js +1034 -57
- package/dist/cli/beignet-node.js.map +1 -1
- package/dist/cli/cli.js +1257 -18
- package/dist/cli/cli.js.map +1 -1
- package/dist/cli/config.js +45 -1
- package/dist/cli/config.js.map +1 -1
- package/dist/cli/daemon.js +398 -38
- package/dist/cli/daemon.js.map +1 -1
- package/dist/cli/errors.js +2 -0
- package/dist/cli/errors.js.map +1 -1
- package/dist/cli/index.js +6 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/openapi.js +1343 -89
- package/dist/cli/openapi.js.map +1 -1
- package/dist/cli/restore.js +89 -0
- package/dist/cli/restore.js.map +1 -0
- package/dist/cli/wallet-storage.js +34 -0
- package/dist/cli/wallet-storage.js.map +1 -0
- package/dist/electrum/index.js +107 -12
- package/dist/electrum/index.js.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/lightning/advisor/fee-tuner.js +42 -0
- package/dist/lightning/advisor/fee-tuner.js.map +1 -0
- package/dist/lightning/advisor/index.js +9 -1
- package/dist/lightning/advisor/index.js.map +1 -1
- package/dist/lightning/advisor/rebalance-planner.js +55 -0
- package/dist/lightning/advisor/rebalance-planner.js.map +1 -0
- package/dist/lightning/backup/index.js +18 -0
- package/dist/lightning/backup/index.js.map +1 -0
- package/dist/lightning/backup/scb.js +42 -0
- package/dist/lightning/backup/scb.js.map +1 -0
- package/dist/lightning/chain/chain-monitor.js +27 -0
- package/dist/lightning/chain/chain-monitor.js.map +1 -1
- package/dist/lightning/chain/output-resolver.js +66 -15
- package/dist/lightning/chain/output-resolver.js.map +1 -1
- package/dist/lightning/chain/types.js +1 -0
- package/dist/lightning/chain/types.js.map +1 -1
- package/dist/lightning/channel/channel-manager.js +91 -24
- package/dist/lightning/channel/channel-manager.js.map +1 -1
- package/dist/lightning/channel/channel-state.js.map +1 -1
- package/dist/lightning/channel/channel.js +122 -4
- package/dist/lightning/channel/channel.js.map +1 -1
- package/dist/lightning/channel/commitment-builder.js +4 -7
- package/dist/lightning/channel/commitment-builder.js.map +1 -1
- package/dist/lightning/channel/dual-funding.js +6 -5
- package/dist/lightning/channel/dual-funding.js.map +1 -1
- package/dist/lightning/channel/splice-tx.js.map +1 -1
- package/dist/lightning/channel/types.js +3 -1
- package/dist/lightning/channel/types.js.map +1 -1
- package/dist/lightning/channel/validation.js +3 -3
- package/dist/lightning/channel/validation.js.map +1 -1
- package/dist/lightning/crypto/index.js +1 -0
- package/dist/lightning/crypto/index.js.map +1 -1
- package/dist/lightning/crypto/message-signing.js +116 -0
- package/dist/lightning/crypto/message-signing.js.map +1 -0
- package/dist/lightning/crypto/xchacha20poly1305.js +77 -0
- package/dist/lightning/crypto/xchacha20poly1305.js.map +1 -0
- package/dist/lightning/features/flags.js +1 -0
- package/dist/lightning/features/flags.js.map +1 -1
- package/dist/lightning/gossip/messages.js +37 -1
- package/dist/lightning/gossip/messages.js.map +1 -1
- package/dist/lightning/index.js +2 -1
- package/dist/lightning/index.js.map +1 -1
- package/dist/lightning/keys/signer.js +24 -3
- package/dist/lightning/keys/signer.js.map +1 -1
- package/dist/lightning/keys/wallet-keys.js +1 -0
- package/dist/lightning/keys/wallet-keys.js.map +1 -1
- package/dist/lightning/message/index.js +1 -0
- package/dist/lightning/message/index.js.map +1 -1
- package/dist/lightning/message/peer-storage.js +43 -0
- package/dist/lightning/message/peer-storage.js.map +1 -0
- package/dist/lightning/message/types.js +2 -0
- package/dist/lightning/message/types.js.map +1 -1
- package/dist/lightning/node/lightning-node.js +1593 -236
- package/dist/lightning/node/lightning-node.js.map +1 -1
- package/dist/lightning/node/types.js +18 -1
- package/dist/lightning/node/types.js.map +1 -1
- package/dist/lightning/offer/offer-manager.js +2 -5
- package/dist/lightning/offer/offer-manager.js.map +1 -1
- package/dist/lightning/offer/tlv.js +3 -1
- package/dist/lightning/offer/tlv.js.map +1 -1
- package/dist/lightning/storage/encryption.js +67 -0
- package/dist/lightning/storage/encryption.js.map +1 -0
- package/dist/lightning/storage/index.js +1 -0
- package/dist/lightning/storage/index.js.map +1 -1
- package/dist/lightning/storage/serialization.js +6 -2
- package/dist/lightning/storage/serialization.js.map +1 -1
- package/dist/lightning/storage/sqlite-storage.js +405 -33
- package/dist/lightning/storage/sqlite-storage.js.map +1 -1
- package/dist/lightning/watchtower/blob.js +221 -0
- package/dist/lightning/watchtower/blob.js.map +1 -0
- package/dist/lightning/watchtower/index.js +55 -0
- package/dist/lightning/watchtower/index.js.map +1 -0
- package/dist/lightning/watchtower/justice.js +272 -0
- package/dist/lightning/watchtower/justice.js.map +1 -0
- package/dist/lightning/watchtower/tower-connection.js +208 -0
- package/dist/lightning/watchtower/tower-connection.js.map +1 -0
- package/dist/lightning/watchtower/types.js +3 -0
- package/dist/lightning/watchtower/types.js.map +1 -0
- package/dist/lightning/watchtower/watchtower-client.js +472 -0
- package/dist/lightning/watchtower/watchtower-client.js.map +1 -0
- package/dist/lightning/watchtower/wtwire.js +201 -0
- package/dist/lightning/watchtower/wtwire.js.map +1 -0
- package/dist/logger.js +41 -0
- package/dist/logger.js.map +1 -0
- package/dist/shapes/electrum.js +32 -1
- package/dist/shapes/electrum.js.map +1 -1
- package/dist/shapes/wallet.js +11 -1
- package/dist/shapes/wallet.js.map +1 -1
- package/dist/transaction/index.js +163 -15
- package/dist/transaction/index.js.map +1 -1
- package/dist/types/cli/auth.d.ts +34 -0
- package/dist/types/cli/beignet-node.d.ts +148 -4
- package/dist/types/cli/daemon.d.ts +5 -0
- package/dist/types/cli/errors.d.ts +2 -0
- package/dist/types/cli/index.d.ts +1 -0
- package/dist/types/cli/restore.d.ts +10 -0
- package/dist/types/cli/types.d.ts +235 -1
- package/dist/types/cli/wallet-storage.d.ts +3 -0
- package/dist/types/electrum/index.d.ts +12 -0
- package/dist/types/electrum.js +1 -0
- package/dist/types/electrum.js.map +1 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/lightning/advisor/fee-tuner.d.ts +20 -0
- package/dist/types/lightning/advisor/index.d.ts +4 -0
- package/dist/types/lightning/advisor/rebalance-planner.d.ts +12 -0
- package/dist/types/lightning/backup/index.d.ts +1 -0
- package/dist/types/lightning/backup/scb.d.ts +24 -0
- package/dist/types/lightning/chain/chain-monitor.d.ts +1 -0
- package/dist/types/lightning/chain/types.d.ts +1 -0
- package/dist/types/lightning/channel/channel-manager.d.ts +11 -0
- package/dist/types/lightning/channel/channel-state.d.ts +2 -0
- package/dist/types/lightning/channel/channel.d.ts +11 -5
- package/dist/types/lightning/channel/commitment-builder.d.ts +7 -7
- package/dist/types/lightning/channel/dual-funding.d.ts +2 -1
- package/dist/types/lightning/channel/splice-tx.d.ts +2 -2
- package/dist/types/lightning/channel/types.d.ts +2 -0
- package/dist/types/lightning/channel/validation.d.ts +1 -1
- package/dist/types/lightning/crypto/index.d.ts +1 -0
- package/dist/types/lightning/crypto/message-signing.d.ts +9 -0
- package/dist/types/lightning/crypto/xchacha20poly1305.d.ts +3 -0
- package/dist/types/lightning/features/flags.d.ts +1 -0
- package/dist/types/lightning/gossip/messages.d.ts +4 -0
- package/dist/types/lightning/index.d.ts +1 -0
- package/dist/types/lightning/keys/signer.d.ts +18 -1
- package/dist/types/lightning/keys/wallet-keys.d.ts +2 -1
- package/dist/types/lightning/message/index.d.ts +1 -0
- package/dist/types/lightning/message/peer-storage.d.ts +9 -0
- package/dist/types/lightning/message/types.d.ts +2 -0
- package/dist/types/lightning/node/lightning-node.d.ts +120 -4
- package/dist/types/lightning/node/types.d.ts +66 -1
- package/dist/types/lightning/storage/encryption.d.ts +11 -0
- package/dist/types/lightning/storage/index.d.ts +1 -0
- package/dist/types/lightning/storage/serialization.d.ts +2 -0
- package/dist/types/lightning/storage/sqlite-storage.d.ts +43 -3
- package/dist/types/lightning/storage/types.d.ts +62 -0
- package/dist/types/lightning/watchtower/blob.d.ts +46 -0
- package/dist/types/lightning/watchtower/index.d.ts +9 -0
- package/dist/types/lightning/watchtower/justice.d.ts +30 -0
- package/dist/types/lightning/watchtower/tower-connection.d.ts +34 -0
- package/dist/types/lightning/watchtower/types.d.ts +46 -0
- package/dist/types/lightning/watchtower/watchtower-client.d.ts +68 -0
- package/dist/types/lightning/watchtower/wtwire.d.ts +90 -0
- package/dist/types/logger.d.ts +17 -0
- package/dist/types/shapes/electrum.d.ts +3 -1
- package/dist/types/transaction/index.d.ts +6 -0
- package/dist/types/transaction.js.map +1 -1
- package/dist/types/types/electrum.d.ts +3 -2
- package/dist/types/types/transaction.d.ts +1 -1
- package/dist/types/types/wallet.d.ts +80 -4
- package/dist/types/utils/descriptors.d.ts +2 -0
- package/dist/types/utils/fees.d.ts +3 -0
- package/dist/types/utils/helpers.d.ts +3 -1
- package/dist/types/utils/index.d.ts +5 -0
- package/dist/types/utils/multisig.d.ts +29 -0
- package/dist/types/utils/transaction.d.ts +7 -0
- package/dist/types/utils/wallet-storage-encryption.d.ts +3 -0
- package/dist/types/utils/wallet.d.ts +1 -1
- package/dist/types/utils/watch-only.d.ts +12 -0
- package/dist/types/wallet/constants.d.ts +1 -0
- package/dist/types/wallet/index.d.ts +63 -4
- package/dist/types/wallet.js +3 -0
- package/dist/types/wallet.js.map +1 -1
- package/dist/utils/derivation-path.js +8 -7
- package/dist/utils/derivation-path.js.map +1 -1
- package/dist/utils/descriptors.js +56 -0
- package/dist/utils/descriptors.js.map +1 -0
- package/dist/utils/electrum.js +14 -4
- package/dist/utils/electrum.js.map +1 -1
- package/dist/utils/fees.js +19 -0
- package/dist/utils/fees.js.map +1 -0
- package/dist/utils/helpers.js +22 -5
- package/dist/utils/helpers.js.map +1 -1
- package/dist/utils/index.js +5 -0
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/multisig.js +90 -0
- package/dist/utils/multisig.js.map +1 -0
- package/dist/utils/transaction.js +30 -3
- package/dist/utils/transaction.js.map +1 -1
- package/dist/utils/wallet-storage-encryption.js +45 -0
- package/dist/utils/wallet-storage-encryption.js.map +1 -0
- package/dist/utils/wallet.js +3 -1
- package/dist/utils/wallet.js.map +1 -1
- package/dist/utils/watch-only.js +102 -0
- package/dist/utils/watch-only.js.map +1 -0
- package/dist/wallet/constants.js +2 -1
- package/dist/wallet/constants.js.map +1 -1
- package/dist/wallet/index.js +810 -51
- package/dist/wallet/index.js.map +1 -1
- package/docs/ROADMAP.md +365 -0
- package/package.json +1 -1
package/dist/wallet/index.js
CHANGED
|
@@ -29,7 +29,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
29
29
|
exports.Wallet = void 0;
|
|
30
30
|
const bip39 = __importStar(require("bip39"));
|
|
31
31
|
const bitcoin = __importStar(require("bitcoinjs-lib"));
|
|
32
|
-
const bitcoinjs_lib_1 = require("bitcoinjs-lib");
|
|
33
32
|
const bip32_1 = __importDefault(require("bip32"));
|
|
34
33
|
const ecc = __importStar(require("@bitcoinerlab/secp256k1"));
|
|
35
34
|
const lodash_clonedeep_1 = __importDefault(require("lodash.clonedeep"));
|
|
@@ -40,14 +39,15 @@ const electrum_1 = require("../electrum");
|
|
|
40
39
|
const transaction_1 = require("../transaction");
|
|
41
40
|
const constants_1 = require("./constants");
|
|
42
41
|
const conversion_1 = require("../utils/conversion");
|
|
42
|
+
const logger_1 = require("../logger");
|
|
43
43
|
const bip32 = (0, bip32_1.default)(ecc);
|
|
44
44
|
class Wallet {
|
|
45
|
-
constructor({ mnemonic, passphrase, name, network = types_1.EAvailableNetworks.mainnet, addressType =
|
|
45
|
+
constructor({ mnemonic, xpub, passphrase, name, network = types_1.EAvailableNetworks.mainnet, addressType, account = 0, birthdayHeight, coinSelectPreference = types_1.ECoinSelectPreference.consolidate, storage, electrumOptions, onMessage = () => null, customGetAddress, customGetScriptHash, rbf = false, selectedFeeId = types_1.EFeeId.normal, feeEstimationSource = 'auto', disableMessages = false, disableMessagesOnCreate = false, logger, addressTypesToMonitor = Object.values(types_1.EAddressType), gapLimitOptions = {
|
|
46
46
|
lookBehind: constants_1.GAP_LIMIT,
|
|
47
47
|
lookAhead: constants_1.GAP_LIMIT,
|
|
48
48
|
lookBehindChange: constants_1.GAP_LIMIT_CHANGE,
|
|
49
49
|
lookAheadChange: constants_1.GAP_LIMIT_CHANGE
|
|
50
|
-
} }) {
|
|
50
|
+
}, multisig }) {
|
|
51
51
|
this._pendingRefreshPromises = [];
|
|
52
52
|
this.savingOperations = {};
|
|
53
53
|
this.getReceiveAddress = async ({ addressType = this.addressType }) => {
|
|
@@ -73,7 +73,7 @@ class Wallet {
|
|
|
73
73
|
return (0, utils_1.ok)(generatedAddress.value.address);
|
|
74
74
|
}
|
|
75
75
|
else {
|
|
76
|
-
|
|
76
|
+
this.logger.warn(generatedAddress.error.message);
|
|
77
77
|
}
|
|
78
78
|
return (0, utils_1.err)('No receive address available.');
|
|
79
79
|
}
|
|
@@ -95,7 +95,9 @@ class Wallet {
|
|
|
95
95
|
};
|
|
96
96
|
this.getBip32Interface = async () => {
|
|
97
97
|
try {
|
|
98
|
-
|
|
98
|
+
if (!this._mnemonic)
|
|
99
|
+
return (0, utils_1.err)(new utils_1.WatchOnlySigningError());
|
|
100
|
+
const network = (0, utils_1.getBitcoinJsNetwork)(this._network);
|
|
99
101
|
const seed = await bip39.mnemonicToSeed(this._mnemonic, this._passphrase);
|
|
100
102
|
const root = bip32.fromSeed(seed, network);
|
|
101
103
|
return (0, utils_1.ok)(root);
|
|
@@ -104,17 +106,68 @@ class Wallet {
|
|
|
104
106
|
return (0, utils_1.err)(e);
|
|
105
107
|
}
|
|
106
108
|
};
|
|
107
|
-
if (!mnemonic)
|
|
109
|
+
if (!mnemonic && !xpub && !multisig) {
|
|
108
110
|
throw new Error('No mnemonic specified.');
|
|
109
|
-
|
|
110
|
-
throw new Error('Invalid mnemonic.');
|
|
111
|
+
}
|
|
111
112
|
if (name && name.includes('-'))
|
|
112
113
|
throw new Error('Wallet name cannot include a hyphen (-).');
|
|
114
|
+
if (!Number.isInteger(account) || account < 0) {
|
|
115
|
+
throw new Error('account must be a non-negative integer.');
|
|
116
|
+
}
|
|
117
|
+
if (birthdayHeight !== undefined &&
|
|
118
|
+
(!Number.isInteger(birthdayHeight) || birthdayHeight < 0)) {
|
|
119
|
+
throw new Error('birthdayHeight must be a non-negative integer.');
|
|
120
|
+
}
|
|
121
|
+
this._account = String(account);
|
|
122
|
+
this._birthdayHeightOption = birthdayHeight;
|
|
113
123
|
this._network = network;
|
|
114
|
-
this._mnemonic = mnemonic;
|
|
115
124
|
this._passphrase = passphrase ?? '';
|
|
116
|
-
this.
|
|
117
|
-
|
|
125
|
+
this.isWatchOnly = !mnemonic;
|
|
126
|
+
if (mnemonic) {
|
|
127
|
+
if (!(0, utils_1.validateMnemonic)(mnemonic))
|
|
128
|
+
throw new Error('Invalid mnemonic.');
|
|
129
|
+
this._mnemonic = mnemonic;
|
|
130
|
+
this._seed = (0, utils_1.getSeed)(this._mnemonic, this._passphrase);
|
|
131
|
+
this._root = bip32.fromSeed(this._seed, this.getBitcoinNetwork(this._network));
|
|
132
|
+
}
|
|
133
|
+
if (multisig) {
|
|
134
|
+
if (addressType && addressType !== types_1.EAddressType.p2wsh) {
|
|
135
|
+
throw new Error('Multisig wallets only support the p2wsh address type.');
|
|
136
|
+
}
|
|
137
|
+
const setup = this._initMultisig(multisig);
|
|
138
|
+
this._multisig = setup.config;
|
|
139
|
+
this.id = setup.id;
|
|
140
|
+
this.addressType = types_1.EAddressType.p2wsh;
|
|
141
|
+
addressTypesToMonitor = [types_1.EAddressType.p2wsh];
|
|
142
|
+
}
|
|
143
|
+
else if (mnemonic) {
|
|
144
|
+
if (addressType === types_1.EAddressType.p2wsh) {
|
|
145
|
+
throw new Error('p2wsh is a multisig address type. Use Wallet.createMultisig.');
|
|
146
|
+
}
|
|
147
|
+
this.id = (0, utils_1.generateWalletId)(this._seed);
|
|
148
|
+
this.addressType = addressType ?? types_1.EAddressType.p2wpkh;
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
const parseRes = (0, utils_1.parseExtendedPublicKey)(xpub, this._network);
|
|
152
|
+
if (parseRes.isErr())
|
|
153
|
+
throw parseRes.error;
|
|
154
|
+
const { node, addressType: inferredType } = parseRes.value;
|
|
155
|
+
if (addressType && inferredType && addressType !== inferredType) {
|
|
156
|
+
throw new Error(`The provided xpub prefix implies ${inferredType}, but addressType ${addressType} was requested.`);
|
|
157
|
+
}
|
|
158
|
+
const resolvedType = addressType ?? inferredType ?? types_1.EAddressType.p2wpkh;
|
|
159
|
+
if (resolvedType === types_1.EAddressType.p2wsh) {
|
|
160
|
+
throw new Error('p2wsh is a multisig address type. Use Wallet.createMultisig with every cosigner xpub.');
|
|
161
|
+
}
|
|
162
|
+
this._accountNode = node;
|
|
163
|
+
this.addressType = resolvedType;
|
|
164
|
+
this.id = (0, utils_1.generateWalletId)(Buffer.concat([node.publicKey, node.chainCode]));
|
|
165
|
+
addressTypesToMonitor = [this.addressType];
|
|
166
|
+
}
|
|
167
|
+
if (!this._multisig) {
|
|
168
|
+
addressTypesToMonitor = addressTypesToMonitor.filter((type) => type !== types_1.EAddressType.p2wsh);
|
|
169
|
+
}
|
|
170
|
+
this.logger = logger ?? (0, logger_1.createConsoleLogger)('info');
|
|
118
171
|
this._data = (0, utils_1.getDefaultWalletData)();
|
|
119
172
|
this._getData = storage?.getData ?? utils_1.getDataFallback;
|
|
120
173
|
this._setData = storage?.setData;
|
|
@@ -123,9 +176,7 @@ class Wallet {
|
|
|
123
176
|
this._customGetAddress = customGetAddress;
|
|
124
177
|
if (customGetScriptHash)
|
|
125
178
|
this._customGetScriptHash = customGetScriptHash;
|
|
126
|
-
this.id = (0, utils_1.generateWalletId)(this._seed);
|
|
127
179
|
this.name = name ?? this.id;
|
|
128
|
-
this.addressType = addressType;
|
|
129
180
|
this.coinSelectPreference = coinSelectPreference;
|
|
130
181
|
this.transaction = new transaction_1.Transaction({
|
|
131
182
|
wallet: this
|
|
@@ -145,6 +196,7 @@ class Wallet {
|
|
|
145
196
|
});
|
|
146
197
|
this.rbf = rbf;
|
|
147
198
|
this.selectedFeeId = selectedFeeId;
|
|
199
|
+
this.feeEstimationSource = feeEstimationSource;
|
|
148
200
|
this.isRefreshing = false;
|
|
149
201
|
this.isSwitchingNetworks = false;
|
|
150
202
|
this.addressTypesToMonitor = addressTypesToMonitor;
|
|
@@ -185,6 +237,23 @@ class Wallet {
|
|
|
185
237
|
get network() {
|
|
186
238
|
return this._network;
|
|
187
239
|
}
|
|
240
|
+
get account() {
|
|
241
|
+
return Number(this._account);
|
|
242
|
+
}
|
|
243
|
+
get birthdayHeight() {
|
|
244
|
+
return this._data.birthdayHeight ?? 0;
|
|
245
|
+
}
|
|
246
|
+
get isMultisig() {
|
|
247
|
+
return !!this._multisig;
|
|
248
|
+
}
|
|
249
|
+
get multisigInfo() {
|
|
250
|
+
if (!this._multisig)
|
|
251
|
+
return undefined;
|
|
252
|
+
return {
|
|
253
|
+
threshold: this._multisig.threshold,
|
|
254
|
+
totalCosigners: this._multisig.cosigners.length
|
|
255
|
+
};
|
|
256
|
+
}
|
|
188
257
|
static async create(params) {
|
|
189
258
|
try {
|
|
190
259
|
const wallet = new Wallet(params);
|
|
@@ -201,6 +270,136 @@ class Wallet {
|
|
|
201
270
|
return (0, utils_1.err)(e);
|
|
202
271
|
}
|
|
203
272
|
}
|
|
273
|
+
static async createWatchOnly(params) {
|
|
274
|
+
if (!params?.xpub)
|
|
275
|
+
return (0, utils_1.err)('No xpub provided.');
|
|
276
|
+
return await Wallet.create(params);
|
|
277
|
+
}
|
|
278
|
+
static async createMultisig(params) {
|
|
279
|
+
if (!params)
|
|
280
|
+
return (0, utils_1.err)('No multisig parameters provided.');
|
|
281
|
+
const { threshold, cosigners, ourXpub, ...walletParams } = params;
|
|
282
|
+
return await Wallet.create({
|
|
283
|
+
...walletParams,
|
|
284
|
+
addressType: types_1.EAddressType.p2wsh,
|
|
285
|
+
multisig: { threshold, cosigners, ourXpub }
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
_initMultisig(multisig) {
|
|
289
|
+
const { threshold, cosigners, ourXpub } = multisig;
|
|
290
|
+
if (!Number.isInteger(threshold) || threshold < 1) {
|
|
291
|
+
throw new Error('Multisig threshold must be a positive integer.');
|
|
292
|
+
}
|
|
293
|
+
const keyId = (node) => Buffer.concat([node.publicKey, node.chainCode]).toString('hex');
|
|
294
|
+
const entries = [];
|
|
295
|
+
const addCosigner = (xpub) => {
|
|
296
|
+
const res = (0, utils_1.parseExtendedPublicKey)(xpub, this._network);
|
|
297
|
+
if (res.isErr())
|
|
298
|
+
throw res.error;
|
|
299
|
+
const node = res.value.node;
|
|
300
|
+
if (entries.some((e) => keyId(e.node) === keyId(node))) {
|
|
301
|
+
throw new Error(`Duplicate cosigner xpub provided: ${xpub}`);
|
|
302
|
+
}
|
|
303
|
+
entries.push({ node, isOurs: false });
|
|
304
|
+
};
|
|
305
|
+
for (const xpub of cosigners ?? [])
|
|
306
|
+
addCosigner(xpub);
|
|
307
|
+
if (this._root) {
|
|
308
|
+
const coinType = this._network === types_1.EAvailableNetworks.bitcoin ? '0' : '1';
|
|
309
|
+
const ourNode = this._root
|
|
310
|
+
.derivePath(`m/48'/${coinType}'/${this._account}'/2'`)
|
|
311
|
+
.neutered();
|
|
312
|
+
if (ourXpub) {
|
|
313
|
+
const parsed = (0, utils_1.parseExtendedPublicKey)(ourXpub, this._network);
|
|
314
|
+
if (parsed.isErr())
|
|
315
|
+
throw parsed.error;
|
|
316
|
+
if (keyId(parsed.value.node) !== keyId(ourNode)) {
|
|
317
|
+
throw new Error(`ourXpub does not match the account xpub derived from the mnemonic at m/48'/${coinType}'/${this._account}'/2'.`);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
const existing = entries.find((e) => keyId(e.node) === keyId(ourNode));
|
|
321
|
+
if (existing) {
|
|
322
|
+
existing.isOurs = true;
|
|
323
|
+
}
|
|
324
|
+
else {
|
|
325
|
+
entries.push({ node: ourNode, isOurs: true });
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
else if (ourXpub) {
|
|
329
|
+
addCosigner(ourXpub);
|
|
330
|
+
}
|
|
331
|
+
const n = entries.length;
|
|
332
|
+
if (threshold > n) {
|
|
333
|
+
throw new Error(`Multisig threshold (${threshold}) exceeds the number of cosigners (${n}).`);
|
|
334
|
+
}
|
|
335
|
+
if (n > 15) {
|
|
336
|
+
throw new Error(`Multisig supports at most 15 cosigners, received ${n}.`);
|
|
337
|
+
}
|
|
338
|
+
entries.sort((a, b) => a.node.publicKey.compare(b.node.publicKey));
|
|
339
|
+
const id = (0, utils_1.generateWalletId)(Buffer.concat([
|
|
340
|
+
Buffer.from('sortedmulti'),
|
|
341
|
+
Buffer.from([threshold, n]),
|
|
342
|
+
...entries.map((e) => Buffer.concat([e.node.publicKey, e.node.chainCode]))
|
|
343
|
+
]));
|
|
344
|
+
return { config: { threshold, cosigners: entries }, id };
|
|
345
|
+
}
|
|
346
|
+
getMultisigPayment(path) {
|
|
347
|
+
try {
|
|
348
|
+
if (!this._multisig) {
|
|
349
|
+
return (0, utils_1.err)('This wallet is not a multisig wallet.');
|
|
350
|
+
}
|
|
351
|
+
const parsed = (0, utils_1.parseBip48Path)(path);
|
|
352
|
+
if (parsed.isErr())
|
|
353
|
+
return (0, utils_1.err)(parsed.error.message);
|
|
354
|
+
const { coinType, account, change, index } = parsed.value;
|
|
355
|
+
const expectedCoin = this._network === types_1.EAvailableNetworks.bitcoin ? '0' : '1';
|
|
356
|
+
if (coinType !== expectedCoin || account !== this._account) {
|
|
357
|
+
return (0, utils_1.err)(`Path ${path} does not match this wallet (expected m/48'/${expectedCoin}'/${this._account}'/2'/change/index).`);
|
|
358
|
+
}
|
|
359
|
+
const payment = (0, utils_1.buildSortedMultisigPayment)({
|
|
360
|
+
threshold: this._multisig.threshold,
|
|
361
|
+
cosignerNodes: this._multisig.cosigners.map((c) => c.node),
|
|
362
|
+
change,
|
|
363
|
+
index,
|
|
364
|
+
network: this.getBitcoinNetwork()
|
|
365
|
+
});
|
|
366
|
+
if (payment.isErr())
|
|
367
|
+
return (0, utils_1.err)(payment.error.message);
|
|
368
|
+
const perCosigner = this._multisig.cosigners
|
|
369
|
+
.map((c) => {
|
|
370
|
+
let masterFingerprint;
|
|
371
|
+
if (c.isOurs && this._root) {
|
|
372
|
+
masterFingerprint = Buffer.from(this._root.fingerprint);
|
|
373
|
+
}
|
|
374
|
+
else {
|
|
375
|
+
masterFingerprint = Buffer.alloc(4);
|
|
376
|
+
masterFingerprint.writeUInt32BE(c.node.parentFingerprint ?? 0, 0);
|
|
377
|
+
}
|
|
378
|
+
return {
|
|
379
|
+
masterFingerprint,
|
|
380
|
+
path,
|
|
381
|
+
pubkey: c.node.derive(change).derive(index).publicKey,
|
|
382
|
+
isOurs: c.isOurs
|
|
383
|
+
};
|
|
384
|
+
})
|
|
385
|
+
.sort((a, b) => a.pubkey.compare(b.pubkey));
|
|
386
|
+
const ours = perCosigner.find((d) => d.isOurs);
|
|
387
|
+
return (0, utils_1.ok)({
|
|
388
|
+
address: payment.value.address,
|
|
389
|
+
output: payment.value.output,
|
|
390
|
+
witnessScript: payment.value.witnessScript,
|
|
391
|
+
publicKey: ours && this._root ? ours.pubkey.toString('hex') : '',
|
|
392
|
+
derivations: perCosigner.map((d) => ({
|
|
393
|
+
masterFingerprint: d.masterFingerprint,
|
|
394
|
+
path: d.path,
|
|
395
|
+
pubkey: d.pubkey
|
|
396
|
+
}))
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
catch (e) {
|
|
400
|
+
return (0, utils_1.err)(e);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
204
403
|
async stop() {
|
|
205
404
|
try {
|
|
206
405
|
if (this.isRefreshing) {
|
|
@@ -219,6 +418,12 @@ class Wallet {
|
|
|
219
418
|
this.coinSelectPreference = coinSelectPreference;
|
|
220
419
|
}
|
|
221
420
|
async switchNetwork(network, servers) {
|
|
421
|
+
if (this._multisig) {
|
|
422
|
+
return (0, utils_1.err)('Multisig wallets cannot switch networks. Create a new multisig wallet with cosigner xpubs for the target network.');
|
|
423
|
+
}
|
|
424
|
+
if (this.isWatchOnly) {
|
|
425
|
+
return (0, utils_1.err)('Watch-only wallets cannot switch networks. Create a new watch-only wallet with an xpub for the target network.');
|
|
426
|
+
}
|
|
222
427
|
this.isSwitchingNetworks = true;
|
|
223
428
|
await this.electrum.disconnect();
|
|
224
429
|
this._network = network;
|
|
@@ -227,6 +432,8 @@ class Wallet {
|
|
|
227
432
|
...this,
|
|
228
433
|
mnemonic: this._mnemonic,
|
|
229
434
|
passphrase: this._passphrase,
|
|
435
|
+
account: Number(this._account),
|
|
436
|
+
birthdayHeight: this._birthdayHeightOption,
|
|
230
437
|
network,
|
|
231
438
|
electrumOptions: {
|
|
232
439
|
servers,
|
|
@@ -249,6 +456,12 @@ class Wallet {
|
|
|
249
456
|
return (0, utils_1.ok)(this);
|
|
250
457
|
}
|
|
251
458
|
async updateAddressType(addressType) {
|
|
459
|
+
if (addressType === types_1.EAddressType.p2wsh && !this._multisig) {
|
|
460
|
+
throw new Error('p2wsh is a multisig address type. Use Wallet.createMultisig.');
|
|
461
|
+
}
|
|
462
|
+
if (this._multisig && addressType !== types_1.EAddressType.p2wsh) {
|
|
463
|
+
throw new Error('Multisig wallets only support the p2wsh address type.');
|
|
464
|
+
}
|
|
252
465
|
this.addressType = addressType;
|
|
253
466
|
if (!this.addressTypesToMonitor.includes(this.addressType)) {
|
|
254
467
|
this.addressTypesToMonitor.push(this.addressType);
|
|
@@ -324,6 +537,7 @@ class Wallet {
|
|
|
324
537
|
if (walletDataResponse.isErr())
|
|
325
538
|
return (0, utils_1.err)(walletDataResponse.error.message);
|
|
326
539
|
this._data = walletDataResponse.value;
|
|
540
|
+
await this._applyBirthdayHeightOption();
|
|
327
541
|
return (0, utils_1.ok)(true);
|
|
328
542
|
}
|
|
329
543
|
catch (e) {
|
|
@@ -340,13 +554,25 @@ class Wallet {
|
|
|
340
554
|
}
|
|
341
555
|
if (res.value !== id) {
|
|
342
556
|
const msg = 'Mismatched id found in storage. Change the wallet name or delete the old wallet from storage and try again.';
|
|
343
|
-
|
|
557
|
+
this.logger.warn(msg);
|
|
344
558
|
return (0, utils_1.err)(msg);
|
|
345
559
|
}
|
|
346
560
|
return (0, utils_1.ok)("ID's match, it's safe to continue.");
|
|
347
561
|
}
|
|
348
562
|
getWalletDataKey(key) {
|
|
349
|
-
return (0, utils_1.getWalletDataStorageKey)(this.name, this._network, key);
|
|
563
|
+
return (0, utils_1.getWalletDataStorageKey)(this.name, this._network, key, Number(this._account));
|
|
564
|
+
}
|
|
565
|
+
async _applyBirthdayHeightOption() {
|
|
566
|
+
if (this._birthdayHeightOption === undefined)
|
|
567
|
+
return;
|
|
568
|
+
const stored = this._data.birthdayHeight ?? 0;
|
|
569
|
+
const next = stored > 0
|
|
570
|
+
? Math.min(stored, this._birthdayHeightOption)
|
|
571
|
+
: this._birthdayHeightOption;
|
|
572
|
+
if (next !== stored) {
|
|
573
|
+
this._data.birthdayHeight = next;
|
|
574
|
+
await this.saveWalletData('birthdayHeight', next);
|
|
575
|
+
}
|
|
350
576
|
}
|
|
351
577
|
async getWalletData() {
|
|
352
578
|
try {
|
|
@@ -363,7 +589,7 @@ class Wallet {
|
|
|
363
589
|
dataResult = getDataRes?.value;
|
|
364
590
|
}
|
|
365
591
|
catch (e) {
|
|
366
|
-
|
|
592
|
+
this.logger.error('Failed to read wallet data from storage.', e);
|
|
367
593
|
}
|
|
368
594
|
const data = dataResult ?? walletData[key];
|
|
369
595
|
switch (key) {
|
|
@@ -413,28 +639,98 @@ class Wallet {
|
|
|
413
639
|
case 'feeEstimates':
|
|
414
640
|
walletData[key] = data;
|
|
415
641
|
break;
|
|
642
|
+
case 'addressLabels':
|
|
643
|
+
walletData[key] = data;
|
|
644
|
+
break;
|
|
645
|
+
case 'birthdayHeight':
|
|
646
|
+
walletData[key] = data;
|
|
647
|
+
break;
|
|
416
648
|
default:
|
|
417
|
-
|
|
649
|
+
this.logger.warn(`Unhandled key in getWalletData: ${key}`);
|
|
418
650
|
break;
|
|
419
651
|
}
|
|
420
652
|
}));
|
|
421
653
|
return (0, utils_1.ok)(walletData);
|
|
422
654
|
}
|
|
423
655
|
catch (e) {
|
|
424
|
-
|
|
656
|
+
this.logger.error('Failed to get wallet data.', e);
|
|
425
657
|
return (0, utils_1.err)(e);
|
|
426
658
|
}
|
|
427
659
|
}
|
|
428
660
|
getBitcoinNetwork(network) {
|
|
429
661
|
if (!network)
|
|
430
662
|
network = this._network;
|
|
431
|
-
return
|
|
663
|
+
return (0, utils_1.getBitcoinJsNetwork)(network);
|
|
432
664
|
}
|
|
433
665
|
isValid(mnemonic) {
|
|
434
|
-
return
|
|
666
|
+
return (!!this._mnemonic &&
|
|
667
|
+
mnemonic === this._mnemonic &&
|
|
668
|
+
(0, utils_1.validateMnemonic)(mnemonic));
|
|
669
|
+
}
|
|
670
|
+
_derivePathKeyPair(path) {
|
|
671
|
+
if (this._root)
|
|
672
|
+
return this._root.derivePath(path);
|
|
673
|
+
return this._deriveWatchOnlyNode(path);
|
|
674
|
+
}
|
|
675
|
+
_deriveWatchOnlyNode(path) {
|
|
676
|
+
if (!this._accountNode) {
|
|
677
|
+
throw new Error('Watch-only account node is unavailable.');
|
|
678
|
+
}
|
|
679
|
+
const segments = path.replace(/'/g, '').split('/');
|
|
680
|
+
if (segments.length !== 6) {
|
|
681
|
+
throw new Error(`Watch-only wallets require a full path (m/purpose'/coin'/account'/change/index): ${path}`);
|
|
682
|
+
}
|
|
683
|
+
const expectedPathRes = (0, utils_1.getKeyDerivationPathObject)({
|
|
684
|
+
path: shapes_1.addressTypes[this.addressType].path,
|
|
685
|
+
network: this._network
|
|
686
|
+
});
|
|
687
|
+
if (expectedPathRes.isErr())
|
|
688
|
+
throw expectedPathRes.error;
|
|
689
|
+
if (segments[1] !== expectedPathRes.value.purpose) {
|
|
690
|
+
throw new Error(`This watch-only wallet only derives ${this.addressType} paths (purpose ${expectedPathRes.value.purpose}'), received: ${path}`);
|
|
691
|
+
}
|
|
692
|
+
const change = Number(segments[4]);
|
|
693
|
+
const index = Number(segments[5]);
|
|
694
|
+
if (!Number.isInteger(change) ||
|
|
695
|
+
!Number.isInteger(index) ||
|
|
696
|
+
change < 0 ||
|
|
697
|
+
index < 0) {
|
|
698
|
+
throw new Error(`Invalid change/index segments in path: ${path}`);
|
|
699
|
+
}
|
|
700
|
+
return this._accountNode.derive(change).derive(index);
|
|
701
|
+
}
|
|
702
|
+
derivePublicNode(path) {
|
|
703
|
+
try {
|
|
704
|
+
if (this._root)
|
|
705
|
+
return (0, utils_1.ok)(this._root.derivePath(path).neutered());
|
|
706
|
+
return (0, utils_1.ok)(this._deriveWatchOnlyNode(path));
|
|
707
|
+
}
|
|
708
|
+
catch (e) {
|
|
709
|
+
return (0, utils_1.err)(e);
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
getMasterFingerprint() {
|
|
713
|
+
if (this._root)
|
|
714
|
+
return Buffer.from(this._root.fingerprint);
|
|
715
|
+
const fingerprint = Buffer.alloc(4);
|
|
716
|
+
fingerprint.writeUInt32BE(this._accountNode?.parentFingerprint ?? 0, 0);
|
|
717
|
+
return fingerprint;
|
|
435
718
|
}
|
|
436
719
|
async _getAddress(path, addressType) {
|
|
437
720
|
try {
|
|
721
|
+
if (addressType === types_1.EAddressType.p2wsh) {
|
|
722
|
+
if (!this._multisig) {
|
|
723
|
+
return (0, utils_1.err)('p2wsh addresses require a multisig wallet (Wallet.createMultisig).');
|
|
724
|
+
}
|
|
725
|
+
const payment = this.getMultisigPayment(path);
|
|
726
|
+
if (payment.isErr())
|
|
727
|
+
return (0, utils_1.err)(payment.error.message);
|
|
728
|
+
return (0, utils_1.ok)({
|
|
729
|
+
address: payment.value.address,
|
|
730
|
+
publicKey: payment.value.publicKey,
|
|
731
|
+
path
|
|
732
|
+
});
|
|
733
|
+
}
|
|
438
734
|
if (this._customGetAddress) {
|
|
439
735
|
const data = {
|
|
440
736
|
path,
|
|
@@ -445,7 +741,7 @@ class Wallet {
|
|
|
445
741
|
if (res.isErr())
|
|
446
742
|
return (0, utils_1.err)(res.error.message);
|
|
447
743
|
}
|
|
448
|
-
const keyPair = this.
|
|
744
|
+
const keyPair = this._derivePathKeyPair(path);
|
|
449
745
|
const network = this.getBitcoinNetwork(this._network);
|
|
450
746
|
const addressInfo = (0, utils_1.getAddressFromKeyPair)({
|
|
451
747
|
keyPair,
|
|
@@ -473,6 +769,7 @@ class Wallet {
|
|
|
473
769
|
addressType,
|
|
474
770
|
changeAddress,
|
|
475
771
|
index,
|
|
772
|
+
accountType: this._account,
|
|
476
773
|
network: this._network
|
|
477
774
|
});
|
|
478
775
|
if (pathRes.isErr()) {
|
|
@@ -515,7 +812,7 @@ class Wallet {
|
|
|
515
812
|
});
|
|
516
813
|
let msg = 'Unable to connect to Electrum server.';
|
|
517
814
|
if (res.isErr()) {
|
|
518
|
-
|
|
815
|
+
this.logger.warn(msg);
|
|
519
816
|
return (0, utils_1.err)(msg);
|
|
520
817
|
}
|
|
521
818
|
msg = 'Connected to Electrum server.';
|
|
@@ -559,6 +856,8 @@ class Wallet {
|
|
|
559
856
|
return (0, utils_1.getScriptHash)({ address, network });
|
|
560
857
|
}
|
|
561
858
|
getPrivateKey(path) {
|
|
859
|
+
if (!this._root)
|
|
860
|
+
throw new utils_1.WatchOnlySigningError();
|
|
562
861
|
const keyPair = this._root.derivePath(path);
|
|
563
862
|
return keyPair.toWIF();
|
|
564
863
|
}
|
|
@@ -582,6 +881,7 @@ class Wallet {
|
|
|
582
881
|
if (keyDerivationPathResponse.isErr())
|
|
583
882
|
return (0, utils_1.err)(keyDerivationPathResponse.error.message);
|
|
584
883
|
keyDerivationPath = keyDerivationPathResponse.value;
|
|
884
|
+
keyDerivationPath.account = this._account;
|
|
585
885
|
}
|
|
586
886
|
const addresses = {};
|
|
587
887
|
const changeAddresses = {};
|
|
@@ -681,6 +981,7 @@ class Wallet {
|
|
|
681
981
|
return (0, utils_1.err)(result.error.message);
|
|
682
982
|
}
|
|
683
983
|
const { pathObject: keyDerivationPath } = result.value;
|
|
984
|
+
keyDerivationPath.account = this._account;
|
|
684
985
|
let addressIndex = currentWallet.addressIndex[addressType];
|
|
685
986
|
let lastUsedAddressIndex = currentWallet.lastUsedAddressIndex[addressType];
|
|
686
987
|
let changeAddressIndex = currentWallet.changeAddressIndex[addressType];
|
|
@@ -782,7 +1083,7 @@ class Wallet {
|
|
|
782
1083
|
scriptHashes: combinedAddressesToScan
|
|
783
1084
|
});
|
|
784
1085
|
if (addressHistory.isErr()) {
|
|
785
|
-
|
|
1086
|
+
this.logger.warn(addressHistory.error.message);
|
|
786
1087
|
return lastKnownIndexes;
|
|
787
1088
|
}
|
|
788
1089
|
const txHashes = addressHistory.value;
|
|
@@ -794,7 +1095,7 @@ class Wallet {
|
|
|
794
1095
|
changeAddressIndex: lastUsedChangeAddressIndex
|
|
795
1096
|
});
|
|
796
1097
|
if (highestUsedIndex.isErr()) {
|
|
797
|
-
|
|
1098
|
+
this.logger.warn(highestUsedIndex.error.message);
|
|
798
1099
|
return lastKnownIndexes;
|
|
799
1100
|
}
|
|
800
1101
|
if (highestUsedIndex.value.foundAddressIndex) {
|
|
@@ -812,7 +1113,7 @@ class Wallet {
|
|
|
812
1113
|
addressType
|
|
813
1114
|
});
|
|
814
1115
|
if (highestStoredIndex.isErr()) {
|
|
815
|
-
|
|
1116
|
+
this.logger.warn(highestStoredIndex.error.message);
|
|
816
1117
|
return lastKnownIndexes;
|
|
817
1118
|
}
|
|
818
1119
|
const { addressIndex: highestUsedAddressIndex, changeAddressIndex: highestUsedChangeAddressIndex } = highestUsedIndex.value;
|
|
@@ -941,6 +1242,7 @@ class Wallet {
|
|
|
941
1242
|
return (0, utils_1.err)(keyDerivationPathResponse.error.message);
|
|
942
1243
|
}
|
|
943
1244
|
keyDerivationPath = keyDerivationPathResponse.value;
|
|
1245
|
+
keyDerivationPath.account = this._account;
|
|
944
1246
|
}
|
|
945
1247
|
const generatedAddresses = await this.generateAddresses({
|
|
946
1248
|
addressAmount,
|
|
@@ -1141,6 +1443,7 @@ class Wallet {
|
|
|
1141
1443
|
return (0, utils_1.err)(keyDerivationPathResponse.error.message);
|
|
1142
1444
|
}
|
|
1143
1445
|
keyDerivationPath = keyDerivationPathResponse.value;
|
|
1446
|
+
keyDerivationPath.account = this._account;
|
|
1144
1447
|
}
|
|
1145
1448
|
const addresses = currentWallet.addresses[addressType];
|
|
1146
1449
|
const currentAddressIndex = currentWallet.addressIndex[addressType].index;
|
|
@@ -1234,6 +1537,174 @@ class Wallet {
|
|
|
1234
1537
|
listUtxos() {
|
|
1235
1538
|
return this.data.utxos;
|
|
1236
1539
|
}
|
|
1540
|
+
isUtxoFrozen(txid, index) {
|
|
1541
|
+
return this.data.blacklistedUtxos.some((frozen) => frozen.tx_hash === txid && frozen.tx_pos === index);
|
|
1542
|
+
}
|
|
1543
|
+
async freezeUtxo({ txid, index }) {
|
|
1544
|
+
if (typeof txid !== 'string' || !/^[0-9a-fA-F]{64}$/.test(txid)) {
|
|
1545
|
+
return (0, utils_1.err)('txid must be a 64-character hex string.');
|
|
1546
|
+
}
|
|
1547
|
+
if (!Number.isInteger(index) || index < 0) {
|
|
1548
|
+
return (0, utils_1.err)('index must be a non-negative integer.');
|
|
1549
|
+
}
|
|
1550
|
+
const utxo = this.data.utxos.find((u) => u.tx_hash === txid && u.tx_pos === index);
|
|
1551
|
+
if (!utxo) {
|
|
1552
|
+
return (0, utils_1.err)(`UTXO ${txid}:${index} is not known to this wallet.`);
|
|
1553
|
+
}
|
|
1554
|
+
if (this.isUtxoFrozen(txid, index)) {
|
|
1555
|
+
return (0, utils_1.ok)(`UTXO ${txid}:${index} is already frozen.`);
|
|
1556
|
+
}
|
|
1557
|
+
const { keyPair, ...frozen } = utxo;
|
|
1558
|
+
void keyPair;
|
|
1559
|
+
this._data.blacklistedUtxos.push(frozen);
|
|
1560
|
+
await this.saveWalletData('blacklistedUtxos', this._data.blacklistedUtxos);
|
|
1561
|
+
return (0, utils_1.ok)(`UTXO ${txid}:${index} frozen.`);
|
|
1562
|
+
}
|
|
1563
|
+
async unfreezeUtxo({ txid, index }) {
|
|
1564
|
+
const before = this._data.blacklistedUtxos.length;
|
|
1565
|
+
this._data.blacklistedUtxos = this._data.blacklistedUtxos.filter((frozen) => !(frozen.tx_hash === txid && frozen.tx_pos === index));
|
|
1566
|
+
if (this._data.blacklistedUtxos.length === before) {
|
|
1567
|
+
return (0, utils_1.err)(`UTXO ${txid}:${index} is not frozen.`);
|
|
1568
|
+
}
|
|
1569
|
+
await this.saveWalletData('blacklistedUtxos', this._data.blacklistedUtxos);
|
|
1570
|
+
return (0, utils_1.ok)(`UTXO ${txid}:${index} unfrozen.`);
|
|
1571
|
+
}
|
|
1572
|
+
listFrozenUtxos() {
|
|
1573
|
+
return this.data.blacklistedUtxos;
|
|
1574
|
+
}
|
|
1575
|
+
getBalanceBreakdown() {
|
|
1576
|
+
const total = this.getBalance();
|
|
1577
|
+
const frozen = this.data.utxos.reduce((acc, utxo) => {
|
|
1578
|
+
return this.isUtxoFrozen(utxo.tx_hash, utxo.tx_pos)
|
|
1579
|
+
? acc + utxo.value
|
|
1580
|
+
: acc;
|
|
1581
|
+
}, 0);
|
|
1582
|
+
return { total, spendable: total - frozen, frozen };
|
|
1583
|
+
}
|
|
1584
|
+
async setAddressLabel(address, label) {
|
|
1585
|
+
if (!address || !this.validateAddress(address)) {
|
|
1586
|
+
return (0, utils_1.err)(`Invalid ${this._network} address: ${address}`);
|
|
1587
|
+
}
|
|
1588
|
+
if (typeof label !== 'string') {
|
|
1589
|
+
return (0, utils_1.err)('label must be a string.');
|
|
1590
|
+
}
|
|
1591
|
+
if (label.length > 255) {
|
|
1592
|
+
return (0, utils_1.err)('label must be 255 characters or fewer.');
|
|
1593
|
+
}
|
|
1594
|
+
if (label === '') {
|
|
1595
|
+
delete this._data.addressLabels[address];
|
|
1596
|
+
}
|
|
1597
|
+
else {
|
|
1598
|
+
this._data.addressLabels[address] = label;
|
|
1599
|
+
}
|
|
1600
|
+
await this.saveWalletData('addressLabels', this._data.addressLabels);
|
|
1601
|
+
return (0, utils_1.ok)(label === '' ? 'Label removed.' : 'Label saved.');
|
|
1602
|
+
}
|
|
1603
|
+
getAddressLabel(address) {
|
|
1604
|
+
return this.data.addressLabels[address];
|
|
1605
|
+
}
|
|
1606
|
+
listAddressLabels() {
|
|
1607
|
+
return { ...this.data.addressLabels };
|
|
1608
|
+
}
|
|
1609
|
+
exportDescriptors() {
|
|
1610
|
+
try {
|
|
1611
|
+
const fingerprint = this.getMasterFingerprint().toString('hex');
|
|
1612
|
+
const descriptors = [];
|
|
1613
|
+
const wrap = (addressType, keyExpr) => {
|
|
1614
|
+
switch (addressType) {
|
|
1615
|
+
case types_1.EAddressType.p2pkh:
|
|
1616
|
+
return `pkh(${keyExpr})`;
|
|
1617
|
+
case types_1.EAddressType.p2sh:
|
|
1618
|
+
return `sh(wpkh(${keyExpr}))`;
|
|
1619
|
+
case types_1.EAddressType.p2wpkh:
|
|
1620
|
+
return `wpkh(${keyExpr})`;
|
|
1621
|
+
case types_1.EAddressType.p2tr:
|
|
1622
|
+
return `tr(${keyExpr})`;
|
|
1623
|
+
case types_1.EAddressType.p2wsh:
|
|
1624
|
+
return `wsh(${keyExpr})`;
|
|
1625
|
+
}
|
|
1626
|
+
};
|
|
1627
|
+
if (this._multisig) {
|
|
1628
|
+
const coinType = this._network === types_1.EAvailableNetworks.bitcoin ? '0' : '1';
|
|
1629
|
+
const keyExpr = (chain) => {
|
|
1630
|
+
const keys = this._multisig.cosigners.map((cosigner) => {
|
|
1631
|
+
const xpub = this._toStandardBase58(cosigner.node);
|
|
1632
|
+
let origin;
|
|
1633
|
+
if (cosigner.isOurs && this._root) {
|
|
1634
|
+
origin = `[${fingerprint}/48h/${coinType}h/${this._account}h/2h]`;
|
|
1635
|
+
}
|
|
1636
|
+
else {
|
|
1637
|
+
const parentFp = Buffer.alloc(4);
|
|
1638
|
+
parentFp.writeUInt32BE(cosigner.node.parentFingerprint ?? 0, 0);
|
|
1639
|
+
origin = `[${parentFp.toString('hex')}]`;
|
|
1640
|
+
}
|
|
1641
|
+
return `${origin}${xpub}/${chain}/*`;
|
|
1642
|
+
});
|
|
1643
|
+
return `sortedmulti(${this._multisig.threshold},${keys.join(',')})`;
|
|
1644
|
+
};
|
|
1645
|
+
descriptors.push({
|
|
1646
|
+
addressType: types_1.EAddressType.p2wsh,
|
|
1647
|
+
external: (0, utils_1.appendDescriptorChecksum)(`wsh(${keyExpr(0)})`),
|
|
1648
|
+
internal: (0, utils_1.appendDescriptorChecksum)(`wsh(${keyExpr(1)})`)
|
|
1649
|
+
});
|
|
1650
|
+
}
|
|
1651
|
+
else if (this.isWatchOnly) {
|
|
1652
|
+
if (!this._accountNode) {
|
|
1653
|
+
return (0, utils_1.err)('Watch-only account node is unavailable.');
|
|
1654
|
+
}
|
|
1655
|
+
const xpub = this._toStandardBase58(this._accountNode);
|
|
1656
|
+
descriptors.push({
|
|
1657
|
+
addressType: this.addressType,
|
|
1658
|
+
external: (0, utils_1.appendDescriptorChecksum)(wrap(this.addressType, `${xpub}/0/*`)),
|
|
1659
|
+
internal: (0, utils_1.appendDescriptorChecksum)(wrap(this.addressType, `${xpub}/1/*`))
|
|
1660
|
+
});
|
|
1661
|
+
}
|
|
1662
|
+
else {
|
|
1663
|
+
if (!this._root)
|
|
1664
|
+
return (0, utils_1.err)('Wallet root is unavailable.');
|
|
1665
|
+
const coinType = this._network === types_1.EAvailableNetworks.bitcoin ? '0' : '1';
|
|
1666
|
+
const singleSigTypes = Object.values(types_1.EAddressType).filter((type) => type !== types_1.EAddressType.p2wsh);
|
|
1667
|
+
for (const addressType of singleSigTypes) {
|
|
1668
|
+
const pathRes = (0, utils_1.getKeyDerivationPathObject)({
|
|
1669
|
+
path: shapes_1.addressTypes[addressType].path,
|
|
1670
|
+
network: this._network
|
|
1671
|
+
});
|
|
1672
|
+
if (pathRes.isErr())
|
|
1673
|
+
return (0, utils_1.err)(pathRes.error.message);
|
|
1674
|
+
const purpose = pathRes.value.purpose;
|
|
1675
|
+
const accountPath = `m/${purpose}'/${coinType}'/${this._account}'`;
|
|
1676
|
+
const accountXpub = this._toStandardBase58(this._root.derivePath(accountPath).neutered());
|
|
1677
|
+
const origin = `[${fingerprint}/${purpose}h/${coinType}h/${this._account}h]`;
|
|
1678
|
+
descriptors.push({
|
|
1679
|
+
addressType,
|
|
1680
|
+
external: (0, utils_1.appendDescriptorChecksum)(wrap(addressType, `${origin}${accountXpub}/0/*`)),
|
|
1681
|
+
internal: (0, utils_1.appendDescriptorChecksum)(wrap(addressType, `${origin}${accountXpub}/1/*`))
|
|
1682
|
+
});
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
const birthdayHeight = this.birthdayHeight;
|
|
1686
|
+
return (0, utils_1.ok)({
|
|
1687
|
+
fingerprint,
|
|
1688
|
+
network: this._network,
|
|
1689
|
+
account: Number(this._account),
|
|
1690
|
+
...(birthdayHeight > 0 ? { birthdayHeight } : {}),
|
|
1691
|
+
watchOnly: this.isWatchOnly,
|
|
1692
|
+
descriptors
|
|
1693
|
+
});
|
|
1694
|
+
}
|
|
1695
|
+
catch (e) {
|
|
1696
|
+
return (0, utils_1.err)(e);
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
_toStandardBase58(node) {
|
|
1700
|
+
const standard = this.getBitcoinNetwork(this._network);
|
|
1701
|
+
if (node.network?.bip32?.public === standard.bip32.public) {
|
|
1702
|
+
return node.toBase58();
|
|
1703
|
+
}
|
|
1704
|
+
const clone = Object.create(Object.getPrototypeOf(node), Object.getOwnPropertyDescriptors(node));
|
|
1705
|
+
clone.network = standard;
|
|
1706
|
+
return clone.toBase58();
|
|
1707
|
+
}
|
|
1237
1708
|
async saveWalletData(key, data) {
|
|
1238
1709
|
if (!this._setData)
|
|
1239
1710
|
return 'No setData method has been provided';
|
|
@@ -1604,7 +2075,7 @@ class Wallet {
|
|
|
1604
2075
|
return (0, utils_1.ok)('Successfully updated unconfirmed transactions.');
|
|
1605
2076
|
}
|
|
1606
2077
|
catch (e) {
|
|
1607
|
-
|
|
2078
|
+
this.logger.error('Failed to update unconfirmed transactions.', e);
|
|
1608
2079
|
return (0, utils_1.err)(e);
|
|
1609
2080
|
}
|
|
1610
2081
|
}
|
|
@@ -1821,12 +2292,12 @@ class Wallet {
|
|
|
1821
2292
|
return { addresses, value, key };
|
|
1822
2293
|
}
|
|
1823
2294
|
_logGetInputDataError(error, data) {
|
|
1824
|
-
|
|
2295
|
+
this.logger.error('\nError:', error);
|
|
1825
2296
|
if (data) {
|
|
1826
|
-
|
|
2297
|
+
this.logger.warn('Unable to retrieve input data for:', data);
|
|
1827
2298
|
}
|
|
1828
2299
|
if (error?.code && error.code === -32600)
|
|
1829
|
-
|
|
2300
|
+
this.logger.info('Suggestion: Please increase the response limit on your Electrum server.');
|
|
1830
2301
|
}
|
|
1831
2302
|
validateAddress(address) {
|
|
1832
2303
|
return (0, utils_1.validateAddress)({ address, network: this._network }).isValid;
|
|
@@ -1844,30 +2315,65 @@ class Wallet {
|
|
|
1844
2315
|
addressType
|
|
1845
2316
|
});
|
|
1846
2317
|
if (generateAddressResponse.isErr()) {
|
|
1847
|
-
|
|
2318
|
+
this.logger.warn(generateAddressResponse.error.message);
|
|
1848
2319
|
return (0, utils_1.err)('Unable to successfully generate a change address.');
|
|
1849
2320
|
}
|
|
1850
2321
|
return (0, utils_1.ok)(generateAddressResponse.value.changeAddresses[0]);
|
|
1851
2322
|
}
|
|
1852
2323
|
async getFeeEstimates(network = this._network) {
|
|
2324
|
+
if (network === types_1.EAvailableNetworks.bitcoinRegtest) {
|
|
2325
|
+
return { ...shapes_1.defaultFeesShape, timestamp: Date.now() };
|
|
2326
|
+
}
|
|
2327
|
+
if (this.feeEstimationSource !== 'http') {
|
|
2328
|
+
const electrumFees = await this.getFeeEstimatesFromElectrum();
|
|
2329
|
+
if (electrumFees.isOk()) {
|
|
2330
|
+
return electrumFees.value;
|
|
2331
|
+
}
|
|
2332
|
+
if (this.feeEstimationSource === 'electrum') {
|
|
2333
|
+
return this.feeEstimates;
|
|
2334
|
+
}
|
|
2335
|
+
}
|
|
2336
|
+
return this.getFeeEstimatesFromHttp(network);
|
|
2337
|
+
}
|
|
2338
|
+
async getFeeEstimatesFromElectrum() {
|
|
2339
|
+
if (!this.electrum.connectedToElectrum) {
|
|
2340
|
+
return (0, utils_1.err)('Not connected to an Electrum server.');
|
|
2341
|
+
}
|
|
2342
|
+
const targets = [2, 6, 24, 144];
|
|
2343
|
+
const results = await Promise.all(targets.map((blocks) => this.electrum.getFeeEstimate(blocks)));
|
|
2344
|
+
const [fast, normal, slow, minimum] = results;
|
|
2345
|
+
if (fast.isErr() || normal.isErr() || slow.isErr() || minimum.isErr()) {
|
|
2346
|
+
return (0, utils_1.err)('Electrum returned unusable fee estimates.');
|
|
2347
|
+
}
|
|
2348
|
+
return (0, utils_1.ok)({
|
|
2349
|
+
fast: fast.value,
|
|
2350
|
+
normal: normal.value,
|
|
2351
|
+
slow: slow.value,
|
|
2352
|
+
minimum: minimum.value,
|
|
2353
|
+
timestamp: Date.now()
|
|
2354
|
+
});
|
|
2355
|
+
}
|
|
2356
|
+
async getFeeEstimatesFromHttp(network = this._network) {
|
|
1853
2357
|
try {
|
|
1854
|
-
|
|
1855
|
-
|
|
2358
|
+
let urlModifier = '';
|
|
2359
|
+
if (network !== types_1.EAvailableNetworks.bitcoinMainnet) {
|
|
2360
|
+
urlModifier =
|
|
2361
|
+
network === types_1.EAvailableNetworks.bitcoinSignet ? 'signet/' : 'testnet/';
|
|
1856
2362
|
}
|
|
1857
|
-
const urlModifier = network === 'bitcoin' ? '' : 'testnet/';
|
|
1858
2363
|
const response = await fetch(`https://mempool.space/${urlModifier}api/v1/fees/recommended`);
|
|
1859
2364
|
const res = await response.json();
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
2365
|
+
const fast = (0, utils_1.clampFeeRate)(res.fastestFee);
|
|
2366
|
+
const normal = (0, utils_1.clampFeeRate)(res.halfHourFee);
|
|
2367
|
+
const slow = (0, utils_1.clampFeeRate)(res.hourFee);
|
|
2368
|
+
const minimum = (0, utils_1.clampFeeRate)(res.minimumFee);
|
|
2369
|
+
if (!(fast > 0 && normal > 0 && slow > 0 && minimum > 0)) {
|
|
1864
2370
|
throw new Error('Unexpected response from mempool.space');
|
|
1865
2371
|
}
|
|
1866
2372
|
return {
|
|
1867
|
-
fast
|
|
1868
|
-
normal
|
|
1869
|
-
slow
|
|
1870
|
-
minimum
|
|
2373
|
+
fast,
|
|
2374
|
+
normal,
|
|
2375
|
+
slow,
|
|
2376
|
+
minimum,
|
|
1871
2377
|
timestamp: Date.now()
|
|
1872
2378
|
};
|
|
1873
2379
|
}
|
|
@@ -1888,17 +2394,22 @@ class Wallet {
|
|
|
1888
2394
|
res?.onchain?.feeRates?.slow > 0)) {
|
|
1889
2395
|
throw new Error('Unexpected response from blocktank');
|
|
1890
2396
|
}
|
|
1891
|
-
const
|
|
2397
|
+
const fast = (0, utils_1.clampFeeRate)(res.onchain.feeRates.fast);
|
|
2398
|
+
const normal = (0, utils_1.clampFeeRate)(res.onchain.feeRates.mid);
|
|
2399
|
+
const slow = (0, utils_1.clampFeeRate)(res.onchain.feeRates.slow);
|
|
2400
|
+
if (!(fast > 0 && normal > 0 && slow > 0)) {
|
|
2401
|
+
throw new Error('Unexpected response from blocktank');
|
|
2402
|
+
}
|
|
1892
2403
|
return {
|
|
1893
2404
|
fast,
|
|
1894
|
-
normal
|
|
2405
|
+
normal,
|
|
1895
2406
|
slow,
|
|
1896
2407
|
minimum: slow,
|
|
1897
2408
|
timestamp: Date.now()
|
|
1898
2409
|
};
|
|
1899
2410
|
}
|
|
1900
2411
|
catch (e) {
|
|
1901
|
-
|
|
2412
|
+
this.logger.warn('Unable to fetch fee estimates.', e);
|
|
1902
2413
|
return this.feeEstimates;
|
|
1903
2414
|
}
|
|
1904
2415
|
}
|
|
@@ -1915,6 +2426,10 @@ class Wallet {
|
|
|
1915
2426
|
});
|
|
1916
2427
|
}
|
|
1917
2428
|
async sendMany({ txs = [], satsPerByte = this.feeEstimates.normal, rbf, broadcast = true, shuffleOutputs = true }) {
|
|
2429
|
+
if (this._multisig)
|
|
2430
|
+
return (0, utils_1.err)(new utils_1.MultisigSpendError());
|
|
2431
|
+
if (this.isWatchOnly)
|
|
2432
|
+
return (0, utils_1.err)(new utils_1.WatchOnlySigningError());
|
|
1918
2433
|
if (!this.data.utxos.length) {
|
|
1919
2434
|
return (0, utils_1.err)('No UTXOs available.');
|
|
1920
2435
|
}
|
|
@@ -1968,6 +2483,10 @@ class Wallet {
|
|
|
1968
2483
|
return (0, utils_1.ok)(broadcastRes.value);
|
|
1969
2484
|
}
|
|
1970
2485
|
async sendMax({ address, satsPerByte, rbf = false, broadcast = true } = {}) {
|
|
2486
|
+
if (this._multisig)
|
|
2487
|
+
return (0, utils_1.err)(new utils_1.MultisigSpendError());
|
|
2488
|
+
if (this.isWatchOnly)
|
|
2489
|
+
return (0, utils_1.err)(new utils_1.WatchOnlySigningError());
|
|
1971
2490
|
if (!this.data.utxos.length) {
|
|
1972
2491
|
return (0, utils_1.err)('No UTXOs available.');
|
|
1973
2492
|
}
|
|
@@ -2009,6 +2528,242 @@ class Wallet {
|
|
|
2009
2528
|
shuffleOutputs
|
|
2010
2529
|
});
|
|
2011
2530
|
}
|
|
2531
|
+
async buildPsbt({ txs, address, amount, message = '', satsPerByte = this.feeEstimates.normal, rbf = false, shuffleOutputs = true }) {
|
|
2532
|
+
try {
|
|
2533
|
+
let sendTxs = txs ?? [];
|
|
2534
|
+
if (!sendTxs.length) {
|
|
2535
|
+
if (!address || !amount) {
|
|
2536
|
+
return (0, utils_1.err)('No outputs provided. Specify txs or address and amount.');
|
|
2537
|
+
}
|
|
2538
|
+
sendTxs = [{ address, amount, message }];
|
|
2539
|
+
}
|
|
2540
|
+
if (!this.data.utxos.length) {
|
|
2541
|
+
return (0, utils_1.err)('No UTXOs available.');
|
|
2542
|
+
}
|
|
2543
|
+
await this.resetSendTransaction();
|
|
2544
|
+
const setupTransactionRes = await this.transaction.setupTransaction({
|
|
2545
|
+
rbf,
|
|
2546
|
+
satsPerByte
|
|
2547
|
+
});
|
|
2548
|
+
if (setupTransactionRes.isErr()) {
|
|
2549
|
+
return (0, utils_1.err)(setupTransactionRes.error.message);
|
|
2550
|
+
}
|
|
2551
|
+
let index = 0;
|
|
2552
|
+
for (const tx of sendTxs) {
|
|
2553
|
+
const updateSendTransactionRes = this.transaction.updateSendTransaction({
|
|
2554
|
+
transaction: {
|
|
2555
|
+
label: tx.message,
|
|
2556
|
+
outputs: [{ address: tx.address, value: tx.amount, index }]
|
|
2557
|
+
}
|
|
2558
|
+
});
|
|
2559
|
+
if (updateSendTransactionRes.isErr()) {
|
|
2560
|
+
return (0, utils_1.err)(updateSendTransactionRes.error.message);
|
|
2561
|
+
}
|
|
2562
|
+
index++;
|
|
2563
|
+
}
|
|
2564
|
+
const updateFeeRes = this.transaction.updateFee({ satsPerByte });
|
|
2565
|
+
if (updateFeeRes.isErr())
|
|
2566
|
+
return (0, utils_1.err)(updateFeeRes.error.message);
|
|
2567
|
+
const psbtRes = await this.transaction.createUnsignedPsbt({
|
|
2568
|
+
shuffleOutputs
|
|
2569
|
+
});
|
|
2570
|
+
if (psbtRes.isErr())
|
|
2571
|
+
return (0, utils_1.err)(psbtRes.error.message);
|
|
2572
|
+
const psbt = psbtRes.value;
|
|
2573
|
+
const txData = this.transaction.data;
|
|
2574
|
+
const inputValue = this.transaction.getTransactionInputValue({
|
|
2575
|
+
inputs: txData.inputs
|
|
2576
|
+
});
|
|
2577
|
+
const outputValue = psbt.txOutputs.reduce((acc, o) => acc + o.value, 0);
|
|
2578
|
+
const fee = inputValue - outputValue;
|
|
2579
|
+
const effectiveSatsPerByte = txData.satsPerByte || satsPerByte;
|
|
2580
|
+
const vsizeEstimate = effectiveSatsPerByte > 0 ? Math.round(fee / effectiveSatsPerByte) : 0;
|
|
2581
|
+
const network = this.getBitcoinNetwork();
|
|
2582
|
+
const outputs = psbt.txOutputs.map((output, i) => {
|
|
2583
|
+
let outputAddress = output.address;
|
|
2584
|
+
if (!outputAddress) {
|
|
2585
|
+
try {
|
|
2586
|
+
outputAddress = bitcoin.address.fromOutputScript(output.script, network);
|
|
2587
|
+
}
|
|
2588
|
+
catch {
|
|
2589
|
+
outputAddress = undefined;
|
|
2590
|
+
}
|
|
2591
|
+
}
|
|
2592
|
+
return { address: outputAddress, value: output.value, index: i };
|
|
2593
|
+
});
|
|
2594
|
+
const inputs = txData.inputs.map((input) => ({
|
|
2595
|
+
tx_hash: input.tx_hash,
|
|
2596
|
+
tx_pos: input.tx_pos,
|
|
2597
|
+
address: input.address,
|
|
2598
|
+
value: input.value,
|
|
2599
|
+
path: input.path
|
|
2600
|
+
}));
|
|
2601
|
+
return (0, utils_1.ok)({
|
|
2602
|
+
psbtBase64: psbt.toBase64(),
|
|
2603
|
+
fee,
|
|
2604
|
+
vsizeEstimate,
|
|
2605
|
+
satsPerByte: effectiveSatsPerByte,
|
|
2606
|
+
inputs,
|
|
2607
|
+
outputs
|
|
2608
|
+
});
|
|
2609
|
+
}
|
|
2610
|
+
catch (e) {
|
|
2611
|
+
return (0, utils_1.err)(e);
|
|
2612
|
+
}
|
|
2613
|
+
}
|
|
2614
|
+
importSignedPsbt(psbtBase64) {
|
|
2615
|
+
try {
|
|
2616
|
+
if (!psbtBase64)
|
|
2617
|
+
return (0, utils_1.err)('No PSBT provided.');
|
|
2618
|
+
const network = this.getBitcoinNetwork();
|
|
2619
|
+
const psbt = bitcoin.Psbt.fromBase64(psbtBase64, { network });
|
|
2620
|
+
if (psbt.inputCount === 0)
|
|
2621
|
+
return (0, utils_1.err)('PSBT has no inputs.');
|
|
2622
|
+
for (let i = 0; i < psbt.inputCount; i++) {
|
|
2623
|
+
const input = psbt.data.inputs[i];
|
|
2624
|
+
const finalized = !!(input.finalScriptSig || input.finalScriptWitness);
|
|
2625
|
+
if (finalized)
|
|
2626
|
+
continue;
|
|
2627
|
+
const hasSignature = (input.partialSig?.length ?? 0) > 0 ||
|
|
2628
|
+
!!input.tapKeySig ||
|
|
2629
|
+
(input.tapScriptSig?.length ?? 0) > 0;
|
|
2630
|
+
if (!hasSignature) {
|
|
2631
|
+
return (0, utils_1.err)(`Input ${i} is missing a signature.`);
|
|
2632
|
+
}
|
|
2633
|
+
const multisigCheck = this._enforceMultisigThreshold(psbt, i);
|
|
2634
|
+
if (multisigCheck.isErr())
|
|
2635
|
+
return (0, utils_1.err)(multisigCheck.error.message);
|
|
2636
|
+
let valid = false;
|
|
2637
|
+
try {
|
|
2638
|
+
valid = psbt.validateSignaturesOfInput(i, utils_1.validatePsbtSignature);
|
|
2639
|
+
}
|
|
2640
|
+
catch (e) {
|
|
2641
|
+
return (0, utils_1.err)(`Unable to validate the signature for input ${i}: ${e instanceof Error ? e.message : String(e)}`);
|
|
2642
|
+
}
|
|
2643
|
+
if (!valid)
|
|
2644
|
+
return (0, utils_1.err)(`Input ${i} has an invalid signature.`);
|
|
2645
|
+
psbt.finalizeInput(i);
|
|
2646
|
+
}
|
|
2647
|
+
const tx = psbt.extractTransaction();
|
|
2648
|
+
return (0, utils_1.ok)({ txHex: tx.toHex(), txid: tx.getId() });
|
|
2649
|
+
}
|
|
2650
|
+
catch (e) {
|
|
2651
|
+
return (0, utils_1.err)(e);
|
|
2652
|
+
}
|
|
2653
|
+
}
|
|
2654
|
+
_enforceMultisigThreshold(psbt, i) {
|
|
2655
|
+
const input = psbt.data.inputs[i];
|
|
2656
|
+
const witnessScript = input.witnessScript;
|
|
2657
|
+
if (!witnessScript)
|
|
2658
|
+
return (0, utils_1.ok)('Not a multisig input.');
|
|
2659
|
+
let m;
|
|
2660
|
+
let pubkeys;
|
|
2661
|
+
try {
|
|
2662
|
+
const p2ms = bitcoin.payments.p2ms({ output: witnessScript });
|
|
2663
|
+
m = p2ms.m;
|
|
2664
|
+
pubkeys = p2ms.pubkeys;
|
|
2665
|
+
}
|
|
2666
|
+
catch {
|
|
2667
|
+
return (0, utils_1.ok)('Not a multisig witnessScript.');
|
|
2668
|
+
}
|
|
2669
|
+
if (!m || !pubkeys?.length)
|
|
2670
|
+
return (0, utils_1.ok)('Not a multisig witnessScript.');
|
|
2671
|
+
const p2wsh = bitcoin.payments.p2wsh({
|
|
2672
|
+
redeem: { output: witnessScript }
|
|
2673
|
+
});
|
|
2674
|
+
if (!input.witnessUtxo ||
|
|
2675
|
+
!p2wsh.output ||
|
|
2676
|
+
!input.witnessUtxo.script.equals(p2wsh.output)) {
|
|
2677
|
+
return (0, utils_1.err)(`Input ${i}: witnessScript does not match the witnessUtxo script.`);
|
|
2678
|
+
}
|
|
2679
|
+
const partials = input.partialSig ?? [];
|
|
2680
|
+
for (const ps of partials) {
|
|
2681
|
+
if (!pubkeys.some((pk) => pk.equals(ps.pubkey))) {
|
|
2682
|
+
return (0, utils_1.err)(`Input ${i} carries a signature from a key that is not in the multisig script.`);
|
|
2683
|
+
}
|
|
2684
|
+
}
|
|
2685
|
+
const signedKeys = new Set(partials.map((ps) => ps.pubkey.toString('hex')));
|
|
2686
|
+
const have = signedKeys.size;
|
|
2687
|
+
if (have < m) {
|
|
2688
|
+
return (0, utils_1.err)(`Input ${i} is below the multisig threshold: have ${have} signature(s), need ${m}.`);
|
|
2689
|
+
}
|
|
2690
|
+
if (have > m) {
|
|
2691
|
+
const keep = [];
|
|
2692
|
+
for (const pk of pubkeys) {
|
|
2693
|
+
if (keep.length >= m)
|
|
2694
|
+
break;
|
|
2695
|
+
const ps = partials.find((p) => p.pubkey.equals(pk));
|
|
2696
|
+
if (ps)
|
|
2697
|
+
keep.push(ps);
|
|
2698
|
+
}
|
|
2699
|
+
input.partialSig = keep;
|
|
2700
|
+
}
|
|
2701
|
+
return (0, utils_1.ok)('Multisig threshold satisfied.');
|
|
2702
|
+
}
|
|
2703
|
+
signPsbtWithOurKey(psbtBase64) {
|
|
2704
|
+
try {
|
|
2705
|
+
if (!this._root)
|
|
2706
|
+
return (0, utils_1.err)(new utils_1.WatchOnlySigningError());
|
|
2707
|
+
if (!psbtBase64)
|
|
2708
|
+
return (0, utils_1.err)('No PSBT provided.');
|
|
2709
|
+
const network = this.getBitcoinNetwork();
|
|
2710
|
+
const psbt = bitcoin.Psbt.fromBase64(psbtBase64, { network });
|
|
2711
|
+
if (psbt.inputCount === 0)
|
|
2712
|
+
return (0, utils_1.err)('PSBT has no inputs.');
|
|
2713
|
+
let matched = 0;
|
|
2714
|
+
for (let i = 0; i < psbt.inputCount; i++) {
|
|
2715
|
+
const input = psbt.data.inputs[i];
|
|
2716
|
+
if (input.finalScriptSig || input.finalScriptWitness)
|
|
2717
|
+
continue;
|
|
2718
|
+
for (const derivation of input.bip32Derivation ?? []) {
|
|
2719
|
+
let keyPair;
|
|
2720
|
+
try {
|
|
2721
|
+
keyPair = this._root.derivePath(derivation.path);
|
|
2722
|
+
}
|
|
2723
|
+
catch {
|
|
2724
|
+
continue;
|
|
2725
|
+
}
|
|
2726
|
+
if (!keyPair.publicKey.equals(derivation.pubkey))
|
|
2727
|
+
continue;
|
|
2728
|
+
matched++;
|
|
2729
|
+
const alreadySigned = (input.partialSig ?? []).some((ps) => ps.pubkey.equals(keyPair.publicKey));
|
|
2730
|
+
if (!alreadySigned)
|
|
2731
|
+
psbt.signInput(i, keyPair);
|
|
2732
|
+
break;
|
|
2733
|
+
}
|
|
2734
|
+
}
|
|
2735
|
+
if (matched === 0) {
|
|
2736
|
+
return (0, utils_1.err)('No PSBT input carries a bip32Derivation this wallet can sign for.');
|
|
2737
|
+
}
|
|
2738
|
+
return (0, utils_1.ok)(psbt.toBase64());
|
|
2739
|
+
}
|
|
2740
|
+
catch (e) {
|
|
2741
|
+
return (0, utils_1.err)(e);
|
|
2742
|
+
}
|
|
2743
|
+
}
|
|
2744
|
+
combinePsbts(psbts) {
|
|
2745
|
+
try {
|
|
2746
|
+
if (!psbts || psbts.length < 2) {
|
|
2747
|
+
return (0, utils_1.err)('Provide at least two PSBTs to combine.');
|
|
2748
|
+
}
|
|
2749
|
+
const network = this.getBitcoinNetwork();
|
|
2750
|
+
const parsed = psbts.map((psbtBase64) => bitcoin.Psbt.fromBase64(psbtBase64, { network }));
|
|
2751
|
+
const [base, ...rest] = parsed;
|
|
2752
|
+
base.combine(...rest);
|
|
2753
|
+
return (0, utils_1.ok)(base.toBase64());
|
|
2754
|
+
}
|
|
2755
|
+
catch (e) {
|
|
2756
|
+
return (0, utils_1.err)(e);
|
|
2757
|
+
}
|
|
2758
|
+
}
|
|
2759
|
+
async broadcastTransaction(txHex) {
|
|
2760
|
+
if (!txHex)
|
|
2761
|
+
return (0, utils_1.err)('No transaction hex provided.');
|
|
2762
|
+
return await this.electrum.broadcastTransaction({
|
|
2763
|
+
rawTx: txHex,
|
|
2764
|
+
subscribeToOutputAddress: false
|
|
2765
|
+
});
|
|
2766
|
+
}
|
|
2012
2767
|
getAddressFromScriptHash(scriptHash) {
|
|
2013
2768
|
const addresses = this.data.addresses;
|
|
2014
2769
|
const changeAddresses = this.data.changeAddresses;
|
|
@@ -2192,7 +2947,7 @@ class Wallet {
|
|
|
2192
2947
|
}
|
|
2193
2948
|
}
|
|
2194
2949
|
catch (e) {
|
|
2195
|
-
|
|
2950
|
+
this.logger.error('Failed to get input value.', e);
|
|
2196
2951
|
}
|
|
2197
2952
|
}
|
|
2198
2953
|
for (let i = 0; i < vouts.length; i++) {
|
|
@@ -2330,7 +3085,7 @@ class Wallet {
|
|
|
2330
3085
|
return (0, utils_1.ok)(newInputs);
|
|
2331
3086
|
}
|
|
2332
3087
|
catch (e) {
|
|
2333
|
-
|
|
3088
|
+
this.logger.error('Failed to add transaction input.', e);
|
|
2334
3089
|
return (0, utils_1.err)(e);
|
|
2335
3090
|
}
|
|
2336
3091
|
}
|
|
@@ -2351,7 +3106,7 @@ class Wallet {
|
|
|
2351
3106
|
return (0, utils_1.ok)(newInputs);
|
|
2352
3107
|
}
|
|
2353
3108
|
catch (e) {
|
|
2354
|
-
|
|
3109
|
+
this.logger.error('Failed to remove transaction input.', e);
|
|
2355
3110
|
return (0, utils_1.err)(e);
|
|
2356
3111
|
}
|
|
2357
3112
|
}
|
|
@@ -2369,7 +3124,7 @@ class Wallet {
|
|
|
2369
3124
|
return (0, utils_1.ok)('Tag successfully added');
|
|
2370
3125
|
}
|
|
2371
3126
|
catch (e) {
|
|
2372
|
-
|
|
3127
|
+
this.logger.error('Failed to add transaction tag.', e);
|
|
2373
3128
|
return (0, utils_1.err)(e);
|
|
2374
3129
|
}
|
|
2375
3130
|
}
|
|
@@ -2387,7 +3142,7 @@ class Wallet {
|
|
|
2387
3142
|
return (0, utils_1.ok)('Tag successfully added');
|
|
2388
3143
|
}
|
|
2389
3144
|
catch (e) {
|
|
2390
|
-
|
|
3145
|
+
this.logger.error('Failed to remove transaction tag.', e);
|
|
2391
3146
|
return (0, utils_1.err)(e);
|
|
2392
3147
|
}
|
|
2393
3148
|
}
|
|
@@ -2406,7 +3161,7 @@ class Wallet {
|
|
|
2406
3161
|
selectedFeeId
|
|
2407
3162
|
});
|
|
2408
3163
|
if (res.isErr()) {
|
|
2409
|
-
|
|
3164
|
+
this.logger.warn(res.error.message);
|
|
2410
3165
|
return (0, utils_1.err)(res.error.message);
|
|
2411
3166
|
}
|
|
2412
3167
|
return (0, utils_1.ok)('Fee has been changed successfully');
|
|
@@ -2436,7 +3191,7 @@ class Wallet {
|
|
|
2436
3191
|
await this.saveWalletData('feeEstimates', feeEstimates);
|
|
2437
3192
|
return (0, utils_1.ok)(feeEstimates);
|
|
2438
3193
|
}
|
|
2439
|
-
getAddressesFromPrivateKey(privateKey, _addressTypes = (0, shapes_1.getAddressTypes)()) {
|
|
3194
|
+
getAddressesFromPrivateKey(privateKey, _addressTypes = (0, shapes_1.getAddressTypes)().filter((t) => t !== types_1.EAddressType.p2wsh)) {
|
|
2440
3195
|
try {
|
|
2441
3196
|
const network = this.getBitcoinNetwork();
|
|
2442
3197
|
return (0, utils_1.getAddressesFromPrivateKey)({
|
|
@@ -2481,6 +3236,10 @@ class Wallet {
|
|
|
2481
3236
|
return (0, utils_1.ok)({ balance, utxos, keyPair, addresses });
|
|
2482
3237
|
}
|
|
2483
3238
|
async sweepPrivateKey({ privateKey, toAddress, satsPerByte = this.feeEstimates.normal, broadcast = true, combineWithWalletUtxos = false }) {
|
|
3239
|
+
if (this._multisig)
|
|
3240
|
+
return (0, utils_1.err)(new utils_1.MultisigSpendError());
|
|
3241
|
+
if (this.isWatchOnly)
|
|
3242
|
+
return (0, utils_1.err)(new utils_1.WatchOnlySigningError());
|
|
2484
3243
|
const privateKeyInfo = await this.getPrivateKeyInfo(privateKey);
|
|
2485
3244
|
if (privateKeyInfo.isErr()) {
|
|
2486
3245
|
return (0, utils_1.err)(privateKeyInfo.error.message);
|