diff --git a/modules/core/package.json b/modules/core/package.json index c353bf2f2c..8371df18f3 100644 --- a/modules/core/package.json +++ b/modules/core/package.json @@ -40,11 +40,6 @@ "dependencies": { "@bitgo/statics": "^1.2.0", "@bitgo/unspents": "^0.6.0", - "@types/bluebird": "^3.5.25", - "@types/lodash": "=4.14.121", - "@types/mocha": "^5.2.6", - "@types/nock": "^9.3.1", - "@types/node": "^11.11.4", "algosdk": "git+https://github.com/BitGo/algosdk-bitgo.git", "assert": "^0.4.9", "big.js": "^3.1.3", @@ -71,7 +66,13 @@ "superagent-proxy": "^1.0.3" }, "devDependencies": { - "@types/create-hmac": "1.1.0", + "@types/bluebird": "^3.5.25", + "@types/create-hmac": "^1.1.0", + "@types/debug": "^4.1.4", + "@types/lodash": "=4.14.121", + "@types/mocha": "^5.2.6", + "@types/nock": "^9.3.1", + "@types/node": "^11.11.4", "@types/sinon": "^7.0.6", "@typescript-eslint/eslint-plugin": "^1.4.2", "@typescript-eslint/parser": "^1.4.2", diff --git a/modules/core/src/v2/baseCoin.ts b/modules/core/src/v2/baseCoin.ts index 26af44f455..b5d376630f 100644 --- a/modules/core/src/v2/baseCoin.ts +++ b/modules/core/src/v2/baseCoin.ts @@ -4,10 +4,11 @@ import { hdPath } from '../bitcoin'; const bitcoinMessage = require('bitcoinjs-message'); import * as Bluebird from 'bluebird'; import * as errors from '../errors'; +import { NodeCallback } from './types'; const co = Bluebird.coroutine; -const Wallet = require('./wallet'); -const Wallets = require('./wallets'); +import { Wallet } from './wallet'; +import { Wallets } from './wallets'; const Keychains = require('./keychains'); const Webhooks = require('./webhooks'); const PendingApprovals = require('./pendingApprovals'); @@ -157,7 +158,7 @@ export abstract class BaseCoin { /** * Verify that a transaction prebuild complies with the original intention */ - verifyTransaction(params, callback) { + verifyTransaction(params, callback?: NodeCallback) { return Bluebird.resolve(true).asCallback(callback); } @@ -189,7 +190,7 @@ export abstract class BaseCoin { /** * Get extra parameters for prebuilding a tx. Add things like hop transaction params */ - getExtraPrebuildParams(buildParams, callback): Bluebird { + getExtraPrebuildParams(buildParams, callback?: NodeCallback): Bluebird { return Bluebird.method(function() { return {}; }).call(this).asCallback(callback); @@ -198,7 +199,7 @@ export abstract class BaseCoin { /** * Modify prebuild after receiving it from the server. Add things like nlocktime */ - postProcessPrebuild(prebuildResponse, callback): Bluebird { + postProcessPrebuild(prebuildResponse, callback?: NodeCallback): Bluebird { return Bluebird.method(function() { return prebuildResponse; }).call(this).asCallback(callback); @@ -207,7 +208,7 @@ export abstract class BaseCoin { /** * Coin-specific things done before signing a transaction, i.e. verification */ - presignTransaction(params, callback): Bluebird { + presignTransaction(params, callback?: NodeCallback): Bluebird { return Bluebird.method(function() { return params; }).call(this).asCallback(callback); @@ -228,7 +229,7 @@ export abstract class BaseCoin { * @param callback * @returns {Object} The info returned from the merchant server */ - feeEstimate(params, callback): Bluebird { + feeEstimate(params, callback?: NodeCallback): Bluebird { return co(function *coFeeEstimate() { const query: any = {}; if (params && params.numBlocks) { @@ -355,7 +356,7 @@ export abstract class BaseCoin { return Bluebird.reject(new errors.MethodNotImplementedError()); } - parseTransaction(params, callback): Bluebird { + parseTransaction(params, callback?: NodeCallback): Bluebird { return Bluebird.resolve({}); } diff --git a/modules/core/src/v2/coins/eth.ts b/modules/core/src/v2/coins/eth.ts index 7ec7dd5155..ccf0d8cd6b 100644 --- a/modules/core/src/v2/coins/eth.ts +++ b/modules/core/src/v2/coins/eth.ts @@ -1,5 +1,5 @@ import { BaseCoin } from '../baseCoin'; -const Wallet = require('../wallet'); +import { Wallet } from '../wallet'; import * as common from '../../common'; import * as config from '../../config'; const BigNumber = require('bignumber.js'); diff --git a/modules/core/src/v2/keycard.ts b/modules/core/src/v2/keycard.ts new file mode 100644 index 0000000000..6495f9f59a --- /dev/null +++ b/modules/core/src/v2/keycard.ts @@ -0,0 +1,381 @@ +/** + * Return the list of questions that will appear on the second page of the keycard + * @param coin name of the coin + */ +const generateQuestions = (coin: string) => { + return [ + { + q: 'What is the KeyCard?', + a: + [ + 'The KeyCard contains important information which can be used to recover the ' + coin + ' ', + 'from your wallet in several situations. Each BitGo wallet' + + ' has its own, unique KeyCard. ', + 'If you have created multiple wallets, you should retain the KeyCard for each of them.' + ] + }, + { + q: 'What should I do with it?', + a: + [ + 'You should print the KeyCard and/or save the PDF to an offline storage device. The print-out ', + 'or USB stick should be kept in a safe place, such as a bank vault or home safe. It\'s a good idea ', + 'to keep a second copy in a different location.', + '', + 'Important: If you haven\'t provided an external backup key, then the original PDF should be ', + 'deleted from any machine where the wallet will be regularly accessed to prevent malware from ', + 'capturing both the KeyCard and your wallet passcode.' + ] + }, + { + q: 'What should I do if I lose it?', + a: + [ + 'If you have lost or damaged all copies of your KeyCard, your ' + coin + ' is still safe, but this ', + 'wallet should be considered at risk for loss. As soon as is convenient, you should use BitGo ', + 'to empty the wallet into a new wallet', + ', and discontinue use of the old wallet.' + ] + }, + { + q: 'What if someone sees my KeyCard?', + a: + [ + 'Don\'t panic! All sensitive information on the KeyCard is encrypted with your passcode, or with a', + 'key which only BitGo has. But, in general, you should make best efforts to keep your ', + 'KeyCard private. If your KeyCard does get exposed or copied in a way that makes you ', + 'uncomfortable, the best course of action is to empty the corresponding wallet into another ', + 'wallet and discontinue use of the old wallet.' + ] + }, + { + q: 'What if I forget or lose my wallet password?', + a: + [ + 'BitGo can use the information in QR Code D to help you recover access to your wallet. ', + 'Without the KeyCard, BitGo is not able to recover funds from a wallet with a lost password.' + ] + }, + { + q: 'What if BitGo becomes inaccessible for an extended period?', + a: + [ + 'Your KeyCard and wallet passcode can be used together with BitGo’s published open ', + 'source tools at https://github.com/bitgo to recover your ' + coin + '. Note: You should never enter ', + 'information from your KeyCard into tools other than the tools BitGo has published, or your ', + 'funds may be at risk for theft.' + ] + }, + { + q: 'Should I write my wallet password on my KeyCard?', + a: + [ + 'No! BitGo’s multi-signature approach to security depends on there not being a single point ', + 'of attack. But if your wallet password is on your KeyCard, then anyone who gains access to ', + 'your KeyCard will be able to steal your ' + coin + '.' + ' We recommend keeping your wallet password ', + 'safe in a secure password manager such as LastPass, 1Password or KeePass.' + ] + } + ]; +}; + +interface GetKeyDataOptions { + userKeychain: any; + bitgoKeychain: any; + backupKeychain: any; + coinShortName: string; + passphrase: string; + passcodeEncryptionCode: string; + walletKeyID: string; + backupKeyID: string +} + +/** + * Collect all data which will go onto the keycard + * @param options + */ +function getKeyData(options: GetKeyDataOptions): any { + const { + userKeychain, + bitgoKeychain, + backupKeychain, + coinShortName, + passphrase, + passcodeEncryptionCode, + walletKeyID, + backupKeyID + } = options; + + // When using just 'generateWallet', we get back an unencrypted prv for the backup keychain + // If the user passes in their passphrase, we can encrypt it + if (backupKeychain.prv && passphrase) { + backupKeychain.encryptedPrv = this.bitgo.encrypt({ + input: backupKeychain.prv, + password: passphrase + }); + } + + // If we have the passcode encryption code, create a box D with the encryptedWalletPasscode + let encryptedWalletPasscode; + if (passphrase && passcodeEncryptionCode) { + encryptedWalletPasscode = this.bitgo.encrypt({ + input: passphrase, + password: passcodeEncryptionCode + }); + } + + // PDF QR Code data + const qrData: any = { + user: { + title: 'A: User Key', + desc: 'This is your private key, encrypted with your passcode.', + data: userKeychain.encryptedPrv + }, + backup: { + title: 'B: Backup Key', + desc: 'This is your backup private key, encrypted with your passcode.', + data: backupKeychain.encryptedPrv + }, + bitgo: { + title: 'C: BitGo Public Key', + desc: 'This is the public part of the key that BitGo will use to ' + + 'co-sign transactions\r\nwith you on your wallet.', + data: bitgoKeychain.pub + }, + passcode: { + title: 'D: Encrypted Wallet Password', + desc: 'This is the wallet password, encrypted client-side ' + + 'with a key held by\r\nBitGo.', + data: encryptedWalletPasscode + } + }; + + if (walletKeyID) { + qrData.user.keyID = walletKeyID; + } + + if (backupKeyID) { + qrData.backup.keyID = backupKeyID; + } + + if (!userKeychain.encryptedPrv) { + // User provided their own key - this is a cold wallet + qrData.user.title = 'A: Provided User Key'; + qrData.user.desc = 'This is the public key you provided for your wallet.'; + qrData.user.data = userKeychain.pub; + + // The user provided their own public key, we can remove box D + delete qrData.passcode; + } else if (!encryptedWalletPasscode) { + delete qrData.passcode; + } + + if (backupKeychain.provider) { + const backupKeyProviderName = backupKeychain.provider; + // Backup key held with KRS + qrData.backup = { + title: 'B: Backup Key', + desc: + 'This is the public key held at ' + backupKeyProviderName + + ', an ' + coinShortName + ' recovery service. If you lose\r\nyour key, ' + backupKeyProviderName + + ' will be able to sign transactions to recover funds.', + data: backupKeychain.pub + }; + } else if (!backupKeychain.encryptedPrv) { + // User supplied the xpub + qrData.backup = { + title: 'B: Backup Key', + desc: 'This is the public portion of your backup key, which you provided.', + data: backupKeychain.pub + }; + } + + + return qrData; +} + +interface DrawKeycardOptions extends GetKeyDataOptions { + jsPDF: any; + QRCode: any; + coinShortName: string; + activationCode: string; + walletLabel: string; + coinName: string; +} + +/** + * Draw a keycard into a new pdf document object + * @param options + */ +export function drawKeycard(options: DrawKeycardOptions): any { + const { + jsPDF, + QRCode, + coinShortName, + activationCode, + walletLabel, + coinName, + } = options; + + const margin = 30; + + const font = { + header: 24, + subheader: 15, + body: 12 + }; + + const color = { + black: '#000000', + darkgray: '#4c4c4c', + gray: '#9b9b9b', + red: '#e21e1e' + }; + + // document details + const width = 8.5 * 72; + let y = 0; + + // Helpers for data formatting / positioning on the paper + const left = (x) => margin + x; + const moveDown = (yDelta) => { y += yDelta; }; + + const doc = new jsPDF('portrait', 'pt', 'letter'); + doc.setFont('helvetica'); + + // PDF Header Area - includes the logo and company name + // This is data for the BitGo logo in the top left of the PDF + moveDown(30); + + // We don't currently add an image, since that path is dependent on BitGo frontend + // doc.addImage(coinUtility.getSelectedCoinObj().keyCardImage, left(0), y + 10); + + // Activation Code + moveDown(8); + doc.setFontSize(font.body).setTextColor(color.gray); + doc.text('Activation Code', left(460), y); + + doc.setFontSize(font.header).setTextColor(color.black); + moveDown(25); + doc.text('Your BitGo KeyCard', left(150), y); + doc.setFontSize(font.header).setTextColor(color.gray); + doc.text(activationCode.toString(), left(460), y); + + // Subheader + // titles + moveDown(margin); + doc.setFontSize(font.body).setTextColor(color.gray); + doc.text(`Created on ${new Date().toDateString()} by ${window.location.hostname} for wallet named ${walletLabel}`, left(0), y); + // copy + moveDown(25); + doc.setFontSize(font.subheader).setTextColor(color.black); + doc.text(walletLabel, left(0), y); + // Red Bar + moveDown(20); + doc.setFillColor(255, 230, 230); + doc.rect(left(0), y, width - 2 * margin, 32, 'F'); + + // warning message + moveDown(20); + doc.setFontSize(font.body).setTextColor(color.red); + doc.text('Print this document, or keep it securely offline. See second page for FAQ.', left(75), y); + + const { + passphrase, + passcodeEncryptionCode, + walletKeyID, + backupKeyID, + userKeychain, + bitgoKeychain, + backupKeychain, + } = options; + + // Get the data for the first page (qr codes) + const keyData = getKeyData({ + coinShortName, + passphrase, + passcodeEncryptionCode, + walletKeyID, + backupKeyID, + userKeychain, + bitgoKeychain, + backupKeychain, + }); + + // Generate the first page's data for the backup PDF + moveDown(35); + const qrSize = 130; + + // Draw each Box with QR code and description + Object.keys(keyData).forEach(function(keyType) { + const key = keyData[keyType]; + const topY = y; + + // Don't indent if we're not producing QR codes + const textLeft = !!QRCode ? left(qrSize + 15) : left(15); + + // Draw a QR code if library is available + if (QRCode) { + const dataURL = new QRCode({ value: key.data, size: qrSize }).toDataURL('image/jpeg'); + doc.addImage(dataURL, left(0), y, qrSize, qrSize); + } + + doc.setFontSize(font.subheader).setTextColor(color.black); + moveDown(10); + doc.text(key.title, textLeft, y); + moveDown(15); + doc.setFontSize(font.body).setTextColor(color.darkgray); + doc.text(key.desc, textLeft, y); + moveDown(30); + doc.setFontSize(font.body - 2); + doc.text('Data:', textLeft, y); + moveDown(15); + const innerWidth = 72 * 8.5 - textLeft - 30; + doc.setFont('courier').setFontSize(9).setTextColor(color.black); + const lines = doc.splitTextToSize(key.data, innerWidth); + doc.text(lines, textLeft, y); + + // Add key ID (derivation string) if it exists + if (key.keyID) { + const text = 'Key Id: ' + key.keyID; + // Gray bar + moveDown(45); + doc.setFillColor(247, 249, 249); // Gray background + doc.setDrawColor(0, 0, 0); // Border + doc.rect(textLeft, y, width, 15, 'FD'); + + doc.text(text, textLeft + 5, y + 10); + } + + doc.setFont('helvetica'); + // Move down the size of the QR code minus accumulated height on the right side, plus buffer + moveDown(qrSize - (y - topY) + 15); + }); + + // Add a new page (Q + A page) + doc.addPage(); + + // 2nd page title + y = 0; + moveDown(55); + doc.setFontSize(font.header).setTextColor(color.black); + doc.text('BitGo KeyCard FAQ', left(0), y); + + const questions = generateQuestions(coinName); + + // Draw the Q + A data on the second page + moveDown(30); + questions.forEach(function(q) { + doc.setFontSize(font.subheader).setTextColor(color.black); + doc.text(q.q, left(0), y); + moveDown(20); + doc.setFontSize(font.body).setTextColor(color.darkgray); + q.a.forEach(function(line) { + doc.text(line, left(0), y); + moveDown(font.body + 3); + }); + moveDown(22); + }); + + return doc; +} diff --git a/modules/core/src/v2/types.ts b/modules/core/src/v2/types.ts new file mode 100644 index 0000000000..e03a9ae700 --- /dev/null +++ b/modules/core/src/v2/types.ts @@ -0,0 +1 @@ +export type NodeCallback = (err: Error, res: R) => void; diff --git a/modules/core/src/v2/wallet.ts b/modules/core/src/v2/wallet.ts index 66dae1cdd3..0b642fa4dd 100644 --- a/modules/core/src/v2/wallet.ts +++ b/modules/core/src/v2/wallet.ts @@ -1,1782 +1,1677 @@ -import * as common from '../common'; -const BigNumber = require('bignumber.js'); -import { makeRandomKey } from '../bitcoin'; -const PendingApproval = require('./pendingApproval'); +import { BigNumber } from 'bignumber.js'; import * as Bluebird from 'bluebird'; -const co = Bluebird.coroutine; import * as _ from 'lodash'; -import { TradingAccount } from './trading/tradingAccount'; -const debug = require('debug')('bitgo:v2:wallet'); +import * as debugLib from 'debug'; + +import { makeRandomKey } from '../bitcoin'; +import * as common from '../common'; +import { BaseCoin } from './baseCoin'; import * as internal from './internal'; +import { drawKeycard } from './keycard'; +import { TradingAccount } from './trading/tradingAccount'; +import { NodeCallback } from './types'; + +const PendingApproval = require('./pendingApproval'); const util = require('../util'); -const Wallet = function(bitgo, baseCoin, walletData) { - this.bitgo = bitgo; - this.baseCoin = baseCoin; - this._wallet = walletData; - const userId = _.get(bitgo, '_user.id'); - if (_.isString(userId)) { - const userDetails = _.find(walletData.users, { user: userId }); - this._permissions = _.get(userDetails, 'permissions'); +const debug = debugLib('bitgo:v2:wallet'); +const co = Bluebird.coroutine; + +export class Wallet { + + public readonly bitgo: any; + public readonly baseCoin: BaseCoin; + public readonly _wallet: any; + public readonly _permissions?: any; + + constructor(bitgo: any, baseCoin: BaseCoin, walletData: any) { + this.bitgo = bitgo; + this.baseCoin = baseCoin; + this._wallet = walletData; + const userId = _.get(bitgo, '_user.id'); + if (_.isString(userId)) { + const userDetails = _.find(walletData.users, { user: userId }); + this._permissions = _.get(userDetails, 'permissions'); + } } -}; - -Wallet.prototype.url = function(extra) { - extra = extra || ''; - return this.baseCoin.url('/wallet/' + this.id() + extra); -}; - -Wallet.prototype.id = function() { - return this._wallet.id; -}; - -Wallet.prototype.approvalsRequired = function() { - return this._wallet.approvalsRequired; -}; - -Wallet.prototype.balance = function() { - return this._wallet.balance; -}; - -Wallet.prototype.confirmedBalance = function() { - return this._wallet.confirmedBalance; -}; - -Wallet.prototype.spendableBalance = function() { - return this._wallet.spendableBalance; -}; - -Wallet.prototype.balanceString = function() { - return this._wallet.balanceString; -}; - -Wallet.prototype.confirmedBalanceString = function() { - return this._wallet.confirmedBalanceString; -}; - -Wallet.prototype.spendableBalanceString = function() { - return this._wallet.spendableBalanceString; -}; - -Wallet.prototype.coin = function() { - return this._wallet.coin; -}; - -Wallet.prototype.label = function() { - return this._wallet.label; -}; - -Wallet.prototype.keyIds = function() { - return this._wallet.keys; -}; - -Wallet.prototype.receiveAddress = function() { - return this._wallet.receiveAddress.address; -}; - -/** - * Return the token flush thresholds for this wallet - * @return {*|Object} pairs of { [tokenName]: thresholds } base units - */ -Wallet.prototype.tokenFlushThresholds = function() { - if (this.baseCoin.getFamily() !== 'eth') { - throw new Error('not supported for this wallet'); + + /** + * Build a URL using this wallet's id which can be used for BitGo API operations + * @param extra API specific string to append to the wallet id + */ + url(extra: string = '') { + return this.baseCoin.url('/wallet/' + this.id() + extra); } - return this._wallet.coinSpecific.tokenFlushThresholds; -}; - -Wallet.prototype.pendingApprovals = function() { - const self = this; - return this._wallet.pendingApprovals.map(function(currentApproval) { - return new PendingApproval(self.bitgo, self.baseCoin, currentApproval, self); - }); -}; - -/** - * Refresh the wallet object by syncing with the back-end - * @param callback - * @returns {Wallet} - */ -Wallet.prototype.refresh = function(params, callback) { - return co(function *() { - const res = yield this.bitgo.get(this.url()).result(); - this._wallet = res; - return this; - }).call(this).asCallback(callback); -}; - -/** - * List the transactions for a given wallet - * @param params - * @param callback - * @returns {*} - */ -Wallet.prototype.transactions = function(params, callback) { - params = params || {}; - common.validateParams(params, [], [], callback); - - const query: any = {}; - if (params.prevId) { - if (!_.isString(params.prevId)) { - throw new Error('invalid prevId argument, expecting string'); - } - query.prevId = params.prevId; + + /** + * Get this wallet's id + */ + id() { + return this._wallet.id; } - if (params.limit) { - if (!_.isNumber(params.limit)) { - throw new Error('invalid limit argument, expecting number'); - } - query.limit = params.limit; + /** + * Get the number of approvals required for spending funds from this wallet + */ + approvalsRequired() { + return this._wallet.approvalsRequired; } - return this.bitgo.get(this.baseCoin.url('/wallet/' + this._wallet.id + '/tx')) - .query(query) - .result() - .nodeify(callback); -}; - -/** - * List the transactions for a given wallet - * @param params - * - txHash the transaction hash to search for - * @param callback - * @returns {*} - */ -Wallet.prototype.getTransaction = function getTransaction(params, callback) { - params = params || {}; - common.validateParams(params, ['txHash'], [], callback); - - const query: any = {}; - if (!_.isUndefined(params.prevId)) { - if (!_.isString(params.prevId)) { - throw new Error('invalid prevId argument, expecting string'); - } - query.prevId = params.prevId; + /** + * Get the current balance of this wallet + */ + balance() { + return this._wallet.balance; } - if (!_.isUndefined(params.limit)) { - if (!_.isInteger(params.limit) || params.limit < 1) { - throw new Error('invalid limit argument, expecting positive integer'); - } - query.limit = params.limit; + /** + * Get the confirmed balance of this wallet + */ + confirmedBalance() { + return this._wallet.confirmedBalance; } - return this.bitgo.get(this.url('/tx/' + params.txHash)) - .query(query) - .result() - .nodeify(callback); -}; - -/** - * List the transfers for a given wallet - * @param params - * @param callback - * @returns {*} - */ -Wallet.prototype.transfers = function(params, callback) { - params = params || {}; - common.validateParams(params, [], [], callback); - - const query: any = {}; - if (params.prevId) { - if (!_.isString(params.prevId)) { - throw new Error('invalid prevId argument, expecting string'); - } - query.prevId = params.prevId; + /** + * Get the spendable balance of this wallet + */ + spendableBalance() { + return this._wallet.spendableBalance; } - if (params.limit) { - if (!_.isNumber(params.limit)) { - throw new Error('invalid limit argument, expecting number'); - } - query.limit = params.limit; + /** + * Get a string representation of the balance of this wallet + * + * This is useful when balances have the potential to overflow standard javascript numbers + */ + balanceString() { + return this._wallet.balanceString; } - if (params.allTokens) { - if (!_.isBoolean(params.allTokens)) { - throw new Error('invalid allTokens argument, expecting boolean'); - } - query.allTokens = params.allTokens; + /** + * Get a string representation of the confirmed balance of this wallet + * + * This is useful when balances have the potential to overflow standard javascript numbers + */ + confirmedBalanceString() { + return this._wallet.confirmedBalanceString; + } + + /** + * Get a string representation of the spendable balance of this wallet + * + * This is useful when balances have the potential to overflow standard javascript numbers + */ + spendableBalanceString() { + return this._wallet.spendableBalanceString; } - if (params.searchLabel) { - if (!_.isString(params.searchLabel)) { - throw new Error('invalid searchLabel argument, expecting string'); + /** + * Get the coin identifier for the type of coin this wallet holds + */ + coin() { + return this._wallet.coin; + } + + /** + * Get the label (name) for this wallet + */ + public label(): string { + return this._wallet.label; + } + + /** + * Get the public object ids for the keychains on this wallet. + */ + public keyIds(): string[] { + return this._wallet.keys; + } + + /** + * Get a receive address for this wallet + */ + public receiveAddress(): string { + return this._wallet.receiveAddress.address; + } + + /** + * Return the token flush thresholds for this wallet + * @return {*|Object} pairs of { [tokenName]: thresholds } base units + */ + tokenFlushThresholds(): any { + if (this.baseCoin.getFamily() !== 'eth') { + throw new Error('not supported for this wallet'); } - query.searchLabel = params.searchLabel; + return this._wallet.coinSpecific.tokenFlushThresholds; + } + + /** + * Get all pending approvals on this wallet + */ + pendingApprovals(): any[] { // TODO: return PendingApproval[] once we have a type for that + return this._wallet.pendingApprovals.map((currentApproval) => { + return new PendingApproval(this.bitgo, this.baseCoin, currentApproval, this); + }); + } + + /** + * Refresh the wallet object by syncing with the back-end + * @param params + * @param callback + * @returns {Wallet} + */ + refresh(params: any = {}, callback?: NodeCallback): Bluebird { + return co(function *() { + this._wallet = yield this.bitgo.get(this.url()).result(); + return this; + }).call(this).asCallback(callback); } - if (params.address) { - if (!_.isArray(params.address) && !_.isString(params.address)) { - throw new Error('invalid address argument, expecting string or array'); + /** + * List the transactions for a given wallet + * @param params + * @param callback + * @returns {*} + */ + transactions(params: any = {}, callback?: NodeCallback): Bluebird { + params = params || {}; + common.validateParams(params, [], [], callback); + + const query: any = {}; + if (params.prevId) { + if (!_.isString(params.prevId)) { + throw new Error('invalid prevId argument, expecting string'); + } + query.prevId = params.prevId; } - if (_.isArray(params.address)) { - params.address.forEach(address => { - if (!_.isString(address)) { - throw new Error('invalid address argument, expecting array of address strings'); - } - }); + + if (params.limit) { + if (!_.isNumber(params.limit)) { + throw new Error('invalid limit argument, expecting number'); + } + query.limit = params.limit; } - query.address = params.address; + + return this.bitgo.get(this.baseCoin.url('/wallet/' + this._wallet.id + '/tx')) + .query(query) + .result() + .nodeify(callback); } - if (params.dateGte) { - if (!_.isString(params.dateGte)) { - throw new Error('invalid dateGte argument, expecting string'); + /** + * List the transactions for a given wallet + * @param params + * - txHash the transaction hash to search for + * @param callback + * @returns {*} + */ + getTransaction(params: any = {}, callback?: NodeCallback): Bluebird { + common.validateParams(params, ['txHash'], [], callback); + + const query: any = {}; + if (!_.isUndefined(params.prevId)) { + if (!_.isString(params.prevId)) { + throw new Error('invalid prevId argument, expecting string'); + } + query.prevId = params.prevId; } - query.dateGte = params.dateGte; - } - if (params.dateLt) { - if (!_.isString(params.dateLt)) { - throw new Error('invalid dateLt argument, expecting string'); + if (!_.isUndefined(params.limit)) { + if (!_.isInteger(params.limit) || params.limit < 1) { + throw new Error('invalid limit argument, expecting positive integer'); + } + query.limit = params.limit; } - query.dateLt = params.dateLt; + + return this.bitgo.get(this.url('/tx/' + params.txHash)) + .query(query) + .result() + .nodeify(callback); } - if (!_.isNil(params.valueGte)) { - if (!_.isNumber(params.valueGte)) { - throw new Error('invalid valueGte argument, expecting number'); + /** + * List the transfers for a given wallet + * @param params + * @param callback + * @returns {*} + */ + transfers(params: any = {}, callback?: NodeCallback): Bluebird { + const query: any = {}; + if (params.prevId) { + if (!_.isString(params.prevId)) { + throw new Error('invalid prevId argument, expecting string'); + } + query.prevId = params.prevId; } - query.valueGte = params.valueGte; - } - if (!_.isNil(params.valueLt)) { - if (!_.isNumber(params.valueLt)) { - throw new Error('invalid valueLt argument, expecting number'); + if (params.limit) { + if (!_.isNumber(params.limit)) { + throw new Error('invalid limit argument, expecting number'); + } + query.limit = params.limit; } - query.valueLt = params.valueLt; - } - if (!_.isNil(params.includeHex)) { - if (!_.isBoolean(params.includeHex)) { - throw new Error('invalid includeHex argument, expecting boolean'); + if (params.allTokens) { + if (!_.isBoolean(params.allTokens)) { + throw new Error('invalid allTokens argument, expecting boolean'); + } + query.allTokens = params.allTokens; } - query.includeHex = params.includeHex; - } - if (!_.isNil(params.state)) { - if (!Array.isArray(params.state) && !_.isString(params.state)) { - throw new Error('invalid state argument, expecting string or array'); + if (params.searchLabel) { + if (!_.isString(params.searchLabel)) { + throw new Error('invalid searchLabel argument, expecting string'); + } + query.searchLabel = params.searchLabel; } - if (Array.isArray(params.state)) { - params.state.forEach(state => { - if (!_.isString(state)) { - throw new Error('invalid state argument, expecting array of state strings'); - } - }); + if (params.address) { + if (!_.isArray(params.address) && !_.isString(params.address)) { + throw new Error('invalid address argument, expecting string or array'); + } + if (_.isArray(params.address)) { + params.address.forEach(address => { + if (!_.isString(address)) { + throw new Error('invalid address argument, expecting array of address strings'); + } + }); + } + query.address = params.address; } - query.state = params.state; - } - if (!_.isNil(params.type)) { - if (!_.isString(params.type)) { - throw new Error('invalid type argument, expecting string'); + if (params.dateGte) { + if (!_.isString(params.dateGte)) { + throw new Error('invalid dateGte argument, expecting string'); + } + query.dateGte = params.dateGte; } - query.type = params.type; - } - return this.bitgo.get(this.url('/transfer')) - .query(query) - .result() - .nodeify(callback); -}; - -Wallet.prototype.getTransfer = function(params, callback) { - params = params || {}; - common.validateParams(params, ['id'], [], callback); - - return this.bitgo.get(this.url('/transfer/' + params.id)) - .result() - .nodeify(callback); -}; - -// Get a transaction by sequence id for a given wallet -Wallet.prototype.transferBySequenceId = function(params, callback) { - params = params || {}; - common.validateParams(params, ['sequenceId'], [], callback); - - return this.bitgo.get(this.url('/transfer/sequenceId/' + params.sequenceId)) - .result() - .nodeify(callback); -}; - -/** - * Get the maximum amount you can spend in a single transaction - * - * @param {Object} params - parameters object - * @param {Number} params.limit - maximum number of selectable unspents - * @param {Number} params.minValue - the minimum value of unspents to use in satoshis - * @param {Number} params.maxValue - the maximum value of unspents to use in satoshis - * @param {Number} params.minHeight - the minimum height of unspents on the block chain to use - * @param {Number} params.minConfirms - all selected unspents will have at least this many confirmations - * @param {Boolean} params.enforceMinConfirmsForChange - Enforces minConfirms on change inputs - * @param {Number} params.feeRate - fee rate to use in calculation of maximum spendable in satoshis/kB - * @param {Number} params.maxFeeRate - upper limit for feeRate in satoshis/kB - * @param {String} params.recipientAddress - recipient addresses for a more accurate calculation of the maximum available to send - * @param callback - * @returns {{maximumSpendable: Number, coin: String}} - * NOTE : feeTxConfirmTarget omitted on purpose because gauging the maximum spendable amount with dynamic fees does not make sense - */ -Wallet.prototype.maximumSpendable = function maximumSpendable(params, callback) { - return co(function *() { - params = params || {}; + if (params.dateLt) { + if (!_.isString(params.dateLt)) { + throw new Error('invalid dateLt argument, expecting string'); + } + query.dateLt = params.dateLt; + } - const filteredParams = _.pick(params, [ - 'minValue', 'maxValue', 'minHeight', 'target', 'plainTarget', 'limit', 'minConfirms', - 'enforceMinConfirmsForChange', 'feeRate', 'maxFeeRate', 'recipientAddress' - ]); - - return this.bitgo.get(this.url('/maximumSpendable')) - .query(filteredParams) - .result(); - - }).call(this).asCallback(callback); - -}; - -/** - * List the unspents for a given wallet - * @param params - * @param callback - * @returns {*} - */ -Wallet.prototype.unspents = function(params, callback) { - params = params || {}; - - const query = _.pick(params, ['prevId', 'limit', 'minValue', 'maxValue', 'minHeight', 'minConfirms', 'target', 'segwit', 'chains']); - - return this.bitgo.get(this.url('/unspents')) - .query(query) - .result() - .nodeify(callback); -}; - -/** - * Consolidate unspents on a wallet - * - * @param {Object} params - parameters object - * @param {String} params.walletPassphrase - the users wallet passphrase - * @param {String} params.prevId - used in batch requests - * @param {Number} params.limit - the number of unspents retrieved per call - * @param {Number} params.minValue - the minimum value of unspents to use in satoshis/kB - * @param {Number} params.maxValue - the maximum value of unspents to use in satoshis/kB - * @param {Number} params.minHeight - the minimum height of unspents on the block chain to use - * @param {Number} params.targetUnspentPoolSize - the number of unspents you want after the consolidation of valid unspents - * @param {Number} params.minConfirms - all selected unspents will have at least this many confirmations - * @param {Number} params.feeRate - The fee rate to use for the consolidation in satoshis/kB - * @param {Number} params.maxFeeRate - upper limit for feeRate in satoshis/kB - * @param {Number} params.maxFeePercentage - the maximum relative portion that you're willing to spend towards fees - * @param callback - * @returns txHex {String} the txHex of the incomplete transaction that needs to be signed by the user in the SDK - */ -Wallet.prototype.consolidateUnspents = function consolidateUnspents(params, callback) { - return co(function *() { - params = params || {}; - common.validateParams(params, [], ['walletPassphrase', 'xprv'], callback); - - const reqId = util.createRequestId(); - const keychain = yield this.baseCoin.keychains().get({ id: this._wallet.keys[0], reqId }); - const filteredParams = _.pick(params, ['minValue', 'maxValue', 'minHeight', 'numUnspentsToMake', 'feeTxConfirmTarget', 'limit', 'minConfirms', 'enforceMinConfirmsForChange', 'feeRate', 'maxFeeRate', 'maxFeePercentage']); - this.bitgo._reqId = reqId; - const response = yield this.bitgo.post(this.url('/consolidateUnspents')) - .send(filteredParams) - .result(); - - const transactionParams = _.extend({}, params, { txPrebuild: response, keychain: keychain }); - const signedTransaction = yield this.signTransaction(transactionParams); - const selectParams = _.pick(params, ['comment', 'otp']); - const finalTxParams = _.extend({}, signedTransaction, selectParams); - - this.bitgo._reqId = reqId; - return this.bitgo.post(this.baseCoin.url('/wallet/' + this._wallet.id + '/tx/send')) - .send(finalTxParams) - .result(); - }).call(this).asCallback(callback); -}; - -/** - * Fanout unspents for a wallet - * - * @param {Object} params - parameters object - * @param {String} params.walletPassphrase - the users wallet passphrase - * @param {String} params.xprv - the private key in string form if the walletPassphrase is not available - * @param {Number} params.minValue - the minimum value of unspents to use - * @param {Number} params.maxValue - the maximum value of unspents to use - * @param {Number} params.minHeight - the minimum height of unspents on the block chain to use - * @param {Number} params.minConfirms - all selected unspents will have at least this many confirmations - * @param {Number} params.maxFeePercentage - the maximum proportion of an unspent you are willing to lose to fees - * @param {Number} params.feeTxConfirmTarget - estimate the fees to aim for first confirmation with this number of blocks - * @param {Number} params.feeRate - The desired fee rate for the transaction in satoshis/kB - * @param {Number} params.maxFeeRate - The max limit for a fee rate in satoshis/kB - * @param {Number} params.maxNumInputsToUse - the number of unspents you want to use in the transaction - * @param {Number} params.numUnspentsToMake - the number of new unspents to make - * @param callback - * @returns txHex {String} the txHex of the incomplete transaction that needs to be signed by the user in the SDK - */ -Wallet.prototype.fanoutUnspents = function fanoutUnspents(params, callback) { - return co(function *() { - params = params || {}; - common.validateParams(params, [], ['walletPassphrase', 'xprv'], callback); - - const filteredParams = _.pick(params, ['minValue', 'maxValue', 'minHeight', 'maxNumInputsToUse', 'numUnspentsToMake', 'minConfirms', 'enforceMinConfirmsForChange', 'feeRate', 'maxFeeRate', 'maxFeePercentage', 'feeTxConfirmTarget']); - const reqId = util.createRequestId(); - this.bitgo._reqId = reqId; - const response = yield this.bitgo.post(this.url('/fanoutUnspents')) - .send(filteredParams) - .result(); - - const keychain = yield this.baseCoin.keychains().get({ id: this._wallet.keys[0], reqId }); - const transactionParams = _.extend({}, params, { txPrebuild: response, keychain: keychain, prv: params.xprv }); - const signedTransaction = yield this.signTransaction(transactionParams); - - const selectParams = _.pick(params, ['comment', 'otp']); - const finalTxParams = _.extend({}, signedTransaction, selectParams); - this.bitgo._reqId = reqId; - return this.bitgo.post(this.baseCoin.url('/wallet/' + this._wallet.id + '/tx/send')) - .send(finalTxParams) - .result(); - }).call(this).asCallback(callback); - -}; - -/** - * Set the token flush thresholds for the wallet. Updates the wallet. - * Tokens will only be flushed from forwarder contracts if the balance is greater than the threshold defined here. - * @param thresholds {Object} - pairs of { [tokenName]: threshold } (base units) - * @param [callback] - */ -Wallet.prototype.updateTokenFlushThresholds = function(thresholds, callback) { - return co(function *() { - if (this.baseCoin.getFamily() !== 'eth') { - throw new Error('not supported for this wallet'); + if (!_.isNil(params.valueGte)) { + if (!_.isNumber(params.valueGte)) { + throw new Error('invalid valueGte argument, expecting number'); + } + query.valueGte = params.valueGte; } - this._wallet = yield this.bitgo.put(this.url()).send({ - tokenFlushThresholds: thresholds - }).result(); - }).call(this).asCallback(callback); -}; - -/** - * Sweep funds for a wallet - * - * @param {Object} params - parameters object - * @param {String} params.address - The address to send all the funds in the wallet to - * @param {String} params.walletPassphrase - the users wallet passphrase - * @param {String} params.xprv - the private key in string form if the walletPassphrase is not available - * @param {String} params.otp - Two factor auth code to enable sending the transaction - * @param {Number} params.feeTxConfirmTarget - Estimate the fees to aim for first confirmation within this number of blocks - * @param {Number} params.feeRate - The desired fee rate for the transaction in satoshis/kB - * @param {Number} [params.maxFeeRate] - upper limit for feeRate in satoshis/kB - * @param [callback] - * @returns txHex {String} the txHex of the signed transaction - */ -Wallet.prototype.sweep = function sweep(params, callback) { - return co(function *() { - params = params || {}; - common.validateParams(params, ['address'], ['walletPassphrase', 'xprv', 'otp'], callback); + if (!_.isNil(params.valueLt)) { + if (!_.isNumber(params.valueLt)) { + throw new Error('invalid valueLt argument, expecting number'); + } + query.valueLt = params.valueLt; + } - if (['eth', 'xrp'].includes(this.baseCoin.getFamily())) { - if (this.confirmedBalanceString() !== this.balanceString()) { - throw new Error('cannot sweep when unconfirmed funds exist on the wallet, please wait until all inbound transactions confirm'); + if (!_.isNil(params.includeHex)) { + if (!_.isBoolean(params.includeHex)) { + throw new Error('invalid includeHex argument, expecting boolean'); } + query.includeHex = params.includeHex; + } - const value = this.spendableBalanceString(); - if (!value || value === '0') { - throw new Error('no funds to sweep'); + if (!_.isNil(params.state)) { + if (!Array.isArray(params.state) && !_.isString(params.state)) { + throw new Error('invalid state argument, expecting string or array'); } - params.recipients = [{ - address: params.address, - amount: value - }]; - return this.sendMany(params); + if (Array.isArray(params.state)) { + params.state.forEach(state => { + if (!_.isString(state)) { + throw new Error('invalid state argument, expecting array of state strings'); + } + }); + } + query.state = params.state; } - // the following flow works for all UTXO coins - - const reqId = util.createRequestId(); - const filteredParams = _.pick(params, ['address', 'feeRate', 'maxFeeRate', 'feeTxConfirmTarget']); - this.bitgo._reqId = reqId; - const response = yield this.bitgo.post(this.url('/sweepWallet')) - .send(filteredParams) - .result(); - // TODO: add txHex validation to protect man in the middle attacks replacing the txHex, BG-3588 - - const keychain = yield this.baseCoin.keychains().get({ id: this._wallet.keys[0], reqId }); - const transactionParams = _.extend({}, params, { txPrebuild: response, keychain: keychain, prv: params.xprv }); - const signedTransaction = yield this.signTransaction(transactionParams); - - const selectParams = _.pick(params, ['otp']); - const finalTxParams = _.extend({}, signedTransaction, selectParams); - this.bitgo._reqId = reqId; - return this.bitgo.post(this.baseCoin.url('/wallet/' + this._wallet.id + '/tx/send')) - .send(finalTxParams) - .result(); - }).call(this).asCallback(callback); - -}; - -/** - * Freeze a given wallet - * @param params - * @param callback - * @returns {*} - */ -Wallet.prototype.freeze = function(params, callback) { - params = params || {}; - common.validateParams(params, [], [], callback); - - if (params.duration) { - if (!_.isNumber(params.duration)) { - throw new Error('invalid duration: should be number of seconds'); + + if (!_.isNil(params.type)) { + if (!_.isString(params.type)) { + throw new Error('invalid type argument, expecting string'); + } + query.type = params.type; } + + return this.bitgo.get(this.url('/transfer')) + .query(query) + .result() + .nodeify(callback); } - return this.bitgo.post(this.url('/freeze')) - .result() - .nodeify(callback); -}; - -/** - * Update comment of a transfer - * @param params - * @param callback - * @returns {*} - */ -Wallet.prototype.transferComment = function(params, callback) { - params = params || {}; - common.validateParams(params, ['id'], ['comment'], callback); - - return this.bitgo.post(this.baseCoin.url('/wallet/' + this._wallet.id + '/transfer/' + params.id + '/comment')) - .send(params) - .result() - .nodeify(callback); -}; - -/** - * List the addresses for a given wallet - * @param params - * @param callback - * @returns {*} - */ -Wallet.prototype.addresses = function(params, callback) { - params = params || {}; - common.validateParams(params, [], [], callback); - - const query: any = {}; - - if (params.mine) { - query.mine = !!params.mine; + /** + * Get transfers on this wallet + * @param params + * @param callback + */ + getTransfer(params: any = {}, callback?: NodeCallback): Bluebird { + common.validateParams(params, ['id'], [], callback); + + return this.bitgo.get(this.url('/transfer/' + params.id)) + .result() + .nodeify(callback); } - if (!_.isUndefined(params.prevId)) { - if (!_.isString(params.prevId)) { - throw new Error('invalid prevId argument, expecting string'); - } - query.prevId = params.prevId; + /** + * Get a transaction by sequence id for a given wallet + * @param params + * @param callback + */ + transferBySequenceId(params: any = {}, callback?: NodeCallback): Bluebird { + common.validateParams(params, ['sequenceId'], [], callback); + + return this.bitgo.get(this.url('/transfer/sequenceId/' + params.sequenceId)) + .result() + .nodeify(callback); } - if (params.sort) { - if (!_.isNumber(params.sort)) { - throw new Error('invalid sort argument, expecting number'); - } - query.sort = params.sort; + /** + * Get the maximum amount you can spend in a single transaction + * + * @param {Object} params - parameters object + * @param {Number} params.limit - maximum number of selectable unspents + * @param {Number} params.minValue - the minimum value of unspents to use in satoshis + * @param {Number} params.maxValue - the maximum value of unspents to use in satoshis + * @param {Number} params.minHeight - the minimum height of unspents on the block chain to use + * @param {Number} params.minConfirms - all selected unspents will have at least this many confirmations + * @param {Boolean} params.enforceMinConfirmsForChange - Enforces minConfirms on change inputs + * @param {Number} params.feeRate - fee rate to use in calculation of maximum spendable in satoshis/kB + * @param {Number} params.maxFeeRate - upper limit for feeRate in satoshis/kB + * @param {String} params.recipientAddress - recipient addresses for a more accurate calculation of the maximum available to send + * @param callback + * @returns {{maximumSpendable: Number, coin: String}} + * NOTE : feeTxConfirmTarget omitted on purpose because gauging the maximum spendable amount with dynamic fees does not make sense + */ + maximumSpendable(params: any = {}, callback?: NodeCallback): Bluebird { + return co(function *() { + const filteredParams = _.pick(params, [ + 'minValue', 'maxValue', 'minHeight', 'target', 'plainTarget', 'limit', 'minConfirms', + 'enforceMinConfirmsForChange', 'feeRate', 'maxFeeRate', 'recipientAddress' + ]); + + return this.bitgo.get(this.url('/maximumSpendable')) + .query(filteredParams) + .result(); + }).call(this).asCallback(callback); } - if (params.limit) { - if (!_.isNumber(params.limit)) { - throw new Error('invalid limit argument, expecting number'); - } - query.limit = params.limit; + /** + * List the unspents for a given wallet + * @param params + * @param callback + * @returns {*} + */ + unspents(params: any = {}, callback?: NodeCallback): Bluebird { + const query = _.pick(params, ['prevId', 'limit', 'minValue', 'maxValue', 'minHeight', 'minConfirms', 'target', 'segwit', 'chains']); + + return this.bitgo.get(this.url('/unspents')) + .query(query) + .result() + .nodeify(callback); } - if (params.labelContains) { - if (!_.isString(params.labelContains)) { - throw new Error('invalid labelContains argument, expecting string'); - } - query.labelContains = params.labelContains; + /** + * Consolidate unspents on a wallet + * + * @param {Object} params - parameters object + * @param {String} params.walletPassphrase - the users wallet passphrase + * @param {String} params.prevId - used in batch requests + * @param {Number} params.limit - the number of unspents retrieved per call + * @param {Number} params.minValue - the minimum value of unspents to use in satoshis/kB + * @param {Number} params.maxValue - the maximum value of unspents to use in satoshis/kB + * @param {Number} params.minHeight - the minimum height of unspents on the block chain to use + * @param {Number} params.targetUnspentPoolSize - the number of unspents you want after the consolidation of valid unspents + * @param {Number} params.minConfirms - all selected unspents will have at least this many confirmations + * @param {Number} params.feeRate - The fee rate to use for the consolidation in satoshis/kB + * @param {Number} params.maxFeeRate - upper limit for feeRate in satoshis/kB + * @param {Number} params.maxFeePercentage - the maximum relative portion that you're willing to spend towards fees + * @param callback + * @returns txHex {String} the txHex of the incomplete transaction that needs to be signed by the user in the SDK + */ + consolidateUnspents(params: any = {}, callback?: NodeCallback): Bluebird { + return co(function *() { + common.validateParams(params, [], ['walletPassphrase', 'xprv'], callback); + + const reqId = util.createRequestId(); + const keychain = yield this.baseCoin.keychains().get({ id: this._wallet.keys[0], reqId }); + const filteredParams = _.pick(params, ['minValue', 'maxValue', 'minHeight', 'numUnspentsToMake', 'feeTxConfirmTarget', 'limit', 'minConfirms', 'enforceMinConfirmsForChange', 'feeRate', 'maxFeeRate', 'maxFeePercentage']); + this.bitgo._reqId = reqId; + const response = yield this.bitgo.post(this.url('/consolidateUnspents')) + .send(filteredParams) + .result(); + + const transactionParams = _.extend({}, params, { txPrebuild: response, keychain: keychain }); + const signedTransaction = yield this.signTransaction(transactionParams); + const selectParams = _.pick(params, ['comment', 'otp']); + const finalTxParams = _.extend({}, signedTransaction, selectParams); + + this.bitgo._reqId = reqId; + return this.bitgo.post(this.baseCoin.url('/wallet/' + this._wallet.id + '/tx/send')) + .send(finalTxParams) + .result(); + }).call(this).asCallback(callback); } - if (!_.isUndefined(params.segwit)) { - if (!_.isBoolean(params.segwit)) { - throw new Error('invalid segwit argument, expecting boolean'); - } - query.segwit = params.segwit; + /** + * Fanout unspents for a wallet + * + * @param {Object} params - parameters object + * @param {String} params.walletPassphrase - the users wallet passphrase + * @param {String} params.xprv - the private key in string form if the walletPassphrase is not available + * @param {Number} params.minValue - the minimum value of unspents to use + * @param {Number} params.maxValue - the maximum value of unspents to use + * @param {Number} params.minHeight - the minimum height of unspents on the block chain to use + * @param {Number} params.minConfirms - all selected unspents will have at least this many confirmations + * @param {Number} params.maxFeePercentage - the maximum proportion of an unspent you are willing to lose to fees + * @param {Number} params.feeTxConfirmTarget - estimate the fees to aim for first confirmation with this number of blocks + * @param {Number} params.feeRate - The desired fee rate for the transaction in satoshis/kB + * @param {Number} params.maxFeeRate - The max limit for a fee rate in satoshis/kB + * @param {Number} params.maxNumInputsToUse - the number of unspents you want to use in the transaction + * @param {Number} params.numUnspentsToMake - the number of new unspents to make + * @param callback + * @returns txHex {String} the txHex of the incomplete transaction that needs to be signed by the user in the SDK + */ + fanoutUnspents(params: any = {}, callback?: NodeCallback): Bluebird { + return co(function *() { + common.validateParams(params, [], ['walletPassphrase', 'xprv'], callback); + + const filteredParams = _.pick(params, ['minValue', 'maxValue', 'minHeight', 'maxNumInputsToUse', 'numUnspentsToMake', 'minConfirms', 'enforceMinConfirmsForChange', 'feeRate', 'maxFeeRate', 'maxFeePercentage', 'feeTxConfirmTarget']); + const reqId = util.createRequestId(); + this.bitgo._reqId = reqId; + const response = yield this.bitgo.post(this.url('/fanoutUnspents')) + .send(filteredParams) + .result(); + + const keychain = yield this.baseCoin.keychains().get({ id: this._wallet.keys[0], reqId }); + const transactionParams = _.extend({}, params, { txPrebuild: response, keychain: keychain, prv: params.xprv }); + const signedTransaction = yield this.signTransaction(transactionParams); + + const selectParams = _.pick(params, ['comment', 'otp']); + const finalTxParams = _.extend({}, signedTransaction, selectParams); + this.bitgo._reqId = reqId; + return this.bitgo.post(this.baseCoin.url('/wallet/' + this._wallet.id + '/tx/send')) + .send(finalTxParams) + .result(); + }).call(this).asCallback(callback); } - if (!_.isUndefined(params.chains)) { - if (!_.isArray(params.chains)) { - throw new Error('invalid chains argument, expecting array of numbers'); - } - query.chains = params.chains; + /** + * Set the token flush thresholds for the wallet. Updates the wallet. + * Tokens will only be flushed from forwarder contracts if the balance is greater than the threshold defined here. + * @param thresholds {Object} - pairs of { [tokenName]: threshold } (base units) + * @param [callback] + */ + updateTokenFlushThresholds(thresholds: any = {}, callback?: NodeCallback): Bluebird { + return co(function *() { + if (this.baseCoin.getFamily() !== 'eth') { + throw new Error('not supported for this wallet'); + } + + this._wallet = yield this.bitgo.put(this.url()).send({ + tokenFlushThresholds: thresholds + }).result(); + }).call(this).asCallback(callback); } - return this.bitgo.get(this.baseCoin.url('/wallet/' + this._wallet.id + '/addresses')) - .query(query) - .result() - .nodeify(callback); -}; - -/** - * Get a single wallet address by its id - * @param params - * @param callback - * @returns {*} - */ -Wallet.prototype.getAddress = function(params, callback) { - params = params || {}; - common.validateParams(params, [], ['address', 'id'], callback); - let query; - if (!params.address && !params.id) { - throw new Error('address or id of address required'); + /** + * Sweep funds for a wallet + * + * @param {Object} params - parameters object + * @param {String} params.address - The address to send all the funds in the wallet to + * @param {String} params.walletPassphrase - the users wallet passphrase + * @param {String} params.xprv - the private key in string form if the walletPassphrase is not available + * @param {String} params.otp - Two factor auth code to enable sending the transaction + * @param {Number} params.feeTxConfirmTarget - Estimate the fees to aim for first confirmation within this number of blocks + * @param {Number} params.feeRate - The desired fee rate for the transaction in satoshis/kB + * @param {Number} [params.maxFeeRate] - upper limit for feeRate in satoshis/kB + * @param [callback] + * @returns txHex {String} the txHex of the signed transaction + */ + sweep(params: any = {}, callback?: NodeCallback): Bluebird { + return co(function *() { + params = params || {}; + common.validateParams(params, ['address'], ['walletPassphrase', 'xprv', 'otp'], callback); + + if (['eth', 'xrp'].includes(this.baseCoin.getFamily())) { + if (this.confirmedBalanceString() !== this.balanceString()) { + throw new Error('cannot sweep when unconfirmed funds exist on the wallet, please wait until all inbound transactions confirm'); + } + + const value = this.spendableBalanceString(); + if (!value || value === '0') { + throw new Error('no funds to sweep'); + } + params.recipients = [{ + address: params.address, + amount: value + }]; + + return this.sendMany(params); + } + // the following flow works for all UTXO coins + + const reqId = util.createRequestId(); + const filteredParams = _.pick(params, ['address', 'feeRate', 'maxFeeRate', 'feeTxConfirmTarget']); + this.bitgo._reqId = reqId; + const response = yield this.bitgo.post(this.url('/sweepWallet')) + .send(filteredParams) + .result(); + // TODO: add txHex validation to protect man in the middle attacks replacing the txHex, BG-3588 + + const keychain = yield this.baseCoin.keychains().get({ id: this._wallet.keys[0], reqId }); + const transactionParams = _.extend({}, params, { txPrebuild: response, keychain: keychain, prv: params.xprv }); + const signedTransaction = yield this.signTransaction(transactionParams); + + const selectParams = _.pick(params, ['otp']); + const finalTxParams = _.extend({}, signedTransaction, selectParams); + this.bitgo._reqId = reqId; + return this.bitgo.post(this.baseCoin.url('/wallet/' + this._wallet.id + '/tx/send')) + .send(finalTxParams) + .result(); + }).call(this).asCallback(callback); } - if (params.address) { - query = params.address; - } else { - query = params.id; + + /** + * Freeze a given wallet + * @param params + * @param callback + * @returns {*} + */ + freeze(params: any = {}, callback?: NodeCallback): Bluebird { + params = params || {}; + common.validateParams(params, [], [], callback); + + if (params.duration) { + if (!_.isNumber(params.duration)) { + throw new Error('invalid duration: should be number of seconds'); + } + } + + return this.bitgo.post(this.url('/freeze')) + .result() + .nodeify(callback); } - if (params.reqId) { - this.bitgo._reqId = params.reqId; + /** + * Update comment of a transfer + * @param params + * @param callback + * @returns {*} + */ + transferComment(params: any = {}, callback?: NodeCallback): Bluebird { + common.validateParams(params, ['id'], ['comment'], callback); + + return this.bitgo.post(this.baseCoin.url('/wallet/' + this._wallet.id + '/transfer/' + params.id + '/comment')) + .send(params) + .result() + .nodeify(callback); } - return this.bitgo.get(this.baseCoin.url(`/wallet/${this._wallet.id}/address/${encodeURIComponent(query)}`)) - .result() - .nodeify(callback); -}; - -/** - * Create one or more new address(es) for use with this wallet. - * - * If the `count` field is defined and greater than 1, an object with a single - * array property named `addresses` containing `count` address objects - * will be returned. Otherwise, a single address object is returned. - * - * @param {Number} [chain] on which the new address should be created - * @param {(Number|String)} [gasPrice] gas price for new address creation, if applicable - * @param {String} [label] label for the new address(es) - * @param {Number} [count=1] number of new addresses which should be created (maximum 250) - * @param {Boolean} [lowPriority] Ethereum-specific param to create address using low priority fee address - * @param callback - */ -Wallet.prototype.createAddress = function({ chain = undefined, gasPrice = undefined, count = 1, label = undefined, lowPriority = undefined } = {}, callback) { - return co(function *createAddress() { - - const addressParams: any = {}; - const reqId = util.createRequestId(); - - if (!_.isUndefined(chain)) { - if (!_.isInteger(chain)) { - throw new Error('chain has to be an integer'); - } - addressParams.chain = chain; + /** + * List the addresses for a given wallet + * @param params + * @param callback + * @returns {*} + */ + addresses(params: any = {}, callback?: NodeCallback): Bluebird { + common.validateParams(params, [], [], callback); + + const query: any = {}; + + if (params.mine) { + query.mine = !!params.mine; + } + + if (!_.isUndefined(params.prevId)) { + if (!_.isString(params.prevId)) { + throw new Error('invalid prevId argument, expecting string'); + } + query.prevId = params.prevId; + } + + if (params.sort) { + if (!_.isNumber(params.sort)) { + throw new Error('invalid sort argument, expecting number'); + } + query.sort = params.sort; } - if (!_.isUndefined(gasPrice)) { - if (!_.isInteger(gasPrice) && (isNaN(Number(gasPrice)) || !_.isString(gasPrice))) { - throw new Error('gasPrice has to be an integer or numeric string'); + if (params.limit) { + if (!_.isNumber(params.limit)) { + throw new Error('invalid limit argument, expecting number'); } - addressParams.gasPrice = gasPrice; + query.limit = params.limit; } - if (!_.isUndefined(label)) { - if (!_.isString(label)) { - throw new Error('label has to be a string'); + if (params.labelContains) { + if (!_.isString(params.labelContains)) { + throw new Error('invalid labelContains argument, expecting string'); } - addressParams.label = label; + query.labelContains = params.labelContains; } - if (!_.isInteger(count) || count <= 0 || count > 250) { - throw new Error('count has to be a number between 1 and 250'); + if (!_.isUndefined(params.segwit)) { + if (!_.isBoolean(params.segwit)) { + throw new Error('invalid segwit argument, expecting boolean'); + } + query.segwit = params.segwit; } - if (!_.isUndefined(lowPriority)) { - if (!_.isBoolean(lowPriority)) { - throw new Error('lowPriority has to be a boolean'); + if (!_.isUndefined(params.chains)) { + if (!_.isArray(params.chains)) { + throw new Error('invalid chains argument, expecting array of numbers'); } - addressParams.lowPriority = lowPriority; + query.chains = params.chains; } - // get keychains for address verification - const keychains = yield Bluebird.map(this._wallet.keys, k => this.baseCoin.keychains().get({ id: k, reqId })); - const rootAddress = _.get(this._wallet, 'receiveAddress.address'); + return this.bitgo.get(this.baseCoin.url('/wallet/' + this._wallet.id + '/addresses')) + .query(query) + .result() + .nodeify(callback); + } - const newAddresses = _.times(count, co(function *createAndVerifyAddress() { - this.bitgo._reqId = reqId; - const newAddress = yield this.bitgo.post(this.baseCoin.url('/wallet/' + this._wallet.id + '/address')) - .send(addressParams) - .result(); + /** + * Get a single wallet address by its id + * @param params + * @param callback + * @returns {*} + */ + getAddress(params: any = {}, callback?: NodeCallback): Bluebird { + common.validateParams(params, [], ['address', 'id'], callback); + let query; + if (!params.address && !params.id) { + throw new Error('address or id of address required'); + } + if (params.address) { + query = params.address; + } else { + query = params.id; + } + + if (params.reqId) { + this.bitgo._reqId = params.reqId; + } + + return this.bitgo.get(this.baseCoin.url(`/wallet/${this._wallet.id}/address/${encodeURIComponent(query)}`)) + .result() + .nodeify(callback); + } + + /** + * Create one or more new address(es) for use with this wallet. + * + * If the `count` field is defined and greater than 1, an object with a single + * array property named `addresses` containing `count` address objects + * will be returned. Otherwise, a single address object is returned. + * + * @param {Number} [chain] on which the new address should be created + * @param {(Number|String)} [gasPrice] gas price for new address creation, if applicable + * @param {String} [label] label for the new address(es) + * @param {Number} [count=1] number of new addresses which should be created (maximum 250) + * @param {Boolean} [lowPriority] Ethereum-specific param to create address using low priority fee address + * @param callback + */ + createAddress({ + chain = undefined, + gasPrice = undefined, + count = 1, + label = undefined, + lowPriority = undefined + } = {}, callback?: NodeCallback): Bluebird { + return co(function *() { + const addressParams: any = {}; + const reqId = util.createRequestId(); + + if (!_.isUndefined(chain)) { + if (!_.isInteger(chain)) { + throw new Error('chain has to be an integer'); + } + addressParams.chain = chain; + } + + if (!_.isUndefined(gasPrice)) { + if (!_.isInteger(gasPrice) && (isNaN(Number(gasPrice)) || !_.isString(gasPrice))) { + throw new Error('gasPrice has to be an integer or numeric string'); + } + addressParams.gasPrice = gasPrice; + } + + if (!_.isUndefined(label)) { + if (!_.isString(label)) { + throw new Error('label has to be a string'); + } + addressParams.label = label; + } + + if (!_.isInteger(count) || count <= 0 || count > 250) { + throw new Error('count has to be a number between 1 and 250'); + } - // infer its address type - if (_.isObject(newAddress.coinSpecific) && _.isFunction(this.baseCoin.constructor.inferAddressType)) { - newAddress.addressType = this.baseCoin.constructor.inferAddressType(newAddress); + if (!_.isUndefined(lowPriority)) { + if (!_.isBoolean(lowPriority)) { + throw new Error('lowPriority has to be a boolean'); + } + addressParams.lowPriority = lowPriority; } - newAddress.keychains = keychains; - const verificationData = _.merge({}, newAddress, { rootAddress }); - this.baseCoin.verifyAddress(verificationData); + // get keychains for address verification + const keychains = yield Bluebird.map(this._wallet.keys, k => this.baseCoin.keychains().get({ id: k, reqId })); + const rootAddress = _.get(this._wallet, 'receiveAddress.address'); + + const newAddresses = _.times(count, co(function *createAndVerifyAddress() { + this.bitgo._reqId = reqId; + const newAddress = yield this.bitgo.post(this.baseCoin.url('/wallet/' + this._wallet.id + '/address')) + .send(addressParams) + .result(); - return newAddress; - }).bind(this)); + // infer its address type + if (_.isObject(newAddress.coinSpecific) && _.isFunction(this.baseCoin.constructor.inferAddressType)) { + newAddress.addressType = this.baseCoin.constructor.inferAddressType(newAddress); + } + + newAddress.keychains = keychains; + const verificationData = _.merge({}, newAddress, { rootAddress }); + this.baseCoin.verifyAddress(verificationData); + + return newAddress; + }).bind(this)); + + if (newAddresses.length === 1) { + return newAddresses[0]; + } - if (newAddresses.length === 1) { - return newAddresses[0]; + return { + addresses: yield Promise.all(newAddresses) + }; + }).call(this).asCallback(callback); + } + + /** + * Update properties on an address + * @param params + * @param callback + * @returns {*} + */ + updateAddress(params: any = {}, callback?: NodeCallback): Bluebird { + return co(function *() { + const address = params.address; + + const putParams = _.pick(params, ['label']); + const url = this.url('/address/' + encodeURIComponent(address)); + + return this.bitgo.put(url).send(putParams).result(); + }).call(this).asCallback(callback); + } + + /** + * List webhooks on this wallet + * @param params + * @param callback + */ + listWebhooks(params: any = {}, callback?: NodeCallback): Bluebird { + const query: any = {}; + if (params.prevId) { + if (!_.isString(params.prevId)) { + throw new Error('invalid prevId argument, expecting string'); + } + query.prevId = params.prevId; } - return { - addresses: yield Promise.all(newAddresses) - }; - }).call(this).asCallback(callback); -}; - -/** - * - * @param params - * @param callback - * @returns {*} - */ -Wallet.prototype.updateAddress = function(params, callback) { - return co(function *gUpdateAddress() { - const address = params.address; - - const putParams = _.pick(params, ['label']); - const url = this.url('/address/' + encodeURIComponent(address)); - - return this.bitgo.put(url).send(putParams).result(); - }).call(this).asCallback(callback); -}; - -Wallet.prototype.listWebhooks = function(params, callback) { - params = params || {}; - common.validateParams(params, [], [], callback); - - const query: any = {}; - if (params.prevId) { - if (!_.isString(params.prevId)) { - throw new Error('invalid prevId argument, expecting string'); + if (params.limit) { + if (!_.isNumber(params.limit)) { + throw new Error('invalid limit argument, expecting number'); + } + query.limit = params.limit; } - query.prevId = params.prevId; + + return this.bitgo.get(this.url('/webhooks')) + .query(query) + .result() + .nodeify(callback); } - if (params.limit) { - if (!_.isNumber(params.limit)) { - throw new Error('invalid limit argument, expecting number'); + /** + * Simulate wallet webhook, currently for webhooks of type transfer and pending approval + * @param params + * - webhookId (required) id of the webhook to be simulated + * - transferId (optional but required for transfer webhooks) id of the simulated transfer + * - pendingApprovalId (optional but required for pending approval webhooks) id of the simulated pending approval + * @param callback + * @returns {*} + */ + simulateWebhook(params: any = {}, callback?: NodeCallback): Bluebird { + common.validateParams(params, ['webhookId'], ['transferId', 'pendingApprovalId'], callback); + + const hasTransferId = !!params.transferId; + const hasPendingApprovalId = !!params.pendingApprovalId; + if (!hasTransferId && !hasPendingApprovalId) { + throw new Error('must supply either transferId or pendingApprovalId'); + } + + if (hasTransferId && hasPendingApprovalId) { + throw new Error('must supply either transferId or pendingApprovalId, but not both'); } - query.limit = params.limit; + + // depending on the coin type of the wallet, the txHash has to adhere to its respective format + // but the server takes care of that + + // only take the transferId and pendingApprovalId properties + const filteredParams = _.pick(params, ['transferId', 'pendingApprovalId']); + + const webhookId = params.webhookId; + return this.bitgo.post(this.url('/webhooks/' + webhookId + '/simulate')) + .send(filteredParams) + .result() + .nodeify(callback); } - return this.bitgo.get(this.url('/webhooks')) - .query(query) - .result() - .nodeify(callback); -}; - -/** - * Simulate wallet webhook, currently for webhooks of type transfer and pending approval - * @param params - * - webhookId (required) id of the webhook to be simulated - * - transferId (optional but required for transfer webhooks) id of the simulated transfer - * - pendingApprovalId (optional but required for pending approval webhooks) id of the simulated pending approval - * @param callback - * @returns {*} - */ -Wallet.prototype.simulateWebhook = function(params, callback) { - params = params || {}; - common.validateParams(params, ['webhookId'], ['transferId', 'pendingApprovalId'], callback); - - const hasTransferId = !!params.transferId; - const hasPendingApprovalId = !!params.pendingApprovalId; - if (!hasTransferId && !hasPendingApprovalId) { - throw new Error('must supply either transferId or pendingApprovalId'); + /** + * Add a webhook to this wallet + * @param params + * @param callback + */ + addWebhook(params: any = {}, callback?: NodeCallback): Bluebird { + common.validateParams(params, ['url', 'type'], [], callback); + + return this.bitgo.post(this.url('/webhooks')) + .send(params) + .result() + .nodeify(callback); } - if (hasTransferId && hasPendingApprovalId) { - throw new Error('must supply either transferId or pendingApprovalId, but not both'); + /** + * Remove a webhook from this wallet + * @param params + * @param callback + */ + removeWebhook(params: any = {}, callback?: NodeCallback): Bluebird { + common.validateParams(params, ['url', 'type'], [], callback); + + return this.bitgo.del(this.url('/webhooks')) + .send(params) + .result() + .nodeify(callback); } - // depending on the coin type of the wallet, the txHash has to adhere to its respective format - // but the server takes care of that - - // only take the transferId and pendingApprovalId properties - const filteredParams = _.pick(params, ['transferId', 'pendingApprovalId']); - - const webhookId = params.webhookId; - return this.bitgo.post(this.url('/webhooks/' + webhookId + '/simulate')) - .send(filteredParams) - .result() - .nodeify(callback); -}; - -Wallet.prototype.addWebhook = function(params, callback) { - params = params || {}; - common.validateParams(params, ['url', 'type'], [], callback); - - return this.bitgo.post(this.url('/webhooks')) - .send(params) - .result() - .nodeify(callback); -}; - -Wallet.prototype.removeWebhook = function(params, callback) { - params = params || {}; - common.validateParams(params, ['url', 'type'], [], callback); - - return this.bitgo.del(this.url('/webhooks')) - .send(params) - .result() - .nodeify(callback); -}; - -// -// Key chains -// Gets the user key chain for this wallet -// The user key chain is typically the first keychain of the wallet and has the encrypted prv stored on BitGo. -// Useful when trying to get the users' keychain from the server before decrypting to sign a transaction. -Wallet.prototype.getEncryptedUserKeychain = function(params, callback) { - params = params || {}; - common.validateParams(params, [], [], callback); - const self = this; - - const tryKeyChain = function(index) { - if (!self._wallet.keys || index >= self._wallet.keys.length) { - return self.bitgo.reject('No encrypted keychains on this wallet.', callback); - } + /** + * Gets the user key chain for this wallet + * + * The user key chain is the first keychain of the wallet and usually has the encrypted prv stored on BitGo. + * Useful when trying to get the users' keychain from the server before decrypting to sign a transaction. + * @param params + * @param callback + */ + getEncryptedUserKeychain(params: any = {}, callback?: NodeCallback): Bluebird { + const tryKeyChain = co(function *(index: number) { + if (!this._wallet.keys || index >= this._wallet.keys.length) { + throw new Error('No encrypted keychains on this wallet.'); + } - const params = { id: self._wallet.keys[index] }; + const params = { id: this._wallet.keys[index] }; - return self.baseCoin.keychains().get(params) - .then(function(keychain) { + const keychain = yield this.baseCoin.keychains().get(params); // If we find the prv, then this is probably the user keychain we're looking for if (keychain.encryptedPrv) { return keychain; } return tryKeyChain(index + 1); - }); - }; + }).bind(this); - return tryKeyChain(0).nodeify(callback); -}; + return tryKeyChain(0).nodeify(callback); + } -// Key chains -// Gets the unencrypted private key for this wallet (be careful!) -// Requires wallet passphrase -Wallet.prototype.getPrv = function(params, callback) { - return co(function *() { - common.validateParams(params, [], ['walletPassphrase', 'prv'], callback); + /** + * Gets the unencrypted private key for this wallet (be careful!) + * Requires wallet passphrase + * + * @param params + * @param callback + */ + getPrv(params: any = {}, callback?: NodeCallback): Bluebird { + return co(function *() { + common.validateParams(params, [], ['walletPassphrase', 'prv'], callback); + + // Prepare signing key + if (_.isUndefined(params.prv) && _.isUndefined(params.walletPassphrase)) { + throw new Error('must either provide prv or wallet passphrase'); + } - // Prepare signing key - if (_.isUndefined(params.prv) && _.isUndefined(params.walletPassphrase)) { - throw new Error('must either provide prv or wallet passphrase'); - } + if (!_.isUndefined(params.prv) && !_.isString(params.prv)) { + throw new Error('prv must be a string'); + } - if (!_.isUndefined(params.prv) && !_.isString(params.prv)) { - throw new Error('prv must be a string'); - } + if (!_.isUndefined(params.walletPassphrase) && !_.isString(params.walletPassphrase)) { + throw new Error('walletPassphrase must be a string'); + } - if (!_.isUndefined(params.walletPassphrase) && !_.isString(params.walletPassphrase)) { - throw new Error('walletPassphrase must be a string'); - } + if (params.prv) { + return params.prv; + } - if (params.prv) { - return params.prv; - } + const userKeychain = yield this.getEncryptedUserKeychain(); + const userEncryptedPrv = userKeychain.encryptedPrv; - const userKeychain = yield this.getEncryptedUserKeychain(); - const userEncryptedPrv = userKeychain.encryptedPrv; + let userPrv; + try { + userPrv = this.bitgo.decrypt({ input: userEncryptedPrv, password: params.walletPassphrase }); + } catch (e) { + throw new Error('error decrypting wallet passphrase'); + } - let userPrv; - try { - userPrv = this.bitgo.decrypt({ input: userEncryptedPrv, password: params.walletPassphrase }); - } catch (e) { - throw new Error('error decrypting wallet passphrase'); - } + return userPrv; + }).call(this).asCallback(callback); + } - return userPrv; - }).call(this).asCallback(callback); -}; - -// -// createShare -// share the wallet with an existing BitGo user. -// Parameters: -// user - the recipient, must have a corresponding user record in our database -// keychain - the keychain to be shared with the recipient -// permissions - the recipient's permissions if the share is accepted -// Returns: -// -Wallet.prototype.createShare = function(params, callback) { - params = params || {}; - common.validateParams(params, ['user', 'permissions'], [], callback); - - if (params.keychain && !_.isEmpty(params.keychain)) { - if (!params.keychain.pub || !params.keychain.encryptedPrv || !params.keychain.fromPubKey || !params.keychain.toPubKey || !params.keychain.path) { - throw new Error('requires keychain parameters - pub, encryptedPrv, fromPubKey, toPubKey, path'); + /** + * Send an encrypted wallet share to BitGo. + * @param params + * @param callback + */ + createShare(params: any = {}, callback?: NodeCallback): Bluebird { + common.validateParams(params, ['user', 'permissions'], [], callback); + + if (params.keychain && !_.isEmpty(params.keychain)) { + if (!params.keychain.pub || !params.keychain.encryptedPrv || !params.keychain.fromPubKey || !params.keychain.toPubKey || !params.keychain.path) { + throw new Error('requires keychain parameters - pub, encryptedPrv, fromPubKey, toPubKey, path'); + } } - } - return this.bitgo.post(this.url('/share')) - .send(params) - .result() - .nodeify(callback); -}; - -/** - * - * @param params - * @param callback - * @returns {*} - */ -Wallet.prototype.shareWallet = function(params, callback) { - params = params || {}; - common.validateParams(params, ['email', 'permissions'], ['walletPassphrase', 'message'], callback); - - if (params.reshare !== undefined && !_.isBoolean(params.reshare)) { - throw new Error('Expected reshare to be a boolean.'); + return this.bitgo.post(this.url('/share')) + .send(params) + .result() + .nodeify(callback); } - if (params.skipKeychain !== undefined && !_.isBoolean(params.skipKeychain)) { - throw new Error('Expected skipKeychain to be a boolean. '); - } - const needsKeychain = !params.skipKeychain && params.permissions.indexOf('spend') !== -1; + /** + * Share this wallet with another BitGo user. + * @param params + * @param callback + * @returns {*} + */ + shareWallet(params: any = {}, callback?: NodeCallback): Bluebird { + return co(function *() { + common.validateParams(params, ['email', 'permissions'], ['walletPassphrase', 'message'], callback); + + if (params.reshare !== undefined && !_.isBoolean(params.reshare)) { + throw new Error('Expected reshare to be a boolean.'); + } - if (params.disableEmail !== undefined && !_.isBoolean(params.disableEmail)) { - throw new Error('Expected disableEmail to be a boolean.'); - } + if (params.skipKeychain !== undefined && !_.isBoolean(params.skipKeychain)) { + throw new Error('Expected skipKeychain to be a boolean. '); + } + const needsKeychain = !params.skipKeychain && params.permissions.indexOf('spend') !== -1; + + if (params.disableEmail !== undefined && !_.isBoolean(params.disableEmail)) { + throw new Error('Expected disableEmail to be a boolean.'); + } - const self = this; - let sharing; - let sharedKeychain; - return this.bitgo.getSharingKey({ email: params.email.toLowerCase() }) - .then(function(result) { - sharing = result; - - if (needsKeychain) { - return self.getEncryptedUserKeychain({}) - .then(function(keychain) { - // Decrypt the user key with a passphrase - if (keychain.encryptedPrv) { - if (!params.walletPassphrase) { - throw new Error('Missing walletPassphrase argument'); + const sharing = yield this.bitgo.getSharingKey({ email: params.email.toLowerCase() }); + let sharedKeychain; + if (needsKeychain) { + try { + const keychain = yield this.getEncryptedUserKeychain({}); + // Decrypt the user key with a passphrase + if (keychain.encryptedPrv) { + if (!params.walletPassphrase) { + throw new Error('Missing walletPassphrase argument'); + } + try { + keychain.prv = this.bitgo.decrypt({ password: params.walletPassphrase, input: keychain.encryptedPrv }); + } catch (e) { + throw new Error('Unable to decrypt user keychain'); + } + + const eckey = makeRandomKey(); + const secret = this.bitgo.getECDHSecret({ eckey: eckey, otherPubKeyHex: sharing.pubkey }); + const newEncryptedPrv = this.bitgo.encrypt({ password: secret, input: keychain.prv }); + + sharedKeychain = { + pub: keychain.pub, + encryptedPrv: newEncryptedPrv, + fromPubKey: eckey.getPublicKeyBuffer().toString('hex'), + toPubKey: sharing.pubkey, + path: sharing.path + }; } - try { - keychain.prv = self.bitgo.decrypt({ password: params.walletPassphrase, input: keychain.encryptedPrv }); - } catch (e) { - throw new Error('Unable to decrypt user keychain'); + } catch (e) { + if (e.message === 'No encrypted keychains on this wallet.') { + sharedKeychain = {}; + // ignore this error because this looks like a cold wallet + } else { + throw e; } - - const eckey = makeRandomKey(); - const secret = self.bitgo.getECDHSecret({ eckey: eckey, otherPubKeyHex: sharing.pubkey }); - const newEncryptedPrv = self.bitgo.encrypt({ password: secret, input: keychain.prv }); - - sharedKeychain = { - pub: keychain.pub, - encryptedPrv: newEncryptedPrv, - fromPubKey: eckey.getPublicKeyBuffer().toString('hex'), - toPubKey: sharing.pubkey, - path: sharing.path - }; - } - }).catch(function(e) { - if (e.message === 'No encrypted keychains on this wallet.') { - sharedKeychain = {}; - return; // ignore this error because this looks like a cold wallet } - throw e; - }); - } - }) - .then(function() { - const options: any = { - user: sharing.userId, - permissions: params.permissions, - reshare: params.reshare, - message: params.message, - disableEmail: params.disableEmail - }; - if (sharedKeychain) { - options.keychain = sharedKeychain; - } else if (params.skipKeychain) { - options.keychain = {}; - } + } - return self.createShare(options); - }) - .nodeify(callback); -}; - -/** - * Remove user from wallet - * @param params - * - userId Id of the user to remove - * @param callback - * @return {*} - */ -Wallet.prototype.removeUser = function(params, callback) { - params = params || {}; - common.validateParams(params, ['userId'], [], callback); - - const userId = params.userId; - - return this.bitgo.del(this.url('/user/' + userId)) - .result() - .nodeify(callback); -}; - -/** - * - * @param {Object} params - * @param {{address: string, amount: string}} params.recipients - list of recipients and necessary recipient information - * @param {Number} params.numBlocks - Estimates the approximate fee per kilobyte necessary for a transaction confirmation within numBlocks blocks - * @param {Number} params.feeRate - the desired feeRate for the transaction in base units/kB - * @param {Number} params.maxFeeRate - upper limit for feeRate in base units/kB - * @param {Number} params.minConfirms - Minimum number of confirmations unspents going into this transaction should have - * @param {Boolean} params.enforceMinConfirmsForChange - Enforce minimum number of confirmations on change (internal) inputs. - * @param {Number} params.targetWalletUnspents - The desired count of unspents in the wallet. If the wallet’s current unspent count is lower than the target, up to four additional change outputs will be added to the transaction. - * @param {Number} params.minValue - Ignore unspents smaller than this amount of base units - * @param {Number} params.maxValue - Ignore unspents larger than this amount of base units - * @param {Number} params.sequenceId - The sequence ID of the transaction - * @param {Number} params.lastLedgerSequence - Absolute max ledger the transaction should be accepted in, whereafter it will be rejected. - * @param {String} params.ledgerSequenceDelta - Relative ledger height (in relation to the current ledger) that the transaction should be accepted in, whereafter it will be rejected. - * @param {Number} params.gasPrice - Custom gas price to be used for sending the transaction - * @param {Boolean} params.noSplitChange - Set to true to disable automatic change splitting for purposes of unspent management - * @param {Array} params.unspents - The unspents to use in the transaction. Each unspent should be in the form prevTxId:nOutput - * @param {String} params.changeAddress - Specifies the destination of the change output - * @param {Number} params.validFromBlock - (Algorand) The minimum round this will run on - * @param {Number} params.validToBlock - (Algorand) The maximum round this will run on - * @param {Boolean} params.instant - Build this transaction to conform with instant sending coin-specific method (if available) - * @param {{value: String, type: String}} params.memo - Memo to use in transaction (supported by Stellar) - * @param {String} params.addressType - The type of address to create for change. One of `p2sh`, `p2shP2wsh`, and `p2wsh`. Case-sensitive. - * @param {Boolean} params.hop - Build this as an Ethereum hop transaction - * @param {String} params.walletPassphrase The passphrase to the wallet user key, to sign commitment data for Ethereum hop transactions - * @param callback - * @returns {*} - */ -Wallet.prototype.prebuildTransaction = function(params, callback) { - return co(function *() { - // Whitelist params to build tx - const whitelistedParams = _.pick(params, [ - 'recipients', 'numBlocks', 'feeRate', 'maxFeeRate', 'minConfirms', 'enforceMinConfirmsForChange', - 'targetWalletUnspents', 'message', 'minValue', 'maxValue', 'sequenceId', 'lastLedgerSequence', - 'ledgerSequenceDelta', 'gasPrice', 'noSplitChange', 'unspents', 'changeAddress', 'instant', 'memo', 'addressType', - 'cpfpTxIds', 'cpfpFeeRate', 'maxFee', 'idfVersion', 'idfSignedTimestamp', 'idfUserId', 'strategy', - 'validFromBlock', 'validToBlock', - ]); - debug('prebuilding transaction: %O', whitelistedParams); + const options: any = { + user: sharing.userId, + permissions: params.permissions, + reshare: params.reshare, + message: params.message, + disableEmail: params.disableEmail + }; - if (params.reqId) { - this.bitgo._reqId = params.reqId; - } - const extraParams = yield this.baseCoin.getExtraPrebuildParams(Object.assign(params, { wallet: this })); - Object.assign(whitelistedParams, extraParams); - const buildQuery = this.bitgo.post(this.baseCoin.url('/wallet/' + this.id() + '/tx/build')) - .send(whitelistedParams) - .result(); - const blockHeightQuery = this.baseCoin.getLatestBlockHeight ? this.baseCoin.getLatestBlockHeight(params.reqId) : Promise.resolve(undefined); - const queries = [buildQuery, blockHeightQuery]; - const [buildResponse, blockHeight] = yield Promise.all(queries); - debug('postprocessing transaction prebuild: %O', buildResponse); - if (!_.isUndefined(blockHeight)) { - buildResponse.blockHeight = blockHeight; - } - let prebuild = yield this.baseCoin.postProcessPrebuild(Object.assign(buildResponse, { wallet: this, buildParams: whitelistedParams })); - delete prebuild.wallet; - delete prebuild.buildParams; - prebuild = _.extend({}, prebuild, { walletId: this.id() }); - debug('final transaction prebuild: %O', prebuild); - return prebuild; - }).call(this).asCallback(callback); -}; - -/** - * Sign a transaction - * @param params - * - txPrebuild - * - [keychain / key] (object) or prv (string) - * - walletPassphrase - * @param callback - * @return {*} - */ -Wallet.prototype.signTransaction = function(params, callback) { - const txPrebuild = params.txPrebuild; - if (!txPrebuild || typeof txPrebuild !== 'object') { - throw new Error('txPrebuild must be an object'); - } - const self = this; - return Bluebird.try(function() { - return self.baseCoin.presignTransaction(params); - }).then(function(params) { - const userPrv = self.getUserPrv(params); - const signingParams = _.extend({}, params, { txPrebuild: txPrebuild, prv: userPrv }); - return self.baseCoin.signTransaction(signingParams); - }); -}; - -/** - * Get the user private key from either a derivation or an encrypted keychain - * @param [params.keychain / params.key] (object) or params.prv (string) - * @param params.walletPassphrase (string) - */ -Wallet.prototype.getUserPrv = function(params) { - const userKeychain = params.keychain || params.key; - let userPrv = params.prv; - if (userPrv && typeof userPrv !== 'string') { - throw new Error('prv must be a string'); + if (sharedKeychain) { + options.keychain = sharedKeychain; + } else if (params.skipKeychain) { + options.keychain = {}; + } + + return this.createShare(options); + }).call(this).asCallback(callback); } - if (userPrv && params.coldDerivationSeed) { - // the derivation only makes sense when a key already exists - const derivation = this.baseCoin.deriveKeyWithSeed({ key: userPrv, seed: params.coldDerivationSeed }); - userPrv = derivation.key; - } else if (!userPrv) { - if (!userKeychain || typeof userKeychain !== 'object') { - throw new Error('keychain must be an object'); - } - const userEncryptedPrv = userKeychain.encryptedPrv; - if (!userEncryptedPrv) { - throw new Error('keychain does not have property encryptedPrv'); - } - if (!params.walletPassphrase) { - throw new Error('walletPassphrase property missing'); - } - userPrv = this.bitgo.decrypt({ input: userEncryptedPrv, password: params.walletPassphrase }); + /** + * Remove user from wallet + * @param params + * - userId Id of the user to remove + * @param callback + * @return {*} + */ + removeUser(params: any = {}, callback?: NodeCallback): Bluebird { + common.validateParams(params, ['userId'], [], callback); + + const userId = params.userId; + return this.bitgo.del(this.url('/user/' + userId)) + .result() + .nodeify(callback); } - return userPrv; -}; -Wallet.prototype.prebuildAndSignTransaction = function(params, callback) { - return co(function *() { - params = params || {}; + /** + * Fetch a transaction prebuild (unsigned transaction) from BitGo + * + * @param {Object} params + * @param {{address: string, amount: string}} params.recipients - list of recipients and necessary recipient information + * @param {Number} params.numBlocks - Estimates the approximate fee per kilobyte necessary for a transaction confirmation within numBlocks blocks + * @param {Number} params.feeRate - the desired feeRate for the transaction in base units/kB + * @param {Number} params.maxFeeRate - upper limit for feeRate in base units/kB + * @param {Number} params.minConfirms - Minimum number of confirmations unspents going into this transaction should have + * @param {Boolean} params.enforceMinConfirmsForChange - Enforce minimum number of confirmations on change (internal) inputs. + * @param {Number} params.targetWalletUnspents - The desired count of unspents in the wallet. If the wallet’s current unspent count is lower than the target, up to four additional change outputs will be added to the transaction. + * @param {Number} params.minValue - Ignore unspents smaller than this amount of base units + * @param {Number} params.maxValue - Ignore unspents larger than this amount of base units + * @param {Number} params.sequenceId - The sequence ID of the transaction + * @param {Number} params.lastLedgerSequence - Absolute max ledger the transaction should be accepted in, whereafter it will be rejected. + * @param {String} params.ledgerSequenceDelta - Relative ledger height (in relation to the current ledger) that the transaction should be accepted in, whereafter it will be rejected. + * @param {Number} params.gasPrice - Custom gas price to be used for sending the transaction + * @param {Boolean} params.noSplitChange - Set to true to disable automatic change splitting for purposes of unspent management + * @param {Array} params.unspents - The unspents to use in the transaction. Each unspent should be in the form prevTxId:nOutput + * @param {String} params.changeAddress - Specifies the destination of the change output + * @param {Number} params.validFromBlock - (Algorand) The minimum round this will run on + * @param {Number} params.validToBlock - (Algorand) The maximum round this will run on + * @param {Boolean} params.instant - Build this transaction to conform with instant sending coin-specific method (if available) + * @param {{value: String, type: String}} params.memo - Memo to use in transaction (supported by Stellar) + * @param {String} params.addressType - The type of address to create for change. One of `p2sh`, `p2shP2wsh`, and `p2wsh`. Case-sensitive. + * @param {Boolean} params.hop - Build this as an Ethereum hop transaction + * @param {String} params.walletPassphrase The passphrase to the wallet user key, to sign commitment data for Ethereum hop transactions + * @param callback + * @returns {*} + */ + prebuildTransaction(params: any = {}, callback?: NodeCallback): Bluebird { + return co(function *() { + // Whitelist params to build tx + const whitelistedParams = _.pick(params, [ + 'recipients', 'numBlocks', 'feeRate', 'maxFeeRate', 'minConfirms', 'enforceMinConfirmsForChange', + 'targetWalletUnspents', 'message', 'minValue', 'maxValue', 'sequenceId', 'lastLedgerSequence', + 'ledgerSequenceDelta', 'gasPrice', 'noSplitChange', 'unspents', 'changeAddress', 'instant', 'memo', 'addressType', + 'cpfpTxIds', 'cpfpFeeRate', 'maxFee', 'idfVersion', 'idfSignedTimestamp', 'idfUserId', 'strategy', + 'validFromBlock', 'validToBlock', + ]); + debug('prebuilding transaction: %O', whitelistedParams); + + if (params.reqId) { + this.bitgo._reqId = params.reqId; + } + const extraParams = yield this.baseCoin.getExtraPrebuildParams(Object.assign(params, { wallet: this })); + Object.assign(whitelistedParams, extraParams); + const buildQuery = this.bitgo.post(this.baseCoin.url('/wallet/' + this.id() + '/tx/build')) + .send(whitelistedParams) + .result(); + const blockHeightQuery = this.baseCoin.getLatestBlockHeight ? + this.baseCoin.getLatestBlockHeight(params.reqId) : + Promise.resolve(undefined); + const queries = [buildQuery, blockHeightQuery]; + const [buildResponse, blockHeight] = yield Promise.all(queries); + debug('postprocessing transaction prebuild: %O', buildResponse); + if (!_.isUndefined(blockHeight)) { + buildResponse.blockHeight = blockHeight; + } + let prebuild = yield this.baseCoin.postProcessPrebuild(Object.assign(buildResponse, { wallet: this, buildParams: whitelistedParams })); + delete prebuild.wallet; + delete prebuild.buildParams; + prebuild = _.extend({}, prebuild, { walletId: this.id() }); + debug('final transaction prebuild: %O', prebuild); + return prebuild; + }).call(this).asCallback(callback); + } - if (params.prebuildTx && params.recipients) { - const error: any = new Error('Only one of prebuildTx and recipients may be specified'); - error.code = 'both_prebuildtx_and_recipients_specified'; - throw error; - } + /** + * Sign a transaction + * @param params + * - txPrebuild + * - [keychain / key] (object) or prv (string) + * - walletPassphrase + * @param callback + * @return {*} + */ + signTransaction(params: any = {}, callback?: NodeCallback): Bluebird { + return co(function *() { + const txPrebuild = params.txPrebuild; + if (!txPrebuild || typeof txPrebuild !== 'object') { + throw new Error('txPrebuild must be an object'); + } + const presign = yield this.baseCoin.presignTransaction(params); + const userPrv = this.getUserPrv(presign); + const signingParams = _.extend({}, presign, { txPrebuild: txPrebuild, prv: userPrv }); + return this.baseCoin.signTransaction(signingParams); + }).call(this).asCallback(callback); + } - if (params.recipients && !Array.isArray(params.recipients)) { - const error: any = new Error('expecting recipients array'); - error.code = 'recipients_not_array'; - throw error; + /** + * Get the user private key from either a derivation or an encrypted keychain + * @param [params.keychain / params.key] (object) or params.prv (string) + * @param params.walletPassphrase (string) + */ + getUserPrv(params: any = {}): any { + const userKeychain = params.keychain || params.key; + let userPrv = params.prv; + if (userPrv && typeof userPrv !== 'string') { + throw new Error('prv must be a string'); } + if (userPrv && params.coldDerivationSeed) { + // the derivation only makes sense when a key already exists + const derivation = this.baseCoin.deriveKeyWithSeed({ key: userPrv, seed: params.coldDerivationSeed }); + userPrv = derivation.key; + } else if (!userPrv) { + if (!userKeychain || typeof userKeychain !== 'object') { + throw new Error('keychain must be an object'); + } + const userEncryptedPrv = userKeychain.encryptedPrv; + if (!userEncryptedPrv) { + throw new Error('keychain does not have property encryptedPrv'); + } + if (!params.walletPassphrase) { + throw new Error('walletPassphrase property missing'); + } - if (_.isArray(this._permissions) && !this._permissions.includes('spend')) { - const error: any = new Error('no spend permission on this wallet'); - error.code = 'user_not_allowed_to_spend_from_wallet'; - throw error; + userPrv = this.bitgo.decrypt({ input: userEncryptedPrv, password: params.walletPassphrase }); } + return userPrv; + } - // call prebuildTransaction and keychains-get in parallel - // the prebuild can be overridden by providing an explicit tx - const txPrebuildQuery = params.prebuildTx ? Promise.resolve(params.prebuildTx) : this.prebuildTransaction(params); - - // retrieve our keychains needed to run the prebuild - some coins use all pubs - const ids = this.baseCoin.keyIdsForSigning(); - const keychainQueriesPromises = ids.map(keyId => this.baseCoin.keychains().get({ id: this._wallet.keys[keyId], reqId: params.reqId })); - const keychains = yield Promise.all(keychainQueriesPromises); + /** + * Get a transaction prebuild from BitGo, validate it, and then decrypt the user key and sign the transaction + * @param params + * @param callback + */ + prebuildAndSignTransaction(params: any = {}, callback?: NodeCallback): Bluebird { + return co(function *() { + if (params.prebuildTx && params.recipients) { + const error: any = new Error('Only one of prebuildTx and recipients may be specified'); + error.code = 'both_prebuildtx_and_recipients_specified'; + throw error; + } - const txPrebuild = yield txPrebuildQuery; + if (params.recipients && !Array.isArray(params.recipients)) { + const error: any = new Error('expecting recipients array'); + error.code = 'recipients_not_array'; + throw error; + } - try { - const verificationParams = _.pick(params.verification || {}, ['disableNetworking', 'keychains', 'addresses']); - yield this.baseCoin.verifyTransaction({ txParams: params, txPrebuild, wallet: this, verification: verificationParams, reqId: params.reqId }); - } catch (e) { - debug('Transaction prebuild failure:', e); - console.error('transaction prebuild failed local validation:'); - throw e; - } + if (_.isArray(this._permissions) && !this._permissions.includes('spend')) { + const error: any = new Error('no spend permission on this wallet'); + error.code = 'user_not_allowed_to_spend_from_wallet'; + throw error; + } - // pass our three keys - const signingParams = _.extend({}, params, { - txPrebuild: txPrebuild, - wallet: { - // this is the version of the multisig address at wallet creation time - addressVersion: this._wallet.coinSpecific.addressVersion - }, - keychain: keychains[0], - backupKeychain: (keychains.length > 1) ? keychains[1] : null, - bitgoKeychain: (keychains.length > 2) ? keychains[2] : null, - }); + // call prebuildTransaction and keychains-get in parallel + // the prebuild can be overridden by providing an explicit tx + const txPrebuildQuery = params.prebuildTx ? Promise.resolve(params.prebuildTx) : this.prebuildTransaction(params); + + // retrieve our keychains needed to run the prebuild - some coins use all pubs + const ids = this.baseCoin.keyIdsForSigning(); + const keychainQueriesPromises = ids.map( + keyId => this.baseCoin.keychains().get({ id: this._wallet.keys[keyId], reqId: params.reqId }) + ); + const keychains = yield Promise.all(keychainQueriesPromises); + + const txPrebuild = yield txPrebuildQuery; + + try { + const verificationParams = _.pick(params.verification || {}, ['disableNetworking', 'keychains', 'addresses']); + yield this.baseCoin.verifyTransaction({ + txParams: params, + txPrebuild, + wallet: this, + verification: verificationParams, + reqId: params.reqId + }); + } catch (e) { + debug('Transaction prebuild failure:', e); + console.error('transaction prebuild failed local validation:'); + throw e; + } - try { - return yield this.signTransaction(signingParams); - } catch (error) { - if (error.message.includes('insufficient funds')) { - error.code = 'insufficient_funds'; - error.walletBalances = { - balanceString: this.balanceString(), - confirmedBalanceString: this.confirmedBalanceString(), - spendableBalanceString: this.spendableBalanceString(), - balance: this.balance(), - confirmedBalance: this.confirmedBalance(), - spendableBalance: this.spendableBalance() - }; - error.txParams = _.omit(params, ['keychain', 'prv', 'passphrase', 'walletPassphrase', 'key']); - } - throw error; - } - }).call(this).asCallback(callback); -}; - -Wallet.prototype.accelerateTransaction = function(params, callback) { - return co(function *() { - // TODO(BG-9349): change the last check to > 0 and the error message once platform allows multiple transactions to - // be bumped in the same CPFP transaction - if (_.isUndefined(params.cpfpTxIds) || !Array.isArray(params.cpfpTxIds) || params.cpfpTxIds.length !== 1) { - const error: any = new Error('expecting cpfpTxIds to be an array of length 1'); - error.code = 'cpfptxids_not_array'; - throw error; - } + // pass our three keys + const signingParams = _.extend({}, params, { + txPrebuild: txPrebuild, + wallet: { + // this is the version of the multisig address at wallet creation time + addressVersion: this._wallet.coinSpecific.addressVersion + }, + keychain: keychains[0], + backupKeychain: (keychains.length > 1) ? keychains[1] : null, + bitgoKeychain: (keychains.length > 2) ? keychains[2] : null, + }); - if (_.isUndefined(params.cpfpFeeRate)) { - if (params.noCpfpFeeRate !== true) { - const error: any = new Error('cpfpFeeRate must be set unless noCpfpFeeRate is set'); - error.code = 'cpfpfeerate_not_set'; + try { + return yield this.signTransaction(signingParams); + } catch (error) { + if (error.message.includes('insufficient funds')) { + error.code = 'insufficient_funds'; + error.walletBalances = { + balanceString: this.balanceString(), + confirmedBalanceString: this.confirmedBalanceString(), + spendableBalanceString: this.spendableBalanceString(), + balance: this.balance(), + confirmedBalance: this.confirmedBalance(), + spendableBalance: this.spendableBalance() + }; + error.txParams = _.omit(params, ['keychain', 'prv', 'passphrase', 'walletPassphrase', 'key']); + } throw error; } - } else { - if (!_.isInteger(params.cpfpFeeRate) || params.cpfpFeeRate < 0) { - const error: any = new Error('cpfpFeeRate must be a non-negative integer'); - error.code = 'cpfpfeerate_not_nonnegative_integer'; + }).call(this).asCallback(callback); + } + + /** + * Accelerate a transaction's confirmation using Child-Pays-For-Parent (CPFP) + * @param params + * @param callback + */ + accelerateTransaction(params: any = {}, callback?: NodeCallback): Bluebird { + return co(function *() { + // TODO(BG-9349): change the last check to > 0 and the error message once platform allows multiple transactions to + // be bumped in the same CPFP transaction + if (_.isUndefined(params.cpfpTxIds) || !Array.isArray(params.cpfpTxIds) || params.cpfpTxIds.length !== 1) { + const error: any = new Error('expecting cpfpTxIds to be an array of length 1'); + error.code = 'cpfptxids_not_array'; throw error; } - } - if (_.isUndefined(params.maxFee)) { - if (params.noMaxFee !== true) { - const error: any = new Error('maxFee must be set unless noMaxFee is set'); - error.code = 'maxfee_not_set'; - throw error; + if (_.isUndefined(params.cpfpFeeRate)) { + if (params.noCpfpFeeRate !== true) { + const error: any = new Error('cpfpFeeRate must be set unless noCpfpFeeRate is set'); + error.code = 'cpfpfeerate_not_set'; + throw error; + } + } else { + if (!_.isInteger(params.cpfpFeeRate) || params.cpfpFeeRate < 0) { + const error: any = new Error('cpfpFeeRate must be a non-negative integer'); + error.code = 'cpfpfeerate_not_nonnegative_integer'; + throw error; + } } - } else { - if (!_.isInteger(params.maxFee) || params.maxFee < 0) { - const error: any = new Error('maxFee must be a non-negative integer'); - error.code = 'maxfee_not_nonnegative_integer'; - throw error; + + if (_.isUndefined(params.maxFee)) { + if (params.noMaxFee !== true) { + const error: any = new Error('maxFee must be set unless noMaxFee is set'); + error.code = 'maxfee_not_set'; + throw error; + } + } else { + if (!_.isInteger(params.maxFee) || params.maxFee < 0) { + const error: any = new Error('maxFee must be a non-negative integer'); + error.code = 'maxfee_not_nonnegative_integer'; + throw error; + } } - } - if (params.recipients !== undefined) { - if (!Array.isArray(params.recipients) || params.recipients.length !== 0) { - throw new Error(`invalid value for 'recipients': must be empty array when set`); + if (params.recipients !== undefined) { + if (!Array.isArray(params.recipients) || params.recipients.length !== 0) { + throw new Error(`invalid value for 'recipients': must be empty array when set`); + } } - } - params.recipients = []; - - // We must pass the build params through to submit in case the CPFP tx ever has to be rebuilt. - const submitParams = Object.assign(params, yield this.prebuildAndSignTransaction(params)); - return yield this.submitTransaction(submitParams); - }).call(this).asCallback(callback); -}; - -/** - * Submits a half-signed transaction to BitGo - * @param params - * - txHex: transaction hex to submit - * - halfSigned: object containing transaction (txHex or txBase64) to submit - * @param callback - */ -Wallet.prototype.submitTransaction = function(params, callback) { - common.validateParams(params, [], ['otp', 'txHex'], callback); - const hasTxHex = !!params.txHex; - const hasHalfSigned = !!params.halfSigned; - - if ((hasTxHex && hasHalfSigned) || (!hasTxHex && !hasHalfSigned)) { - throw new Error('must supply either txHex or halfSigned, but not both'); - } - return this.bitgo.post(this.baseCoin.url('/wallet/' + this.id() + '/tx/send')) - .send(params) - .result() - .nodeify(callback); -}; - -/** - * Send coins to a recipient - * @param params - * @param params.address - the destination address - * @param params.amount - the amount in satoshis/wei/base value to be sent - * @param params.message - optional message to attach to transaction - * @param params.data - [Ethereum Specific] optional data to pass to transaction - * @param params.walletPassphrase - the passphrase to be used to decrypt the user key on this wallet - * @param params.prv - the private key in string form, if walletPassphrase is not available - * @param params.minConfirms - the minimum confirmation threshold for inputs - * @param params.enforceMinConfirmsForChange - whether to enforce minConfirms for change inputs - * @param callback - * @returns {*} - */ -Wallet.prototype.send = function(params, callback) { - params = params || {}; - common.validateParams(params, ['address'], ['message', 'data'], callback); - const coin = this.baseCoin; - - const amount = new BigNumber(params.amount); - if (amount.isNegative()) { - throw new Error('invalid argument for amount - positive number greater than zero or numeric string expected'); - } + params.recipients = []; - if (!coin.valuelessTransferAllowed() && amount.isZero()) { - throw new Error('invalid argument for amount - positive number greater than zero or numeric string expected'); + // We must pass the build params through to submit in case the CPFP tx ever has to be rebuilt. + const submitParams = Object.assign(params, yield this.prebuildAndSignTransaction(params)); + return yield this.submitTransaction(submitParams); + }).call(this).asCallback(callback); } - params.recipients = [{ - address: params.address, - amount: params.amount - }]; - if (params.data && coin.transactionDataAllowed()) { - params.recipients[0].data = params.data; + /** + * Submit a half-signed transaction to BitGo + * @param params + * - txHex: transaction hex to submit + * - halfSigned: object containing transaction (txHex or txBase64) to submit + * @param callback + */ + submitTransaction(params: any = {}, callback?: NodeCallback): Bluebird { + common.validateParams(params, [], ['otp', 'txHex'], callback); + const hasTxHex = !!params.txHex; + const hasHalfSigned = !!params.halfSigned; + + if ((hasTxHex && hasHalfSigned) || (!hasTxHex && !hasHalfSigned)) { + throw new Error('must supply either txHex or halfSigned, but not both'); + } + return this.bitgo.post(this.baseCoin.url('/wallet/' + this.id() + '/tx/send')) + .send(params) + .result() + .nodeify(callback); } - return this.sendMany(params) - .nodeify(callback); -}; - -/** - * Send money to multiple recipients - * 1. Gets the user keychain by checking the wallet for a key which has an encrypted prv - * 2. Decrypts user key - * 3. Creates the transaction with default fee - * 4. Signs transaction with decrypted user key - * 5. Sends the transaction to BitGo - * @param {object} params - * @param {{address: string, amount: string}} params.recipients - list of recipients and necessary recipient information - * @param {Number} params.numBlocks - Estimates the approximate fee per kilobyte necessary for a transaction confirmation within numBlocks blocks - * @param {Number} params.feeRate - the desired feeRate for the transaction in satothis/kB - * @param {Number} params.maxFeeRate - upper limit for feeRate in satoshis/kB - * @param {Number} params.minConfirms - all selected unspents will have at least this many confirmations - * @param {Boolean} params.enforceMinConfirmsForChange - Enforces minConfirms on change inputs - * @param {Number} params.targetWalletUnspents - The desired count of unspents in the wallet - * @param {String} params.message - optional message to attach to transaction - * @param {Number} params.minValue - Ignore unspents smaller than this amount of satoshis - * @param {Number} params.maxValue - Ignore unspents larger than this amount of satoshis - * @param {Number} params.sequenceId - The sequence ID of the transaction - * @param {Number} params.lastLedgerSequence - Absolute max ledger the transaction should be accepted in, whereafter it will be rejected. - * @param {String} params.ledgerSequenceDelta - Relative ledger height (in relation to the current ledger) that the transaction should be accepted in, whereafter it will be rejected. - * @param {Number} params.gasPrice - Custom gas price to be used for sending the transaction - * @param {Boolean} params.noSplitChange - Set to true to disable automatic change splitting for purposes of unspent management - * @param {Array} params.unspents - The unspents to use in the transaction. Each unspent should be in the form prevTxId:nOutput - * @param {String} params.comment - Any additional comment to attach to the transaction - * @param {String} params.otp - Two factor auth code to enable sending the transaction - * @param {String} params.changeAddress - Specifies the destination of the change output - * @param {Boolean} params.instant - Send this transaction using coin-specific instant sending method (if available) - * @param {{value: String, type: String}} params.memo - Memo to use in transaction (supported by Stellar) - * @param callback - * @returns {*} - */ -Wallet.prototype.sendMany = function(params, callback) { - return co(function *() { - params = params || {}; - common.validateParams(params, [], ['comment', 'otp'], callback); - debug('sendMany called'); - const reqId = params.reqId || util.createRequestId(); - params.reqId = reqId; + /** + * Send coins to a recipient + * @param params + * @param params.address - the destination address + * @param params.amount - the amount in satoshis/wei/base value to be sent + * @param params.message - optional message to attach to transaction + * @param params.data - [Ethereum Specific] optional data to pass to transaction + * @param params.walletPassphrase - the passphrase to be used to decrypt the user key on this wallet + * @param params.prv - the private key in string form, if walletPassphrase is not available + * @param params.minConfirms - the minimum confirmation threshold for inputs + * @param params.enforceMinConfirmsForChange - whether to enforce minConfirms for change inputs + * @param callback + * @returns {*} + */ + send(params: any = {}, callback?: NodeCallback): Bluebird { + common.validateParams(params, ['address'], ['message', 'data'], callback); const coin = this.baseCoin; - if (_.isObject(params.recipients)) { - params.recipients.map(function(recipient) { - const amount = new BigNumber(recipient.amount); - if (amount.isNegative()) { - throw new Error('invalid argument for amount - positive number greater than zero or numeric string expected'); - } - if (!coin.valuelessTransferAllowed() && amount.isZero()) { - throw new Error('invalid argument for amount - positive number greater than zero or numeric string expected'); - } - }); - } - const halfSignedTransaction = yield this.prebuildAndSignTransaction(params); - const selectParams = _.pick(params, [ - 'recipients', 'numBlocks', 'feeRate', 'maxFeeRate', 'minConfirms', - 'enforceMinConfirmsForChange', 'targetWalletUnspents', - 'message', 'minValue', 'maxValue', 'sequenceId', - 'lastLedgerSequence', 'ledgerSequenceDelta', 'gasPrice', - 'noSplitChange', 'unspents', 'comment', 'otp', 'changeAddress', - 'instant', 'memo' - ]); - const finalTxParams = _.extend({}, halfSignedTransaction, selectParams); - this.bitgo._reqId = reqId; - return this.bitgo.post(this.url('/tx/send')) - .send(finalTxParams) - .result(); - - }).call(this).asCallback(callback); -}; - -/** - * Recover an unsupported token from a BitGo multisig wallet - * params are validated in Eth.prototype.recoverToken - * @param params - * @param params.tokenContractAddress the contract address of the unsupported token - * @param params.recipient the destination address recovered tokens should be sent to - * @param params.walletPassphrase the wallet passphrase - * @param params.prv the xprv - */ -Wallet.prototype.recoverToken = function(params, callback) { - return co(function *() { - if (this.baseCoin.getFamily() !== 'eth') { - throw new Error('token recovery only supported for eth wallets'); + const amount = new BigNumber(params.amount); + if (amount.isNegative()) { + throw new Error('invalid argument for amount - positive number greater than zero or numeric string expected'); } - return this.baseCoin.recoverToken(_.merge(params, { wallet: this })); + if (!coin.valuelessTransferAllowed() && amount.isZero()) { + throw new Error('invalid argument for amount - positive number greater than zero or numeric string expected'); + } - }).call(this).asCallback(callback); -}; + params.recipients = [{ + address: params.address, + amount: params.amount + }]; + if (params.data && coin.transactionDataAllowed()) { + params.recipients[0].data = params.data; + } -/** - * Get transaction metadata for the oldest transaction that is still pending or attempted - * @param callback - * @param params.walletId [Optional] The ID of the wallet (must provide one of walletId and enterpriseId) - * @param params.enterpriseId [Optional] The ID of the enterprise (must provide one of walletId and enterpriseId) - * @returns {Object} Object with txid, walletId, tx, and fee (if supported for coin) - */ -Wallet.prototype.getFirstPendingTransaction = function(params, callback) { - return co(function *() { - params = params || {}; - common.validateParams(params, [], [], callback); - const query = { walletId: this.id() }; - return internal.getFirstPendingTransaction(query, this.baseCoin, this.bitgo); - }).call(this).asCallback(callback); -}; - -/** - * Change the fee on the pending transaction that corresponds to the given txid to the given new fee - * @param {String} params.txid The transaction Id corresponding to the transaction whose fee is to be changed - * @param {Integer} params.fee The new fee to apply to the denoted transaction - * @param callback - * @returns {String} The transaction ID of the new transaction that contains the new fee rate - */ -Wallet.prototype.changeFee = function(params, callback) { - return co(function *() { - params = params || {}; - common.validateParams(params, ['txid', 'fee'], [], callback); - - return this.bitgo.post(this.baseCoin.url('/wallet/' + this.id() + '/tx/changeFee')) - .send(params) - .result(); - }).call(this).asCallback(callback); -}; - -/** - * Fetch info from merchant server - * @param {Object} params The params passed into the function - * @param {String} params.url The Url to retrieve info from - * @param callback - * @returns {Object} The info returned from the merchant server - */ -Wallet.prototype.getPaymentInfo = function(params, callback) { - return co(function *coGetPaymentInfo() { - params = params || {}; - common.validateParams(params, ['url'], [], callback); - - return this.bitgo.get(this.url('/paymentInfo')) - .query(params) - .result(); - - }).call(this).asCallback(callback); -}; - -/** - * Send json payment response - * @param {Object} params The params passed into the function - * @param {String} params.paymentUrl - The url to send the fully signed transaction to - * @param {String} params.txHex - The transaction hex of the payment - * @param {String} params.memo {String} - A memo supplied by the merchant, to be inserted into the transfer as the comment - * @param {String} params.expires {String} - ISO Date format of when the payment request expires - * @param callback - * @returns {Object} The info returned from the merchant server Payment Ack - */ -Wallet.prototype.sendPaymentResponse = function(params, callback) { - return co(function *coSendPaymentResponse() { - - return this.bitgo.post(this.url('/sendPayment')) - .send(params) - .result(); - }).call(this).asCallback(callback); -}; - - -/** - * Create a policy rule - * @param params - * @param params.condition condition object - * @param params.action action object - * @param callback - * @returns {*} - */ -Wallet.prototype.createPolicyRule = function(params, callback) { - return co(function *() { - params = params || {}; - common.validateParams(params, ['id', 'type'], ['message'], callback); + return this.sendMany(params).nodeify(callback); + } - if (!_.isObject(params.condition)) { - throw new Error('missing parameter: conditions object'); - } + /** + * Send money to multiple recipients + * 1. Gets the user keychain by checking the wallet for a key which has an encrypted prv + * 2. Decrypts user key + * 3. Creates the transaction with default fee + * 4. Signs transaction with decrypted user key + * 5. Sends the transaction to BitGo + * @param {object} params + * @param {{address: string, amount: string}} params.recipients - list of recipients and necessary recipient information + * @param {Number} params.numBlocks - Estimates the approximate fee per kilobyte necessary for a transaction confirmation within numBlocks blocks + * @param {Number} params.feeRate - the desired feeRate for the transaction in satothis/kB + * @param {Number} params.maxFeeRate - upper limit for feeRate in satoshis/kB + * @param {Number} params.minConfirms - all selected unspents will have at least this many confirmations + * @param {Boolean} params.enforceMinConfirmsForChange - Enforces minConfirms on change inputs + * @param {Number} params.targetWalletUnspents - The desired count of unspents in the wallet + * @param {String} params.message - optional message to attach to transaction + * @param {Number} params.minValue - Ignore unspents smaller than this amount of satoshis + * @param {Number} params.maxValue - Ignore unspents larger than this amount of satoshis + * @param {Number} params.sequenceId - The sequence ID of the transaction + * @param {Number} params.lastLedgerSequence - Absolute max ledger the transaction should be accepted in, whereafter it will be rejected. + * @param {String} params.ledgerSequenceDelta - Relative ledger height (in relation to the current ledger) that the transaction should be accepted in, whereafter it will be rejected. + * @param {Number} params.gasPrice - Custom gas price to be used for sending the transaction + * @param {Boolean} params.noSplitChange - Set to true to disable automatic change splitting for purposes of unspent management + * @param {Array} params.unspents - The unspents to use in the transaction. Each unspent should be in the form prevTxId:nOutput + * @param {String} params.comment - Any additional comment to attach to the transaction + * @param {String} params.otp - Two factor auth code to enable sending the transaction + * @param {String} params.changeAddress - Specifies the destination of the change output + * @param {Boolean} params.instant - Send this transaction using coin-specific instant sending method (if available) + * @param {{value: String, type: String}} params.memo - Memo to use in transaction (supported by Stellar) + * @param callback + * @returns {*} + */ + sendMany(params: any = {}, callback?: NodeCallback): Bluebird { + return co(function *() { + common.validateParams(params, [], ['comment', 'otp'], callback); + debug('sendMany called'); + const reqId = params.reqId || util.createRequestId(); + params.reqId = reqId; + const coin = this.baseCoin; + if (_.isObject(params.recipients)) { + params.recipients.map(function(recipient) { + const amount = new BigNumber(recipient.amount); + if (amount.isNegative()) { + throw new Error('invalid argument for amount - positive number greater than zero or numeric string expected'); + } + if (!coin.valuelessTransferAllowed() && amount.isZero()) { + throw new Error('invalid argument for amount - positive number greater than zero or numeric string expected'); + } + }); + } - if (!_.isObject(params.action)) { - throw new Error('missing parameter: action object'); - } + const halfSignedTransaction = yield this.prebuildAndSignTransaction(params); + const selectParams = _.pick(params, [ + 'recipients', 'numBlocks', 'feeRate', 'maxFeeRate', 'minConfirms', + 'enforceMinConfirmsForChange', 'targetWalletUnspents', + 'message', 'minValue', 'maxValue', 'sequenceId', + 'lastLedgerSequence', 'ledgerSequenceDelta', 'gasPrice', + 'noSplitChange', 'unspents', 'comment', 'otp', 'changeAddress', + 'instant', 'memo' + ]); + const finalTxParams = _.extend({}, halfSignedTransaction, selectParams); + this.bitgo._reqId = reqId; + return this.bitgo.post(this.url('/tx/send')) + .send(finalTxParams) + .result(); + }).call(this).asCallback(callback); + } - return this.bitgo.post(this.url('/policy/rule')) - .send(params) - .result(); - }).call(this).asCallback(callback); -}; - -/** - * Update a policy rule - * @param params - * @param params.condition condition object - * @param params.action action object - * @param callback - * @returns {*} - */ -Wallet.prototype.setPolicyRule = function(params, callback) { - return co(function *() { - params = params || {}; - common.validateParams(params, ['id', 'type'], ['message'], callback); + /** + * Recover an unsupported token from a BitGo multisig wallet + * params are validated in Eth.prototype.recoverToken + * @param params + * @param params.tokenContractAddress the contract address of the unsupported token + * @param params.recipient the destination address recovered tokens should be sent to + * @param params.walletPassphrase the wallet passphrase + * @param params.prv the xprv + * @param callback + */ + recoverToken(params: any = {}, callback?: NodeCallback): Bluebird { + return co(function *() { + if (this.baseCoin.getFamily() !== 'eth') { + throw new Error('token recovery only supported for eth wallets'); + } - if (!_.isObject(params.condition)) { - throw new Error('missing parameter: conditions object'); - } + return this.baseCoin.recoverToken(_.merge(params, { wallet: this })); + }).call(this).asCallback(callback); + } - if (!_.isObject(params.action)) { - throw new Error('missing parameter: action object'); - } + /** + * Get transaction metadata for the oldest transaction that is still pending or attempted + * @param callback + * @param params + * @param params.walletId [Optional] The ID of the wallet (must provide one of walletId and enterpriseId) + * @param params.enterpriseId [Optional] The ID of the enterprise (must provide one of walletId and enterpriseId) + * @returns {Object} Object with txid, walletId, tx, and fee (if supported for coin) + */ + getFirstPendingTransaction(params: any = {}, callback?: NodeCallback): Bluebird { + return co(function *() { + const query = { walletId: this.id() }; + return internal.getFirstPendingTransaction(query, this.baseCoin, this.bitgo); + }).call(this).asCallback(callback); + } - return this.bitgo.put(this.url('/policy/rule')) - .send(params) - .result(); - }).call(this).asCallback(callback); -}; - -/** - * Remove Policy Rule - * @param params - * @param callback - * @returns {*} - */ -Wallet.prototype.removePolicyRule = function(params, callback) { - return co(function *() { - params = params || {}; - common.validateParams(params, ['id'], ['message'], callback); - - return this.bitgo.del(this.url('/policy/rule')) - .send(params) - .result(); - }).call(this).asCallback(callback); -}; - - -/** - * Remove Wallet - * @param params - * @param callback - * @returns {*} - */ -Wallet.prototype.remove = function(params, callback) { - return co(function *() { - params = params || {}; - common.validateParams(params, [], [], callback); + /** + * Change the fee on the pending transaction that corresponds to the given txid to the given new fee + * @param params + * @param {String} params.txid The transaction Id corresponding to the transaction whose fee is to be changed + * @param {Number} params.fee The new fee to apply to the denoted transaction + * @param callback + * @returns {String} The transaction ID of the new transaction that contains the new fee rate + */ + changeFee(params: any = {}, callback?: NodeCallback): Bluebird { + return co(function *() { + common.validateParams(params, ['txid', 'fee'], [], callback); + + return this.bitgo.post(this.baseCoin.url('/wallet/' + this.id() + '/tx/changeFee')) + .send(params) + .result(); + }).call(this).asCallback(callback); + } - return this.bitgo.del(this.url()) - .result(); - }).call(this).asCallback(callback); -}; + /** + * Fetch info from merchant server + * @param {Object} params The params passed into the function + * @param {String} params.url The Url to retrieve info from + * @param callback + * @returns {Object} The info returned from the merchant server + */ + getPaymentInfo(params: any = {}, callback?: NodeCallback): Bluebird { + return co(function *coGetPaymentInfo() { + params = params || {}; + common.validateParams(params, ['url'], [], callback); + + return this.bitgo.get(this.url('/paymentInfo')) + .query(params) + .result(); + + }).call(this).asCallback(callback); + } -Wallet.prototype.toTradingAccount = function() { - if (this.baseCoin.getFamily() !== 'ofc') { - throw new Error('Can only convert an Offchain (OFC) wallet to a trading account'); + /** + * Send json payment response + * @param {Object} params The params passed into the function + * @param {String} params.paymentUrl - The url to send the fully signed transaction to + * @param {String} params.txHex - The transaction hex of the payment + * @param {String} params.memo {String} - A memo supplied by the merchant, to be inserted into the transfer as the comment + * @param {String} params.expires {String} - ISO Date format of when the payment request expires + * @param callback + * @returns {Object} The info returned from the merchant server Payment Ack + */ + sendPaymentResponse(params: any = {}, callback?: NodeCallback): Bluebird { + return co(function *coSendPaymentResponse() { + + return this.bitgo.post(this.url('/sendPayment')) + .send(params) + .result(); + }).call(this).asCallback(callback); } - return new TradingAccount(this, this.bitgo); -}; - -/** - * Creates and downloads PDF keycard for wallet (requires response from wallets.generateWallet) - * @param params - * * jsPDF - an instance of the jsPDF library - * * QRCode - an instance of the QRious library - * * userKeychain - a wallet's private user keychain - * * backupKeychain - a wallet's private backup keychain - * * bitgoKeychain - a wallet's private bitgo keychain - * * passphrase - the wallet passphrase - * * passcodeEncryptionCode - the encryption secret used for Box D - * * activationCode - a randomly generated six-digit activation code - * * walletKeyID - the Key ID used for deriving a cold wallet's signing key - * * backupKeyID - the Key ID used for deriving a cold wallet's backup key - * @param callback - * @returns {*} - */ -Wallet.prototype.downloadKeycard = function(params, callback) { - const getKeyData = (coinShortName, passphrase, passcodeEncryptionCode, walletKeyID, backupKeyID) => { - // When using just 'generateWallet', we get back an unencrypted prv for the backup keychain - // If the user passes in their passphrase, we can encrypt it - if (params.backupKeychain.prv && passphrase) { - params.backupKeychain.encryptedPrv = this.bitgo.encrypt({ - input: params.backupKeychain.prv, - password: passphrase - }); - } + /** + * Create a policy rule + * @param params + * @param params.condition condition object + * @param params.action action object + * @param callback + * @returns {*} + */ + createPolicyRule(params: any = {}, callback?: NodeCallback): Bluebird { + return co(function *() { + params = params || {}; + common.validateParams(params, ['id', 'type'], ['message'], callback); + + if (!_.isObject(params.condition)) { + throw new Error('missing parameter: conditions object'); + } - // If we have the passcode encryption code, create a box D with the encryptedWalletPasscode - if (passphrase && passcodeEncryptionCode) { - params.encryptedWalletPasscode = this.bitgo.encrypt({ - input: passphrase, - password: passcodeEncryptionCode - }); - } + if (!_.isObject(params.action)) { + throw new Error('missing parameter: action object'); + } - // PDF QR Code data - const qrData: any = { - user: { - title: 'A: User Key', - desc: 'This is your private key, encrypted with your passcode.', - data: params.userKeychain.encryptedPrv - }, - backup: { - title: 'B: Backup Key', - desc: 'This is your backup private key, encrypted with your passcode.', - data: params.backupKeychain.encryptedPrv - }, - bitgo: { - title: 'C: BitGo Public Key', - desc: 'This is the public part of the key that BitGo will use to ' + - 'co-sign transactions\r\nwith you on your wallet.', - data: params.bitgoKeychain.pub - }, - passcode: { - title: 'D: Encrypted Wallet Password', - desc: 'This is the wallet password, encrypted client-side ' + - 'with a key held by\r\nBitGo.', - data: params.encryptedWalletPasscode - } - }; - - if (walletKeyID) { - qrData.user.keyID = walletKeyID; - } + return this.bitgo.post(this.url('/policy/rule')) + .send(params) + .result(); + }).call(this).asCallback(callback); + } - if (backupKeyID) { - qrData.backup.keyID = backupKeyID; - } + /** + * Update a policy rule + * @param params + * @param params.condition condition object + * @param params.action action object + * @param callback + * @returns {*} + */ + setPolicyRule(params: any = {}, callback?: NodeCallback) { + return co(function *() { + params = params || {}; + common.validateParams(params, ['id', 'type'], ['message'], callback); + + if (!_.isObject(params.condition)) { + throw new Error('missing parameter: conditions object'); + } - if (!params.userKeychain.encryptedPrv) { - // User provided their own key - this is a cold wallet - qrData.user.title = 'A: Provided User Key'; - qrData.user.desc = 'This is the public key you provided for your wallet.'; - qrData.user.data = params.userKeychain.pub; + if (!_.isObject(params.action)) { + throw new Error('missing parameter: action object'); + } - // The user provided their own public key, we can remove box D - delete qrData.passcode; - } else if (!params.encryptedWalletPasscode) { - delete qrData.passcode; - } + return this.bitgo.put(this.url('/policy/rule')) + .send(params) + .result(); + }).call(this).asCallback(callback); + } - if (params.backupKeychain.provider) { - const backupKeyProviderName = params.backupKeychain.provider; - // Backup key held with KRS - qrData.backup = { - title: 'B: Backup Key', - desc: - 'This is the public key held at ' + backupKeyProviderName + - ', an ' + coinShortName + ' recovery service. If you lose\r\nyour key, ' + backupKeyProviderName + - ' will be able to sign transactions to recover funds.', - data: params.backupKeychain.pub - }; - } else if (!params.backupKeychain.encryptedPrv) { - // User supplied the xpub - qrData.backup = { - title: 'B: Backup Key', - desc: 'This is the public portion of your backup key, which you provided.', - data: params.backupKeychain.pub - }; + /** + * Remove Policy Rule + * @param params + * @param callback + * @returns {*} + */ + removePolicyRule(params: any = {}, callback?: NodeCallback): Bluebird { + return co(function *() { + params = params || {}; + common.validateParams(params, ['id'], ['message'], callback); + + return this.bitgo.del(this.url('/policy/rule')) + .send(params) + .result(); + }).call(this).asCallback(callback); + } + + /** + * Remove this wallet + * @param params + * @param callback + * @returns {*} + */ + remove(params: any = {}, callback?: NodeCallback): Bluebird { + return this.bitgo.del(this.url()).result().asCallback(callback); + } + + /** + * Create a trading account from this wallet + */ + toTradingAccount(): TradingAccount { + if (this.baseCoin.getFamily() !== 'ofc') { + throw new Error('Can only convert an Offchain (OFC) wallet to a trading account'); } - return qrData; - }; - - const generateQuestions = (coin) => { - return [ - { - q: 'What is the KeyCard?', - a: - [ - 'The KeyCard contains important information which can be used to recover the ' + coin + ' ', - 'from your wallet in several situations. Each BitGo wallet' + - ' has its own, unique KeyCard. ', - 'If you have created multiple wallets, you should retain the KeyCard for each of them.' - ] - }, - { - q: 'What should I do with it?', - a: - [ - 'You should print the KeyCard and/or save the PDF to an offline storage device. The print-out ', - 'or USB stick should be kept in a safe place, such as a bank vault or home safe. It\'s a good idea ', - 'to keep a second copy in a different location.', - '', - 'Important: If you haven\'t provided an external backup key, then the original PDF should be ', - 'deleted from any machine where the wallet will be regularly accessed to prevent malware from ', - 'capturing both the KeyCard and your wallet passcode.' - ] - }, - { - q: 'What should I do if I lose it?', - a: - [ - 'If you have lost or damaged all copies of your KeyCard, your ' + coin + ' is still safe, but this ', - 'wallet should be considered at risk for loss. As soon as is convenient, you should use BitGo ', - 'to empty the wallet into a new wallet', - ', and discontinue use of the old wallet.' - ] - }, - { - q: 'What if someone sees my KeyCard?', - a: - [ - 'Don\'t panic! All sensitive information on the KeyCard is encrypted with your passcode, or with a', - 'key which only BitGo has. But, in general, you should make best efforts to keep your ', - 'KeyCard private. If your KeyCard does get exposed or copied in a way that makes you ', - 'uncomfortable, the best course of action is to empty the corresponding wallet into another ', - 'wallet and discontinue use of the old wallet.' - ] - }, - { - q: 'What if I forget or lose my wallet password?', - a: - [ - 'BitGo can use the information in QR Code D to help you recover access to your wallet. ', - 'Without the KeyCard, BitGo is not able to recover funds from a wallet with a lost password.' - ] - }, - { - q: 'What if BitGo becomes inaccessible for an extended period?', - a: - [ - 'Your KeyCard and wallet passcode can be used together with BitGo’s published open ', - 'source tools at https://github.com/bitgo to recover your ' + coin + '. Note: You should never enter ', - 'information from your KeyCard into tools other than the tools BitGo has published, or your ', - 'funds may be at risk for theft.' - ] - }, - { - q: 'Should I write my wallet password on my KeyCard?', - a: - [ - 'No! BitGo’s multi-signature approach to security depends on there not being a single point ', - 'of attack. But if your wallet password is on your KeyCard, then anyone who gains access to ', - 'your KeyCard will be able to steal your ' + coin + '.' + ' We recommend keeping your wallet password ', - 'safe in a secure password manager such as LastPass, 1Password or KeePass.' - ] - } - ]; - }; - - return co(function *() { - params = params || {}; - common.validateParams(params, [], ['activationCode'], callback); + return new TradingAccount(this, this.bitgo); + } + /** + * Creates and downloads PDF keycard for wallet (requires response from wallets.generateWallet) + * + * Note: this is example code and is not the version used on bitgo.com + * + * @param params + * * jsPDF - an instance of the jsPDF library + * * QRCode - an instance of the QRious library + * * userKeychain - a wallet's private user keychain + * * backupKeychain - a wallet's private backup keychain + * * bitgoKeychain - a wallet's private bitgo keychain + * * passphrase - the wallet passphrase + * * passcodeEncryptionCode - the encryption secret used for Box D + * * activationCode - a randomly generated six-digit activation code + * * walletKeyID - the Key ID used for deriving a cold wallet's signing key + * * backupKeyID - the Key ID used for deriving a cold wallet's backup key + * @returns {*} + */ + downloadKeycard(params: any = {}): void { if (!window || !window.location) { throw new Error('The downloadKeycard function is only callable within a browser.'); } @@ -1830,153 +1725,26 @@ Wallet.prototype.downloadKeycard = function(params, callback) { throw new Error('Activation code must be six characters'); } - const font = { - header: 24, - subheader: 15, - body: 12 - }; - - const color = { - black: '#000000', - darkgray: '#4c4c4c', - gray: '#9b9b9b', - red: '#e21e1e' - }; - - const margin = 30; - const coinShortName = this.baseCoin.type; const coinName = this.baseCoin.getFullName(); - // document details - const width = 8.5 * 72; - let y = 0; - - // Helpers for data formatting / positioning on the paper - const left = (x) => margin + x; - const moveDown = (yDelta) => { y += yDelta; }; - - const doc = new jsPDF('portrait', 'pt', 'letter'); - doc.setFont('helvetica'); - - // PDF Header Area - includes the logo and company name - // This is data for the BitGo logo in the top left of the PDF - moveDown(30); - - // We don't currently add an image, since that path is dependent on BitGo frontend - // doc.addImage(coinUtility.getSelectedCoinObj().keyCardImage, left(0), y + 10); - - // Activation Code - moveDown(8); - doc.setFontSize(font.body).setTextColor(color.gray); - doc.text('Activation Code', left(460), y); - - doc.setFontSize(font.header).setTextColor(color.black); - moveDown(25); - doc.text('Your BitGo KeyCard', left(150), y); - doc.setFontSize(font.header).setTextColor(color.gray); - doc.text(activationCode.toString(), left(460), y); - - // Subheader - // titles - moveDown(margin); - doc.setFontSize(font.body).setTextColor(color.gray); - doc.text(`Created on ${new Date().toDateString()} by ${window.location.hostname} for wallet named ${wallet.label()}`, left(0), y); - // copy - moveDown(25); - doc.setFontSize(font.subheader).setTextColor(color.black); - doc.text(params.wallet.label(), left(0), y); - // Red Bar - moveDown(20); - doc.setFillColor(255, 230, 230); - doc.rect(left(0), y, width - 2 * margin, 32, 'F'); - - // warning message - moveDown(20); - doc.setFontSize(font.body).setTextColor(color.red); - doc.text('Print this document, or keep it securely offline. See second page for FAQ.', left(75), y); - - // Get the data for the first page (qr codes) - const keyData = getKeyData(coinShortName, passphrase, passcodeEncryptionCode, walletKeyID, backupKeyID); - - // Generate the first page's data for the backup PDF - moveDown(35); - const qrSize = 130; - - // Draw each Box with QR code and description - Object.keys(keyData).forEach(function(keyType) { - const key = keyData[keyType]; - const topY = y; - - // Don't indent if we're not producing QR codes - const textLeft = !!QRCode ? left(qrSize + 15) : left(15); - - // Draw a QR code if library is available - if (QRCode) { - const dataURL = new QRCode({ value: key.data, size: qrSize }).toDataURL('image/jpeg'); - doc.addImage(dataURL, left(0), y, qrSize, qrSize); - } - - doc.setFontSize(font.subheader).setTextColor(color.black); - moveDown(10); - doc.text(key.title, textLeft, y); - moveDown(15); - doc.setFontSize(font.body).setTextColor(color.darkgray); - doc.text(key.desc, textLeft, y); - moveDown(30); - doc.setFontSize(font.body - 2); - doc.text('Data:', textLeft, y); - moveDown(15); - const innerWidth = 72 * 8.5 - textLeft - 30; - doc.setFont('courier').setFontSize(9).setTextColor(color.black); - const lines = doc.splitTextToSize(key.data, innerWidth); - doc.text(lines, textLeft, y); - - // Add key ID (derivation string) if it exists - if (key.keyID) { - const text = 'Key Id: ' + key.keyID; - // Gray bar - moveDown(45); - doc.setFillColor(247, 249, 249); // Gray background - doc.setDrawColor(0, 0, 0); // Border - doc.rect(textLeft, y, width, 15, 'FD'); - - doc.text(text, textLeft + 5, y + 10); - } - - doc.setFont('helvetica'); - // Move down the size of the QR code minus accumulated height on the right side, plus buffer - moveDown(qrSize - (y - topY) + 15); - }); - - // Add a new page (Q + A page) - doc.addPage(); - - // 2nd page title - y = 0; - moveDown(55); - doc.setFontSize(font.header).setTextColor(color.black); - doc.text('BitGo KeyCard FAQ', left(0), y); - - const questions = generateQuestions(coinName); - - // Draw the Q + A data on the second page - moveDown(30); - questions.forEach(function(q) { - doc.setFontSize(font.subheader).setTextColor(color.black); - doc.text(q.q, left(0), y); - moveDown(20); - doc.setFontSize(font.body).setTextColor(color.darkgray); - q.a.forEach(function(line) { - doc.text(line, left(0), y); - moveDown(font.body + 3); - }); - moveDown(22); + const doc = drawKeycard({ + jsPDF, + QRCode, + coinShortName, + coinName, + activationCode, + walletLabel: this.label(), + passphrase, + passcodeEncryptionCode, + userKeychain, + backupKeychain, + bitgoKeychain, + walletKeyID, + backupKeyID, }); // Save the PDF on the user's browser doc.save(`BitGo Keycard for ${wallet.label()}.pdf`); - }).call(this).asCallback(callback); -}; - -module.exports = Wallet; + } +} diff --git a/modules/core/src/v2/wallets.ts b/modules/core/src/v2/wallets.ts index b945083a40..9b6bbf6f4d 100644 --- a/modules/core/src/v2/wallets.ts +++ b/modules/core/src/v2/wallets.ts @@ -1,579 +1,542 @@ import * as bitcoin from 'bitgo-utxo-lib'; import * as common from '../common'; -const Wallet = require('./wallet'); +import { BaseCoin } from './baseCoin'; +import { NodeCallback } from './types'; +import { Wallet } from './wallet'; import * as Bluebird from 'bluebird'; -const co = Bluebird.coroutine; import * as _ from 'lodash'; import { hdPath } from '../bitcoin'; const util = require('../util'); +const co = Bluebird.coroutine; -const Wallets = function(bitgo, baseCoin) { - this.bitgo = bitgo; - this.baseCoin = baseCoin; - this.coinWallet = Wallet; -}; - -Wallets.prototype.createWalletInstance = function() { - return new this.coinWallet(this.bitgo, this.coin); -}; - -/** - * Get a wallet by ID (proxy for getWallet) - * @param params - * @param callback - */ -Wallets.prototype.get = function(params, callback) { - return this.getWallet(params, callback); -}; - -/** - * List a user's wallets - * @param params - * @param callback - * @returns {*} - */ -Wallets.prototype.list = function(params, callback) { - params = params || {}; - common.validateParams(params, [], [], callback); - - const queryObject: any = {}; - - if (params.skip && params.prevId) { - throw new Error('cannot specify both skip and prevId'); - } +export class Wallets { + private readonly bitgo: any; + private readonly baseCoin: BaseCoin; - if (params.getbalances) { - if (!_.isBoolean(params.getbalances)) { - throw new Error('invalid getbalances argument, expecting boolean'); - } - queryObject.getbalances = params.getbalances; - } - if (params.prevId) { - if (!_.isString(params.prevId)) { - throw new Error('invalid prevId argument, expecting string'); - } - queryObject.prevId = params.prevId; - } - if (params.limit) { - if (!_.isNumber(params.limit)) { - throw new Error('invalid limit argument, expecting number'); - } - queryObject.limit = params.limit; + constructor(bitgo: any, baseCoin: BaseCoin) { + this.bitgo = bitgo; + this.baseCoin = baseCoin; } - if (params.allTokens) { - if (!_.isBoolean(params.allTokens)) { - throw new Error('invalid allTokens argument, expecting boolean'); - } - queryObject.allTokens = params.allTokens; + /** + * Get a wallet by ID (proxy for getWallet) + * @param params + * @param callback + */ + get(params: any = {}, callback?: NodeCallback): Bluebird { + return this.getWallet(params, callback); } - const self = this; - return this.bitgo.get(this.baseCoin.url('/wallet')) - .query(queryObject) - .result() - .then(function(body) { - body.wallets = body.wallets.map(function(w) { - return new self.coinWallet(self.bitgo, self.baseCoin, w); - }); - return body; - }) - .nodeify(callback); -}; - -/** -* add -* Add a new wallet (advanced mode). -* This allows you to manually submit the keys, type, m and n of the wallet -* Parameters include: -* "label": label of the wallet to be shown in UI -* "m": number of keys required to unlock wallet (2) -* "n": number of keys available on the wallet (3) -* "keys": array of keychain ids -*/ -Wallets.prototype.add = function(params, callback) { - params = params || {}; - common.validateParams(params, [], ['label', 'enterprise', 'type'], callback); - - // no need to pass keys for (single) custodial wallets - if (params.type !== 'custodial') { - if (Array.isArray(params.keys) === false || !_.isNumber(params.m) || !_.isNumber(params.n)) { - throw new Error('invalid argument'); - } - - // TODO: support more types of multisig - if (!this.baseCoin.isValidMofNSetup(params)) { - throw new Error('unsupported multi-sig type'); - } - } + /** + * List a user's wallets + * @param params + * @param callback + * @returns {*} + */ + list(params: any = {}, callback?: NodeCallback): Bluebird { + return co(function *() { + const queryObject: any = {}; + + if (params.skip && params.prevId) { + throw new Error('cannot specify both skip and prevId'); + } - if (params.tags && Array.isArray(params.tags) === false) { - throw new Error('invalid argument for tags - array expected'); - } + if (params.getbalances) { + if (!_.isBoolean(params.getbalances)) { + throw new Error('invalid getbalances argument, expecting boolean'); + } + queryObject.getbalances = params.getbalances; + } + if (params.prevId) { + if (!_.isString(params.prevId)) { + throw new Error('invalid prevId argument, expecting string'); + } + queryObject.prevId = params.prevId; + } + if (params.limit) { + if (!_.isNumber(params.limit)) { + throw new Error('invalid limit argument, expecting number'); + } + queryObject.limit = params.limit; + } - if (params.clientFlags && Array.isArray(params.clientFlags) === false) { - throw new Error('invalid argument for clientFlags - array expected'); - } + if (params.allTokens) { + if (!_.isBoolean(params.allTokens)) { + throw new Error('invalid allTokens argument, expecting boolean'); + } + queryObject.allTokens = params.allTokens; + } - if (params.isCold && !_.isBoolean(params.isCold)) { - throw new Error('invalid argument for isCold - boolean expected'); + const self = this; + const body = yield this.bitgo.get(this.baseCoin.url('/wallet')) + .query(queryObject) + .result(); + body.wallets = body.wallets.map((w) => new Wallet(self.bitgo, self.baseCoin, w)); + return body; + }).call(this).asCallback(callback); } - if (params.isCustodial && !_.isBoolean(params.isCustodial)) { - throw new Error('invalid argument for isCustodial - boolean expected'); - } + /** + * add + * Add a new wallet (advanced mode). + * This allows you to manually submit the keys, type, m and n of the wallet + * Parameters include: + * "label": label of the wallet to be shown in UI + * "m": number of keys required to unlock wallet (2) + * "n": number of keys available on the wallet (3) + * "keys": array of keychain ids + */ + add(params: any = {}, callback?: NodeCallback): Bluebird { + return co(function *() { + common.validateParams(params, [], ['label', 'enterprise', 'type'], callback); + + // no need to pass keys for (single) custodial wallets + if (params.type !== 'custodial') { + if (Array.isArray(params.keys) === false || !_.isNumber(params.m) || !_.isNumber(params.n)) { + throw new Error('invalid argument'); + } - const self = this; - const walletParams: any = _.pick(params, ['label', 'm', 'n', 'keys', 'enterprise', 'isCold', 'isCustodial', 'tags', 'clientFlags', 'type']); + // TODO: support more types of multisig + if (!this.baseCoin.isValidMofNSetup(params)) { + throw new Error('unsupported multi-sig type'); + } + } - // Additional params needed for xrp - if (params.rootPub) { - walletParams.rootPub = params.rootPub; - } + if (params.tags && Array.isArray(params.tags) === false) { + throw new Error('invalid argument for tags - array expected'); + } - // In XLM this private key is used only for wallet creation purposes, once the wallet is initialized then we - // update its weight to 0 making it an invalid key. - // https://www.stellar.org/developers/guides/concepts/multi-sig.html#additional-signing-keys - if (params.rootPrivateKey) { - walletParams.rootPrivateKey = params.rootPrivateKey; - } + if (params.clientFlags && Array.isArray(params.clientFlags) === false) { + throw new Error('invalid argument for clientFlags - array expected'); + } - if (params.initializationTxs) { - walletParams.initializationTxs = params.initializationTxs; - } + if (params.isCold && !_.isBoolean(params.isCold)) { + throw new Error('invalid argument for isCold - boolean expected'); + } - if (params.disableTransactionNotifications) { - walletParams.disableTransactionNotifications = params.disableTransactionNotifications; - } + if (params.isCustodial && !_.isBoolean(params.isCustodial)) { + throw new Error('invalid argument for isCustodial - boolean expected'); + } - return self.bitgo.post(self.baseCoin.url('/wallet')).send(walletParams).result() - .then(function(newWallet) { - return { - wallet: new self.coinWallet(self.bitgo, self.baseCoin, newWallet) - }; - }) - .nodeify(callback); -}; - -/** - * Generate a new wallet - * 1. Creates the user keychain locally on the client, and encrypts it with the provided passphrase - * 2. If no pub was provided, creates the backup keychain locally on the client, and encrypts it with the provided passphrase - * 3. Uploads the encrypted user and backup keychains to BitGo - * 4. Creates the BitGo key on the service - * 5. Creates the wallet on BitGo with the 3 public keys above - * @param params - * @param params.label - * @param params.passphrase - * @param params.userKey User xpub - * @param params.backupXpub Backup xpub - * @param params.backupXpubProvider - * @param params.enterprise - * @param params.disableTransactionNotifications - * @param params.passcodeEncryptionCode - * @param params.coldDerivationSeed - * @param params.gasPrice - * @param params.disableKRSEmail - * @param callback - * @returns {*} - */ -Wallets.prototype.generateWallet = function(params, callback) { - return co(function *() { - params = params || {}; - common.validateParams(params, ['label'], ['passphrase', 'userKey', 'backupXpub'], callback); - const self = this; - const label = params.label; - - let walletParams: any = { - label: label, - m: 2, - n: 3 - }; - - const hasBackupXpub = !!params.backupXpub; - const hasBackupXpubProvider = !!params.backupXpubProvider; - if (hasBackupXpub && hasBackupXpubProvider) { - throw new Error('Cannot provide more than one backupXpub or backupXpubProvider flag'); - } - - if (!_.isUndefined(params.passcodeEncryptionCode)) { - if (!_.isString(params.passcodeEncryptionCode)) { - throw new Error('passcodeEncryptionCode must be a string'); + const walletParams: any = _.pick(params, ['label', 'm', 'n', 'keys', 'enterprise', 'isCold', 'isCustodial', 'tags', 'clientFlags', 'type']); + + // Additional params needed for xrp + if (params.rootPub) { + walletParams.rootPub = params.rootPub; } - } - if (!_.isUndefined(params.enterprise)) { - if (!_.isString(params.enterprise)) { - throw new Error('invalid enterprise argument, expecting string'); + // In XLM this private key is used only for wallet creation purposes, once the wallet is initialized then we + // update its weight to 0 making it an invalid key. + // https://www.stellar.org/developers/guides/concepts/multi-sig.html#additional-signing-keys + if (params.rootPrivateKey) { + walletParams.rootPrivateKey = params.rootPrivateKey; } - walletParams.enterprise = params.enterprise; - } - if (!_.isUndefined(params.disableTransactionNotifications)) { - if (!_.isBoolean(params.disableTransactionNotifications)) { - throw new Error('invalid disableTransactionNotifications argument, expecting boolean'); + if (params.initializationTxs) { + walletParams.initializationTxs = params.initializationTxs; } - walletParams.disableTransactionNotifications = params.disableTransactionNotifications; - } - if (!_.isUndefined(params.gasPrice)) { - if (!_.isNumber(params.gasPrice)) { - throw new Error('invalid gas price argument, expecting number'); + if (params.disableTransactionNotifications) { + walletParams.disableTransactionNotifications = params.disableTransactionNotifications; } - walletParams.gasPrice = params.gasPrice; - } - if (!_.isUndefined(params.disableKRSEmail)) { - if (!_.isBoolean(params.disableKRSEmail)) { - throw new Error('invalid disableKRSEmail argument, expecting boolean'); + const newWallet = yield this.bitgo.post(this.baseCoin.url('/wallet')).send(walletParams).result(); + return { + wallet: new Wallet(this.bitgo, this.baseCoin, newWallet) + }; + }).call(this).asCallback(callback); + } + + /** + * Generate a new wallet + * 1. Creates the user keychain locally on the client, and encrypts it with the provided passphrase + * 2. If no pub was provided, creates the backup keychain locally on the client, and encrypts it with the provided passphrase + * 3. Uploads the encrypted user and backup keychains to BitGo + * 4. Creates the BitGo key on the service + * 5. Creates the wallet on BitGo with the 3 public keys above + * @param params + * @param params.label + * @param params.passphrase + * @param params.userKey User xpub + * @param params.backupXpub Backup xpub + * @param params.backupXpubProvider + * @param params.enterprise + * @param params.disableTransactionNotifications + * @param params.passcodeEncryptionCode + * @param params.coldDerivationSeed + * @param params.gasPrice + * @param params.disableKRSEmail + * @param callback + * @returns {*} + */ + generateWallet(params: any = {}, callback?: NodeCallback): Bluebird { + return co(function *() { + common.validateParams(params, ['label'], ['passphrase', 'userKey', 'backupXpub'], callback); + const label = params.label; + + let walletParams: any = { + label: label, + m: 2, + n: 3 + }; + + const hasBackupXpub = !!params.backupXpub; + const hasBackupXpubProvider = !!params.backupXpubProvider; + if (hasBackupXpub && hasBackupXpubProvider) { + throw new Error('Cannot provide more than one backupXpub or backupXpubProvider flag'); } - } - - // Ensure each krsSpecific param is either a string, boolean, or number - if (!_.isUndefined(params.krsSpecific)) { - Object.keys(params.krsSpecific).forEach(key => { - const val = params.krsSpecific[key]; - if (!_.isBoolean(val) && !_.isString(val) && !_.isNumber(val)) { - throw new Error('krsSpecific object contains illegal values. values must be strings, booleans, or numbers'); + + if (!_.isUndefined(params.passcodeEncryptionCode)) { + if (!_.isString(params.passcodeEncryptionCode)) { + throw new Error('passcodeEncryptionCode must be a string'); } - }); - } - - let derivationPath = undefined; - - const passphrase = params.passphrase; - const canEncrypt = (!!passphrase && typeof passphrase === 'string'); - const isCold = (!canEncrypt || !!params.userKey); - const reqId = util.createRequestId(); - - // Add the user keychain - const userKeychainPromise = co(function *() { - let userKeychainParams; - let userKeychain; - // User provided user key - if (params.userKey) { - userKeychain = { pub: params.userKey }; - userKeychainParams = userKeychain; - if (params.coldDerivationSeed) { - // the derivation only makes sense when a key already exists - const derivation = self.baseCoin.deriveKeyWithSeed({ key: params.userKey, seed: params.coldDerivationSeed }); - derivationPath = derivation.derivationPath; - userKeychain.pub = derivation.key; + } + + if (!_.isUndefined(params.enterprise)) { + if (!_.isString(params.enterprise)) { + throw new Error('invalid enterprise argument, expecting string'); } - } else { - if (!canEncrypt) { - throw new Error('cannot generate user keypair without passphrase'); + walletParams.enterprise = params.enterprise; + } + + if (!_.isUndefined(params.disableTransactionNotifications)) { + if (!_.isBoolean(params.disableTransactionNotifications)) { + throw new Error('invalid disableTransactionNotifications argument, expecting boolean'); + } + walletParams.disableTransactionNotifications = params.disableTransactionNotifications; + } + + if (!_.isUndefined(params.gasPrice)) { + if (!_.isNumber(params.gasPrice)) { + throw new Error('invalid gas price argument, expecting number'); + } + walletParams.gasPrice = params.gasPrice; + } + + if (!_.isUndefined(params.disableKRSEmail)) { + if (!_.isBoolean(params.disableKRSEmail)) { + throw new Error('invalid disableKRSEmail argument, expecting boolean'); } - // Create the user key. - userKeychain = self.baseCoin.keychains().create(); - userKeychain.encryptedPrv = self.bitgo.encrypt({ password: passphrase, input: userKeychain.prv }); - userKeychainParams = { - pub: userKeychain.pub, - encryptedPrv: userKeychain.encryptedPrv, - originalPasscodeEncryptionCode: params.passcodeEncryptionCode - }; } - userKeychainParams.reqId = reqId; - const newUserKeychain = yield self.baseCoin.keychains().add(userKeychainParams); - return _.extend({}, newUserKeychain, userKeychain); - })(); - - const backupKeychainPromise = Bluebird.try(function() { - if (params.backupXpubProvider || self.baseCoin.getFamily() === 'rmg') { - // If requested, use a KRS or backup key provider - return self.baseCoin.keychains().createBackup({ - provider: params.backupXpubProvider || 'defaultRMGBackupProvider', - disableKRSEmail: params.disableKRSEmail, - krsSpecific: params.krsSpecific, - type: self.baseCoin.getChain(), - reqId + // Ensure each krsSpecific param is either a string, boolean, or number + if (!_.isUndefined(params.krsSpecific)) { + Object.keys(params.krsSpecific).forEach(key => { + const val = params.krsSpecific[key]; + if (!_.isBoolean(val) && !_.isString(val) && !_.isNumber(val)) { + throw new Error('krsSpecific object contains illegal values. values must be strings, booleans, or numbers'); + } }); } - // User provided backup xpub - if (params.backupXpub) { - // user provided backup ethereum address - return self.baseCoin.keychains().add({ pub: params.backupXpub, source: 'backup', reqId }); - } else { - if (!canEncrypt) { - throw new Error('cannot generate backup keypair without passphrase'); + let derivationPath = undefined; + + const passphrase = params.passphrase; + const canEncrypt = (!!passphrase && typeof passphrase === 'string'); + const isCold = (!canEncrypt || !!params.userKey); + const reqId = util.createRequestId(); + + // Add the user keychain + const userKeychainPromise = co(function *() { + let userKeychainParams; + let userKeychain; + // User provided user key + if (params.userKey) { + userKeychain = { pub: params.userKey }; + userKeychainParams = userKeychain; + if (params.coldDerivationSeed) { + // the derivation only makes sense when a key already exists + const derivation = this.baseCoin.deriveKeyWithSeed({ key: params.userKey, seed: params.coldDerivationSeed }); + derivationPath = derivation.derivationPath; + userKeychain.pub = derivation.key; + } + } else { + if (!canEncrypt) { + throw new Error('cannot generate user keypair without passphrase'); + } + // Create the user key. + userKeychain = this.baseCoin.keychains().create(); + userKeychain.encryptedPrv = this.bitgo.encrypt({ password: passphrase, input: userKeychain.prv }); + userKeychainParams = { + pub: userKeychain.pub, + encryptedPrv: userKeychain.encryptedPrv, + originalPasscodeEncryptionCode: params.passcodeEncryptionCode + }; + } + + userKeychainParams.reqId = reqId; + const newUserKeychain = yield this.baseCoin.keychains().add(userKeychainParams); + return _.extend({}, newUserKeychain, userKeychain); + }).call(this); + + const backupKeychainPromise = co(function *() { + if (params.backupXpubProvider || this.baseCoin.getFamily() === 'rmg') { + // If requested, use a KRS or backup key provider + return this.baseCoin.keychains().createBackup({ + provider: params.backupXpubProvider || 'defaultRMGBackupProvider', + disableKRSEmail: params.disableKRSEmail, + krsSpecific: params.krsSpecific, + type: this.baseCoin.getChain(), + reqId + }); } - // No provided backup xpub or address, so default to creating one here - return self.baseCoin.keychains().createBackup({ reqId }); + + // User provided backup xpub + if (params.backupXpub) { + // user provided backup ethereum address + return this.baseCoin.keychains().add({ pub: params.backupXpub, source: 'backup', reqId }); + } else { + if (!canEncrypt) { + throw new Error('cannot generate backup keypair without passphrase'); + } + // No provided backup xpub or address, so default to creating one here + return this.baseCoin.keychains().createBackup({ reqId }); + } + }).call(this); + + const { userKeychain, backupKeychain, bitgoKeychain } = yield Bluebird.props({ + userKeychain: userKeychainPromise, + backupKeychain: backupKeychainPromise, + bitgoKeychain: this.baseCoin.keychains().createBitGo({ enterprise: params.enterprise, reqId }) + }); + + walletParams.keys = [ + userKeychain.id, + backupKeychain.id, + bitgoKeychain.id + ]; + + walletParams.isCold = isCold; + + if (_.isString(userKeychain.prv)) { + walletParams.keySignatures = { + backup: this.baseCoin.signMessage(userKeychain, backupKeychain.pub).toString('hex'), + bitgo: this.baseCoin.signMessage(userKeychain, bitgoKeychain.pub).toString('hex') + }; + } + + if (_.includes(['xrp', 'xlm'], this.baseCoin.getFamily()) && !_.isUndefined(params.rootPrivateKey)) { + walletParams.rootPrivateKey = params.rootPrivateKey; } - }); - - const { userKeychain, backupKeychain, bitgoKeychain } = yield Bluebird.props({ - userKeychain: userKeychainPromise, - backupKeychain: backupKeychainPromise, - bitgoKeychain: self.baseCoin.keychains().createBitGo({ enterprise: params.enterprise, reqId }) - }); - - walletParams.keys = [ - userKeychain.id, - backupKeychain.id, - bitgoKeychain.id - ]; - - walletParams.isCold = isCold; - - if (_.isString(userKeychain.prv)) { - walletParams.keySignatures = { - backup: self.baseCoin.signMessage(userKeychain, backupKeychain.pub).toString('hex'), - bitgo: self.baseCoin.signMessage(userKeychain, bitgoKeychain.pub).toString('hex') + + const keychains = { + userKeychain, + backupKeychain, + bitgoKeychain + }; + walletParams = yield this.baseCoin.supplementGenerateWallet(walletParams, keychains); + this.bitgo._reqId = reqId; + const newWallet = yield this.bitgo.post(this.baseCoin.url('/wallet')).send(walletParams).result(); + const result: any = { + wallet: new Wallet(this.bitgo, this.baseCoin, newWallet), + userKeychain: userKeychain, + backupKeychain: backupKeychain, + bitgoKeychain: bitgoKeychain }; - } - - if (_.includes(['xrp', 'xlm'], self.baseCoin.getFamily()) && !_.isUndefined(params.rootPrivateKey)) { - walletParams.rootPrivateKey = params.rootPrivateKey; - } - - const keychains = { - userKeychain, - backupKeychain, - bitgoKeychain - }; - walletParams = yield self.baseCoin.supplementGenerateWallet(walletParams, keychains); - self.bitgo._reqId = reqId; - const newWallet = yield self.bitgo.post(self.baseCoin.url('/wallet')).send(walletParams).result(); - const result: any = { - wallet: new self.coinWallet(self.bitgo, self.baseCoin, newWallet), - userKeychain: userKeychain, - backupKeychain: backupKeychain, - bitgoKeychain: bitgoKeychain - }; - - if (!_.isUndefined(backupKeychain.prv)) { - result.warning = 'Be sure to backup the backup keychain -- it is not stored anywhere else!'; - } - - if (!_.isUndefined(derivationPath)) { - userKeychain.derivationPath = derivationPath; - } - - return result; - }).call(this).asCallback(callback); -}; - -// -// listShares -// List the user's wallet shares -// -Wallets.prototype.listShares = function(params, callback) { - params = params || {}; - common.validateParams(params, [], [], callback); - - return this.bitgo.get(this.baseCoin.url('/walletshare')) - .result() - .nodeify(callback); -}; - -// -// getShare -// Gets a wallet share information, including the encrypted sharing keychain. requires unlock if keychain is present. -// Params: -// walletShareId - the wallet share to get information on -// -Wallets.prototype.getShare = function(params, callback) { - params = params || {}; - common.validateParams(params, ['walletShareId'], [], callback); - - return this.bitgo.get(this.baseCoin.url('/walletshare/' + params.walletShareId)) - .result() - .nodeify(callback); -}; - -// -// updateShare -// updates a wallet share -// Params: -// walletShareId - the wallet share to update -// state - the new state of the wallet share -// -Wallets.prototype.updateShare = function(params, callback) { - params = params || {}; - common.validateParams(params, ['walletShareId'], [], callback); - - return this.bitgo.post(this.baseCoin.url('/walletshare/' + params.walletShareId)) - .send(params) - .result() - .nodeify(callback); -}; - -// -// resendShareInvite -// Resends a wallet share invitation email -// Params: -// walletShareId - the wallet share whose invitiation should be resent -// -Wallets.prototype.resendShareInvite = function(params, callback) { - return co(function *() { + + if (!_.isUndefined(backupKeychain.prv)) { + result.warning = 'Be sure to backup the backup keychain -- it is not stored anywhere else!'; + } + + if (!_.isUndefined(derivationPath)) { + userKeychain.derivationPath = derivationPath; + } + + return result; + }).call(this).asCallback(callback); + } + + /** + * List the user's wallet shares + * @param params + * @param callback + */ + listShares(params: any = {}, callback?: NodeCallback): Bluebird { + return this.bitgo.get(this.baseCoin.url('/walletshare')) + .result() + .asCallback(callback); + } + + /** + * Gets a wallet share information, including the encrypted sharing keychain. requires unlock if keychain is present. + * @param params + * @param params.walletShareId - the wallet share to get information on + * @param callback + */ + getShare(params: any = {}, callback?: NodeCallback): Bluebird { + common.validateParams(params, ['walletShareId'], [], callback); + + return this.bitgo.get(this.baseCoin.url('/walletshare/' + params.walletShareId)) + .result() + .nodeify(callback); + } + + /** + * Update a wallet share + * @param params.walletShareId - the wallet share to update + * @param params.state - the new state of the wallet share + * @param params + * @param callback + */ + updateShare(params: any = {}, callback?: NodeCallback): Bluebird { params = params || {}; common.validateParams(params, ['walletShareId'], [], callback); - const urlParts = params.walletShareId + '/resendemail'; - return this.bitgo.post(this.baseCoin.url('/walletshare/' + urlParts)) - .result(); - }).call(this).asCallback(callback); -}; - -// -// cancelShare -// cancels a wallet share -// Params: -// walletShareId - the wallet share to update -// -Wallets.prototype.cancelShare = function(params, callback) { - params = params || {}; - common.validateParams(params, ['walletShareId'], [], callback); - - return this.bitgo.del(this.baseCoin.url('/walletshare/' + params.walletShareId)) - .send() - .result() - .nodeify(callback); -}; - -// -// acceptShare -// Accepts a wallet share, adding the wallet to the user's list -// Needs a user's password to decrypt the shared key -// Params: -// walletShareId - the wallet share to accept -// userPassword - (required if more a keychain was shared) user's password to decrypt the shared wallet -// newWalletPassphrase - new wallet passphrase for saving the shared wallet prv. -// If left blank and a wallet with more than view permissions was shared, then the userpassword is used. -// overrideEncryptedPrv - set only if the prv was received out-of-band. -// -Wallets.prototype.acceptShare = function(params, callback) { - params = params || {}; - common.validateParams(params, ['walletShareId'], ['overrideEncryptedPrv', 'userPassword', 'newWalletPassphrase'], callback); - - const self = this; - let encryptedPrv = params.overrideEncryptedPrv; - - return this.getShare({ walletShareId: params.walletShareId }) - .then(function(walletShare) { - // Return right away if there is no keychain to decrypt, or if explicit encryptedPrv was provided - if (!walletShare.keychain || !walletShare.keychain.encryptedPrv || encryptedPrv) { - return walletShare; - } - - // More than viewing was requested, so we need to process the wallet keys using the shared ecdh scheme - if (!params.userPassword) { - throw new Error('userPassword param must be provided to decrypt shared key'); - } - - return self.bitgo.getECDHSharingKeychain() - .then(function(sharingKeychain) { + return this.bitgo.post(this.baseCoin.url('/walletshare/' + params.walletShareId)) + .send(params) + .result() + .nodeify(callback); + } + + /** + * Resend a wallet share invitation email + * @param params + * @param params.walletShareId - the wallet share whose invitiation should be resent + * @param callback + */ + resendShareInvite(params: any = {}, callback?: NodeCallback): Bluebird { + return co(function *() { + common.validateParams(params, ['walletShareId'], [], callback); + + const urlParts = params.walletShareId + '/resendemail'; + return this.bitgo.post(this.baseCoin.url('/walletshare/' + urlParts)).result(); + }).call(this).asCallback(callback); + } + + /** + * Cancel a wallet share + * @param params + * @param params.walletShareId - the wallet share to update + * @param callback + */ + cancelShare(params: any = {}, callback?: NodeCallback): Bluebird { + params = params || {}; + common.validateParams(params, ['walletShareId'], [], callback); + + return this.bitgo.del(this.baseCoin.url('/walletshare/' + params.walletShareId)) + .send() + .result() + .nodeify(callback); + } + + /** + * Accepts a wallet share, adding the wallet to the user's list + * Needs a user's password to decrypt the shared key + * + * @param params + * @param params.walletShareId - the wallet share to accept + * @param params.userPassword - (required if more a keychain was shared) user's password to decrypt the shared wallet + * @param params.newWalletPassphrase - new wallet passphrase for saving the shared wallet prv. + * If left blank and a wallet with more than view permissions was shared, + * then the user's login password is used. + * @param params.overrideEncryptedPrv - set only if the prv was received out-of-band. + * @param callback + */ + acceptShare(params: any = {}, callback?: NodeCallback): Bluebird { + return co(function *() { + common.validateParams(params, ['walletShareId'], ['overrideEncryptedPrv', 'userPassword', 'newWalletPassphrase'], callback); + + let encryptedPrv = params.overrideEncryptedPrv; + + const walletShare = yield this.getShare({ walletShareId: params.walletShareId }); + + // Return right away if there is no keychain to decrypt, or if explicit encryptedPrv was provided + if (!walletShare.keychain || !walletShare.keychain.encryptedPrv || encryptedPrv) { + return walletShare; + } + + // More than viewing was requested, so we need to process the wallet keys using the shared ecdh scheme + if (!params.userPassword) { + throw new Error('userPassword param must be provided to decrypt shared key'); + } + + const sharingKeychain = yield this.bitgo.getECDHSharingKeychain(); if (!sharingKeychain.encryptedXprv) { throw new Error('encryptedXprv was not found on sharing keychain'); } // Now we have the sharing keychain, we can work out the secret used for sharing the wallet with us - sharingKeychain.prv = self.bitgo.decrypt({ password: params.userPassword, input: sharingKeychain.encryptedXprv }); + sharingKeychain.prv = this.bitgo.decrypt({ password: params.userPassword, input: sharingKeychain.encryptedXprv }); const rootExtKey = bitcoin.HDNode.fromBase58(sharingKeychain.prv); // Derive key by path (which is used between these 2 users only) const privKey = hdPath(rootExtKey).deriveKey(walletShare.keychain.path); - const secret = self.bitgo.getECDHSecret({ eckey: privKey, otherPubKeyHex: walletShare.keychain.fromPubKey }); + const secret = this.bitgo.getECDHSecret({ eckey: privKey, otherPubKeyHex: walletShare.keychain.fromPubKey }); // Yes! We got the secret successfully here, now decrypt the shared wallet prv - const decryptedSharedWalletPrv = self.bitgo.decrypt({ password: secret, input: walletShare.keychain.encryptedPrv }); + const decryptedSharedWalletPrv = this.bitgo.decrypt({ password: secret, input: walletShare.keychain.encryptedPrv }); // We will now re-encrypt the wallet with our own password const newWalletPassphrase = params.newWalletPassphrase || params.userPassword; - encryptedPrv = self.bitgo.encrypt({ password: newWalletPassphrase, input: decryptedSharedWalletPrv }); - - // Carry on to the next block where we will post the acceptance of the share with the encrypted prv - return walletShare; - }); - }) - .then(function() { - const updateParams: any = { - walletShareId: params.walletShareId, - state: 'accepted' - }; - - if (encryptedPrv) { - updateParams.encryptedPrv = encryptedPrv; - } - - return self.updateShare(updateParams); - }) - .nodeify(callback); -}; - -/** - * Get a wallet by its ID - * @param params - * @param callback - * @returns {*} - */ -Wallets.prototype.getWallet = function(params, callback) { - params = params || {}; - common.validateParams(params, ['id'], [], callback); - - const self = this; - - const query: any = {}; - - if (params.allTokens) { - if (!_.isBoolean(params.allTokens)) { - throw new Error('invalid allTokens argument, expecting boolean'); - } - query.allTokens = params.allTokens; + encryptedPrv = this.bitgo.encrypt({ password: newWalletPassphrase, input: decryptedSharedWalletPrv }); + const updateParams: any = { + walletShareId: params.walletShareId, + state: 'accepted' + }; + + if (encryptedPrv) { + updateParams.encryptedPrv = encryptedPrv; + } + + return this.updateShare(updateParams); + }).call(this).nodeify(callback); } - this.bitgo._reqId = params.reqId || util.createRequestId(); - - return this.bitgo.get(this.baseCoin.url('/wallet/' + params.id)) - .query(query) - .result() - .then(function(wallet) { - return new self.coinWallet(self.bitgo, self.baseCoin, wallet); - }) - .nodeify(callback); -}; - -/** - * Get a wallet by its address - * @param params - * @param callback - * @returns {*} - */ -Wallets.prototype.getWalletByAddress = function(params, callback) { - params = params || {}; - common.validateParams(params, ['address'], [], callback); - - const self = this; - - this.bitgo._reqId = params.reqId || util.createRequestId(); - - return this.bitgo.get(this.baseCoin.url('/wallet/address/' + params.address)) - .result() - .then(function(wallet) { - return new self.coinWallet(self.bitgo, self.baseCoin, wallet); - }) - .nodeify(callback); -}; - -/** - * For any given supported coin, get total balances for all wallets of that - * coin type on the account. - * @param params - * @param callback - * @returns {*} - */ -Wallets.prototype.getTotalBalances = function(params, callback) { - return co(function *() { - params = params || {}; - common.validateParams(params, [], [], callback); + /** + * Get a wallet by its ID + * @param params + * @param params.id wallet id + * @param callback + * @returns {*} + */ + getWallet(params: any = {}, callback?: NodeCallback): Bluebird { + return co(function *() { + common.validateParams(params, ['id'], [], callback); + + const query: any = {}; + + if (params.allTokens) { + if (!_.isBoolean(params.allTokens)) { + throw new Error('invalid allTokens argument, expecting boolean'); + } + query.allTokens = params.allTokens; + } + + this.bitgo._reqId = params.reqId || util.createRequestId(); + + const wallet = yield this.bitgo.get(this.baseCoin.url('/wallet/' + params.id)) + .query(query) + .result(); + return new Wallet(this.bitgo, this.baseCoin, wallet); + }).call(this).asCallback(callback); + } - return this.bitgo.get(this.baseCoin.url('/wallet/balances')) - .result() - .nodeify(callback); - }).call(this).asCallback(callback); -}; + /** + * Get a wallet by its address + * @param params + * @param params.address wallet address + * @param callback + * @returns {*} + */ + getWalletByAddress(params: any = {}, callback?: NodeCallback): Bluebird { + return co(function *() { + common.validateParams(params, ['address'], [], callback); + + this.bitgo._reqId = params.reqId || util.createRequestId(); + + const wallet = yield this.bitgo.get(this.baseCoin.url('/wallet/address/' + params.address)).result(); + return new Wallet(this.bitgo, this.baseCoin, wallet); + }).call(this).asCallback(callback); + } -module.exports = Wallets; + /** + * For any given supported coin, get total balances for all wallets of that + * coin type on the account. + * @param params + * @param callback + * @returns {*} + */ + getTotalBalances(params: any = {}, callback?: NodeCallback): Bluebird { + return this.bitgo.get(this.baseCoin.url('/wallet/balances')).result().asCallback(callback); + } +} diff --git a/modules/core/test/lib/test_bitgo.ts b/modules/core/test/lib/test_bitgo.ts index 6319183895..1cc881eb01 100644 --- a/modules/core/test/lib/test_bitgo.ts +++ b/modules/core/test/lib/test_bitgo.ts @@ -5,7 +5,7 @@ // const BitGo = require('../../src/bitgo.ts'); -const Wallet = require('../../src/v2/wallet'); +import { Wallet } from '../../src/v2/wallet'; const BigNumber = require('bignumber.js'); import * as Bluebird from 'bluebird'; const co = Bluebird.coroutine; diff --git a/modules/core/test/v2/integration/wallet.ts b/modules/core/test/v2/integration/wallet.ts index 7d4b15b6dc..70b29acfe0 100644 --- a/modules/core/test/v2/integration/wallet.ts +++ b/modules/core/test/v2/integration/wallet.ts @@ -526,7 +526,6 @@ describe('V2 Wallet:', function() { it('should have sharing user self-remove from accepted wallet and reject it', function() { const receivedWalletId = wallet.id(); - console.log('This is received wallet ID', receivedWalletId); return sharingUserBasecoin.wallets().list() .then(function(sharedWallets) { const receivedWallet = _.find(sharedWallets.wallets, function(w) { return w.id() === receivedWalletId; }); diff --git a/modules/core/test/v2/integration/wallets.ts b/modules/core/test/v2/integration/wallets.ts index e2ba3b75dc..a617299be0 100644 --- a/modules/core/test/v2/integration/wallets.ts +++ b/modules/core/test/v2/integration/wallets.ts @@ -2,7 +2,6 @@ // Tests for Wallets // -import { strict as assert } from 'assert'; import * as should from 'should'; import * as Promise from 'bluebird'; const co = Promise.coroutine; @@ -66,28 +65,6 @@ describe('V2 Wallets:', function() { } }); - describe('List', function() { - it('arguments', function() { - assert.throws(function() { wallets.list({}, 'invalid'); }); - assert.throws(function() { wallets.list('invalid'); }); - }); - - it('skip', function(done) { - // TODO server currently doesn't use this param - done(); - }); - - it('getbalances', function(done) { - // TODO server currently doesn't use this param - done(); - }); - - it('prevId', function(done) { - // TODO server currently doesn't use this param - done(); - }); - }); - describe('Generate Wallet', function() { const passphrase = 'yoplait'; const label = 'v2 wallet'; @@ -254,45 +231,37 @@ describe('V2 Wallets:', function() { let backupKeychainId; let bitgoKeychainId; - it('arguments', function() { - assert.throws(function() {wallets.add();}); - assert.throws(function() {wallets.add('invalid');}); - assert.throws(function() {wallets.add({}, 0);}); - assert.throws(function() { - wallets.add({ - keys: [], - m: 'bad', - n: 3 - }, 0); - }); - - assert.throws(function() { - wallets.add({ - keys: [], - m: 1, - n: 3 - }, 0); - }); - - assert.throws(function() { - wallets.add({ - keys: [], - m: 2, - n: 3, - tags: 'bad arg' - }, 0); - }); - - assert.throws(function() { - wallets.add({ - keys: [], - m: 2, - n: 3, - tags: [], - clientFlags: 'bad arg' - }, 0); - }); - }); + it('arguments', co(function *() { + yield wallets.add().should.be.rejected(); + yield wallets.add('invalid').should.be.rejected(); + yield wallets.add({}, 0).should.be.rejected(); + yield wallets.add({ + keys: [], + m: 'bad', + n: 3 + }, 0).should.be.rejected(); + + yield wallets.add({ + keys: [], + m: 1, + n: 3 + }, 0).should.be.rejected(); + + yield wallets.add({ + keys: [], + m: 2, + n: 3, + tags: 'bad arg' + }, 0).should.be.rejected(); + + yield wallets.add({ + keys: [], + m: 2, + n: 3, + tags: [], + clientFlags: 'bad arg' + }, 0).should.be.rejected(); + })); it('should add a wallet with pre generated keys', function() { diff --git a/modules/core/test/v2/unit/coins/algo.ts b/modules/core/test/v2/unit/coins/algo.ts index b5f361b284..a53c327876 100644 --- a/modules/core/test/v2/unit/coins/algo.ts +++ b/modules/core/test/v2/unit/coins/algo.ts @@ -5,9 +5,9 @@ import 'should'; const algoFixtures = require('../../fixtures/algo'); const co = Promise.coroutine; -const Wallet = require('../../../../src/v2/wallet'); +import { Wallet } from '../../../../src/v2/wallet'; const TestV2BitGo = require('../../../lib/test_bitgo'); -const nock = require('nock'); +import * as nock from 'nock'; describe('ALGO:', function() { let bitgo; @@ -68,7 +68,7 @@ describe('ALGO:', function() { const pub = algosdk.Address.decode(keyPair.pub).publicKey; algosdk.NaclWrapper.verify(message, signature, pub).should.equal(true); }); - + describe('Transaction Verification', function() { let basecoin; let wallet; diff --git a/modules/core/test/v2/unit/coins/btc.ts b/modules/core/test/v2/unit/coins/btc.ts index 655002e761..bfb150b1a4 100644 --- a/modules/core/test/v2/unit/coins/btc.ts +++ b/modules/core/test/v2/unit/coins/btc.ts @@ -4,7 +4,7 @@ const co = Promise.coroutine; const { Codes } = require('@bitgo/unspents'); const TestV2BitGo = require('../../../lib/test_bitgo'); -const Wallet = require('../../../../src/v2/wallet'); +import { Wallet } from '../../../../src/v2/wallet'; describe('BTC:', function() { let bitgo; diff --git a/modules/core/test/v2/unit/coins/dash.ts b/modules/core/test/v2/unit/coins/dash.ts index a39a6262d0..3508a6fea2 100644 --- a/modules/core/test/v2/unit/coins/dash.ts +++ b/modules/core/test/v2/unit/coins/dash.ts @@ -4,7 +4,7 @@ import * as Promise from 'bluebird'; const co = Promise.coroutine; import * as _ from 'lodash'; const TestV2BitGo = require('../../../lib/test_bitgo'); -const Wallet = require('../../../../src/v2/wallet'); +import { Wallet } from '../../../../src/v2/wallet'; describe('DASH:', function() { let bitgo; diff --git a/modules/core/test/v2/unit/coins/ltc.ts b/modules/core/test/v2/unit/coins/ltc.ts index 03d3e6b200..2a09e7586f 100644 --- a/modules/core/test/v2/unit/coins/ltc.ts +++ b/modules/core/test/v2/unit/coins/ltc.ts @@ -8,7 +8,7 @@ const bitcoin = require('bitgo-utxo-lib'); const prova = require('prova-lib'); const { Codes } = require('@bitgo/unspents'); const TestV2BitGo = require('../../../lib/test_bitgo'); -const Wallet = require('../../../../src/v2/wallet'); +import { Wallet } from '../../../../src/v2/wallet'; describe('LTC:', function() { let bitgo; diff --git a/modules/core/test/v2/unit/coins/xlm.ts b/modules/core/test/v2/unit/coins/xlm.ts index 3295cba209..84afdd0dc3 100644 --- a/modules/core/test/v2/unit/coins/xlm.ts +++ b/modules/core/test/v2/unit/coins/xlm.ts @@ -4,7 +4,8 @@ import * as stellar from 'stellar-sdk'; import * as Bluebird from 'bluebird'; const co = Bluebird.coroutine; -const Wallet = require('../../../../src/v2/wallet'); +import { Wallet } from '../../../../src/v2/wallet'; + const TestV2BitGo = require('../../../lib/test_bitgo'); import * as nock from 'nock'; diff --git a/modules/core/test/v2/unit/coins/zec.ts b/modules/core/test/v2/unit/coins/zec.ts index 5ec1081964..a2818a297c 100644 --- a/modules/core/test/v2/unit/coins/zec.ts +++ b/modules/core/test/v2/unit/coins/zec.ts @@ -4,7 +4,7 @@ import * as Promise from 'bluebird'; const co = Promise.coroutine; import * as _ from 'lodash'; const TestV2BitGo = require('../../../lib/test_bitgo'); -const Wallet = require('../../../../src/v2/wallet'); +import { Wallet } from '../../../../src/v2/wallet'; const bitGoUtxoLib = require('bitgo-utxo-lib'); describe('ZEC:', function() { diff --git a/modules/core/test/v2/unit/trading/affirmation.ts b/modules/core/test/v2/unit/trading/affirmation.ts index 3946755556..cea45de9d6 100644 --- a/modules/core/test/v2/unit/trading/affirmation.ts +++ b/modules/core/test/v2/unit/trading/affirmation.ts @@ -5,7 +5,7 @@ import * as nock from 'nock'; import fixtures from '../../fixtures/trading/affirmation'; import { AffirmationStatus } from '../../../../src/v2/trading/affirmation'; -const Wallet = require('../../../../src/v2/wallet'); +import { Wallet } from '../../../../src/v2/wallet'; const Enterprise = require('../../../../src/v2/enterprise'); const TestV2BitGo = require('../../../lib/test_bitgo'); const common = require('../../../../src/common'); diff --git a/modules/core/test/v2/unit/trading/payload.ts b/modules/core/test/v2/unit/trading/payload.ts index fd59c5342d..2a9d701a94 100644 --- a/modules/core/test/v2/unit/trading/payload.ts +++ b/modules/core/test/v2/unit/trading/payload.ts @@ -4,7 +4,7 @@ import * as nock from 'nock'; import * as bitcoinMessage from 'bitcoinjs-message'; import { HDNode } from 'bitgo-utxo-lib'; -const Wallet = require('../../../../src/v2/wallet'); +import { Wallet } from '../../../../src/v2/wallet'; const TestV2BitGo = require('../../../lib/test_bitgo'); const common = require('../../../../src/common'); diff --git a/modules/core/test/v2/unit/trading/settlement.ts b/modules/core/test/v2/unit/trading/settlement.ts index df6c87cfd6..a2071ebd57 100644 --- a/modules/core/test/v2/unit/trading/settlement.ts +++ b/modules/core/test/v2/unit/trading/settlement.ts @@ -7,7 +7,7 @@ import { TradeStatus } from '../../../../src/v2/trading/trade'; import { AffirmationStatus } from '../../../../src/v2/trading/affirmation'; import { SettlementStatus } from '../../../../src/v2/trading/settlement'; -const Wallet = require('../../../../src/v2/wallet'); +import { Wallet } from '../../../../src/v2/wallet'; const Enterprise = require('../../../../src/v2/enterprise'); const TestV2BitGo = require('../../../lib/test_bitgo'); const common = require('../../../../src/common'); diff --git a/modules/core/test/v2/unit/trading/tradingPartner.ts b/modules/core/test/v2/unit/trading/tradingPartner.ts index 41c3d0cc92..ee6a045ead 100644 --- a/modules/core/test/v2/unit/trading/tradingPartner.ts +++ b/modules/core/test/v2/unit/trading/tradingPartner.ts @@ -5,7 +5,7 @@ import * as should from 'should'; import fixtures from '../../fixtures/trading/tradingPartner'; import { TradingPartnerStatus } from '../../../../src/v2/trading/tradingPartner'; -const Wallet = require('../../../../src/v2/wallet'); +import { Wallet } from '../../../../src/v2/wallet'; const TestV2BitGo = require('../../../lib/test_bitgo'); describe('Trading Partners', function() { diff --git a/modules/core/test/v2/unit/wallet.ts b/modules/core/test/v2/unit/wallet.ts index 85aef8aabe..d2949c2183 100644 --- a/modules/core/test/v2/unit/wallet.ts +++ b/modules/core/test/v2/unit/wallet.ts @@ -11,7 +11,7 @@ import * as Promise from 'bluebird'; const co = Promise.coroutine; import * as _ from 'lodash'; const bitcoin = require('bitgo-utxo-lib'); -const Wallet = require('../../../src/v2/wallet'); +import { Wallet } from '../../../src/v2/wallet'; const common = require('../../../src/common'); const TestV2BitGo = require('../../lib/test_bitgo'); @@ -766,7 +766,7 @@ describe('V2 Wallet:', function() { })); }); - describe('Wallet Sharing', function () { + describe('Wallet Sharing', function() { it('should share to cold wallet without passing skipKeychain', co(function *() { const userId = '123'; const email = 'shareto@sdktest.com'; diff --git a/modules/core/test/v2/unit/wallets.ts b/modules/core/test/v2/unit/wallets.ts index 0daae221e5..99229af746 100644 --- a/modules/core/test/v2/unit/wallets.ts +++ b/modules/core/test/v2/unit/wallets.ts @@ -35,23 +35,15 @@ describe('V2 Wallets:', function() { }); describe('Add Wallet:', function() { - it('throws on invalid arguments', function() { - try { - // isCustodial flag is not a boolean - wallets.add({ label: 'label', enterprise: 'enterprise', keys: [], m: 2, n: 3, isCustodial: 1 }); - throw new Error(); - } catch (e) { - e.message.should.equal('invalid argument for isCustodial - boolean expected'); - } - - try { - // isCustodial flag is not a boolean - wallets.add({ label: 'label', enterprise: 'enterprise', keys: [], m: 2, n: 3, type: 1 }); - throw new Error(); - } catch (e) { - e.message.should.equal('Expecting parameter string: type but found number'); - } - }); + it('throws on invalid arguments', co(function *() { + // isCustodial flag is not a boolean + yield wallets.add({ label: 'label', enterprise: 'enterprise', keys: [], m: 2, n: 3, isCustodial: 1 }) + .should.be.rejectedWith('invalid argument for isCustodial - boolean expected'); + + // isCustodial flag is not a boolean + yield wallets.add({ label: 'label', enterprise: 'enterprise', keys: [], m: 2, n: 3, type: 1 }) + .should.be.rejectedWith('Expecting parameter string: type but found number'); + })); it('creates a paired custodial wallet', co(function *createPairedCustodialWallet() { nock(bgUrl) diff --git a/yarn.lock b/yarn.lock index 3e9ac48ec1..89d1eba69d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -913,13 +913,18 @@ dependencies: "@types/node" "*" -"@types/create-hmac@1.1.0": +"@types/create-hmac@^1.1.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@types/create-hmac/-/create-hmac-1.1.0.tgz#d5bc34b65aeaf8f7bbd4a064172ab48318763b63" integrity sha512-BNYNdzdhOZZQWCOpwvIll3FSvgo3e55Y2M6s/jOY6TuOCwqt3cLmQsK4tSmJ5fayDot8EG4k3+hcZagfww9JlQ== dependencies: "@types/node" "*" +"@types/debug@^4.1.4": + version "4.1.4" + resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.4.tgz#56eec47706f0fd0b7c694eae2f3172e6b0b769da" + integrity sha512-D9MyoQFI7iP5VdpEyPZyjjqIJ8Y8EDNQFIFVLOmeg1rI1xiHOChyUPMPRUVfqFCerxfE+yS3vMyj37F6IdtOoQ== + "@types/express-serve-static-core@*": version "4.16.4" resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.16.4.tgz#56bb8be4559401d68af4a3624ae9dd3166103e60"