UNPKG

17.2 kBPlain TextView Raw
1/**
2 * @hidden
3 */
4
5/**
6 */
7//
8// Wallets Object
9// BitGo accessor to a user's wallets.
10//
11// Copyright 2014, BitGo, Inc. All Rights Reserved.
12//
13
14import * as bitcoin from '@bitgo/utxo-lib';
15import { makeRandomKey, hdPath, getNetwork } from './bitcoin';
16import * as common from './common';
17import * as _ from 'lodash';
18import * as Bluebird from 'bluebird';
19const co = Bluebird.coroutine;
20const Wallet = require('./wallet');
21
22//
23// Constructor
24//
25const Wallets = function(bitgo) {
26 this.bitgo = bitgo;
27};
28
29//
30// list
31// List the user's wallets
32//
33Wallets.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
79Wallets.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// listInvites
100// List the invites on a user
101//
102Wallets.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// cancelInvite
113// cancel a wallet invite that a user initiated
114//
115Wallets.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// listShares
126// List the user's wallet shares
127//
128Wallets.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// resendShareInvite
139// Resend the invitation email which shares a wallet with another user
140// Params:
141// walletShareId - the wallet share to get information on
142//
143Wallets.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// getShare
156// Gets a wallet share information, including the encrypted sharing keychain. requires unlock if keychain is present.
157// Params:
158// walletShareId - the wallet share to get information on
159//
160Wallets.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// updateShare
171// updates a wallet share
172// Params:
173// walletShareId - the wallet share to update
174// state - the new state of the wallet share
175//
176Wallets.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// cancelShare
188// cancels a wallet share
189// Params:
190// walletShareId - the wallet share to update
191//
192Wallets.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// acceptShare
204// Accepts a wallet share, adding the wallet to the user's list
205// Needs a user's password to decrypt the shared key
206// Params:
207// walletShareId - the wallet share to accept
208// userPassword - (required if more a keychain was shared) user's password to decrypt the shared wallet
209// newWalletPassphrase - new wallet passphrase for saving the shared wallet xprv.
210// If left blank and a wallet with more than view permissions was shared, then the userpassword is used.
211// overrideEncryptedXprv - set only if the xprv was received out-of-band.
212//
213Wallets.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 // Return right away if there is no keychain to decrypt, or if explicit encryptedXprv was provided
223 if (!walletShare.keychain || !walletShare.keychain.encryptedXprv || encryptedXprv) {
224 return walletShare;
225 }
226
227 // More than viewing was requested, so we need to process the wallet keys using the shared ecdh scheme
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 // Now we have the sharing keychain, we can work out the secret used for sharing the wallet with us
239 sharingKeychain.xprv = self.bitgo.decrypt({ password: params.userPassword, input: sharingKeychain.encryptedXprv });
240 const rootExtKey = bitcoin.HDNode.fromBase58(sharingKeychain.xprv);
241
242 // Derive key by path (which is used between these 2 users only)
243 const privKey = hdPath(rootExtKey).deriveKey(walletShare.keychain.path);
244 const secret = self.bitgo.getECDHSecret({ eckey: privKey, otherPubKeyHex: walletShare.keychain.fromPubKey });
245
246 // Yes! We got the secret successfully here, now decrypt the shared wallet xprv
247 const decryptedSharedWalletXprv = self.bitgo.decrypt({ password: secret, input: walletShare.keychain.encryptedXprv });
248
249 // We will now re-encrypt the wallet with our own password
250 const newWalletPassphrase = params.newWalletPassphrase || params.userPassword;
251 encryptedXprv = self.bitgo.encrypt({ password: newWalletPassphrase, input: decryptedSharedWalletXprv });
252
253 // Carry on to the next block where we will post the acceptance of the share with the encrypted xprv
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// createKey
274// Create a single bitcoin key. This runs locally.
275// Returns: {
276// address: <address>
277// key: <key, in WIF format>
278// }
279Wallets.prototype.createKey = function(params) {
280 const key = makeRandomKey();
281 return {
282 address: key.getAddress(),
283 key: key.toWIF()
284 };
285};
286
287//
288// createWalletWithKeychains
289// Create a new 2-of-3 wallet and it's associated keychains.
290// Returns the locally created keys with their encrypted xprvs.
291// **WARNING: BE SURE TO BACKUP! NOT DOING SO CAN RESULT IN LOSS OF FUNDS!**
292//
293// 1. Creates the user keychain locally on the client, and encrypts it with the provided passphrase
294// 2. If no xpub was provided, creates the backup keychain locally on the client, and encrypts it with the provided passphrase
295// 3. Uploads the encrypted user and backup keychains to BitGo
296// 4. Creates the BitGo key on the service
297// 5. Creates the wallet on BitGo with the 3 public keys above
298//
299// Parameters include:
300// "passphrase": wallet passphrase to encrypt user and backup keys with
301// "label": wallet label, is shown in BitGo UI
302// "backupXpub": backup keychain xpub, it is HIGHLY RECOMMENDED you generate this on a separate machine!
303// BITGO DOES NOT GUARANTEE SAFETY OF WALLETS WITH MULTIPLE KEYS CREATED ON THE SAME MACHINE **
304// "backupXpubProvider": Provision backup key from this provider (KRS), e.g. "keyternal".
305// Setting this value will create an instant-capable wallet.
306// "passcodeEncryptionCode": the code used to encrypt the wallet passcode used in the recovery process
307// Returns: {
308// wallet: newly created wallet model object
309// userKeychain: the newly created user keychain, which has an encrypted xprv stored on BitGo
310// backupKeychain: the newly created backup keychain
311//
312// ** BE SURE TO BACK UP THE ENCRYPTED USER AND BACKUP KEYCHAINS!**
313//
314// }
315Wallets.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 // Create the user and backup key.
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 // Add the user keychain
348 return self.bitgo.keychains().add(keychainData)
349 .then(function() {
350 // Add the backup keychain
351 if (params.backupXpubProvider) {
352 // If requested, use a KRS or backup key provider
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 // user provided backup xpub
364 backupKeychain = { xpub: params.backupXpub };
365 } else {
366 // no provided xpub, so default to creating one here
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// createForwardWallet
416// Creates a forward wallet from a single private key.
417// BitGo will watch the wallet and send any incoming transactions to a destination multi-sig wallet
418// WARNING: THE PRIVATE KEY WILL BE SENT TO BITGO. YOU MUST CONTACT BITGO BEFORE USING THIS FEATURE!
419// WE CANNOT GUARANTEE THE SECURITY OF SINGLE-SIG WALLETS AS CUSTODY IS UNCLEAR.
420//
421// Params:
422// privKey - the private key on a legacy single-signature wallet to be watched (WIF format)
423// sourceAddress - the bitcoin address to forward from (corresponds to the private key)
424// destinationWallet - the wallet object to send the destination coins to (when incoming transactions are detected)
425// label - label for the wallet
426//
427Wallets.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 // Create new address on the destination wallet to receive coins
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* Add a new wallet (advanced mode).
477* This allows you to manually submit the keychains, type, m and n of the wallet
478* @param {string} label label of the wallet to be shown in UI
479* @param {number} m number of keys required to unlock wallet (2)
480* @param {number} n number of keys available on the wallet (3)
481* @param {array} keychains array of keychain xpubs
482* @param {string} enterprise ID of the enterprise entity to create this wallet under.
483* @param {boolean} disableTransactionNotifications When set to true disables notifications for transactions on this wallet.
484*/
485Wallets.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 // TODO: support more types of multisig
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// get
527// Shorthand to getWallet
528// Parameters include:
529// id: the id of the wallet
530//
531Wallets.prototype.get = function(params, callback) {
532 return this.getWallet(params, callback);
533};
534
535//
536// remove
537// Remove an existing wallet.
538// Parameters include:
539// id: the id of the wallet
540//
541Wallets.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
550module.exports = Wallets;