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.
Files changed (215) hide show
  1. package/README.md +306 -1
  2. package/dist/cli/auth.js +238 -0
  3. package/dist/cli/auth.js.map +1 -0
  4. package/dist/cli/beignet-node.js +1034 -57
  5. package/dist/cli/beignet-node.js.map +1 -1
  6. package/dist/cli/cli.js +1257 -18
  7. package/dist/cli/cli.js.map +1 -1
  8. package/dist/cli/config.js +45 -1
  9. package/dist/cli/config.js.map +1 -1
  10. package/dist/cli/daemon.js +398 -38
  11. package/dist/cli/daemon.js.map +1 -1
  12. package/dist/cli/errors.js +2 -0
  13. package/dist/cli/errors.js.map +1 -1
  14. package/dist/cli/index.js +6 -1
  15. package/dist/cli/index.js.map +1 -1
  16. package/dist/cli/openapi.js +1343 -89
  17. package/dist/cli/openapi.js.map +1 -1
  18. package/dist/cli/restore.js +89 -0
  19. package/dist/cli/restore.js.map +1 -0
  20. package/dist/cli/wallet-storage.js +34 -0
  21. package/dist/cli/wallet-storage.js.map +1 -0
  22. package/dist/electrum/index.js +107 -12
  23. package/dist/electrum/index.js.map +1 -1
  24. package/dist/index.js +1 -0
  25. package/dist/index.js.map +1 -1
  26. package/dist/lightning/advisor/fee-tuner.js +42 -0
  27. package/dist/lightning/advisor/fee-tuner.js.map +1 -0
  28. package/dist/lightning/advisor/index.js +9 -1
  29. package/dist/lightning/advisor/index.js.map +1 -1
  30. package/dist/lightning/advisor/rebalance-planner.js +55 -0
  31. package/dist/lightning/advisor/rebalance-planner.js.map +1 -0
  32. package/dist/lightning/backup/index.js +18 -0
  33. package/dist/lightning/backup/index.js.map +1 -0
  34. package/dist/lightning/backup/scb.js +42 -0
  35. package/dist/lightning/backup/scb.js.map +1 -0
  36. package/dist/lightning/chain/chain-monitor.js +27 -0
  37. package/dist/lightning/chain/chain-monitor.js.map +1 -1
  38. package/dist/lightning/chain/output-resolver.js +66 -15
  39. package/dist/lightning/chain/output-resolver.js.map +1 -1
  40. package/dist/lightning/chain/types.js +1 -0
  41. package/dist/lightning/chain/types.js.map +1 -1
  42. package/dist/lightning/channel/channel-manager.js +91 -24
  43. package/dist/lightning/channel/channel-manager.js.map +1 -1
  44. package/dist/lightning/channel/channel-state.js.map +1 -1
  45. package/dist/lightning/channel/channel.js +122 -4
  46. package/dist/lightning/channel/channel.js.map +1 -1
  47. package/dist/lightning/channel/commitment-builder.js +4 -7
  48. package/dist/lightning/channel/commitment-builder.js.map +1 -1
  49. package/dist/lightning/channel/dual-funding.js +6 -5
  50. package/dist/lightning/channel/dual-funding.js.map +1 -1
  51. package/dist/lightning/channel/splice-tx.js.map +1 -1
  52. package/dist/lightning/channel/types.js +3 -1
  53. package/dist/lightning/channel/types.js.map +1 -1
  54. package/dist/lightning/channel/validation.js +3 -3
  55. package/dist/lightning/channel/validation.js.map +1 -1
  56. package/dist/lightning/crypto/index.js +1 -0
  57. package/dist/lightning/crypto/index.js.map +1 -1
  58. package/dist/lightning/crypto/message-signing.js +116 -0
  59. package/dist/lightning/crypto/message-signing.js.map +1 -0
  60. package/dist/lightning/crypto/xchacha20poly1305.js +77 -0
  61. package/dist/lightning/crypto/xchacha20poly1305.js.map +1 -0
  62. package/dist/lightning/features/flags.js +1 -0
  63. package/dist/lightning/features/flags.js.map +1 -1
  64. package/dist/lightning/gossip/messages.js +37 -1
  65. package/dist/lightning/gossip/messages.js.map +1 -1
  66. package/dist/lightning/index.js +2 -1
  67. package/dist/lightning/index.js.map +1 -1
  68. package/dist/lightning/keys/signer.js +24 -3
  69. package/dist/lightning/keys/signer.js.map +1 -1
  70. package/dist/lightning/keys/wallet-keys.js +1 -0
  71. package/dist/lightning/keys/wallet-keys.js.map +1 -1
  72. package/dist/lightning/message/index.js +1 -0
  73. package/dist/lightning/message/index.js.map +1 -1
  74. package/dist/lightning/message/peer-storage.js +43 -0
  75. package/dist/lightning/message/peer-storage.js.map +1 -0
  76. package/dist/lightning/message/types.js +2 -0
  77. package/dist/lightning/message/types.js.map +1 -1
  78. package/dist/lightning/node/lightning-node.js +1593 -236
  79. package/dist/lightning/node/lightning-node.js.map +1 -1
  80. package/dist/lightning/node/types.js +18 -1
  81. package/dist/lightning/node/types.js.map +1 -1
  82. package/dist/lightning/offer/offer-manager.js +2 -5
  83. package/dist/lightning/offer/offer-manager.js.map +1 -1
  84. package/dist/lightning/offer/tlv.js +3 -1
  85. package/dist/lightning/offer/tlv.js.map +1 -1
  86. package/dist/lightning/storage/encryption.js +67 -0
  87. package/dist/lightning/storage/encryption.js.map +1 -0
  88. package/dist/lightning/storage/index.js +1 -0
  89. package/dist/lightning/storage/index.js.map +1 -1
  90. package/dist/lightning/storage/serialization.js +6 -2
  91. package/dist/lightning/storage/serialization.js.map +1 -1
  92. package/dist/lightning/storage/sqlite-storage.js +405 -33
  93. package/dist/lightning/storage/sqlite-storage.js.map +1 -1
  94. package/dist/lightning/watchtower/blob.js +221 -0
  95. package/dist/lightning/watchtower/blob.js.map +1 -0
  96. package/dist/lightning/watchtower/index.js +55 -0
  97. package/dist/lightning/watchtower/index.js.map +1 -0
  98. package/dist/lightning/watchtower/justice.js +272 -0
  99. package/dist/lightning/watchtower/justice.js.map +1 -0
  100. package/dist/lightning/watchtower/tower-connection.js +208 -0
  101. package/dist/lightning/watchtower/tower-connection.js.map +1 -0
  102. package/dist/lightning/watchtower/types.js +3 -0
  103. package/dist/lightning/watchtower/types.js.map +1 -0
  104. package/dist/lightning/watchtower/watchtower-client.js +472 -0
  105. package/dist/lightning/watchtower/watchtower-client.js.map +1 -0
  106. package/dist/lightning/watchtower/wtwire.js +201 -0
  107. package/dist/lightning/watchtower/wtwire.js.map +1 -0
  108. package/dist/logger.js +41 -0
  109. package/dist/logger.js.map +1 -0
  110. package/dist/shapes/electrum.js +32 -1
  111. package/dist/shapes/electrum.js.map +1 -1
  112. package/dist/shapes/wallet.js +11 -1
  113. package/dist/shapes/wallet.js.map +1 -1
  114. package/dist/transaction/index.js +163 -15
  115. package/dist/transaction/index.js.map +1 -1
  116. package/dist/types/cli/auth.d.ts +34 -0
  117. package/dist/types/cli/beignet-node.d.ts +148 -4
  118. package/dist/types/cli/daemon.d.ts +5 -0
  119. package/dist/types/cli/errors.d.ts +2 -0
  120. package/dist/types/cli/index.d.ts +1 -0
  121. package/dist/types/cli/restore.d.ts +10 -0
  122. package/dist/types/cli/types.d.ts +235 -1
  123. package/dist/types/cli/wallet-storage.d.ts +3 -0
  124. package/dist/types/electrum/index.d.ts +12 -0
  125. package/dist/types/electrum.js +1 -0
  126. package/dist/types/electrum.js.map +1 -1
  127. package/dist/types/index.d.ts +1 -0
  128. package/dist/types/lightning/advisor/fee-tuner.d.ts +20 -0
  129. package/dist/types/lightning/advisor/index.d.ts +4 -0
  130. package/dist/types/lightning/advisor/rebalance-planner.d.ts +12 -0
  131. package/dist/types/lightning/backup/index.d.ts +1 -0
  132. package/dist/types/lightning/backup/scb.d.ts +24 -0
  133. package/dist/types/lightning/chain/chain-monitor.d.ts +1 -0
  134. package/dist/types/lightning/chain/types.d.ts +1 -0
  135. package/dist/types/lightning/channel/channel-manager.d.ts +11 -0
  136. package/dist/types/lightning/channel/channel-state.d.ts +2 -0
  137. package/dist/types/lightning/channel/channel.d.ts +11 -5
  138. package/dist/types/lightning/channel/commitment-builder.d.ts +7 -7
  139. package/dist/types/lightning/channel/dual-funding.d.ts +2 -1
  140. package/dist/types/lightning/channel/splice-tx.d.ts +2 -2
  141. package/dist/types/lightning/channel/types.d.ts +2 -0
  142. package/dist/types/lightning/channel/validation.d.ts +1 -1
  143. package/dist/types/lightning/crypto/index.d.ts +1 -0
  144. package/dist/types/lightning/crypto/message-signing.d.ts +9 -0
  145. package/dist/types/lightning/crypto/xchacha20poly1305.d.ts +3 -0
  146. package/dist/types/lightning/features/flags.d.ts +1 -0
  147. package/dist/types/lightning/gossip/messages.d.ts +4 -0
  148. package/dist/types/lightning/index.d.ts +1 -0
  149. package/dist/types/lightning/keys/signer.d.ts +18 -1
  150. package/dist/types/lightning/keys/wallet-keys.d.ts +2 -1
  151. package/dist/types/lightning/message/index.d.ts +1 -0
  152. package/dist/types/lightning/message/peer-storage.d.ts +9 -0
  153. package/dist/types/lightning/message/types.d.ts +2 -0
  154. package/dist/types/lightning/node/lightning-node.d.ts +120 -4
  155. package/dist/types/lightning/node/types.d.ts +66 -1
  156. package/dist/types/lightning/storage/encryption.d.ts +11 -0
  157. package/dist/types/lightning/storage/index.d.ts +1 -0
  158. package/dist/types/lightning/storage/serialization.d.ts +2 -0
  159. package/dist/types/lightning/storage/sqlite-storage.d.ts +43 -3
  160. package/dist/types/lightning/storage/types.d.ts +62 -0
  161. package/dist/types/lightning/watchtower/blob.d.ts +46 -0
  162. package/dist/types/lightning/watchtower/index.d.ts +9 -0
  163. package/dist/types/lightning/watchtower/justice.d.ts +30 -0
  164. package/dist/types/lightning/watchtower/tower-connection.d.ts +34 -0
  165. package/dist/types/lightning/watchtower/types.d.ts +46 -0
  166. package/dist/types/lightning/watchtower/watchtower-client.d.ts +68 -0
  167. package/dist/types/lightning/watchtower/wtwire.d.ts +90 -0
  168. package/dist/types/logger.d.ts +17 -0
  169. package/dist/types/shapes/electrum.d.ts +3 -1
  170. package/dist/types/transaction/index.d.ts +6 -0
  171. package/dist/types/transaction.js.map +1 -1
  172. package/dist/types/types/electrum.d.ts +3 -2
  173. package/dist/types/types/transaction.d.ts +1 -1
  174. package/dist/types/types/wallet.d.ts +80 -4
  175. package/dist/types/utils/descriptors.d.ts +2 -0
  176. package/dist/types/utils/fees.d.ts +3 -0
  177. package/dist/types/utils/helpers.d.ts +3 -1
  178. package/dist/types/utils/index.d.ts +5 -0
  179. package/dist/types/utils/multisig.d.ts +29 -0
  180. package/dist/types/utils/transaction.d.ts +7 -0
  181. package/dist/types/utils/wallet-storage-encryption.d.ts +3 -0
  182. package/dist/types/utils/wallet.d.ts +1 -1
  183. package/dist/types/utils/watch-only.d.ts +12 -0
  184. package/dist/types/wallet/constants.d.ts +1 -0
  185. package/dist/types/wallet/index.d.ts +63 -4
  186. package/dist/types/wallet.js +3 -0
  187. package/dist/types/wallet.js.map +1 -1
  188. package/dist/utils/derivation-path.js +8 -7
  189. package/dist/utils/derivation-path.js.map +1 -1
  190. package/dist/utils/descriptors.js +56 -0
  191. package/dist/utils/descriptors.js.map +1 -0
  192. package/dist/utils/electrum.js +14 -4
  193. package/dist/utils/electrum.js.map +1 -1
  194. package/dist/utils/fees.js +19 -0
  195. package/dist/utils/fees.js.map +1 -0
  196. package/dist/utils/helpers.js +22 -5
  197. package/dist/utils/helpers.js.map +1 -1
  198. package/dist/utils/index.js +5 -0
  199. package/dist/utils/index.js.map +1 -1
  200. package/dist/utils/multisig.js +90 -0
  201. package/dist/utils/multisig.js.map +1 -0
  202. package/dist/utils/transaction.js +30 -3
  203. package/dist/utils/transaction.js.map +1 -1
  204. package/dist/utils/wallet-storage-encryption.js +45 -0
  205. package/dist/utils/wallet-storage-encryption.js.map +1 -0
  206. package/dist/utils/wallet.js +3 -1
  207. package/dist/utils/wallet.js.map +1 -1
  208. package/dist/utils/watch-only.js +102 -0
  209. package/dist/utils/watch-only.js.map +1 -0
  210. package/dist/wallet/constants.js +2 -1
  211. package/dist/wallet/constants.js.map +1 -1
  212. package/dist/wallet/index.js +810 -51
  213. package/dist/wallet/index.js.map +1 -1
  214. package/docs/ROADMAP.md +365 -0
  215. package/package.json +1 -1
