1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 |
|
13 |
|
14 | import * as bitcoin from '@bitgo/utxo-lib';
|
15 | import { makeRandomKey, hdPath, getNetwork } from './bitcoin';
|
16 | import * as common from './common';
|
17 | import * as _ from 'lodash';
|
18 | import * as Bluebird from 'bluebird';
|
19 | const co = Bluebird.coroutine;
|
20 | const Wallet = require('./wallet');
|
21 |
|
22 |
|
23 |
|
24 |
|
25 | const Wallets = function(bitgo) {
|
26 | this.bitgo = bitgo;
|
27 | };
|
28 |
|
29 |
|
30 |
|
31 |
|
32 |
|
33 | Wallets.prototype.list = function(params, callback) {
|
34 | params = params || {};
|
35 | common.validateParams(params, [], [], callback);
|
36 |
|
37 | const args: string[] = [];
|
38 |
|
39 | if (params.skip && params.prevId) {
|
40 | throw new Error('cannot specify both skip and prevId');
|
41 | }
|
42 |
|
43 | if (params.limit) {
|
44 | if (!_.isNumber(params.limit)) {
|
45 | throw new Error('invalid limit argument, expecting number');
|
46 | }
|
47 | args.push('limit=' + params.limit);
|
48 | }
|
49 | if (params.getbalances) {
|
50 | if (!_.isBoolean(params.getbalances)) {
|
51 | throw new Error('invalid getbalances argument, expecting boolean');
|
52 | }
|
53 | args.push('getbalances=' + params.getbalances);
|
54 | }
|
55 | if (params.skip) {
|
56 | if (!_.isNumber(params.skip)) {
|
57 | throw new Error('invalid skip argument, expecting number');
|
58 | }
|
59 | args.push('skip=' + params.skip);
|
60 | } else if (params.prevId) {
|
61 | args.push('prevId=' + params.prevId);
|
62 | }
|
63 |
|
64 | let query = '';
|
65 | if (args.length) {
|
66 | query = '?' + args.join('&');
|
67 | }
|
68 |
|
69 | const self = this;
|
70 | return this.bitgo.get(this.bitgo.url('/wallet' + query))
|
71 | .result()
|
72 | .then(function(body) {
|
73 | body.wallets = body.wallets.map(function(w) { return new Wallet(self.bitgo, w); });
|
74 | return body;
|
75 | })
|
76 | .nodeify(callback);
|
77 | };
|
78 |
|
79 | Wallets.prototype.getWallet = function(params, callback) {
|
80 | params = params || {};
|
81 | common.validateParams(params, ['id'], [], callback);
|
82 |
|
83 | const self = this;
|
84 |
|
85 | let query = '';
|
86 | if (params.gpk) {
|
87 | query = '?gpk=1';
|
88 | }
|
89 |
|
90 | return this.bitgo.get(this.bitgo.url('/wallet/' + params.id + query))
|
91 | .result()
|
92 | .then(function(wallet) {
|
93 | return new Wallet(self.bitgo, wallet);
|
94 | })
|
95 | .nodeify(callback);
|
96 | };
|
97 |
|
98 |
|
99 |
|
100 |
|
101 |
|
102 | Wallets.prototype.listInvites = function(params, callback) {
|
103 | params = params || {};
|
104 | common.validateParams(params, [], [], callback);
|
105 |
|
106 | return this.bitgo.get(this.bitgo.url('/walletinvite'))
|
107 | .result()
|
108 | .nodeify(callback);
|
109 | };
|
110 |
|
111 |
|
112 |
|
113 |
|
114 |
|
115 | Wallets.prototype.cancelInvite = function(params, callback) {
|
116 | params = params || {};
|
117 | common.validateParams(params, ['walletInviteId'], [], callback);
|
118 |
|
119 | return this.bitgo.del(this.bitgo.url('/walletinvite/' + params.walletInviteId))
|
120 | .result()
|
121 | .nodeify(callback);
|
122 | };
|
123 |
|
124 |
|
125 |
|
126 |
|
127 |
|
128 | Wallets.prototype.listShares = function(params, callback) {
|
129 | params = params || {};
|
130 | common.validateParams(params, [], [], callback);
|
131 |
|
132 | return this.bitgo.get(this.bitgo.url('/walletshare'))
|
133 | .result()
|
134 | .nodeify(callback);
|
135 | };
|
136 |
|
137 |
|
138 |
|
139 |
|
140 |
|
141 |
|
142 |
|
143 | Wallets.prototype.resendShareInvite = function(params, callback) {
|
144 | return co(function *() {
|
145 | params = params || {};
|
146 | common.validateParams(params, ['walletShareId'], [], callback);
|
147 |
|
148 | const urlParts = params.walletShareId + '/resendemail';
|
149 | return this.bitgo.post(this.bitgo.url('/walletshare/' + urlParts))
|
150 | .result();
|
151 | }).call(this).asCallback(callback);
|
152 | };
|
153 |
|
154 |
|
155 |
|
156 |
|
157 |
|
158 |
|
159 |
|
160 | Wallets.prototype.getShare = function(params, callback) {
|
161 | params = params || {};
|
162 | common.validateParams(params, ['walletShareId'], [], callback);
|
163 |
|
164 | return this.bitgo.get(this.bitgo.url('/walletshare/' + params.walletShareId))
|
165 | .result()
|
166 | .nodeify(callback);
|
167 | };
|
168 |
|
169 |
|
170 |
|
171 |
|
172 |
|
173 |
|
174 |
|
175 |
|
176 | Wallets.prototype.updateShare = function(params, callback) {
|
177 | params = params || {};
|
178 | common.validateParams(params, ['walletShareId'], [], callback);
|
179 |
|
180 | return this.bitgo.post(this.bitgo.url('/walletshare/' + params.walletShareId))
|
181 | .send(params)
|
182 | .result()
|
183 | .nodeify(callback);
|
184 | };
|
185 |
|
186 |
|
187 |
|
188 |
|
189 |
|
190 |
|
191 |
|
192 | Wallets.prototype.cancelShare = function(params, callback) {
|
193 | params = params || {};
|
194 | common.validateParams(params, ['walletShareId'], [], callback);
|
195 |
|
196 | return this.bitgo.del(this.bitgo.url('/walletshare/' + params.walletShareId))
|
197 | .send()
|
198 | .result()
|
199 | .nodeify(callback);
|
200 | };
|
201 |
|
202 |
|
203 |
|
204 |
|
205 |
|
206 |
|
207 |
|
208 |
|
209 |
|
210 |
|
211 |
|
212 |
|
213 | Wallets.prototype.acceptShare = function(params, callback) {
|
214 | params = params || {};
|
215 | common.validateParams(params, ['walletShareId'], ['overrideEncryptedXprv'], callback);
|
216 |
|
217 | const self = this;
|
218 | let encryptedXprv = params.overrideEncryptedXprv;
|
219 |
|
220 | return this.getShare({ walletShareId: params.walletShareId })
|
221 | .then(function(walletShare) {
|
222 |
|
223 | if (!walletShare.keychain || !walletShare.keychain.encryptedXprv || encryptedXprv) {
|
224 | return walletShare;
|
225 | }
|
226 |
|
227 |
|
228 | if (!params.userPassword) {
|
229 | throw new Error('userPassword param must be provided to decrypt shared key');
|
230 | }
|
231 |
|
232 | return self.bitgo.getECDHSharingKeychain()
|
233 | .then(function(sharingKeychain) {
|
234 | if (!sharingKeychain.encryptedXprv) {
|
235 | throw new Error('EncryptedXprv was not found on sharing keychain');
|
236 | }
|
237 |
|
238 |
|
239 | sharingKeychain.xprv = self.bitgo.decrypt({ password: params.userPassword, input: sharingKeychain.encryptedXprv });
|
240 | const rootExtKey = bitcoin.HDNode.fromBase58(sharingKeychain.xprv);
|
241 |
|
242 |
|
243 | const privKey = hdPath(rootExtKey).deriveKey(walletShare.keychain.path);
|
244 | const secret = self.bitgo.getECDHSecret({ eckey: privKey, otherPubKeyHex: walletShare.keychain.fromPubKey });
|
245 |
|
246 |
|
247 | const decryptedSharedWalletXprv = self.bitgo.decrypt({ password: secret, input: walletShare.keychain.encryptedXprv });
|
248 |
|
249 |
|
250 | const newWalletPassphrase = params.newWalletPassphrase || params.userPassword;
|
251 | encryptedXprv = self.bitgo.encrypt({ password: newWalletPassphrase, input: decryptedSharedWalletXprv });
|
252 |
|
253 |
|
254 | return walletShare;
|
255 | });
|
256 | })
|
257 | .then(function(walletShare) {
|
258 | const updateParams: any = {
|
259 | walletShareId: params.walletShareId,
|
260 | state: 'accepted'
|
261 | };
|
262 |
|
263 | if (encryptedXprv) {
|
264 | updateParams.encryptedXprv = encryptedXprv;
|
265 | }
|
266 |
|
267 | return self.updateShare(updateParams);
|
268 | })
|
269 | .nodeify(callback);
|
270 | };
|
271 |
|
272 |
|
273 |
|
274 |
|
275 |
|
276 |
|
277 |
|
278 |
|
279 | Wallets.prototype.createKey = function(params) {
|
280 | const key = makeRandomKey();
|
281 | return {
|
282 | address: key.getAddress(),
|
283 | key: key.toWIF()
|
284 | };
|
285 | };
|
286 |
|
287 |
|
288 |
|
289 |
|
290 |
|
291 |
|
292 |
|
293 |
|
294 |
|
295 |
|
296 |
|
297 |
|
298 |
|
299 |
|
300 |
|
301 |
|
302 |
|
303 |
|
304 |
|
305 |
|
306 |
|
307 |
|
308 |
|
309 |
|
310 |
|
311 |
|
312 |
|
313 |
|
314 |
|
315 | Wallets.prototype.createWalletWithKeychains = function(params, callback) {
|
316 | params = params || {};
|
317 | common.validateParams(params, ['passphrase'], ['label', 'backupXpub', 'enterprise', 'passcodeEncryptionCode'], callback);
|
318 | const self = this;
|
319 | const label = params.label;
|
320 |
|
321 |
|
322 | const userKeychain = this.bitgo.keychains().create();
|
323 | userKeychain.encryptedXprv = this.bitgo.encrypt({ password: params.passphrase, input: userKeychain.xprv });
|
324 |
|
325 | const keychainData: any = {
|
326 | xpub: userKeychain.xpub,
|
327 | encryptedXprv: userKeychain.encryptedXprv
|
328 | };
|
329 |
|
330 | if (params.passcodeEncryptionCode) {
|
331 | keychainData.originalPasscodeEncryptionCode = params.passcodeEncryptionCode;
|
332 | }
|
333 |
|
334 | const hasBackupXpub = !!params.backupXpub;
|
335 | const hasBackupXpubProvider = !!params.backupXpubProvider;
|
336 | if (hasBackupXpub && hasBackupXpubProvider) {
|
337 | throw new Error('Cannot provide more than one backupXpub or backupXpubProvider flag');
|
338 | }
|
339 |
|
340 | if (params.disableTransactionNotifications !== undefined && !_.isBoolean(params.disableTransactionNotifications)) {
|
341 | throw new Error('Expected disableTransactionNotifications to be a boolean. ');
|
342 | }
|
343 |
|
344 | let backupKeychain;
|
345 | let bitgoKeychain;
|
346 |
|
347 |
|
348 | return self.bitgo.keychains().add(keychainData)
|
349 | .then(function() {
|
350 |
|
351 | if (params.backupXpubProvider) {
|
352 |
|
353 | return self.bitgo.keychains().createBackup({
|
354 | provider: params.backupXpubProvider,
|
355 | disableKRSEmail: params.disableKRSEmail
|
356 | })
|
357 | .then(function(keychain) {
|
358 | backupKeychain = keychain;
|
359 | });
|
360 | }
|
361 |
|
362 | if (params.backupXpub) {
|
363 |
|
364 | backupKeychain = { xpub: params.backupXpub };
|
365 | } else {
|
366 |
|
367 | backupKeychain = self.bitgo.keychains().create();
|
368 | }
|
369 |
|
370 | return self.bitgo.keychains().add(backupKeychain);
|
371 | })
|
372 | .then(function() {
|
373 | return self.bitgo.keychains().createBitGo();
|
374 | })
|
375 | .then(function(keychain) {
|
376 | bitgoKeychain = keychain;
|
377 | const walletParams: any = {
|
378 | label: label,
|
379 | m: 2,
|
380 | n: 3,
|
381 | keychains: [
|
382 | { xpub: userKeychain.xpub },
|
383 | { xpub: backupKeychain.xpub },
|
384 | { xpub: bitgoKeychain.xpub }]
|
385 | };
|
386 |
|
387 | if (params.enterprise) {
|
388 | walletParams.enterprise = params.enterprise;
|
389 | }
|
390 |
|
391 | if (params.disableTransactionNotifications) {
|
392 | walletParams.disableTransactionNotifications = params.disableTransactionNotifications;
|
393 | }
|
394 |
|
395 | return self.add(walletParams);
|
396 | })
|
397 | .then(function(newWallet) {
|
398 | const result: any = {
|
399 | wallet: newWallet,
|
400 | userKeychain: userKeychain,
|
401 | backupKeychain: backupKeychain,
|
402 | bitgoKeychain: bitgoKeychain
|
403 | };
|
404 |
|
405 | if (backupKeychain.xprv) {
|
406 | result.warning = 'Be sure to backup the backup keychain -- it is not stored anywhere else!';
|
407 | }
|
408 |
|
409 | return result;
|
410 | })
|
411 | .nodeify(callback);
|
412 | };
|
413 |
|
414 |
|
415 |
|
416 |
|
417 |
|
418 |
|
419 |
|
420 |
|
421 |
|
422 |
|
423 |
|
424 |
|
425 |
|
426 |
|
427 | Wallets.prototype.createForwardWallet = function(params, callback) {
|
428 | params = params || {};
|
429 | common.validateParams(params, ['privKey', 'sourceAddress'], ['label'], callback);
|
430 |
|
431 | if (!_.isObject(params.destinationWallet) || !params.destinationWallet.id) {
|
432 | throw new Error('expecting destinationWallet object');
|
433 | }
|
434 |
|
435 | const self = this;
|
436 |
|
437 | let newDestinationAddress;
|
438 | let addressFromPrivKey;
|
439 |
|
440 | try {
|
441 | const key = bitcoin.ECPair.fromWIF(params.privKey, getNetwork());
|
442 | addressFromPrivKey = key.getAddress();
|
443 | } catch (e) {
|
444 | throw new Error('expecting a valid privKey');
|
445 | }
|
446 |
|
447 | if (addressFromPrivKey !== params.sourceAddress) {
|
448 | throw new Error('privKey does not match source address - got ' + addressFromPrivKey + ' expected ' + params.sourceAddress);
|
449 | }
|
450 |
|
451 | return params.destinationWallet.createAddress()
|
452 | .then(function(result) {
|
453 |
|
454 | newDestinationAddress = result.address;
|
455 |
|
456 | const walletParams: any = {
|
457 | type: 'forward',
|
458 | sourceAddress: params.sourceAddress,
|
459 | destinationAddress: newDestinationAddress,
|
460 | privKey: params.privKey,
|
461 | label: params.label
|
462 | };
|
463 |
|
464 | if (params.enterprise) {
|
465 | walletParams.enterprise = params.enterprise;
|
466 | }
|
467 |
|
468 | return self.bitgo.post(self.bitgo.url('/wallet'))
|
469 | .send(walletParams)
|
470 | .result()
|
471 | .nodeify(callback);
|
472 | });
|
473 | };
|
474 |
|
475 |
|
476 |
|
477 |
|
478 |
|
479 |
|
480 |
|
481 |
|
482 |
|
483 |
|
484 |
|
485 | Wallets.prototype.add = function(params, callback) {
|
486 | params = params || {};
|
487 | common.validateParams(params, [], ['label', 'enterprise'], callback);
|
488 |
|
489 | if (Array.isArray(params.keychains) === false || !_.isNumber(params.m) ||
|
490 | !_.isNumber(params.n)) {
|
491 | throw new Error('invalid argument');
|
492 | }
|
493 |
|
494 |
|
495 | if (params.m !== 2 || params.n !== 3) {
|
496 | throw new Error('unsupported multi-sig type');
|
497 | }
|
498 |
|
499 | const self = this;
|
500 | const keychains = params.keychains.map(function(k) { return { xpub: k.xpub }; });
|
501 | const walletParams: any = {
|
502 | label: params.label,
|
503 | m: params.m,
|
504 | n: params.n,
|
505 | keychains: keychains
|
506 | };
|
507 |
|
508 | if (params.enterprise) {
|
509 | walletParams.enterprise = params.enterprise;
|
510 | }
|
511 |
|
512 | if (params.disableTransactionNotifications) {
|
513 | walletParams.disableTransactionNotifications = params.disableTransactionNotifications;
|
514 | }
|
515 |
|
516 | return this.bitgo.post(this.bitgo.url('/wallet'))
|
517 | .send(walletParams)
|
518 | .result()
|
519 | .then(function(body) {
|
520 | return new Wallet(self.bitgo, body);
|
521 | })
|
522 | .nodeify(callback);
|
523 | };
|
524 |
|
525 |
|
526 |
|
527 |
|
528 |
|
529 |
|
530 |
|
531 | Wallets.prototype.get = function(params, callback) {
|
532 | return this.getWallet(params, callback);
|
533 | };
|
534 |
|
535 |
|
536 |
|
537 |
|
538 |
|
539 |
|
540 |
|
541 | Wallets.prototype.remove = function(params, callback) {
|
542 | params = params || {};
|
543 | common.validateParams(params, ['id'], [], callback);
|
544 |
|
545 | return this.bitgo.del(this.bitgo.url('/wallet/' + params.id))
|
546 | .result()
|
547 | .nodeify(callback);
|
548 | };
|
549 |
|
550 | module.exports = Wallets;
|