From 590795d643b5a7725b467d3554bda5d1ecee1a56 Mon Sep 17 00:00:00 2001 From: Justin Kook Date: Fri, 23 Aug 2019 14:25:45 -0400 Subject: [PATCH] cherry pick eth-bws changes only for MR --- packages/bitcore-wallet-service/package.json | 1 + packages/bitcore-wallet-service/src/config.ts | 10 +- .../src/lib/blockchainexplorer.ts | 4 + .../src/lib/blockchainexplorers/v8.ts | 33 ++- .../src/lib/common/constants.ts | 6 + .../src/lib/common/defaults.ts | 30 ++ .../src/lib/common/utils.ts | 7 +- .../src/lib/fiatrateservice.ts | 2 +- .../src/lib/model/address.ts | 34 ++- .../src/lib/model/addressmanager.ts | 7 + .../src/lib/model/txproposal.ts | 148 ++++++---- .../src/lib/model/wallet.ts | 3 +- .../bitcore-wallet-service/src/lib/server.ts | 273 +++++++++++++----- .../test/integration/fiatrateservice.js | 27 +- 14 files changed, 437 insertions(+), 148 deletions(-) diff --git a/packages/bitcore-wallet-service/package.json b/packages/bitcore-wallet-service/package.json index b7bb1955202..2d264be57f4 100644 --- a/packages/bitcore-wallet-service/package.json +++ b/packages/bitcore-wallet-service/package.json @@ -28,6 +28,7 @@ "bitcore-lib-cash": "^8.6.0", "body-parser": "^1.11.0", "compression": "^1.6.2", + "crypto-wallet-core": "^8.6.0", "email-validator": "^1.0.1", "express": "^4.10.0", "express-rate-limit": "^2.6.0", diff --git a/packages/bitcore-wallet-service/src/config.ts b/packages/bitcore-wallet-service/src/config.ts index 5369e7e1dc8..3ffb06f10ae 100644 --- a/packages/bitcore-wallet-service/src/config.ts +++ b/packages/bitcore-wallet-service/src/config.ts @@ -45,10 +45,16 @@ module.exports = { url: 'https://api.bitcore.io', }, testnet: { - // url: 'http://localhost:3000', url: 'https://api.bitcore.io', }, - + }, + eth: { + livenet: { + url: 'https://api.bitcore.io', + }, + testnet: { + url: 'https://api.bitcore.io', + }, }, }, pushNotificationsOpts: { diff --git a/packages/bitcore-wallet-service/src/lib/blockchainexplorer.ts b/packages/bitcore-wallet-service/src/lib/blockchainexplorer.ts index c0eaff57d69..712efde80af 100644 --- a/packages/bitcore-wallet-service/src/lib/blockchainexplorer.ts +++ b/packages/bitcore-wallet-service/src/lib/blockchainexplorer.ts @@ -16,6 +16,10 @@ const PROVIDERS = { bch: { livenet: 'https://api.bitpay.com', testnet: 'https://api.bitpay.com' + }, + eth: { + livenet: 'https://api.bitpay.com', + testnet: 'https://api.bitpay.com' } } }; diff --git a/packages/bitcore-wallet-service/src/lib/blockchainexplorers/v8.ts b/packages/bitcore-wallet-service/src/lib/blockchainexplorers/v8.ts index 25598864f1e..26cdf3cb69f 100644 --- a/packages/bitcore-wallet-service/src/lib/blockchainexplorers/v8.ts +++ b/packages/bitcore-wallet-service/src/lib/blockchainexplorers/v8.ts @@ -12,7 +12,8 @@ const BCHAddressTranslator = require('../bchaddresstranslator'); const Bitcore = require('bitcore-lib'); const Bitcore_ = { btc: Bitcore, - bch: require('bitcore-lib-cash') + bch: require('bitcore-lib-cash'), + eth: Bitcore }; const config = require('../../config'); const Constants = Common.Constants, @@ -355,6 +356,34 @@ export class V8 { }); } + getTransactionCount(address, cb) { + const url = this.baseUrl + '/address/' + address + '/txs/count'; + console.log('[v8.js.364:url:] CHECKING ADDRESS NONCE', url); + this.request + .get(url, {}) + .then(ret => { + ret = JSON.parse(ret); + return cb(null, ret.nonce); + }) + .catch(err => { + return cb(err); + }); + } + + estimateGas(opts, cb) { + const url = this.baseUrl + '/fee/gas'; + console.log('[v8.js.378:url:] CHECKING GAS LIMIT', url); + this.request + .post(url, { body: opts, json: true }) + .then(gasLimit => { + gasLimit = JSON.parse(gasLimit); + return cb(null, gasLimit); + }) + .catch(err => { + return cb(err); + }); + } + estimateFee(nbBlocks, cb) { nbBlocks = nbBlocks || [1, 2, 6, 24]; const result = {}; @@ -375,7 +404,7 @@ export class V8 { return icb(); } - result[x] = ret.feerate; + result[x] = ret.feerate ? ret.feerate : ret; } catch (e) { log.warn('fee error:', e); } diff --git a/packages/bitcore-wallet-service/src/lib/common/constants.ts b/packages/bitcore-wallet-service/src/lib/common/constants.ts index d396a32a0d2..7ea9c82861b 100644 --- a/packages/bitcore-wallet-service/src/lib/common/constants.ts +++ b/packages/bitcore-wallet-service/src/lib/common/constants.ts @@ -2,6 +2,12 @@ module.exports = { COINS: { + BTC: 'btc', + BCH: 'bch', + ETH: 'eth' + }, + + UTXO_COINS: { BTC: 'btc', BCH: 'bch' }, diff --git a/packages/bitcore-wallet-service/src/lib/common/defaults.ts b/packages/bitcore-wallet-service/src/lib/common/defaults.ts index bd6ea15cc92..2f49dee3ccd 100644 --- a/packages/bitcore-wallet-service/src/lib/common/defaults.ts +++ b/packages/bitcore-wallet-service/src/lib/common/defaults.ts @@ -7,6 +7,9 @@ module.exports = { MAX_TX_FEE: 0.1 * 1e8, MAX_TX_SIZE_IN_KB: 100, + // ETH + DEFAULT_GAS_LIMIT: 21000, + MAX_KEYS: 100, // Time after which a tx proposal can be erased by any copayer. in seconds @@ -58,6 +61,33 @@ module.exports = { nbBlocks: 2, defaultValue: 2000 } + ], + eth: [ + { + name: 'urgent', + nbBlocks: 10, // < 2 min + defaultValue: 3000000000 + }, + { + name: 'priority', + nbBlocks: 15, // 3 min + defaultValue: 2000000000 + }, + { + name: 'normal', + nbBlocks: 25, // 5 min + defaultValue: 1000000000 + }, + { + name: 'economy', + nbBlocks: 50, // 10 minutes + defaultValue: 1000000000 + }, + { + name: 'superEconomy', + nbBlocks: 75, // 15 minutes + defaultValue: 1000000000 + } ] }, diff --git a/packages/bitcore-wallet-service/src/lib/common/utils.ts b/packages/bitcore-wallet-service/src/lib/common/utils.ts index 90aad046cbc..dcb7befe342 100644 --- a/packages/bitcore-wallet-service/src/lib/common/utils.ts +++ b/packages/bitcore-wallet-service/src/lib/common/utils.ts @@ -113,7 +113,12 @@ export class Utils { toSatoshis: 100000000, maxDecimals: 6, minDecimals: 2 - } + }, + eth: { + toSatoshis: 1e18, + maxDecimals: 6, + minDecimals: 2 + }, }; $.shouldBeNumber(satoshis); diff --git a/packages/bitcore-wallet-service/src/lib/fiatrateservice.ts b/packages/bitcore-wallet-service/src/lib/fiatrateservice.ts index cb58be29fbb..65d837ea631 100644 --- a/packages/bitcore-wallet-service/src/lib/fiatrateservice.ts +++ b/packages/bitcore-wallet-service/src/lib/fiatrateservice.ts @@ -74,7 +74,7 @@ export class FiatRateService { _fetch(cb?) { cb = cb || function() { }; - const coins = ['btc', 'bch']; + const coins = ['btc', 'bch', 'eth']; const provider = this.providers[0]; // async.each(this.providers, (provider, next) => { diff --git a/packages/bitcore-wallet-service/src/lib/model/address.ts b/packages/bitcore-wallet-service/src/lib/model/address.ts index 88098c1893a..d70abf0a142 100644 --- a/packages/bitcore-wallet-service/src/lib/model/address.ts +++ b/packages/bitcore-wallet-service/src/lib/model/address.ts @@ -1,4 +1,6 @@ +import { Deriver } from 'crypto-wallet-core'; import _ from 'lodash'; +import { AddressManager } from './addressmanager'; const $ = require('preconditions').singleton(); const Common = require('../common'); @@ -56,8 +58,10 @@ export class Address { x.publicKeys = opts.publicKeys; x.coin = opts.coin; x.network = Address.Bitcore[opts.coin] - .Address(x.address) - .toObject().network; + ? Address.Bitcore[opts.coin] + .Address(x.address) + .toObject().network + : opts.network; x.type = opts.type || Constants.SCRIPT_TYPES.P2SH; x.hasActivity = undefined; x.beRegistered = null; @@ -96,7 +100,9 @@ export class Address { ); const publicKeys = _.map(publicKeyRing, (item) => { - const xpub = new Address.Bitcore[coin].HDPublicKey(item.xPubKey); + const xpub = Address.Bitcore[coin] + ? new Address.Bitcore[coin].HDPublicKey(item.xPubKey) + : new Address.Bitcore.btc.HDPublicKey(item.xPubKey); return xpub.deriveChild(path).publicKey; }); @@ -111,10 +117,23 @@ export class Address { break; case Constants.SCRIPT_TYPES.P2PKH: $.checkState(_.isArray(publicKeys) && publicKeys.length == 1); - bitcoreAddress = Address.Bitcore[coin].Address.fromPublicKey( - publicKeys[0], - network - ); + + if (Address.Bitcore[coin]) { + bitcoreAddress = Address.Bitcore[coin].Address.fromPublicKey( + publicKeys[0], + network + ); + } else { + const { addressIndex, isChange } = new AddressManager().parseDerivationPath(path); + const [{ xPubKey }] = publicKeyRing; + bitcoreAddress = Deriver.deriveAddress( + coin.toUpperCase(), + network, + xPubKey, + addressIndex, + isChange + ); + } break; } @@ -155,6 +174,7 @@ export class Address { return Address.create( _.extend(raw, { coin, + network, walletId, type: scriptType, isChange diff --git a/packages/bitcore-wallet-service/src/lib/model/addressmanager.ts b/packages/bitcore-wallet-service/src/lib/model/addressmanager.ts index 927c52ee128..031bcf20dbc 100644 --- a/packages/bitcore-wallet-service/src/lib/model/addressmanager.ts +++ b/packages/bitcore-wallet-service/src/lib/model/addressmanager.ts @@ -145,4 +145,11 @@ export class AddressManager { const ret = this.skippedPaths.pop(); return ret; } + + parseDerivationPath(path) { + const pathIndex = /m\/([0-9]*)\/([0-9]*)/; + const [_input, changeIndex, addressIndex] = path.match(pathIndex); + const isChange = changeIndex > 0; + return { _input, addressIndex, isChange }; + } } diff --git a/packages/bitcore-wallet-service/src/lib/model/txproposal.ts b/packages/bitcore-wallet-service/src/lib/model/txproposal.ts index 68e3ffe7965..1a5c977f748 100644 --- a/packages/bitcore-wallet-service/src/lib/model/txproposal.ts +++ b/packages/bitcore-wallet-service/src/lib/model/txproposal.ts @@ -1,4 +1,5 @@ import _ from 'lodash'; +import { Transactions } from 'crypto-wallet-core'; import { TxProposalLegacy } from './txproposal_legacy'; import { TxProposalAction } from './txproposalaction'; @@ -10,7 +11,8 @@ log.disableColor(); const Bitcore = { btc: require('bitcore-lib'), - bch: require('bitcore-lib-cash') + bch: require('bitcore-lib-cash'), + eth: require('bitcore-lib') }; const Common = require('../common'); @@ -30,6 +32,7 @@ export interface ITxProposal { network: string; message: string; payProUrl: string; + from: string; changeAddress: string; inputs: any[]; outputs: Array<{ @@ -60,6 +63,10 @@ export interface ITxProposal { proposalSignaturePubKey: string; proposalSignaturePubKeySig: string; lowFees: boolean; + nonce?: number; + gasLimit?: number; + gasPrice?: number; + data?: string; } export class TxProposal { @@ -74,6 +81,7 @@ export class TxProposal { network: string; message: string; payProUrl: string; + from: string; changeAddress: any; inputs: any[]; outputs: Array<{ @@ -104,6 +112,10 @@ export class TxProposal { proposalSignaturePubKey: string; proposalSignaturePubKeySig: string; raw?: any; + nonce?: number; + gasLimit?: number; + gasPrice?: number; + data?: string; static create(opts) { opts = opts || {}; @@ -155,11 +167,17 @@ export class TxProposal { x.customData = opts.customData; - x.amount = x.getTotalAmount(); + x.amount = opts.amount ? opts.amount : x.getTotalAmount(); x.setInputs(opts.inputs); x.fee = opts.fee; + x.gasLimit = opts.gasLimit; + x.gasPrice = opts.gasPrice; + x.from = opts.from; + x.nonce = opts.nonce; + x.data = opts.data; + return x; } @@ -205,6 +223,13 @@ export class TxProposal { x.proposalSignature = obj.proposalSignature; x.proposalSignaturePubKey = obj.proposalSignaturePubKey; x.proposalSignaturePubKeySig = obj.proposalSignaturePubKeySig; + + x.gasLimit = obj.gasLimit; + x.gasPrice = obj.gasPrice; + x.from = obj.from; + x.nonce = obj.nonce; + x.data = obj.data; + if (x.status == 'broadcasted') { x.raw = obj.raw; } @@ -240,68 +265,81 @@ export class TxProposal { Utils.checkValueInCollection(this.addressType, Constants.SCRIPT_TYPES) ); - switch (this.addressType) { - case Constants.SCRIPT_TYPES.P2SH: - _.each(this.inputs, (i) => { - $.checkState(i.publicKeys, 'Inputs should include public keys'); - t.from(i, i.publicKeys, this.requiredSignatures); - }); - break; - case Constants.SCRIPT_TYPES.P2PKH: - t.from(this.inputs); - break; - } + if (!Constants.UTXO_COINS[this.coin.toUpperCase()]) { + const rawTx = Transactions.create({ + chain: this.coin.toUpperCase(), + recipients: [{ address: this.outputs[0].toAddress, amount: this.amount}], + from: this.from, + nonce: this.nonce, + fee: this.gasPrice, + data: this.data, + gasLimit: this.gasLimit + }); + return { uncheckedSerialize: () => rawTx }; + } else { + switch (this.addressType) { + case Constants.SCRIPT_TYPES.P2SH: + _.each(this.inputs, (i) => { + $.checkState(i.publicKeys, 'Inputs should include public keys'); + t.from(i, i.publicKeys, this.requiredSignatures); + }); + break; + case Constants.SCRIPT_TYPES.P2PKH: + t.from(this.inputs); + break; + } - _.each(this.outputs, (o) => { - $.checkState( - o.script || o.toAddress, - 'Output should have either toAddress or script specified' - ); - if (o.script) { - t.addOutput( - new Bitcore[this.coin].Transaction.Output({ - script: o.script, - satoshis: o.amount - }) + _.each(this.outputs, (o) => { + $.checkState( + o.script || o.toAddress, + 'Output should have either toAddress or script specified' ); - } else { - t.to(o.toAddress, o.amount); - } - }); + if (o.script) { + t.addOutput( + new Bitcore[this.coin].Transaction.Output({ + script: o.script, + satoshis: o.amount + }) + ); + } else { + t.to(o.toAddress, o.amount); + } + }); - t.fee(this.fee); + t.fee(this.fee); - if (this.changeAddress) { - t.change(this.changeAddress.address); - } + if (this.changeAddress) { + t.change(this.changeAddress.address); + } - // Shuffle outputs for improved privacy - if (t.outputs.length > 1) { - const outputOrder = _.reject(this.outputOrder, (order: number) => { - return order >= t.outputs.length; - }); - $.checkState(t.outputs.length == outputOrder.length); - t.sortOutputs((outputs) => { - return _.map(outputOrder, (i) => { - return outputs[i]; + // Shuffle outputs for improved privacy + if (t.outputs.length > 1) { + const outputOrder = _.reject(this.outputOrder, (order: number) => { + return order >= t.outputs.length; }); - }); - } + $.checkState(t.outputs.length == outputOrder.length); + t.sortOutputs((outputs) => { + return _.map(outputOrder, (i) => { + return outputs[i]; + }); + }); + } - // Validate actual inputs vs outputs independently of Bitcore - const totalInputs = _.sumBy(t.inputs, 'output.satoshis'); - const totalOutputs = _.sumBy(t.outputs, 'satoshis'); + // Validate actual inputs vs outputs independently of Bitcore + const totalInputs = _.sumBy(t.inputs, 'output.satoshis'); + const totalOutputs = _.sumBy(t.outputs, 'satoshis'); - $.checkState( - totalInputs > 0 && totalOutputs > 0 && totalInputs >= totalOutputs, - 'not-enought-inputs' - ); - $.checkState( - totalInputs - totalOutputs <= Defaults.MAX_TX_FEE, - 'fee-too-high' - ); + $.checkState( + totalInputs > 0 && totalOutputs > 0 && totalInputs >= totalOutputs, + 'not-enought-inputs' + ); + $.checkState( + totalInputs - totalOutputs <= Defaults.MAX_TX_FEE, + 'fee-too-high' + ); - return t; + return t; + } } _getCurrentSignatures() { diff --git a/packages/bitcore-wallet-service/src/lib/model/wallet.ts b/packages/bitcore-wallet-service/src/lib/model/wallet.ts index 9c8eedf0cfe..27a1090b8dc 100644 --- a/packages/bitcore-wallet-service/src/lib/model/wallet.ts +++ b/packages/bitcore-wallet-service/src/lib/model/wallet.ts @@ -13,7 +13,8 @@ const Constants = Common.Constants, Utils = Common.Utils; const Bitcore = { btc: require('bitcore-lib'), - bch: require('bitcore-lib-cash') + bch: require('bitcore-lib-cash'), + eth: require('bitcore-lib') }; export interface IWallet { diff --git a/packages/bitcore-wallet-service/src/lib/server.ts b/packages/bitcore-wallet-service/src/lib/server.ts index 023f5f821e7..fd407ccb53d 100644 --- a/packages/bitcore-wallet-service/src/lib/server.ts +++ b/packages/bitcore-wallet-service/src/lib/server.ts @@ -25,7 +25,8 @@ const EmailValidator = require('email-validator'); const Bitcore = require('bitcore-lib'); const Bitcore_ = { btc: Bitcore, - bch: require('bitcore-lib-cash') + bch: require('bitcore-lib-cash'), + eth: Bitcore }; const Common = require('./common'); @@ -1698,6 +1699,26 @@ export class WalletService { return balance; } + /** + * Converts Bitcore Balance Response. + * @param {Object} bitcoreBalance - { unconfirmed, confirmed, balance } + * @param {Number} locked - Sum of txp.amount + * @returns {Object} balance - Total amount & locked amount. + */ + _convertBitcoreBalance(bitcoreBalance, locked) { + const { unconfirmed, confirmed, balance } = bitcoreBalance; + const convertedBalance = { + totalAmount: balance, + totalConfirmedAmount: confirmed, + lockedAmount: locked, + lockedConfirmedAmount: confirmed - locked, + availableAmount: balance - unconfirmed, + availableConfirmedAmount: confirmed - unconfirmed + }; + + return convertedBalance; + } + /** * Get wallet balance. * @param {Object} opts @@ -1734,38 +1755,52 @@ export class WalletService { this.syncWallet(wallet, err => { if (err) return cb(err); - this._getUtxosForCurrentWallet( - { - coin: opts.coin, - addresses: opts.addresses - }, - (err, utxos) => { - if (err) return cb(err); - - const balance = { ...this._totalizeUtxos(utxos), byAddress: [] }; - - // Compute balance by address - const byAddress = {}; - _.each(_.keyBy(_.sortBy(utxos, 'address'), 'address'), ( - value, - key - ) => { - byAddress[key] = { - address: key, - path: value.path, - amount: 0 - }; + if (!Constants.UTXO_COINS[wallet.coin.toUpperCase()]) { + bc.getBalance(wallet, (err, balance) => { + if (err) { + return cb(err); + } + this.getPendingTxs({}, (err, txps) => { + if (err) return cb(err); + const lockedSum = _.sumBy(txps, 'amount'); + const convertedBalance = this._convertBitcoreBalance(balance, lockedSum); + return cb(null, convertedBalance); }); + }); + } else { + this._getUtxosForCurrentWallet( + { + coin: opts.coin, + addresses: opts.addresses + }, + (err, utxos) => { + if (err) return cb(err); - _.each(utxos, (utxo) => { - byAddress[utxo.address].amount += utxo.satoshis; - }); + const balance = { ...this._totalizeUtxos(utxos), byAddress: [] }; + + // Compute balance by address + const byAddress = {}; + _.each(_.keyBy(_.sortBy(utxos, 'address'), 'address'), ( + value, + key + ) => { + byAddress[key] = { + address: key, + path: value.path, + amount: 0 + }; + }); - balance.byAddress = _.values(byAddress); + _.each(utxos, (utxo) => { + byAddress[utxo.address].amount += utxo.satoshis; + }); - return cb(null, balance); - } - ); + balance.byAddress = _.values(byAddress); + + return cb(null, balance); + } + ); + } }); }); } @@ -1918,8 +1953,11 @@ export class WalletService { ? +result[p] : -1; if (feePerKb < 0) failed.push(p); - - return [p, Utils.strip(feePerKb * 1e8)]; + if (!Constants.UTXO_COINS[coin.toUpperCase()]) { + return [p, feePerKb]; + } else { + return [p, Utils.strip(feePerKb * 1e8)]; + } }) ); @@ -2574,11 +2612,15 @@ export class WalletService { }, (next) => { if (opts.validateOutputs === false) return next(); - const validationError = this._validateOutputs(opts, wallet, next); - if (validationError) { - return next(validationError); + if (!Constants.UTXO_COINS[wallet.coin.toUpperCase()]) { + next(); + } else { + const validationError = this._validateOutputs(opts, wallet, next); + if (validationError) { + return next(validationError); + } + next(); } - next(); }, (next) => { // check outputs are on 'copay' format for BCH @@ -2641,6 +2683,30 @@ export class WalletService { ); } + _getTransactionCount(wallet, address, cb) { + const bc = this._getBlockchainExplorer(wallet.coin, wallet.network); + if (!bc) return cb(new Error('Could not get blockchain explorer instance')); + bc.getTransactionCount(address, (err, nonce) => { + if (err) { + this.logw('Error estimating nonce', err); + return cb(err); + } + return cb(null, nonce); + }); + } + + _estimateGas(wallet, opts, cb) { + const bc = this._getBlockchainExplorer(wallet.coin, wallet.network); + if (!bc) return cb(new Error('Could not get blockchain explorer instance')); + bc.estimateGas(opts, (err, gasLimit) => { + if (err) { + this.logw('Error estimating gas limit', err); + return cb(err); + } + return cb(null, gasLimit); + }); + } + /** * Creates a new transaction proposal. * @param {Object} opts @@ -2703,7 +2769,7 @@ export class WalletService { this._runLocked( cb, (cb) => { - let changeAddress, feePerKb; + let changeAddress, feePerKb, gasPrice, gasLimit; this.getWallet({}, (err, wallet) => { if (err) return cb(err); if (!wallet.isComplete()) return cb(Errors.WALLET_NOT_COMPLETE); @@ -2728,6 +2794,9 @@ export class WalletService { }, (next) => { if (opts.sendMax) return next(); + if (!Constants.UTXO_COINS[wallet.coin.toUpperCase()]) { + return next(); + } getChangeAddress(wallet, (err, address, isNew) => { if (err) return next(err); changeAddress = address; @@ -2740,7 +2809,24 @@ export class WalletService { return next(); this._getFeePerKb(wallet, opts, (err, fee) => { feePerKb = fee; - next(); + if (!Constants.UTXO_COINS[wallet.coin.toUpperCase()]) { + gasPrice = fee; + const { from, data } = opts; + this._estimateGas(wallet, { + from, + to: opts.outputs[0].toAddress, + value: opts.outputs[0].amount, + data, + gasPrice + }, + (err, gas) => { + gasLimit = gas || Defaults.DEFAULT_GAS_LIMIT; + opts.fee = fee * gasLimit; + return next(); + }); + } else { + next(); + } }); }, (next) => { @@ -2752,6 +2838,7 @@ export class WalletService { network: wallet.network, outputs: opts.outputs, message: opts.message, + from: opts.from, changeAddress, feeLevel: opts.feeLevel, feePerKb, @@ -2766,15 +2853,41 @@ export class WalletService { fee: opts.inputs && !_.isNumber(opts.feePerKb) ? opts.fee - : null, - noShuffleOutputs: opts.noShuffleOutputs + : !Constants.UTXO_COINS[wallet.coin.toUpperCase()] + ? opts.fee + : null, + noShuffleOutputs: opts.noShuffleOutputs, + data: opts.data, + gasPrice, + gasLimit }; - txp = TxProposal.create(txOpts); next(); }, (next) => { - this._selectTxInputs(txp, opts.utxosToExclude, next); + if (!Constants.UTXO_COINS[wallet.coin.toUpperCase()]) { + this.getBalance({ wallet }, (err, balance) => { + const { totalAmount, availableAmount } = balance; + if (totalAmount < txp.getTotalAmount()) { + return cb(Errors.INSUFFICIENT_FUNDS); + } else if (availableAmount < txp.getTotalAmount()) { + return cb(Errors.LOCKED_FUNDS); + } else { + return next(); + } + }); + } else { + this._selectTxInputs(txp, opts.utxosToExclude, next); + } + }, + (next) => { + if (Constants.UTXO_COINS[wallet.coin.toUpperCase()]) { + return next(); + } + this._getTransactionCount(wallet, txp.from, (err, nonce) => { + txp.nonce = nonce; + return next(); + }); }, (next) => { if (!changeAddress || wallet.singleAddress || opts.dryRun || opts.changeAddress) @@ -2868,44 +2981,53 @@ export class WalletService { } // Verify UTXOs are still available + if (Constants.UTXO_COINS[txp.coin.toUpperCase()]) { + log.debug('Rechecking UTXOs availability for publishTx'); - log.debug('Rechecking UTXOs availability for publishTx'); - - this._getUtxosForCurrentWallet( - { - addresses: txp.inputs - }, - (err, utxos) => { - if (err) return cb(err); + this._getUtxosForCurrentWallet( + { + addresses: txp.inputs + }, + (err, utxos) => { + if (err) return cb(err); - const txpInputs = _.map(txp.inputs, utxoKey); - const utxosIndex = _.keyBy(utxos, utxoKey); - const unavailable = _.some(txpInputs, (i) => { - const utxo = utxosIndex[i]; - return !utxo || utxo.locked; - }); + const txpInputs = _.map(txp.inputs, utxoKey); + const utxosIndex = _.keyBy(utxos, utxoKey); + const unavailable = _.some(txpInputs, (i) => { + const utxo = utxosIndex[i]; + return !utxo || utxo.locked; + }); - if (unavailable) return cb(Errors.UNAVAILABLE_UTXOS); + if (unavailable) return cb(Errors.UNAVAILABLE_UTXOS); - txp.status = 'pending'; - this.storage.storeTx(this.walletId, txp, (err) => { - if (err) return cb(err); + txp.status = 'pending'; + this.storage.storeTx(this.walletId, txp, (err) => { + if (err) return cb(err); - this._notifyTxProposalAction('NewTxProposal', txp, () => { - if (opts.noCashAddr && txp.coin == 'bch') { - if (txp.changeAddress) { - txp.changeAddress.address = BCHAddressTranslator.translate( - txp.changeAddress.address, - 'copay' - ); + this._notifyTxProposalAction('NewTxProposal', txp, () => { + if (opts.noCashAddr && txp.coin == 'bch') { + if (txp.changeAddress) { + txp.changeAddress.address = BCHAddressTranslator.translate( + txp.changeAddress.address, + 'copay' + ); + } } - } - - return cb(null, txp); + return cb(null, txp); + }); }); + } + ); + } else { + txp.status = 'pending'; + this.storage.storeTx(this.walletId, txp, (err) => { + if (err) return cb(err); + + this._notifyTxProposalAction('NewTxProposal', txp, () => { + return cb(null, txp); }); - } - ); + }); + } }); }); }); @@ -3586,14 +3708,14 @@ export class WalletService { switch (tx.category) { case 'send': ret.action = 'sent'; - ret.amount = Math.abs(_.sumBy(tx.outputs, 'amount')); + ret.amount = Math.abs(_.sumBy(tx.outputs, 'amount')) || Math.abs(tx.satoshis); ret.addressTo = tx.outputs ? tx.outputs[0].address : null; ret.outputs = tx.outputs; break; case 'receive': ret.action = 'received'; ret.outputs = tx.outputs; - ret.amount = Math.abs(_.sumBy(tx.outputs, 'amount')); + ret.amount = Math.abs(_.sumBy(tx.outputs, 'amount')) || Math.abs(tx.satoshis); ret.dust = ret.amount < dustThreshold; break; case 'move': @@ -4053,8 +4175,9 @@ export class WalletService { bc.getTransactions(wallet, startBlock, (err, txs) => { if (err) return cb(err); - - const dustThreshold = Bitcore_[wallet.coin].Transaction.DUST_AMOUNT; + const dustThreshold = Constants.UTXO_COINS[wallet.coin.toUpperCase()] + ? Bitcore_[wallet.coin].Transaction.DUST_AMOUNT + : 0; this._normalizeTxHistory(wallet.id, txs, dustThreshold, bcHeight, ( err, inTxs: any[] diff --git a/packages/bitcore-wallet-service/test/integration/fiatrateservice.js b/packages/bitcore-wallet-service/test/integration/fiatrateservice.js index e9e1477c885..b5cab532363 100644 --- a/packages/bitcore-wallet-service/test/integration/fiatrateservice.js +++ b/packages/bitcore-wallet-service/test/integration/fiatrateservice.js @@ -228,6 +228,13 @@ describe('Fiat rate service', function() { code: 'EUR', rate: 120, }]; + var eth = [{ + code: 'USD', + rate: 121, + }, { + code: 'EUR', + rate: 121, + }]; request.get.withArgs({ url: 'https://bitpay.com/api/rates/BTC', @@ -237,6 +244,10 @@ describe('Fiat rate service', function() { url: 'https://bitpay.com/api/rates/BCH', json: true }).yields(null, null, bch); + request.get.withArgs({ + url: 'https://bitpay.com/api/rates/ETH', + json: true + }).yields(null, null, eth); service._fetch(function(err) { should.not.exist(err); @@ -254,13 +265,21 @@ describe('Fiat rate service', function() { res.fetchedOn.should.equal(100); res.rate.should.equal(120.00); service.getRate({ - code: 'EUR' + code: 'USD', + coin: 'eth', }, function(err, res) { should.not.exist(err); res.fetchedOn.should.equal(100); - res.rate.should.equal(234.56); - clock.restore(); - done(); + res.rate.should.equal(121.00); + service.getRate({ + code: 'EUR' + }, function(err, res) { + should.not.exist(err); + res.fetchedOn.should.equal(100); + res.rate.should.equal(234.56); + clock.restore(); + done(); + }); }); }); });