@@ -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 = types_1.EAddressType.p2wpkh, coinSelectPreference = types_1.ECoinSelectPreference.consolidate, storage, electrumOptions, onMessage = () => null, customGetAddress, customGetScriptHash, rbf = false, selectedFeeId = types_1.EFeeId.normal, disableMessages = false, disableMessagesOnCreate = false, addressTypesToMonitor = Object.values(types_1.EAddressType), gapLimitOptions = {
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
- console.log(generatedAddress.error.message);
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
- const network = bitcoinjs_lib_1.networks[this._network];
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
- if (!(0, utils_1.validateMnemonic)(mnemonic))
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._seed = (0, utils_1.getSeed)(this._mnemonic, this._passphrase);
117
- this._root = bip32.fromSeed(this._seed, this.getBitcoinNetwork(this._network));
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
- console.log(msg);
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
- console.log(e);
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
- console.log(`Unhandled key in getWalletData: ${key}`);
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
- console.log(e);
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 bitcoin.networks[network];
663
+ return (0, utils_1.getBitcoinJsNetwork)(network);
432
664
  }
433
665
  isValid(mnemonic) {
434
- return mnemonic === this._mnemonic && (0, utils_1.validateMnemonic)(mnemonic);
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._root.derivePath(path);
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
- console.log(msg);
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
- console.log(addressHistory.error.message);
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
- console.log(highestUsedIndex.error.message);
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
- console.log(highestStoredIndex.error.message);
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
- console.log(e);
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
- console.error('\nError:', error);
2295
+ this.logger.error('\nError:', error);
1825
2296
  if (data) {
1826
- console.warn('Unable to retrieve input data for:', data);
2297
+ this.logger.warn('Unable to retrieve input data for:', data);
1827
2298
  }
1828
2299
  if (error?.code && error.code === -32600)
1829
- console.info('Suggestion: Please increase the response limit on your Electrum server.');
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
- console.log(generateAddressResponse.error.message);
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
- if (network === types_1.EAvailableNetworks.bitcoinRegtest) {
1855
- return { ...shapes_1.defaultFeesShape, timestamp: Date.now() };
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
- if (!(res.fastestFee > 0 &&
1861
- res.halfHourFee > 0 &&
1862
- res.hourFee > 0 &&
1863
- res.minimumFee > 0)) {
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: res.fastestFee,
1868
- normal: res.halfHourFee,
1869
- slow: res.hourFee,
1870
- minimum: res.minimumFee,
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 { fast, mid, slow } = res.onchain.feeRates;
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: mid,
2405
+ normal,
1895
2406
  slow,
1896
2407
  minimum: slow,
1897
2408
  timestamp: Date.now()
1898
2409
  };
1899
2410
  }
1900
2411
  catch (e) {
1901
- console.log('Unable to fetch fee estimates.', e);
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
- console.log(e);
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
- console.log(e);
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
- console.log(e);
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
- console.log(e);
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
- console.log(e);
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
- console.log(res.error.message);
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);