1 | var _ = require('lodash'); |
||
2 | var assert = require('assert'); |
||
3 | var q = require('q'); |
||
4 | var async = require('async'); |
||
5 | var bitcoin = require('bitcoinjs-lib'); |
||
6 | var bitcoinMessage = require('bitcoinjs-message'); |
||
7 | var blocktrail = require('./blocktrail'); |
||
8 | var CryptoJS = require('crypto-js'); |
||
9 | var Encryption = require('./encryption'); |
||
10 | var EncryptionMnemonic = require('./encryption_mnemonic'); |
||
11 | var SizeEstimation = require('./size_estimation'); |
||
12 | var bip39 = require('bip39'); |
||
13 | |||
14 | var SignMode = { |
||
15 | SIGN: "sign", |
||
16 | DONT_SIGN: "dont_sign" |
||
17 | }; |
||
18 | |||
19 | /** |
||
20 | * |
||
21 | * @param sdk APIClient SDK instance used to do requests |
||
22 | * @param identifier string identifier of the wallet |
||
23 | * @param walletVersion string |
||
24 | * @param primaryMnemonic string primary mnemonic |
||
25 | * @param encryptedPrimarySeed |
||
26 | * @param encryptedSecret |
||
27 | * @param primaryPublicKeys string primary mnemonic |
||
28 | * @param backupPublicKey string BIP32 master pubKey M/ |
||
29 | * @param blocktrailPublicKeys array list of blocktrail pubKeys indexed by keyIndex |
||
30 | * @param keyIndex int key index to use |
||
31 | * @param segwit int segwit toggle from server |
||
32 | * @param testnet bool testnet |
||
33 | * @param regtest bool regtest |
||
34 | * @param checksum string |
||
35 | * @param upgradeToKeyIndex int |
||
36 | * @param useNewCashAddr bool flag to opt in to bitcoin cash cashaddr's |
||
37 | * @param bypassNewAddressCheck bool flag to indicate if wallet should/shouldn't derive new address locally to verify api |
||
38 | * @constructor |
||
39 | * @internal |
||
40 | */ |
||
41 | var Wallet = function( |
||
42 | sdk, |
||
43 | identifier, |
||
44 | walletVersion, |
||
45 | primaryMnemonic, |
||
46 | encryptedPrimarySeed, |
||
47 | encryptedSecret, |
||
48 | primaryPublicKeys, |
||
49 | backupPublicKey, |
||
50 | blocktrailPublicKeys, |
||
51 | keyIndex, |
||
52 | segwit, |
||
53 | testnet, |
||
54 | regtest, |
||
55 | checksum, |
||
56 | upgradeToKeyIndex, |
||
57 | useNewCashAddr, |
||
58 | bypassNewAddressCheck |
||
59 | ) { |
||
60 | /* jshint -W071 */ |
||
61 | var self = this; |
||
62 | |||
63 | self.sdk = sdk; |
||
64 | self.identifier = identifier; |
||
65 | self.walletVersion = walletVersion; |
||
66 | self.locked = true; |
||
67 | self.bypassNewAddressCheck = !!bypassNewAddressCheck; |
||
68 | self.bitcoinCash = self.sdk.bitcoinCash; |
||
69 | self.segwit = !!segwit; |
||
70 | self.useNewCashAddr = !!useNewCashAddr; |
||
71 | assert(!self.segwit || !self.bitcoinCash); |
||
72 | |||
73 | self.testnet = testnet; |
||
74 | self.regtest = regtest; |
||
75 | if (self.bitcoinCash) { |
||
76 | if (self.regtest) { |
||
77 | self.network = bitcoin.networks.bitcoincashregtest; |
||
78 | } else if (self.testnet) { |
||
79 | self.network = bitcoin.networks.bitcoincashtestnet; |
||
80 | } else { |
||
81 | self.network = bitcoin.networks.bitcoincash; |
||
82 | } |
||
83 | } else { |
||
84 | if (self.regtest) { |
||
85 | self.network = bitcoin.networks.regtest; |
||
86 | } else if (self.testnet) { |
||
87 | self.network = bitcoin.networks.testnet; |
||
88 | } else { |
||
89 | self.network = bitcoin.networks.bitcoin; |
||
90 | } |
||
91 | } |
||
92 | |||
93 | assert(backupPublicKey instanceof bitcoin.HDNode); |
||
94 | assert(_.every(primaryPublicKeys, function(primaryPublicKey) { return primaryPublicKey instanceof bitcoin.HDNode; })); |
||
95 | assert(_.every(blocktrailPublicKeys, function(blocktrailPublicKey) { return blocktrailPublicKey instanceof bitcoin.HDNode; })); |
||
96 | |||
97 | // v1 |
||
98 | self.primaryMnemonic = primaryMnemonic; |
||
99 | |||
100 | // v2 & v3 |
||
101 | self.encryptedPrimarySeed = encryptedPrimarySeed; |
||
102 | self.encryptedSecret = encryptedSecret; |
||
103 | |||
104 | self.primaryPrivateKey = null; |
||
105 | self.backupPrivateKey = null; |
||
106 | |||
107 | self.backupPublicKey = backupPublicKey; |
||
108 | self.blocktrailPublicKeys = blocktrailPublicKeys; |
||
109 | self.primaryPublicKeys = primaryPublicKeys; |
||
110 | self.keyIndex = keyIndex; |
||
111 | |||
112 | if (!self.bitcoinCash) { |
||
113 | if (self.segwit) { |
||
114 | self.chain = Wallet.CHAIN_BTC_DEFAULT; |
||
115 | self.changeChain = Wallet.CHAIN_BTC_SEGWIT; |
||
116 | } else { |
||
117 | self.chain = Wallet.CHAIN_BTC_DEFAULT; |
||
118 | self.changeChain = Wallet.CHAIN_BTC_DEFAULT; |
||
119 | } |
||
120 | } else { |
||
121 | self.chain = Wallet.CHAIN_BCC_DEFAULT; |
||
122 | self.changeChain = Wallet.CHAIN_BCC_DEFAULT; |
||
123 | } |
||
124 | |||
125 | self.checksum = checksum; |
||
126 | self.upgradeToKeyIndex = upgradeToKeyIndex; |
||
127 | |||
128 | self.secret = null; |
||
129 | self.seedHex = null; |
||
130 | }; |
||
131 | |||
132 | Wallet.WALLET_VERSION_V1 = 'v1'; |
||
133 | Wallet.WALLET_VERSION_V2 = 'v2'; |
||
134 | Wallet.WALLET_VERSION_V3 = 'v3'; |
||
135 | |||
136 | Wallet.WALLET_ENTROPY_BITS = 256; |
||
137 | |||
138 | Wallet.OP_RETURN = 'opreturn'; |
||
139 | Wallet.DATA = Wallet.OP_RETURN; // alias |
||
140 | |||
141 | Wallet.PAY_PROGRESS_START = 0; |
||
142 | Wallet.PAY_PROGRESS_COIN_SELECTION = 10; |
||
143 | Wallet.PAY_PROGRESS_CHANGE_ADDRESS = 20; |
||
144 | Wallet.PAY_PROGRESS_SIGN = 30; |
||
145 | Wallet.PAY_PROGRESS_SEND = 40; |
||
146 | Wallet.PAY_PROGRESS_DONE = 100; |
||
147 | |||
148 | Wallet.CHAIN_BTC_DEFAULT = 0; |
||
149 | Wallet.CHAIN_BTC_SEGWIT = 2; |
||
150 | Wallet.CHAIN_BCC_DEFAULT = 1; |
||
151 | |||
152 | Wallet.FEE_STRATEGY_FORCE_FEE = blocktrail.FEE_STRATEGY_FORCE_FEE; |
||
153 | Wallet.FEE_STRATEGY_BASE_FEE = blocktrail.FEE_STRATEGY_BASE_FEE; |
||
154 | Wallet.FEE_STRATEGY_HIGH_PRIORITY = blocktrail.FEE_STRATEGY_HIGH_PRIORITY; |
||
155 | Wallet.FEE_STRATEGY_OPTIMAL = blocktrail.FEE_STRATEGY_OPTIMAL; |
||
156 | Wallet.FEE_STRATEGY_LOW_PRIORITY = blocktrail.FEE_STRATEGY_LOW_PRIORITY; |
||
157 | Wallet.FEE_STRATEGY_MIN_RELAY_FEE = blocktrail.FEE_STRATEGY_MIN_RELAY_FEE; |
||
158 | |||
159 | Wallet.prototype.isSegwit = function() { |
||
160 | return !!this.segwit; |
||
161 | }; |
||
162 | |||
163 | Wallet.prototype.unlock = function(options, cb) { |
||
164 | var self = this; |
||
165 | |||
166 | var deferred = q.defer(); |
||
167 | deferred.promise.nodeify(cb); |
||
168 | |||
169 | // avoid modifying passed options |
||
170 | options = _.merge({}, options); |
||
171 | |||
172 | q.fcall(function() { |
||
173 | switch (self.walletVersion) { |
||
174 | case Wallet.WALLET_VERSION_V1: |
||
175 | return self.unlockV1(options); |
||
176 | |||
177 | case Wallet.WALLET_VERSION_V2: |
||
178 | return self.unlockV2(options); |
||
179 | |||
180 | case Wallet.WALLET_VERSION_V3: |
||
181 | return self.unlockV3(options); |
||
182 | |||
183 | default: |
||
184 | return q.reject(new blocktrail.WalletInitError("Invalid wallet version")); |
||
185 | } |
||
186 | }).then( |
||
187 | function(primaryPrivateKey) { |
||
188 | self.primaryPrivateKey = primaryPrivateKey; |
||
189 | |||
190 | // create a checksum of our private key which we'll later use to verify we used the right password |
||
191 | var checksum = self.primaryPrivateKey.getAddress(); |
||
192 | |||
193 | // check if we've used the right passphrase |
||
194 | if (checksum !== self.checksum) { |
||
195 | throw new blocktrail.WalletChecksumError("Generated checksum [" + checksum + "] does not match " + |
||
196 | "[" + self.checksum + "], most likely due to incorrect password"); |
||
197 | } |
||
198 | |||
199 | self.locked = false; |
||
200 | |||
201 | // if the response suggests we should upgrade to a different blocktrail cosigning key then we should |
||
202 | if (typeof self.upgradeToKeyIndex !== "undefined" && self.upgradeToKeyIndex !== null) { |
||
0 ignored issues
–
show
|
|||
203 | return self.upgradeKeyIndex(self.upgradeToKeyIndex); |
||
204 | } |
||
205 | } |
||
206 | ).then( |
||
207 | function(r) { |
||
208 | deferred.resolve(r); |
||
209 | }, |
||
210 | function(e) { |
||
211 | deferred.reject(e); |
||
212 | } |
||
213 | ); |
||
214 | |||
215 | return deferred.promise; |
||
216 | }; |
||
217 | |||
218 | Wallet.prototype.unlockV1 = function(options) { |
||
219 | var self = this; |
||
220 | |||
221 | options.primaryMnemonic = typeof options.primaryMnemonic !== "undefined" ? options.primaryMnemonic : self.primaryMnemonic; |
||
222 | options.secretMnemonic = typeof options.secretMnemonic !== "undefined" ? options.secretMnemonic : self.secretMnemonic; |
||
223 | |||
224 | return self.sdk.resolvePrimaryPrivateKeyFromOptions(options) |
||
225 | .then(function(options) { |
||
226 | self.primarySeed = options.primarySeed; |
||
227 | |||
228 | return options.primaryPrivateKey; |
||
229 | }); |
||
230 | }; |
||
231 | |||
232 | Wallet.prototype.unlockV2 = function(options, cb) { |
||
233 | var self = this; |
||
234 | |||
235 | var deferred = q.defer(); |
||
236 | deferred.promise.nodeify(cb); |
||
237 | |||
238 | deferred.resolve(q.fcall(function() { |
||
239 | /* jshint -W071, -W074 */ |
||
240 | options.encryptedPrimarySeed = typeof options.encryptedPrimarySeed !== "undefined" ? options.encryptedPrimarySeed : self.encryptedPrimarySeed; |
||
241 | options.encryptedSecret = typeof options.encryptedSecret !== "undefined" ? options.encryptedSecret : self.encryptedSecret; |
||
242 | |||
243 | if (options.secret) { |
||
244 | self.secret = options.secret; |
||
245 | } |
||
246 | |||
247 | if (options.primaryPrivateKey) { |
||
248 | throw new blocktrail.WalletDecryptError("specifying primaryPrivateKey has been deprecated"); |
||
249 | } |
||
250 | |||
251 | if (options.primarySeed) { |
||
252 | self.primarySeed = options.primarySeed; |
||
253 | } else if (options.secret) { |
||
254 | try { |
||
255 | self.primarySeed = new Buffer( |
||
256 | CryptoJS.AES.decrypt(CryptoJS.format.OpenSSL.parse(options.encryptedPrimarySeed), self.secret).toString(CryptoJS.enc.Utf8), 'base64'); |
||
257 | if (!self.primarySeed.length) { |
||
258 | throw new Error(); |
||
259 | } |
||
260 | } catch (e) { |
||
261 | throw new blocktrail.WalletDecryptError("Failed to decrypt primarySeed"); |
||
262 | } |
||
263 | |||
264 | } else { |
||
265 | // avoid conflicting options |
||
266 | if (options.passphrase && options.password) { |
||
267 | throw new blocktrail.WalletCreateError("Can't specify passphrase and password"); |
||
268 | } |
||
269 | // normalize passphrase/password |
||
270 | options.passphrase = options.passphrase || options.password; |
||
271 | |||
272 | try { |
||
273 | self.secret = CryptoJS.AES.decrypt(CryptoJS.format.OpenSSL.parse(options.encryptedSecret), options.passphrase).toString(CryptoJS.enc.Utf8); |
||
274 | if (!self.secret.length) { |
||
275 | throw new Error(); |
||
276 | } |
||
277 | } catch (e) { |
||
278 | throw new blocktrail.WalletDecryptError("Failed to decrypt secret"); |
||
279 | } |
||
280 | try { |
||
281 | self.primarySeed = new Buffer( |
||
282 | CryptoJS.AES.decrypt(CryptoJS.format.OpenSSL.parse(options.encryptedPrimarySeed), self.secret).toString(CryptoJS.enc.Utf8), 'base64'); |
||
283 | if (!self.primarySeed.length) { |
||
284 | throw new Error(); |
||
285 | } |
||
286 | } catch (e) { |
||
287 | throw new blocktrail.WalletDecryptError("Failed to decrypt primarySeed"); |
||
288 | } |
||
289 | } |
||
290 | |||
291 | return bitcoin.HDNode.fromSeedBuffer(self.primarySeed, self.network); |
||
292 | })); |
||
293 | |||
294 | return deferred.promise; |
||
295 | }; |
||
296 | |||
297 | Wallet.prototype.unlockV3 = function(options, cb) { |
||
298 | var self = this; |
||
299 | |||
300 | var deferred = q.defer(); |
||
301 | deferred.promise.nodeify(cb); |
||
302 | |||
303 | deferred.resolve(q.fcall(function() { |
||
304 | return q.when() |
||
305 | .then(function() { |
||
306 | /* jshint -W071, -W074 */ |
||
307 | options.encryptedPrimarySeed = typeof options.encryptedPrimarySeed !== "undefined" ? options.encryptedPrimarySeed : self.encryptedPrimarySeed; |
||
308 | options.encryptedSecret = typeof options.encryptedSecret !== "undefined" ? options.encryptedSecret : self.encryptedSecret; |
||
309 | |||
310 | if (options.secret) { |
||
311 | self.secret = options.secret; |
||
312 | } |
||
313 | |||
314 | if (options.primaryPrivateKey) { |
||
315 | throw new blocktrail.WalletInitError("specifying primaryPrivateKey has been deprecated"); |
||
316 | } |
||
317 | |||
318 | if (options.primarySeed) { |
||
319 | self.primarySeed = options.primarySeed; |
||
320 | } else if (options.secret) { |
||
321 | return self.sdk.promisedDecrypt(new Buffer(options.encryptedPrimarySeed, 'base64'), self.secret) |
||
322 | .then(function(primarySeed) { |
||
323 | self.primarySeed = primarySeed; |
||
324 | }, function() { |
||
325 | throw new blocktrail.WalletDecryptError("Failed to decrypt primarySeed"); |
||
326 | }); |
||
327 | } else { |
||
328 | // avoid conflicting options |
||
329 | if (options.passphrase && options.password) { |
||
330 | throw new blocktrail.WalletCreateError("Can't specify passphrase and password"); |
||
331 | } |
||
332 | // normalize passphrase/password |
||
333 | options.passphrase = options.passphrase || options.password; |
||
334 | delete options.password; |
||
335 | |||
336 | return self.sdk.promisedDecrypt(new Buffer(options.encryptedSecret, 'base64'), new Buffer(options.passphrase)) |
||
337 | .then(function(secret) { |
||
338 | self.secret = secret; |
||
339 | }, function() { |
||
340 | throw new blocktrail.WalletDecryptError("Failed to decrypt secret"); |
||
341 | }) |
||
342 | .then(function() { |
||
343 | return self.sdk.promisedDecrypt(new Buffer(options.encryptedPrimarySeed, 'base64'), self.secret) |
||
344 | .then(function(primarySeed) { |
||
345 | self.primarySeed = primarySeed; |
||
346 | }, function() { |
||
347 | throw new blocktrail.WalletDecryptError("Failed to decrypt primarySeed"); |
||
348 | }); |
||
349 | }); |
||
350 | } |
||
351 | }) |
||
352 | .then(function() { |
||
353 | return bitcoin.HDNode.fromSeedBuffer(self.primarySeed, self.network); |
||
354 | }) |
||
355 | ; |
||
356 | })); |
||
357 | |||
358 | return deferred.promise; |
||
359 | }; |
||
360 | |||
361 | Wallet.prototype.lock = function() { |
||
362 | var self = this; |
||
363 | |||
364 | self.secret = null; |
||
365 | self.primarySeed = null; |
||
366 | self.primaryPrivateKey = null; |
||
367 | self.backupPrivateKey = null; |
||
368 | |||
369 | self.locked = true; |
||
370 | }; |
||
371 | |||
372 | /** |
||
373 | * upgrade wallet to V3 encryption scheme |
||
374 | * |
||
375 | * @param passphrase is required again to reencrypt the data, important that it's the correct password!!! |
||
376 | * @param cb |
||
377 | * @returns {promise} |
||
378 | */ |
||
379 | Wallet.prototype.upgradeToV3 = function(passphrase, cb) { |
||
380 | var self = this; |
||
381 | |||
382 | var deferred = q.defer(); |
||
383 | deferred.promise.nodeify(cb); |
||
384 | |||
385 | q.when(true) |
||
386 | .then(function() { |
||
387 | if (self.locked) { |
||
388 | throw new blocktrail.WalletLockedError("Wallet needs to be unlocked to upgrade"); |
||
389 | } |
||
390 | |||
391 | if (self.walletVersion === Wallet.WALLET_VERSION_V3) { |
||
392 | throw new blocktrail.WalletUpgradeError("Wallet is already V3"); |
||
393 | } else if (self.walletVersion === Wallet.WALLET_VERSION_V2) { |
||
394 | return self._upgradeV2ToV3(passphrase, deferred.notify.bind(deferred)); |
||
395 | } else if (self.walletVersion === Wallet.WALLET_VERSION_V1) { |
||
0 ignored issues
–
show
There is no return statement if
self.walletVersion === Wallet.WALLET_VERSION_V1 is false . Are you sure this is correct? If so, consider adding return; explicitly.
This check looks for functions where a Consider this little piece of code function isBig(a) {
if (a > 5000) {
return "yes";
}
}
console.log(isBig(5001)); //returns yes
console.log(isBig(42)); //returns undefined
The function This behaviour may not be what you had intended. In any case, you can add a
![]() |
|||
396 | return self._upgradeV1ToV3(passphrase, deferred.notify.bind(deferred)); |
||
397 | } |
||
398 | }) |
||
399 | .then(function(r) { deferred.resolve(r); }, function(e) { deferred.reject(e); }); |
||
400 | |||
401 | return deferred.promise; |
||
402 | }; |
||
403 | |||
404 | Wallet.prototype._upgradeV2ToV3 = function(passphrase, notify) { |
||
405 | var self = this; |
||
406 | |||
407 | return q.when(true) |
||
408 | .then(function() { |
||
409 | var options = { |
||
410 | storeDataOnServer: true, |
||
411 | passphrase: passphrase, |
||
412 | primarySeed: self.primarySeed, |
||
413 | recoverySecret: false // don't create new recovery secret, V2 already has ones |
||
414 | }; |
||
415 | |||
416 | return self.sdk.produceEncryptedDataV3(options, notify || function noop() {}) |
||
417 | .then(function(options) { |
||
418 | return self.sdk.updateWallet(self.identifier, { |
||
419 | encrypted_primary_seed: options.encryptedPrimarySeed.toString('base64'), |
||
420 | encrypted_secret: options.encryptedSecret.toString('base64'), |
||
421 | wallet_version: Wallet.WALLET_VERSION_V3 |
||
422 | }).then(function() { |
||
423 | self.secret = options.secret; |
||
424 | self.encryptedPrimarySeed = options.encryptedPrimarySeed; |
||
425 | self.encryptedSecret = options.encryptedSecret; |
||
426 | self.walletVersion = Wallet.WALLET_VERSION_V3; |
||
427 | |||
428 | return self; |
||
429 | }); |
||
430 | }); |
||
431 | }); |
||
432 | |||
433 | }; |
||
434 | |||
435 | Wallet.prototype._upgradeV1ToV3 = function(passphrase, notify) { |
||
436 | var self = this; |
||
437 | |||
438 | return q.when(true) |
||
439 | .then(function() { |
||
440 | var options = { |
||
441 | storeDataOnServer: true, |
||
442 | passphrase: passphrase, |
||
443 | primarySeed: self.primarySeed |
||
444 | }; |
||
445 | |||
446 | return self.sdk.produceEncryptedDataV3(options, notify || function noop() {}) |
||
447 | .then(function(options) { |
||
448 | // store recoveryEncryptedSecret for printing on backup sheet |
||
449 | self.recoveryEncryptedSecret = options.recoveryEncryptedSecret; |
||
450 | |||
451 | return self.sdk.updateWallet(self.identifier, { |
||
452 | primary_mnemonic: '', |
||
453 | encrypted_primary_seed: options.encryptedPrimarySeed.toString('base64'), |
||
454 | encrypted_secret: options.encryptedSecret.toString('base64'), |
||
455 | recovery_secret: options.recoverySecret.toString('hex'), |
||
456 | wallet_version: Wallet.WALLET_VERSION_V3 |
||
457 | }).then(function() { |
||
458 | self.secret = options.secret; |
||
459 | self.encryptedPrimarySeed = options.encryptedPrimarySeed; |
||
460 | self.encryptedSecret = options.encryptedSecret; |
||
461 | self.walletVersion = Wallet.WALLET_VERSION_V3; |
||
462 | |||
463 | return self; |
||
464 | }); |
||
465 | }); |
||
466 | }); |
||
467 | }; |
||
468 | |||
469 | Wallet.prototype.doPasswordChange = function(newPassword) { |
||
470 | var self = this; |
||
471 | |||
472 | return q.when(null) |
||
473 | .then(function() { |
||
474 | |||
475 | if (self.walletVersion === Wallet.WALLET_VERSION_V1) { |
||
476 | throw new blocktrail.WalletLockedError("Wallet version does not support password change!"); |
||
477 | } |
||
478 | |||
479 | if (self.locked) { |
||
480 | throw new blocktrail.WalletLockedError("Wallet needs to be unlocked to change password"); |
||
481 | } |
||
482 | |||
483 | if (!self.secret) { |
||
484 | throw new blocktrail.WalletLockedError("No secret"); |
||
485 | } |
||
486 | |||
487 | var newEncryptedSecret; |
||
488 | var newEncrypedWalletSecretMnemonic; |
||
489 | if (self.walletVersion === Wallet.WALLET_VERSION_V2) { |
||
490 | newEncryptedSecret = CryptoJS.AES.encrypt(self.secret, newPassword).toString(CryptoJS.format.OpenSSL); |
||
491 | newEncrypedWalletSecretMnemonic = bip39.entropyToMnemonic(blocktrail.convert(newEncryptedSecret, 'base64', 'hex')); |
||
492 | |||
493 | } else { |
||
494 | if (typeof newPassword === "string") { |
||
495 | newPassword = new Buffer(newPassword); |
||
496 | } else { |
||
497 | if (!(newPassword instanceof Buffer)) { |
||
498 | throw new Error('New password must be provided as a string or a Buffer'); |
||
499 | } |
||
500 | } |
||
501 | |||
502 | newEncryptedSecret = Encryption.encrypt(self.secret, newPassword); |
||
503 | newEncrypedWalletSecretMnemonic = EncryptionMnemonic.encode(newEncryptedSecret); |
||
504 | |||
505 | // It's a buffer, so convert it back to base64 |
||
506 | newEncryptedSecret = newEncryptedSecret.toString('base64'); |
||
507 | } |
||
508 | |||
509 | return [newEncryptedSecret, newEncrypedWalletSecretMnemonic]; |
||
510 | }); |
||
511 | }; |
||
512 | |||
513 | Wallet.prototype.passwordChange = function(newPassword, cb) { |
||
514 | var self = this; |
||
515 | |||
516 | var deferred = q.defer(); |
||
517 | deferred.promise.nodeify(cb); |
||
518 | |||
519 | q.fcall(function() { |
||
520 | return self.doPasswordChange(newPassword) |
||
521 | .then(function(r) { |
||
522 | var newEncryptedSecret = r[0]; |
||
523 | var newEncrypedWalletSecretMnemonic = r[1]; |
||
524 | |||
525 | return self.sdk.updateWallet(self.identifier, {encrypted_secret: newEncryptedSecret}).then(function() { |
||
526 | self.encryptedSecret = newEncryptedSecret; |
||
527 | |||
528 | // backupInfo |
||
529 | return { |
||
530 | encryptedSecret: newEncrypedWalletSecretMnemonic |
||
531 | }; |
||
532 | }); |
||
533 | }) |
||
534 | .then( |
||
535 | function(r) { |
||
536 | deferred.resolve(r); |
||
537 | }, |
||
538 | function(e) { |
||
539 | deferred.reject(e); |
||
540 | } |
||
541 | ); |
||
542 | }); |
||
543 | |||
544 | return deferred.promise; |
||
545 | }; |
||
546 | |||
547 | /** |
||
548 | * get address for specified path |
||
549 | * |
||
550 | * @param path |
||
551 | * @returns string |
||
552 | */ |
||
553 | Wallet.prototype.getAddressByPath = function(path) { |
||
554 | return this.getWalletScriptByPath(path).address; |
||
555 | }; |
||
556 | |||
557 | /** |
||
558 | * get redeemscript for specified path |
||
559 | * |
||
560 | * @param path |
||
561 | * @returns {Buffer} |
||
562 | */ |
||
563 | Wallet.prototype.getRedeemScriptByPath = function(path) { |
||
564 | return this.getWalletScriptByPath(path).redeemScript; |
||
565 | }; |
||
566 | |||
567 | /** |
||
568 | * Generate scripts, and address. |
||
569 | * @param path |
||
570 | * @returns {{witnessScript: *, redeemScript: *, scriptPubKey, address: *}} |
||
571 | */ |
||
572 | Wallet.prototype.getWalletScriptByPath = function(path) { |
||
573 | var self = this; |
||
574 | |||
575 | // get derived primary key |
||
576 | var derivedPrimaryPublicKey = self.getPrimaryPublicKey(path); |
||
577 | // get derived blocktrail key |
||
578 | var derivedBlocktrailPublicKey = self.getBlocktrailPublicKey(path); |
||
579 | // derive the backup key |
||
580 | var derivedBackupPublicKey = Wallet.deriveByPath(self.backupPublicKey, path.replace("'", ""), "M"); |
||
581 | |||
582 | // sort the pubkeys |
||
583 | var pubKeys = Wallet.sortMultiSigKeys([ |
||
584 | derivedPrimaryPublicKey.keyPair.getPublicKeyBuffer(), |
||
585 | derivedBackupPublicKey.keyPair.getPublicKeyBuffer(), |
||
586 | derivedBlocktrailPublicKey.keyPair.getPublicKeyBuffer() |
||
587 | ]); |
||
588 | |||
589 | var multisig = bitcoin.script.multisig.output.encode(2, pubKeys); |
||
590 | var scriptType = parseInt(path.split("/")[2]); |
||
591 | |||
592 | var ws, rs; |
||
593 | if (this.network !== "bitcoincash" && scriptType === Wallet.CHAIN_BTC_SEGWIT) { |
||
594 | ws = multisig; |
||
595 | rs = bitcoin.script.witnessScriptHash.output.encode(bitcoin.crypto.sha256(ws)); |
||
596 | } else { |
||
597 | ws = null; |
||
598 | rs = multisig; |
||
599 | } |
||
600 | |||
601 | var spk = bitcoin.script.scriptHash.output.encode(bitcoin.crypto.hash160(rs)); |
||
602 | var addr = bitcoin.address.fromOutputScript(spk, this.network, self.useNewCashAddr); |
||
603 | |||
604 | return { |
||
605 | witnessScript: ws, |
||
606 | redeemScript: rs, |
||
607 | scriptPubKey: spk, |
||
608 | address: addr |
||
609 | }; |
||
610 | }; |
||
611 | |||
612 | /** |
||
613 | * get primary public key by path |
||
614 | * first level of the path is used as keyIndex to find the correct key in the dict |
||
615 | * |
||
616 | * @param path string |
||
617 | * @returns {bitcoin.HDNode} |
||
618 | */ |
||
619 | Wallet.prototype.getPrimaryPublicKey = function(path) { |
||
620 | var self = this; |
||
621 | |||
622 | path = path.replace("m", "M"); |
||
623 | |||
624 | var keyIndex = path.split("/")[1].replace("'", ""); |
||
625 | |||
626 | if (!self.primaryPublicKeys[keyIndex]) { |
||
627 | if (self.primaryPrivateKey) { |
||
628 | self.primaryPublicKeys[keyIndex] = Wallet.deriveByPath(self.primaryPrivateKey, "M/" + keyIndex + "'", "m"); |
||
629 | } else { |
||
630 | throw new blocktrail.KeyPathError("Wallet.getPrimaryPublicKey keyIndex (" + keyIndex + ") is unknown to us"); |
||
631 | } |
||
632 | } |
||
633 | |||
634 | var primaryPublicKey = self.primaryPublicKeys[keyIndex]; |
||
635 | return Wallet.deriveByPath(primaryPublicKey, path, "M/" + keyIndex + "'"); |
||
636 | }; |
||
637 | |||
638 | /** |
||
639 | * get blocktrail public key by path |
||
640 | * first level of the path is used as keyIndex to find the correct key in the dict |
||
641 | * |
||
642 | * @param path string |
||
643 | * @returns {bitcoin.HDNode} |
||
644 | */ |
||
645 | Wallet.prototype.getBlocktrailPublicKey = function(path) { |
||
646 | var self = this; |
||
647 | |||
648 | path = path.replace("m", "M"); |
||
649 | |||
650 | var keyIndex = path.split("/")[1].replace("'", ""); |
||
651 | |||
652 | if (!self.blocktrailPublicKeys[keyIndex]) { |
||
653 | throw new blocktrail.KeyPathError("Wallet.getBlocktrailPublicKey keyIndex (" + keyIndex + ") is unknown to us"); |
||
654 | } |
||
655 | |||
656 | var blocktrailPublicKey = self.blocktrailPublicKeys[keyIndex]; |
||
657 | |||
658 | return Wallet.deriveByPath(blocktrailPublicKey, path, "M/" + keyIndex + "'"); |
||
659 | }; |
||
660 | |||
661 | /** |
||
662 | * upgrade wallet to different blocktrail cosign key |
||
663 | * |
||
664 | * @param keyIndex int |
||
665 | * @param [cb] function |
||
666 | * @returns {q.Promise} |
||
667 | */ |
||
668 | Wallet.prototype.upgradeKeyIndex = function(keyIndex, cb) { |
||
669 | var self = this; |
||
670 | |||
671 | var deferred = q.defer(); |
||
672 | deferred.promise.nodeify(cb); |
||
673 | |||
674 | if (self.locked) { |
||
675 | deferred.reject(new blocktrail.WalletLockedError("Wallet needs to be unlocked to upgrade key index")); |
||
676 | return deferred.promise; |
||
677 | } |
||
678 | |||
679 | var primaryPublicKey = self.primaryPrivateKey.deriveHardened(keyIndex).neutered(); |
||
680 | |||
681 | deferred.resolve( |
||
682 | self.sdk.upgradeKeyIndex(self.identifier, keyIndex, [primaryPublicKey.toBase58(), "M/" + keyIndex + "'"]) |
||
683 | .then(function(result) { |
||
684 | self.keyIndex = keyIndex; |
||
685 | _.forEach(result.blocktrail_public_keys, function(publicKey, keyIndex) { |
||
686 | self.blocktrailPublicKeys[keyIndex] = bitcoin.HDNode.fromBase58(publicKey[0], self.network); |
||
687 | }); |
||
688 | |||
689 | self.primaryPublicKeys[keyIndex] = primaryPublicKey; |
||
690 | |||
691 | return true; |
||
692 | }) |
||
693 | ); |
||
694 | |||
695 | return deferred.promise; |
||
696 | }; |
||
697 | |||
698 | /** |
||
699 | * generate a new derived private key and return the new address for it |
||
700 | * |
||
701 | * @param [chainIdx] int |
||
702 | * @param [cb] function callback(err, address) |
||
703 | * @returns {q.Promise} |
||
704 | */ |
||
705 | Wallet.prototype.getNewAddress = function(chainIdx, cb) { |
||
706 | var self = this; |
||
707 | |||
708 | // chainIdx is optional |
||
709 | if (typeof chainIdx === "function") { |
||
710 | cb = chainIdx; |
||
711 | chainIdx = null; |
||
712 | } |
||
713 | |||
714 | var deferred = q.defer(); |
||
715 | deferred.promise.spreadNodeify(cb); |
||
716 | |||
717 | // Only enter if it's not an integer |
||
718 | if (chainIdx !== parseInt(chainIdx, 10)) { |
||
719 | // deal with undefined or null, assume defaults |
||
720 | if (typeof chainIdx === "undefined" || chainIdx === null) { |
||
721 | chainIdx = self.chain; |
||
722 | } else { |
||
723 | // was a variable but not integer |
||
724 | deferred.reject(new Error("Invalid chain index")); |
||
725 | return deferred.promise; |
||
726 | } |
||
727 | } |
||
728 | |||
729 | deferred.resolve( |
||
730 | self.sdk.getNewDerivation(self.identifier, "M/" + self.keyIndex + "'/" + chainIdx) |
||
731 | .then(function(newDerivation) { |
||
732 | var path = newDerivation.path; |
||
733 | var addressFromServer = newDerivation.address; |
||
734 | var decodedFromServer; |
||
735 | |||
736 | try { |
||
737 | // Decode the address the serer gave us |
||
738 | decodedFromServer = self.decodeAddress(addressFromServer); |
||
739 | if ("cashAddrPrefix" in self.network && self.useNewCashAddr && decodedFromServer.type === "base58") { |
||
740 | self.bypassNewAddressCheck = false; |
||
741 | } |
||
742 | } catch (e) { |
||
743 | throw new blocktrail.WalletAddressError("Failed to decode address [" + newDerivation.address + "]"); |
||
744 | } |
||
745 | |||
746 | if (!self.bypassNewAddressCheck) { |
||
747 | // We need to reproduce this address with the same path, |
||
748 | // but the server (for BCH cashaddrs) uses base58? |
||
749 | var verifyAddress = self.getAddressByPath(newDerivation.path); |
||
750 | |||
751 | // If this occasion arises: |
||
752 | if ("cashAddrPrefix" in self.network && self.useNewCashAddr && decodedFromServer.type === "base58") { |
||
753 | // Decode our the address we produced for the path |
||
754 | var decodeOurs; |
||
755 | try { |
||
756 | decodeOurs = self.decodeAddress(verifyAddress); |
||
757 | } catch (e) { |
||
758 | throw new blocktrail.WalletAddressError("Error while verifying address from server [" + e.message + "]"); |
||
759 | } |
||
760 | |||
761 | // Peek beyond the encoding - the hashes must match at least |
||
762 | if (decodeOurs.decoded.hash.toString('hex') !== decodedFromServer.decoded.hash.toString('hex')) { |
||
763 | throw new blocktrail.WalletAddressError("Failed to verify legacy address [hash mismatch]"); |
||
764 | } |
||
765 | |||
766 | var matchedP2PKH = decodeOurs.decoded.version === bitcoin.script.types.P2PKH && |
||
767 | decodedFromServer.decoded.version === self.network.pubKeyHash; |
||
768 | var matchedP2SH = decodeOurs.decoded.version === bitcoin.script.types.P2SH && |
||
769 | decodedFromServer.decoded.version === self.network.scriptHash; |
||
770 | |||
771 | if (!(matchedP2PKH || matchedP2SH)) { |
||
772 | throw new blocktrail.WalletAddressError("Failed to verify legacy address [prefix mismatch]"); |
||
773 | } |
||
774 | |||
775 | // We are satisfied that the address is for the same |
||
776 | // destination, so substitute addressFromServer with our |
||
777 | // 'reencoded' form. |
||
778 | addressFromServer = decodeOurs.address; |
||
779 | } |
||
780 | |||
781 | // debug check |
||
782 | if (verifyAddress !== addressFromServer) { |
||
783 | throw new blocktrail.WalletAddressError("Failed to verify address [" + newDerivation.address + "] !== [" + addressFromServer + "]"); |
||
784 | } |
||
785 | } |
||
786 | |||
787 | return [addressFromServer, path]; |
||
788 | }) |
||
789 | ); |
||
790 | |||
791 | return deferred.promise; |
||
792 | }; |
||
793 | |||
794 | /** |
||
795 | * get the balance for the wallet |
||
796 | * |
||
797 | * @param [cb] function callback(err, confirmed, unconfirmed) |
||
798 | * @returns {q.Promise} |
||
799 | */ |
||
800 | Wallet.prototype.getBalance = function(cb) { |
||
801 | var self = this; |
||
802 | |||
803 | var deferred = q.defer(); |
||
804 | deferred.promise.spreadNodeify(cb); |
||
805 | |||
806 | deferred.resolve( |
||
807 | self.sdk.getWalletBalance(self.identifier) |
||
808 | .then(function(result) { |
||
809 | return [result.confirmed, result.unconfirmed]; |
||
810 | }) |
||
811 | ); |
||
812 | |||
813 | return deferred.promise; |
||
814 | }; |
||
815 | |||
816 | /** |
||
817 | * get the balance for the wallet |
||
818 | * |
||
819 | * @param [cb] function callback(err, confirmed, unconfirmed) |
||
820 | * @returns {q.Promise} |
||
821 | */ |
||
822 | Wallet.prototype.getInfo = function(cb) { |
||
823 | var self = this; |
||
824 | |||
825 | var deferred = q.defer(); |
||
826 | deferred.promise.spreadNodeify(cb); |
||
827 | |||
828 | deferred.resolve( |
||
829 | self.sdk.getWalletBalance(self.identifier) |
||
830 | ); |
||
831 | |||
832 | return deferred.promise; |
||
833 | }; |
||
834 | |||
835 | /** |
||
836 | * |
||
837 | * @param [force] bool ignore warnings (such as non-zero balance) |
||
838 | * @param [cb] function callback(err, success) |
||
839 | * @returns {q.Promise} |
||
840 | */ |
||
841 | Wallet.prototype.deleteWallet = function(force, cb) { |
||
842 | var self = this; |
||
843 | |||
844 | if (typeof force === "function") { |
||
845 | cb = force; |
||
846 | force = false; |
||
847 | } |
||
848 | |||
849 | var deferred = q.defer(); |
||
850 | deferred.promise.nodeify(cb); |
||
851 | |||
852 | if (self.locked) { |
||
853 | deferred.reject(new blocktrail.WalletDeleteError("Wallet needs to be unlocked to delete wallet")); |
||
854 | return deferred.promise; |
||
855 | } |
||
856 | |||
857 | var checksum = self.primaryPrivateKey.getAddress(); |
||
858 | var privBuf = self.primaryPrivateKey.keyPair.d.toBuffer(32); |
||
859 | var signature = bitcoinMessage.sign(checksum, self.network.messagePrefix, privBuf, true).toString('base64'); |
||
860 | |||
861 | deferred.resolve( |
||
862 | self.sdk.deleteWallet(self.identifier, checksum, signature, force) |
||
863 | .then(function(result) { |
||
864 | return result.deleted; |
||
865 | }) |
||
866 | ); |
||
867 | |||
868 | return deferred.promise; |
||
869 | }; |
||
870 | |||
871 | /** |
||
872 | * create, sign and send a transaction |
||
873 | * |
||
874 | * @param pay array {'address': (int)value} coins to send |
||
875 | * @param [changeAddress] bool change address to use (auto generated if NULL) |
||
876 | * @param [allowZeroConf] bool allow zero confirmation unspent outputs to be used in coin selection |
||
877 | * @param [randomizeChangeIdx] bool randomize the index of the change output (default TRUE, only disable if you have a good reason to) |
||
878 | * @param [feeStrategy] string defaults to Wallet.FEE_STRATEGY_OPTIMAL |
||
879 | * @param [twoFactorToken] string 2FA token |
||
880 | * @param options |
||
881 | * @param [cb] function callback(err, txHash) |
||
882 | * @returns {q.Promise} |
||
883 | */ |
||
884 | Wallet.prototype.pay = function(pay, changeAddress, allowZeroConf, randomizeChangeIdx, feeStrategy, twoFactorToken, options, cb) { |
||
885 | |||
886 | /* jshint -W071 */ |
||
887 | var self = this; |
||
888 | |||
889 | View Code Duplication | if (typeof changeAddress === "function") { |
|
890 | cb = changeAddress; |
||
891 | changeAddress = null; |
||
892 | } else if (typeof allowZeroConf === "function") { |
||
893 | cb = allowZeroConf; |
||
894 | allowZeroConf = false; |
||
895 | } else if (typeof randomizeChangeIdx === "function") { |
||
896 | cb = randomizeChangeIdx; |
||
897 | randomizeChangeIdx = true; |
||
898 | } else if (typeof feeStrategy === "function") { |
||
899 | cb = feeStrategy; |
||
900 | feeStrategy = null; |
||
901 | } else if (typeof twoFactorToken === "function") { |
||
902 | cb = twoFactorToken; |
||
903 | twoFactorToken = null; |
||
904 | } else if (typeof options === "function") { |
||
905 | cb = options; |
||
906 | options = {}; |
||
907 | } |
||
908 | |||
909 | randomizeChangeIdx = typeof randomizeChangeIdx !== "undefined" ? randomizeChangeIdx : true; |
||
910 | feeStrategy = feeStrategy || Wallet.FEE_STRATEGY_OPTIMAL; |
||
911 | options = options || {}; |
||
912 | var checkFee = typeof options.checkFee !== "undefined" ? options.checkFee : true; |
||
913 | |||
914 | var deferred = q.defer(); |
||
915 | deferred.promise.nodeify(cb); |
||
916 | |||
917 | if (self.locked) { |
||
918 | deferred.reject(new blocktrail.WalletLockedError("Wallet needs to be unlocked to send coins")); |
||
919 | return deferred.promise; |
||
920 | } |
||
921 | |||
922 | q.nextTick(function() { |
||
923 | deferred.notify(Wallet.PAY_PROGRESS_START); |
||
924 | self.buildTransaction(pay, changeAddress, allowZeroConf, randomizeChangeIdx, feeStrategy, options) |
||
925 | .then( |
||
926 | function(r) { return r; }, |
||
927 | function(e) { deferred.reject(e); }, |
||
928 | function(progress) { |
||
929 | deferred.notify(progress); |
||
930 | } |
||
931 | ) |
||
932 | .spread( |
||
933 | function(tx, utxos) { |
||
934 | |||
935 | deferred.notify(Wallet.PAY_PROGRESS_SEND); |
||
936 | |||
937 | var data = { |
||
938 | signed_transaction: tx.toHex(), |
||
939 | base_transaction: tx.__toBuffer(null, null, false).toString('hex') |
||
940 | }; |
||
941 | |||
942 | return self.sendTransaction(data, utxos.map(function(utxo) { return utxo['path']; }), checkFee, twoFactorToken, options.prioboost) |
||
943 | .then(function(result) { |
||
944 | deferred.notify(Wallet.PAY_PROGRESS_DONE); |
||
945 | |||
946 | if (!result || !result['complete'] || result['complete'] === 'false') { |
||
947 | deferred.reject(new blocktrail.TransactionSignError("Failed to completely sign transaction")); |
||
948 | } else { |
||
949 | return result['txid']; |
||
950 | } |
||
951 | }); |
||
952 | }, |
||
953 | function(e) { |
||
954 | throw e; |
||
955 | } |
||
956 | ) |
||
957 | .then( |
||
958 | function(r) { deferred.resolve(r); }, |
||
959 | function(e) { deferred.reject(e); } |
||
960 | ) |
||
961 | ; |
||
962 | }); |
||
963 | |||
964 | return deferred.promise; |
||
965 | }; |
||
966 | |||
967 | Wallet.prototype.decodeAddress = function(address) { |
||
968 | return Wallet.getAddressAndType(address, this.network, this.useNewCashAddr); |
||
969 | }; |
||
970 | |||
971 | function readBech32Address(address, network) { |
||
972 | var addr; |
||
973 | var err; |
||
974 | try { |
||
975 | addr = bitcoin.address.fromBech32(address, network); |
||
976 | err = null; |
||
977 | |||
978 | } catch (_err) { |
||
979 | err = _err; |
||
980 | } |
||
981 | |||
982 | if (!err) { |
||
983 | // Valid bech32 but invalid network immediately alerts |
||
984 | if (addr.prefix !== network.bech32) { |
||
985 | throw new blocktrail.InvalidAddressError("Address invalid on this network"); |
||
986 | } |
||
987 | } |
||
988 | |||
989 | return [err, addr]; |
||
990 | } |
||
991 | |||
992 | function readCashAddress(address, network) { |
||
993 | var addr; |
||
994 | var err; |
||
995 | address = address.toLowerCase(); |
||
996 | try { |
||
997 | addr = bitcoin.address.fromCashAddress(address); |
||
998 | err = null; |
||
999 | } catch (_err) { |
||
1000 | err = _err; |
||
1001 | } |
||
1002 | |||
1003 | if (err) { |
||
1004 | try { |
||
1005 | addr = bitcoin.address.fromCashAddress(network.cashAddrPrefix + ':' + address); |
||
1006 | err = null; |
||
1007 | } catch (_err) { |
||
1008 | err = _err; |
||
1009 | } |
||
1010 | } |
||
1011 | |||
1012 | if (!err) { |
||
1013 | // Valid base58 but invalid network immediately alerts |
||
1014 | if (addr.prefix !== network.cashAddrPrefix) { |
||
1015 | throw new Error(address + ' has an invalid prefix'); |
||
1016 | } |
||
1017 | } |
||
1018 | |||
1019 | return [err, addr]; |
||
1020 | } |
||
1021 | |||
1022 | function readBase58Address(address, network) { |
||
1023 | var addr; |
||
1024 | var err; |
||
1025 | try { |
||
1026 | addr = bitcoin.address.fromBase58Check(address); |
||
1027 | err = null; |
||
1028 | } catch (_err) { |
||
1029 | err = _err; |
||
1030 | } |
||
1031 | |||
1032 | if (!err) { |
||
1033 | // Valid base58 but invalid network immediately alerts |
||
1034 | if (addr.version !== network.pubKeyHash && addr.version !== network.scriptHash) { |
||
1035 | throw new blocktrail.InvalidAddressError("Address invalid on this network"); |
||
1036 | } |
||
1037 | } |
||
1038 | |||
1039 | return [err, addr]; |
||
1040 | } |
||
1041 | |||
1042 | Wallet.getAddressAndType = function(address, network, allowCashAddress) { |
||
1043 | var addr; |
||
1044 | var type; |
||
1045 | var err; |
||
1046 | |||
1047 | function readAddress(reader, readType) { |
||
1048 | var decoded = reader(address, network); |
||
1049 | if (decoded[0] === null) { |
||
1050 | addr = decoded[1]; |
||
1051 | type = readType; |
||
1052 | } else { |
||
1053 | err = decoded[0]; |
||
1054 | } |
||
1055 | } |
||
1056 | |||
1057 | if (network === bitcoin.networks.bitcoin || |
||
1058 | network === bitcoin.networks.testnet || |
||
1059 | network === bitcoin.networks.regtest |
||
1060 | ) { |
||
1061 | readAddress(readBech32Address, "bech32"); |
||
1062 | } |
||
1063 | |||
1064 | if (!addr && 'cashAddrPrefix' in network && allowCashAddress) { |
||
1065 | readAddress(readCashAddress, "cashaddr"); |
||
1066 | } |
||
1067 | |||
1068 | if (!addr) { |
||
1069 | readAddress(readBase58Address, "base58"); |
||
1070 | } |
||
1071 | |||
1072 | if (addr) { |
||
1073 | return { |
||
1074 | address: address, |
||
1075 | decoded: addr, |
||
1076 | type: type |
||
1077 | }; |
||
1078 | } else { |
||
1079 | throw new blocktrail.InvalidAddressError(err.message); |
||
1080 | } |
||
1081 | }; |
||
1082 | |||
1083 | Wallet.convertPayToOutputs = function(pay, network, allowCashAddr) { |
||
1084 | var send = []; |
||
1085 | |||
1086 | var readFunc; |
||
1087 | |||
1088 | // Deal with two different forms |
||
1089 | if (Array.isArray(pay)) { |
||
1090 | // output[] |
||
1091 | readFunc = function(i, output, obj) { |
||
1092 | if (typeof output !== "object") { |
||
1093 | throw new Error("Invalid transaction output for numerically indexed list [1]"); |
||
1094 | } |
||
1095 | |||
1096 | var keys = Object.keys(output); |
||
1097 | if (keys.indexOf("scriptPubKey") !== -1 && keys.indexOf("value") !== -1) { |
||
1098 | obj.scriptPubKey = output["scriptPubKey"]; |
||
1099 | obj.value = output["value"]; |
||
1100 | } else if (keys.indexOf("address") !== -1 && keys.indexOf("value") !== -1) { |
||
1101 | obj.address = output["address"]; |
||
1102 | obj.value = output["value"]; |
||
1103 | } else if (keys.length === 2 && output.length === 2 && keys[0] === '0' && keys[1] === '1') { |
||
1104 | obj.address = output[0]; |
||
1105 | obj.value = output[1]; |
||
1106 | } else { |
||
1107 | throw new Error("Invalid transaction output for numerically indexed list [2]"); |
||
1108 | } |
||
1109 | }; |
||
1110 | } else if (typeof pay === "object") { |
||
1111 | // map[addr]amount |
||
1112 | readFunc = function(address, value, obj) { |
||
1113 | obj.address = address.trim(); |
||
1114 | obj.value = value; |
||
1115 | if (obj.address === Wallet.OP_RETURN) { |
||
1116 | var datachunk = Buffer.isBuffer(value) ? value : new Buffer(value, 'utf-8'); |
||
1117 | obj.scriptPubKey = bitcoin.script.nullData.output.encode(datachunk).toString('hex'); |
||
1118 | obj.value = 0; |
||
1119 | obj.address = null; |
||
1120 | } |
||
1121 | }; |
||
1122 | } else { |
||
1123 | throw new Error("Invalid input"); |
||
1124 | } |
||
1125 | |||
1126 | Object.keys(pay).forEach(function(key) { |
||
1127 | var obj = {}; |
||
1128 | readFunc(key, pay[key], obj); |
||
1129 | |||
1130 | if (parseInt(obj.value, 10).toString() !== obj.value.toString()) { |
||
1131 | throw new blocktrail.WalletSendError("Values should be in Satoshis"); |
||
1132 | } |
||
1133 | |||
1134 | // Remove address, replace with scriptPubKey |
||
1135 | if (typeof obj.address === "string") { |
||
1136 | try { |
||
1137 | var addrAndType = Wallet.getAddressAndType(obj.address, network, allowCashAddr); |
||
1138 | obj.scriptPubKey = bitcoin.address.toOutputScript(addrAndType.address, network, allowCashAddr).toString('hex'); |
||
1139 | delete obj.address; |
||
1140 | } catch (e) { |
||
1141 | throw new blocktrail.InvalidAddressError("Invalid address [" + obj.address + "] (" + e.message + ")"); |
||
1142 | } |
||
1143 | } |
||
1144 | |||
1145 | // Extra checks when the output isn't OP_RETURN |
||
1146 | if (obj.scriptPubKey.slice(0, 2) !== "6a") { |
||
1147 | if (!(obj.value = parseInt(obj.value, 10))) { |
||
1148 | throw new blocktrail.WalletSendError("Values should be non zero"); |
||
1149 | } else if (obj.value <= blocktrail.DUST) { |
||
1150 | throw new blocktrail.WalletSendError("Values should be more than dust (" + blocktrail.DUST + ")"); |
||
1151 | } |
||
1152 | } |
||
1153 | |||
1154 | // Value fully checked now |
||
1155 | obj.value = parseInt(obj.value, 10); |
||
1156 | |||
1157 | send.push(obj); |
||
1158 | }); |
||
1159 | |||
1160 | return send; |
||
1161 | }; |
||
1162 | |||
1163 | Wallet.prototype.buildTransaction = function(pay, changeAddress, allowZeroConf, randomizeChangeIdx, feeStrategy, options, cb) { |
||
1164 | /* jshint -W071 */ |
||
1165 | var self = this; |
||
1166 | |||
1167 | View Code Duplication | if (typeof changeAddress === "function") { |
|
1168 | cb = changeAddress; |
||
1169 | changeAddress = null; |
||
1170 | } else if (typeof allowZeroConf === "function") { |
||
1171 | cb = allowZeroConf; |
||
1172 | allowZeroConf = false; |
||
1173 | } else if (typeof randomizeChangeIdx === "function") { |
||
1174 | cb = randomizeChangeIdx; |
||
1175 | randomizeChangeIdx = true; |
||
1176 | } else if (typeof feeStrategy === "function") { |
||
1177 | cb = feeStrategy; |
||
1178 | feeStrategy = null; |
||
1179 | } else if (typeof options === "function") { |
||
1180 | cb = options; |
||
1181 | options = {}; |
||
1182 | } |
||
1183 | |||
1184 | randomizeChangeIdx = typeof randomizeChangeIdx !== "undefined" ? randomizeChangeIdx : true; |
||
1185 | feeStrategy = feeStrategy || Wallet.FEE_STRATEGY_OPTIMAL; |
||
1186 | options = options || {}; |
||
1187 | |||
1188 | var deferred = q.defer(); |
||
1189 | deferred.promise.spreadNodeify(cb); |
||
1190 | |||
1191 | q.nextTick(function() { |
||
1192 | var send; |
||
1193 | try { |
||
1194 | send = Wallet.convertPayToOutputs(pay, self.network, self.useNewCashAddr); |
||
1195 | } catch (e) { |
||
1196 | deferred.reject(e); |
||
1197 | return deferred.promise; |
||
1198 | } |
||
1199 | |||
1200 | if (!send.length) { |
||
1201 | deferred.reject(new blocktrail.WalletSendError("Need at least one recipient")); |
||
1202 | return deferred.promise; |
||
1203 | } |
||
1204 | |||
1205 | deferred.notify(Wallet.PAY_PROGRESS_COIN_SELECTION); |
||
1206 | |||
1207 | deferred.resolve( |
||
1208 | self.coinSelection(send, true, allowZeroConf, feeStrategy, options) |
||
1209 | /** |
||
1210 | * |
||
1211 | * @param {Object[]} utxos |
||
1212 | * @param fee |
||
1213 | * @param change |
||
1214 | * @param randomizeChangeIdx |
||
1215 | * @returns {*} |
||
1216 | */ |
||
1217 | .spread(function(utxos, fee, change) { |
||
1218 | var tx, txb, outputs = []; |
||
1219 | |||
1220 | var deferred = q.defer(); |
||
1221 | |||
1222 | async.waterfall([ |
||
1223 | /** |
||
1224 | * prepare |
||
1225 | * |
||
1226 | * @param cb |
||
1227 | */ |
||
1228 | function(cb) { |
||
1229 | var inputsTotal = utxos.map(function(utxo) { |
||
1230 | return utxo['value']; |
||
1231 | }).reduce(function(a, b) { |
||
1232 | return a + b; |
||
1233 | }); |
||
1234 | var outputsTotal = send.map(function(output) { |
||
1235 | return output.value; |
||
1236 | }).reduce(function(a, b) { |
||
1237 | return a + b; |
||
1238 | }); |
||
1239 | var estimatedChange = inputsTotal - outputsTotal - fee; |
||
1240 | |||
1241 | if (estimatedChange > blocktrail.DUST * 2 && estimatedChange !== change) { |
||
1242 | return cb(new blocktrail.WalletFeeError("the amount of change (" + change + ") " + |
||
1243 | "suggested by the coin selection seems incorrect (" + estimatedChange + ")")); |
||
1244 | } |
||
1245 | |||
1246 | cb(); |
||
1247 | }, |
||
1248 | /** |
||
1249 | * init transaction builder |
||
1250 | * |
||
1251 | * @param cb |
||
1252 | */ |
||
1253 | function(cb) { |
||
1254 | txb = new bitcoin.TransactionBuilder(self.network); |
||
1255 | if (self.bitcoinCash) { |
||
1256 | txb.enableBitcoinCash(); |
||
1257 | } |
||
1258 | |||
1259 | cb(); |
||
1260 | }, |
||
1261 | /** |
||
1262 | * add UTXOs as inputs |
||
1263 | * |
||
1264 | * @param cb |
||
1265 | */ |
||
1266 | function(cb) { |
||
1267 | var i; |
||
1268 | |||
1269 | for (i = 0; i < utxos.length; i++) { |
||
1270 | txb.addInput(utxos[i]['hash'], utxos[i]['idx']); |
||
1271 | } |
||
1272 | |||
1273 | cb(); |
||
1274 | }, |
||
1275 | /** |
||
1276 | * build desired outputs |
||
1277 | * |
||
1278 | * @param cb |
||
1279 | */ |
||
1280 | function(cb) { |
||
1281 | send.forEach(function(_send) { |
||
1282 | if (_send.scriptPubKey) { |
||
1283 | outputs.push({scriptPubKey: new Buffer(_send.scriptPubKey, 'hex'), value: _send.value}); |
||
1284 | } else { |
||
1285 | throw new Error("Invalid send"); |
||
1286 | } |
||
1287 | }); |
||
1288 | cb(); |
||
1289 | }, |
||
1290 | /** |
||
1291 | * get change address if required |
||
1292 | * |
||
1293 | * @param cb |
||
1294 | */ |
||
1295 | function(cb) { |
||
1296 | if (change > 0) { |
||
1297 | if (change <= blocktrail.DUST) { |
||
1298 | change = 0; // don't do a change output if it would be a dust output |
||
1299 | |||
1300 | } else { |
||
1301 | if (!changeAddress) { |
||
1302 | deferred.notify(Wallet.PAY_PROGRESS_CHANGE_ADDRESS); |
||
1303 | |||
1304 | return self.getNewAddress(self.changeChain, function(err, address) { |
||
1305 | if (err) { |
||
1306 | return cb(err); |
||
1307 | } |
||
1308 | changeAddress = address; |
||
1309 | cb(); |
||
1310 | }); |
||
1311 | } |
||
1312 | } |
||
1313 | } |
||
1314 | |||
1315 | cb(); |
||
1316 | }, |
||
1317 | /** |
||
1318 | * add change to outputs |
||
1319 | * |
||
1320 | * @param cb |
||
1321 | */ |
||
1322 | function(cb) { |
||
1323 | if (change > 0) { |
||
1324 | var changeOutput = { |
||
1325 | scriptPubKey: bitcoin.address.toOutputScript(changeAddress, self.network, self.useNewCashAddr), |
||
1326 | value: change |
||
1327 | }; |
||
1328 | if (randomizeChangeIdx) { |
||
1329 | outputs.splice(_.random(0, outputs.length), 0, changeOutput); |
||
1330 | } else { |
||
1331 | outputs.push(changeOutput); |
||
1332 | } |
||
1333 | } |
||
1334 | |||
1335 | cb(); |
||
1336 | }, |
||
1337 | /** |
||
1338 | * add outputs to txb |
||
1339 | * |
||
1340 | * @param cb |
||
1341 | */ |
||
1342 | function(cb) { |
||
1343 | outputs.forEach(function(outputInfo) { |
||
1344 | txb.addOutput(outputInfo.scriptPubKey, outputInfo.value); |
||
1345 | }); |
||
1346 | |||
1347 | cb(); |
||
1348 | }, |
||
1349 | /** |
||
1350 | * sign |
||
1351 | * |
||
1352 | * @param cb |
||
1353 | */ |
||
1354 | function(cb) { |
||
1355 | var i, privKey, path, redeemScript, witnessScript; |
||
1356 | |||
1357 | deferred.notify(Wallet.PAY_PROGRESS_SIGN); |
||
1358 | |||
1359 | for (i = 0; i < utxos.length; i++) { |
||
1360 | var mode = SignMode.SIGN; |
||
1361 | if (utxos[i].sign_mode) { |
||
1362 | mode = utxos[i].sign_mode; |
||
1363 | } |
||
1364 | |||
1365 | redeemScript = null; |
||
1366 | witnessScript = null; |
||
1367 | if (mode === SignMode.SIGN) { |
||
1368 | path = utxos[i]['path'].replace("M", "m"); |
||
1369 | |||
1370 | // todo: regenerate scripts for path and compare for utxo (paranoid mode) |
||
1371 | if (self.primaryPrivateKey) { |
||
1372 | privKey = Wallet.deriveByPath(self.primaryPrivateKey, path, "m").keyPair; |
||
1373 | } else if (self.backupPrivateKey) { |
||
1374 | privKey = Wallet.deriveByPath(self.backupPrivateKey, path.replace(/^m\/(\d+)\'/, 'm/$1'), "m").keyPair; |
||
1375 | } else { |
||
1376 | throw new Error("No master privateKey present"); |
||
1377 | } |
||
1378 | |||
1379 | redeemScript = new Buffer(utxos[i]['redeem_script'], 'hex'); |
||
1380 | if (typeof utxos[i]['witness_script'] === 'string') { |
||
1381 | witnessScript = new Buffer(utxos[i]['witness_script'], 'hex'); |
||
1382 | } |
||
1383 | |||
1384 | var sigHash = bitcoin.Transaction.SIGHASH_ALL; |
||
1385 | if (self.bitcoinCash) { |
||
1386 | sigHash |= bitcoin.Transaction.SIGHASH_BITCOINCASHBIP143; |
||
1387 | } |
||
1388 | |||
1389 | txb.sign(i, privKey, redeemScript, sigHash, utxos[i].value, witnessScript); |
||
1390 | } |
||
1391 | } |
||
1392 | |||
1393 | tx = txb.buildIncomplete(); |
||
1394 | |||
1395 | cb(); |
||
1396 | }, |
||
1397 | /** |
||
1398 | * estimate fee to verify that the API is not providing us wrong data |
||
1399 | * |
||
1400 | * @param cb |
||
1401 | */ |
||
1402 | function(cb) { |
||
1403 | var estimatedFee = Wallet.estimateVsizeFee(tx, utxos); |
||
1404 | |||
1405 | if (self.sdk.feeSanityCheck) { |
||
1406 | switch (feeStrategy) { |
||
1407 | case Wallet.FEE_STRATEGY_BASE_FEE: |
||
1408 | if (Math.abs(estimatedFee - fee) > blocktrail.BASE_FEE) { |
||
1409 | return cb(new blocktrail.WalletFeeError("the fee suggested by the coin selection (" + fee + ") " + |
||
1410 | "seems incorrect (" + estimatedFee + ") for FEE_STRATEGY_BASE_FEE")); |
||
1411 | } |
||
1412 | break; |
||
1413 | |||
1414 | case Wallet.FEE_STRATEGY_HIGH_PRIORITY: |
||
1415 | case Wallet.FEE_STRATEGY_OPTIMAL: |
||
1416 | case Wallet.FEE_STRATEGY_LOW_PRIORITY: |
||
1417 | if (fee > estimatedFee * self.feeSanityCheckBaseFeeMultiplier) { |
||
1418 | return cb(new blocktrail.WalletFeeError("the fee suggested by the coin selection (" + fee + ") " + |
||
1419 | "seems awefully high (" + estimatedFee + ") for FEE_STRATEGY_OPTIMAL")); |
||
1420 | } |
||
1421 | break; |
||
1422 | } |
||
1423 | } |
||
1424 | |||
1425 | cb(); |
||
1426 | } |
||
1427 | ], function(err) { |
||
1428 | if (err) { |
||
1429 | deferred.reject(new blocktrail.WalletSendError(err)); |
||
1430 | return; |
||
1431 | } |
||
1432 | |||
1433 | deferred.resolve([tx, utxos]); |
||
1434 | }); |
||
1435 | |||
1436 | return deferred.promise; |
||
1437 | } |
||
1438 | ) |
||
1439 | ); |
||
1440 | }); |
||
1441 | |||
1442 | return deferred.promise; |
||
1443 | }; |
||
1444 | |||
1445 | |||
1446 | /** |
||
1447 | * use the API to get the best inputs to use based on the outputs |
||
1448 | * |
||
1449 | * @param pay array {'address': (int)value} coins to send |
||
1450 | * @param [lockUTXO] bool lock UTXOs for a few seconds to allow for transaction to be created |
||
1451 | * @param [allowZeroConf] bool allow zero confirmation unspent outputs to be used in coin selection |
||
1452 | * @param [feeStrategy] string defaults to FEE_STRATEGY_OPTIMAL |
||
1453 | * @param [options] object |
||
1454 | * @param [cb] function callback(err, utxos, fee, change) |
||
1455 | * @returns {q.Promise} |
||
1456 | */ |
||
1457 | Wallet.prototype.coinSelection = function(pay, lockUTXO, allowZeroConf, feeStrategy, options, cb) { |
||
1458 | var self = this; |
||
1459 | |||
1460 | if (typeof lockUTXO === "function") { |
||
1461 | cb = lockUTXO; |
||
1462 | lockUTXO = true; |
||
1463 | } else if (typeof allowZeroConf === "function") { |
||
1464 | cb = allowZeroConf; |
||
1465 | allowZeroConf = false; |
||
1466 | } else if (typeof feeStrategy === "function") { |
||
1467 | cb = feeStrategy; |
||
1468 | feeStrategy = null; |
||
1469 | } else if (typeof options === "function") { |
||
1470 | cb = options; |
||
1471 | options = {}; |
||
1472 | } |
||
1473 | |||
1474 | lockUTXO = typeof lockUTXO !== "undefined" ? lockUTXO : true; |
||
1475 | feeStrategy = feeStrategy || Wallet.FEE_STRATEGY_OPTIMAL; |
||
1476 | options = options || {}; |
||
1477 | |||
1478 | var send; |
||
1479 | try { |
||
1480 | send = Wallet.convertPayToOutputs(pay, self.network, self.useNewCashAddr); |
||
1481 | } catch (e) { |
||
1482 | var deferred = q.defer(); |
||
1483 | deferred.promise.nodeify(cb); |
||
1484 | deferred.reject(e); |
||
1485 | return deferred.promise; |
||
1486 | } |
||
1487 | |||
1488 | return self.sdk.coinSelection(self.identifier, send, lockUTXO, allowZeroConf, feeStrategy, options, cb); |
||
1489 | }; |
||
1490 | |||
1491 | /** |
||
1492 | * send the transaction using the API |
||
1493 | * |
||
1494 | * @param txHex string partially signed transaction as hex string |
||
1495 | * @param paths array list of paths used in inputs which should be cosigned by the API |
||
1496 | * @param checkFee bool when TRUE the API will verify if the fee is 100% correct and otherwise throw an exception |
||
1497 | * @param [twoFactorToken] string 2FA token |
||
1498 | * @param prioboost bool |
||
1499 | * @param [cb] function callback(err, txHash) |
||
1500 | * @returns {q.Promise} |
||
1501 | */ |
||
1502 | Wallet.prototype.sendTransaction = function(txHex, paths, checkFee, twoFactorToken, prioboost, cb) { |
||
1503 | var self = this; |
||
1504 | |||
1505 | if (typeof twoFactorToken === "function") { |
||
1506 | cb = twoFactorToken; |
||
1507 | twoFactorToken = null; |
||
1508 | prioboost = false; |
||
1509 | } else if (typeof prioboost === "function") { |
||
1510 | cb = twoFactorToken; |
||
1511 | prioboost = false; |
||
1512 | } |
||
1513 | |||
1514 | var deferred = q.defer(); |
||
1515 | deferred.promise.nodeify(cb); |
||
1516 | |||
1517 | self.sdk.sendTransaction(self.identifier, txHex, paths, checkFee, twoFactorToken, prioboost) |
||
1518 | .then( |
||
1519 | function(result) { |
||
1520 | deferred.resolve(result); |
||
1521 | }, |
||
1522 | function(e) { |
||
1523 | if (e.requires_2fa) { |
||
1524 | deferred.reject(new blocktrail.WalletMissing2FAError()); |
||
1525 | } else if (e.message.match(/Invalid two_factor_token/)) { |
||
1526 | deferred.reject(new blocktrail.WalletInvalid2FAError()); |
||
1527 | } else { |
||
1528 | deferred.reject(e); |
||
1529 | } |
||
1530 | } |
||
1531 | ) |
||
1532 | ; |
||
1533 | |||
1534 | return deferred.promise; |
||
1535 | }; |
||
1536 | |||
1537 | /** |
||
1538 | * setup a webhook for this wallet |
||
1539 | * |
||
1540 | * @param url string URL to receive webhook events |
||
1541 | * @param [identifier] string identifier for the webhook, defaults to WALLET- + wallet.identifier |
||
1542 | * @param [cb] function callback(err, webhook) |
||
1543 | * @returns {q.Promise} |
||
1544 | */ |
||
1545 | Wallet.prototype.setupWebhook = function(url, identifier, cb) { |
||
1546 | var self = this; |
||
1547 | |||
1548 | if (typeof identifier === "function") { |
||
1549 | cb = identifier; |
||
1550 | identifier = null; |
||
1551 | } |
||
1552 | |||
1553 | identifier = identifier || ('WALLET-' + self.identifier); |
||
1554 | |||
1555 | return self.sdk.setupWalletWebhook(self.identifier, identifier, url, cb); |
||
1556 | }; |
||
1557 | |||
1558 | /** |
||
1559 | * delete a webhook that was created for this wallet |
||
1560 | * |
||
1561 | * @param [identifier] string identifier for the webhook, defaults to WALLET- + wallet.identifier |
||
1562 | * @param [cb] function callback(err, success) |
||
1563 | * @returns {q.Promise} |
||
1564 | */ |
||
1565 | Wallet.prototype.deleteWebhook = function(identifier, cb) { |
||
1566 | var self = this; |
||
1567 | |||
1568 | if (typeof identifier === "function") { |
||
1569 | cb = identifier; |
||
1570 | identifier = null; |
||
1571 | } |
||
1572 | |||
1573 | identifier = identifier || ('WALLET-' + self.identifier); |
||
1574 | |||
1575 | return self.sdk.deleteWalletWebhook(self.identifier, identifier, cb); |
||
1576 | }; |
||
1577 | |||
1578 | /** |
||
1579 | * get all transactions for the wallet (paginated) |
||
1580 | * |
||
1581 | * @param [params] object pagination: {page: 1, limit: 20, sort_dir: 'asc'} |
||
1582 | * @param [cb] function callback(err, transactions) |
||
1583 | * @returns {q.Promise} |
||
1584 | */ |
||
1585 | Wallet.prototype.transactions = function(params, cb) { |
||
1586 | var self = this; |
||
1587 | |||
1588 | return self.sdk.walletTransactions(self.identifier, params, cb); |
||
1589 | }; |
||
1590 | |||
1591 | Wallet.prototype.maxSpendable = function(allowZeroConf, feeStrategy, options, cb) { |
||
1592 | var self = this; |
||
1593 | |||
1594 | if (typeof allowZeroConf === "function") { |
||
1595 | cb = allowZeroConf; |
||
1596 | allowZeroConf = false; |
||
1597 | } else if (typeof feeStrategy === "function") { |
||
1598 | cb = feeStrategy; |
||
1599 | feeStrategy = null; |
||
1600 | } else if (typeof options === "function") { |
||
1601 | cb = options; |
||
1602 | options = {}; |
||
1603 | } |
||
1604 | |||
1605 | if (typeof allowZeroConf === "object") { |
||
1606 | options = allowZeroConf; |
||
1607 | allowZeroConf = false; |
||
1608 | } else if (typeof feeStrategy === "object") { |
||
1609 | options = feeStrategy; |
||
1610 | feeStrategy = null; |
||
1611 | } |
||
1612 | |||
1613 | options = options || {}; |
||
1614 | |||
1615 | if (typeof options.allowZeroConf !== "undefined") { |
||
1616 | allowZeroConf = options.allowZeroConf; |
||
1617 | } |
||
1618 | if (typeof options.feeStrategy !== "undefined") { |
||
1619 | feeStrategy = options.feeStrategy; |
||
1620 | } |
||
1621 | |||
1622 | feeStrategy = feeStrategy || Wallet.FEE_STRATEGY_OPTIMAL; |
||
1623 | |||
1624 | return self.sdk.walletMaxSpendable(self.identifier, allowZeroConf, feeStrategy, options, cb); |
||
1625 | }; |
||
1626 | |||
1627 | /** |
||
1628 | * get all addresses for the wallet (paginated) |
||
1629 | * |
||
1630 | * @param [params] object pagination: {page: 1, limit: 20, sort_dir: 'asc'} |
||
1631 | * @param [cb] function callback(err, addresses) |
||
1632 | * @returns {q.Promise} |
||
1633 | */ |
||
1634 | Wallet.prototype.addresses = function(params, cb) { |
||
1635 | var self = this; |
||
1636 | |||
1637 | return self.sdk.walletAddresses(self.identifier, params, cb); |
||
1638 | }; |
||
1639 | |||
1640 | /** |
||
1641 | * @param address string the address to label |
||
1642 | * @param label string the label |
||
1643 | * @param [cb] function callback(err, res) |
||
1644 | * @returns {q.Promise} |
||
1645 | */ |
||
1646 | Wallet.prototype.labelAddress = function(address, label, cb) { |
||
1647 | var self = this; |
||
1648 | |||
1649 | return self.sdk.labelWalletAddress(self.identifier, address, label, cb); |
||
1650 | }; |
||
1651 | |||
1652 | /** |
||
1653 | * get all UTXOs for the wallet (paginated) |
||
1654 | * |
||
1655 | * @param [params] object pagination: {page: 1, limit: 20, sort_dir: 'asc'} |
||
1656 | * @param [cb] function callback(err, addresses) |
||
1657 | * @returns {q.Promise} |
||
1658 | */ |
||
1659 | Wallet.prototype.utxos = function(params, cb) { |
||
1660 | var self = this; |
||
1661 | |||
1662 | return self.sdk.walletUTXOs(self.identifier, params, cb); |
||
1663 | }; |
||
1664 | |||
1665 | Wallet.prototype.unspentOutputs = Wallet.prototype.utxos; |
||
1666 | |||
1667 | /** |
||
1668 | * sort list of pubkeys to be used in a multisig redeemscript |
||
1669 | * sorted in lexicographical order on the hex of the pubkey |
||
1670 | * |
||
1671 | * @param pubKeys {bitcoin.HDNode[]} |
||
1672 | * @returns string[] |
||
1673 | */ |
||
1674 | Wallet.sortMultiSigKeys = function(pubKeys) { |
||
1675 | pubKeys.sort(function(key1, key2) { |
||
1676 | return key1.toString('hex').localeCompare(key2.toString('hex')); |
||
1677 | }); |
||
1678 | |||
1679 | return pubKeys; |
||
1680 | }; |
||
1681 | |||
1682 | /** |
||
1683 | * determine how much fee is required based on the inputs and outputs |
||
1684 | * this is an estimation, not a proper 100% correct calculation |
||
1685 | * |
||
1686 | * @todo: mark deprecated in favor of estimations where UTXOS are known |
||
1687 | * @param {bitcoin.Transaction} tx |
||
1688 | * @param {int} feePerKb when not null use this feePerKb, otherwise use BASE_FEE legacy calculation |
||
1689 | * @returns {number} |
||
1690 | */ |
||
1691 | Wallet.estimateIncompleteTxFee = function(tx, feePerKb) { |
||
1692 | var size = Wallet.estimateIncompleteTxSize(tx); |
||
1693 | var sizeKB = size / 1000; |
||
1694 | var sizeKBCeil = Math.ceil(size / 1000); |
||
1695 | |||
1696 | if (feePerKb) { |
||
1697 | return parseInt(sizeKB * feePerKb, 10); |
||
1698 | } else { |
||
1699 | return parseInt(sizeKBCeil * blocktrail.BASE_FEE, 10); |
||
1700 | } |
||
1701 | }; |
||
1702 | |||
1703 | /** |
||
1704 | * Takes tx and utxos, computing their estimated vsize, |
||
1705 | * and uses feePerKb (or BASEFEE as default) to estimate |
||
1706 | * the number of satoshis in fee. |
||
1707 | * |
||
1708 | * @param {bitcoin.Transaction} tx |
||
1709 | * @param {Array} utxos |
||
1710 | * @param feePerKb |
||
1711 | * @returns {Number} |
||
1712 | */ |
||
1713 | Wallet.estimateVsizeFee = function(tx, utxos, feePerKb) { |
||
1714 | var vsize = SizeEstimation.estimateTxVsize(tx, utxos); |
||
1715 | var sizeKB = vsize / 1000; |
||
1716 | var sizeKBCeil = Math.ceil(vsize / 1000); |
||
1717 | |||
1718 | if (feePerKb) { |
||
1719 | return parseInt(sizeKB * feePerKb, 10); |
||
1720 | } else { |
||
1721 | return parseInt(sizeKBCeil * blocktrail.BASE_FEE, 10); |
||
1722 | } |
||
1723 | }; |
||
1724 | |||
1725 | /** |
||
1726 | * determine how much fee is required based on the inputs and outputs |
||
1727 | * this is an estimation, not a proper 100% correct calculation |
||
1728 | * |
||
1729 | * @param {bitcoin.Transaction} tx |
||
1730 | * @returns {number} |
||
1731 | */ |
||
1732 | Wallet.estimateIncompleteTxSize = function(tx) { |
||
1733 | var size = 4 + 4 + 4 + 4; // version + txinVarInt + txoutVarInt + locktime |
||
1734 | |||
1735 | size += tx.outs.length * 34; |
||
1736 | |||
1737 | tx.ins.forEach(function(txin) { |
||
1738 | var scriptSig = txin.script, |
||
1739 | scriptType = bitcoin.script.classifyInput(scriptSig); |
||
1740 | |||
1741 | var multiSig = [2, 3]; // tmp hardcoded |
||
1742 | |||
1743 | // Re-classify if P2SH |
||
1744 | if (!multiSig && scriptType === 'scripthash') { |
||
1745 | var sigChunks = bitcoin.script.decompile(scriptSig); |
||
1746 | var redeemScript = sigChunks.slice(-1)[0]; |
||
1747 | scriptSig = bitcoin.script.compile(sigChunks.slice(0, -1)); |
||
1748 | scriptType = bitcoin.script.classifyInput(scriptSig); |
||
1749 | |||
1750 | if (bitcoin.script.classifyOutput(redeemScript) !== scriptType) { |
||
1751 | throw new blocktrail.TransactionInputError('Non-matching scriptSig and scriptPubKey in input'); |
||
1752 | } |
||
1753 | |||
1754 | // figure out M of N for multisig (code from internal usage of bitcoinjs) |
||
1755 | if (scriptType === 'multisig') { |
||
1756 | var rsChunks = bitcoin.script.decompile(redeemScript); |
||
1757 | var mOp = rsChunks[0]; |
||
1758 | if (mOp === bitcoin.opcodes.OP_0 || mOp < bitcoin.opcodes.OP_1 || mOp > bitcoin.opcodes.OP_16) { |
||
1759 | throw new blocktrail.TransactionInputError("Invalid multisig redeemScript"); |
||
1760 | } |
||
1761 | |||
1762 | var nOp = rsChunks[redeemScript.chunks.length - 2]; |
||
1763 | if (mOp === bitcoin.opcodes.OP_0 || mOp < bitcoin.opcodes.OP_1 || mOp > bitcoin.opcodes.OP_16) { |
||
1764 | throw new blocktrail.TransactionInputError("Invalid multisig redeemScript"); |
||
1765 | } |
||
1766 | |||
1767 | var m = mOp - (bitcoin.opcodes.OP_1 - 1); |
||
1768 | var n = nOp - (bitcoin.opcodes.OP_1 - 1); |
||
1769 | if (n < m) { |
||
1770 | throw new blocktrail.TransactionInputError("Invalid multisig redeemScript"); |
||
1771 | } |
||
1772 | |||
1773 | multiSig = [m, n]; |
||
1774 | } |
||
1775 | } |
||
1776 | |||
1777 | if (multiSig) { |
||
1778 | size += ( |
||
1779 | 32 + // txhash |
||
1780 | 4 + // idx |
||
1781 | 3 + // scriptVarInt[>=253] |
||
1782 | 1 + // OP_0 |
||
1783 | ((1 + 72) * multiSig[0]) + // (OP_PUSHDATA[<75] + 72) * sigCnt |
||
1784 | (2 + 105) + // OP_PUSHDATA[>=75] + script |
||
1785 | 4 // sequence |
||
1786 | ); |
||
1787 | |||
1788 | } else { |
||
1789 | size += 32 + // txhash |
||
1790 | 4 + // idx |
||
1791 | 73 + // sig |
||
1792 | 34 + // script |
||
1793 | 4; // sequence |
||
1794 | } |
||
1795 | }); |
||
1796 | |||
1797 | return size; |
||
1798 | }; |
||
1799 | |||
1800 | /** |
||
1801 | * determine how much fee is required based on the amount of inputs and outputs |
||
1802 | * this is an estimation, not a proper 100% correct calculation |
||
1803 | * this asumes all inputs are 2of3 multisig |
||
1804 | * |
||
1805 | * @todo: mark deprecated in favor of situations where UTXOS are known |
||
1806 | * @param txinCnt {number} |
||
1807 | * @param txoutCnt {number} |
||
1808 | * @returns {number} |
||
1809 | */ |
||
1810 | Wallet.estimateFee = function(txinCnt, txoutCnt) { |
||
1811 | var size = 4 + 4 + 4 + 4; // version + txinVarInt + txoutVarInt + locktime |
||
1812 | |||
1813 | size += txoutCnt * 34; |
||
1814 | |||
1815 | size += ( |
||
1816 | 32 + // txhash |
||
1817 | 4 + // idx |
||
1818 | 3 + // scriptVarInt[>=253] |
||
1819 | 1 + // OP_0 |
||
1820 | ((1 + 72) * 2) + // (OP_PUSHDATA[<75] + 72) * sigCnt |
||
1821 | (2 + 105) + // OP_PUSHDATA[>=75] + script |
||
1822 | 4 // sequence |
||
1823 | ) * txinCnt; |
||
1824 | |||
1825 | var sizeKB = Math.ceil(size / 1000); |
||
1826 | |||
1827 | return sizeKB * blocktrail.BASE_FEE; |
||
1828 | }; |
||
1829 | |||
1830 | /** |
||
1831 | * create derived key from parent key by path |
||
1832 | * |
||
1833 | * @param hdKey {bitcoin.HDNode} |
||
1834 | * @param path string |
||
1835 | * @param keyPath string |
||
1836 | * @returns {bitcoin.HDNode} |
||
1837 | */ |
||
1838 | Wallet.deriveByPath = function(hdKey, path, keyPath) { |
||
1839 | keyPath = keyPath || (!!hdKey.keyPair.d ? "m" : "M"); |
||
1840 | |||
1841 | if (path[0].toLowerCase() !== "m" || keyPath[0].toLowerCase() !== "m") { |
||
1842 | throw new blocktrail.KeyPathError("Wallet.deriveByPath only works with absolute paths. (" + path + ", " + keyPath + ")"); |
||
1843 | } |
||
1844 | |||
1845 | if (path[0] === "m" && keyPath[0] === "M") { |
||
1846 | throw new blocktrail.KeyPathError("Wallet.deriveByPath can't derive private path from public parent. (" + path + ", " + keyPath + ")"); |
||
1847 | } |
||
1848 | |||
1849 | // if the desired path is public while the input is private |
||
1850 | var toPublic = path[0] === "M" && keyPath[0] === "m"; |
||
1851 | if (toPublic) { |
||
1852 | // derive the private path, convert to public when returning |
||
1853 | path[0] = "m"; |
||
1854 | } |
||
1855 | |||
1856 | // keyPath should be the parent parent of path |
||
1857 | if (path.toLowerCase().indexOf(keyPath.toLowerCase()) !== 0) { |
||
1858 | throw new blocktrail.KeyPathError("Wallet.derivePath requires path (" + path + ") to be a child of keyPath (" + keyPath + ")"); |
||
1859 | } |
||
1860 | |||
1861 | // remove the part of the path we already have |
||
1862 | path = path.substr(keyPath.length); |
||
1863 | |||
1864 | // iterate over the chunks and derive |
||
1865 | var newKey = hdKey; |
||
1866 | path.replace(/^\//, "").split("/").forEach(function(chunk) { |
||
1867 | if (!chunk) { |
||
1868 | return; |
||
1869 | } |
||
1870 | |||
1871 | if (chunk.indexOf("'") !== -1) { |
||
1872 | chunk = parseInt(chunk.replace("'", ""), 10) + bitcoin.HDNode.HIGHEST_BIT; |
||
1873 | } |
||
1874 | |||
1875 | newKey = newKey.derive(parseInt(chunk, 10)); |
||
1876 | }); |
||
1877 | |||
1878 | if (toPublic) { |
||
1879 | return newKey.neutered(); |
||
1880 | } else { |
||
1881 | return newKey; |
||
1882 | } |
||
1883 | }; |
||
1884 | |||
1885 | module.exports = Wallet; |
||
1886 |
This check looks for functions where a
return
statement is found in some execution paths, but not in all.Consider this little piece of code
The function
isBig
will only return a specific value when its parameter is bigger than 5000. In any other case, it will implicitly returnundefined
.This behaviour may not be what you had intended. In any case, you can add a
return undefined
to the other execution path to make the return value explicit.