import assert from 'assert' import sinon from 'sinon' import proxyquire from 'proxyquire' import { BASE_TOKEN_GAS_COST, ONE_GWEI_IN_WEI_HEX, SIMPLE_GAS_COST, INSUFFICIENT_FUNDS_ERROR, INSUFFICIENT_TOKENS_ERROR, } from '../send.constants' import { addCurrencies, subtractCurrencies } from '../../../helpers/utils/conversion-util' const stubs = { addCurrencies: sinon.stub().callsFake((a, b) => { if (String(a).match(/^0x.+/)) { a = Number(String(a).slice(2)) } if (String(b).match(/^0x.+/)) { b = Number(String(b).slice(2)) } return a + b }), conversionUtil: sinon.stub().callsFake((val) => parseInt(val, 16)), conversionGTE: sinon.stub().callsFake((obj1, obj2) => obj1.value >= obj2.value), multiplyCurrencies: sinon.stub().callsFake((a, b) => `${a}x${b}`), calcTokenAmount: sinon.stub().callsFake((a, d) => 'calc:' + a + d), rawEncode: sinon.stub().returns([16, 1100]), conversionGreaterThan: sinon.stub().callsFake((obj1, obj2) => obj1.value > obj2.value), conversionLessThan: sinon.stub().callsFake((obj1, obj2) => obj1.value < obj2.value), } const sendUtils = proxyquire('../send.utils.js', { '../../helpers/utils/conversion-util': { addCurrencies: stubs.addCurrencies, conversionUtil: stubs.conversionUtil, conversionGTE: stubs.conversionGTE, multiplyCurrencies: stubs.multiplyCurrencies, conversionGreaterThan: stubs.conversionGreaterThan, conversionLessThan: stubs.conversionLessThan, }, '../../helpers/utils/token-util': { calcTokenAmount: stubs.calcTokenAmount }, 'ethereumjs-abi': { rawEncode: stubs.rawEncode, }, }) const { calcGasTotal, estimateGas, doesAmountErrorRequireUpdate, estimateGasPriceFromRecentBlocks, generateTokenTransferData, getAmountErrorObject, getGasFeeErrorObject, getToAddressForGasUpdate, calcTokenBalance, isBalanceSufficient, isTokenBalanceSufficient, removeLeadingZeroes, } = sendUtils describe('send utils', () => { describe('calcGasTotal()', () => { it('should call multiplyCurrencies with the correct params and return the multiplyCurrencies return', () => { const result = calcGasTotal(12, 15) assert.equal(result, '12x15') const call_ = stubs.multiplyCurrencies.getCall(0).args assert.deepEqual( call_, [12, 15, { toNumericBase: 'hex', multiplicandBase: 16, multiplierBase: 16, } ] ) }) }) describe('doesAmountErrorRequireUpdate()', () => { const config = { 'should return true if balances are different': { balance: 0, prevBalance: 1, expectedResult: true, }, 'should return true if gasTotals are different': { gasTotal: 0, prevGasTotal: 1, expectedResult: true, }, 'should return true if token balances are different': { tokenBalance: 0, prevTokenBalance: 1, selectedToken: 'someToken', expectedResult: true, }, 'should return false if they are all the same': { balance: 1, prevBalance: 1, gasTotal: 1, prevGasTotal: 1, tokenBalance: 1, prevTokenBalance: 1, selectedToken: 'someToken', expectedResult: false, }, } Object.entries(config).map(([description, obj]) => { it(description, () => { assert.equal(doesAmountErrorRequireUpdate(obj), obj.expectedResult) }) }) }) describe('generateTokenTransferData()', () => { it('should return undefined if not passed a selected token', () => { assert.equal(generateTokenTransferData({ toAddress: 'mockAddress', amount: '0xa', selectedToken: false }), undefined) }) it('should call abi.rawEncode with the correct params', () => { stubs.rawEncode.resetHistory() generateTokenTransferData({ toAddress: 'mockAddress', amount: 'ab', selectedToken: true }) assert.deepEqual( stubs.rawEncode.getCall(0).args, [['address', 'uint256'], ['mockAddress', '0xab']] ) }) it('should return encoded token transfer data', () => { assert.equal( generateTokenTransferData({ toAddress: 'mockAddress', amount: '0xa', selectedToken: true }), '0xa9059cbb104c' ) }) }) describe('getAmountErrorObject()', () => { const config = { 'should return insufficientFunds error if isBalanceSufficient returns false': { amount: 15, amountConversionRate: 2, balance: 1, conversionRate: 3, gasTotal: 17, primaryCurrency: 'ABC', expectedResult: { amount: INSUFFICIENT_FUNDS_ERROR }, }, 'should not return insufficientFunds error if selectedToken is truthy': { amount: '0x0', amountConversionRate: 2, balance: 1, conversionRate: 3, gasTotal: 17, primaryCurrency: 'ABC', selectedToken: { symbole: 'DEF', decimals: 0 }, decimals: 0, tokenBalance: 'sometokenbalance', expectedResult: { amount: null }, }, 'should return insufficientTokens error if token is selected and isTokenBalanceSufficient returns false': { amount: '0x10', amountConversionRate: 2, balance: 100, conversionRate: 3, decimals: 10, gasTotal: 17, primaryCurrency: 'ABC', selectedToken: 'someToken', tokenBalance: 123, expectedResult: { amount: INSUFFICIENT_TOKENS_ERROR }, }, } Object.entries(config).map(([description, obj]) => { it(description, () => { assert.deepEqual(getAmountErrorObject(obj), obj.expectedResult) }) }) }) describe('getGasFeeErrorObject()', () => { const config = { 'should return insufficientFunds error if isBalanceSufficient returns false': { amountConversionRate: 2, balance: 16, conversionRate: 3, gasTotal: 17, primaryCurrency: 'ABC', expectedResult: { gasFee: INSUFFICIENT_FUNDS_ERROR }, }, 'should return null error if isBalanceSufficient returns true': { amountConversionRate: 2, balance: 16, conversionRate: 3, gasTotal: 15, primaryCurrency: 'ABC', expectedResult: { gasFee: null }, }, } Object.entries(config).map(([description, obj]) => { it(description, () => { assert.deepEqual(getGasFeeErrorObject(obj), obj.expectedResult) }) }) }) describe('calcTokenBalance()', () => { it('should return the calculated token blance', () => { assert.equal(calcTokenBalance({ selectedToken: { decimals: 11, }, usersToken: { balance: 20, }, }), 'calc:2011') }) }) describe('isBalanceSufficient()', () => { it('should correctly call addCurrencies and return the result of calling conversionGTE', () => { stubs.conversionGTE.resetHistory() const result = isBalanceSufficient({ amount: 15, amountConversionRate: 2, balance: 100, conversionRate: 3, gasTotal: 17, primaryCurrency: 'ABC', }) assert.deepEqual( stubs.addCurrencies.getCall(0).args, [ 15, 17, { aBase: 16, bBase: 16, toNumericBase: 'hex', }, ] ) assert.deepEqual( stubs.conversionGTE.getCall(0).args, [ { value: 100, fromNumericBase: 'hex', fromCurrency: 'ABC', conversionRate: 3, }, { value: 32, fromNumericBase: 'hex', conversionRate: 2, fromCurrency: 'ABC', }, ] ) assert.equal(result, true) }) }) describe('isTokenBalanceSufficient()', () => { it('should correctly call conversionUtil and return the result of calling conversionGTE', () => { stubs.conversionGTE.resetHistory() stubs.conversionUtil.resetHistory() const result = isTokenBalanceSufficient({ amount: '0x10', tokenBalance: 123, decimals: 10, }) assert.deepEqual( stubs.conversionUtil.getCall(0).args, [ '0x10', { fromNumericBase: 'hex', }, ] ) assert.deepEqual( stubs.conversionGTE.getCall(0).args, [ { value: 123, fromNumericBase: 'hex', }, { value: 'calc:1610', }, ] ) assert.equal(result, false) }) }) describe('estimateGas', () => { const baseMockParams = { blockGasLimit: '0x64', selectedAddress: 'mockAddress', to: '0xisContract', estimateGasMethod: sinon.stub().callsFake( ({ to }, cb) => { const err = typeof to === 'string' && to.match(/willFailBecauseOf:/) ? new Error(to.match(/:(.+)$/)[1]) : null const result = { toString: (n) => `0xabc${n}` } return cb(err, result) } ), } const baseExpectedCall = { from: 'mockAddress', gas: '0x64x0.95', to: '0xisContract', value: '0xff', } beforeEach(() => { global.eth = { getCode: sinon.stub().callsFake( (address) => Promise.resolve(address.match(/isContract/) ? 'not-0x' : '0x') ), } }) afterEach(() => { baseMockParams.estimateGasMethod.resetHistory() global.eth.getCode.resetHistory() }) it('should call ethQuery.estimateGas with the expected params', async () => { const result = await sendUtils.estimateGas(baseMockParams) assert.equal(baseMockParams.estimateGasMethod.callCount, 1) assert.deepEqual( baseMockParams.estimateGasMethod.getCall(0).args[0], Object.assign({ gasPrice: undefined, value: undefined }, baseExpectedCall) ) assert.equal(result, '0xabc16') }) it('should call ethQuery.estimateGas with the expected params when initialGasLimitHex is lower than the upperGasLimit', async () => { const result = await estimateGas(Object.assign({}, baseMockParams, { blockGasLimit: '0xbcd' })) assert.equal(baseMockParams.estimateGasMethod.callCount, 1) assert.deepEqual( baseMockParams.estimateGasMethod.getCall(0).args[0], Object.assign({ gasPrice: undefined, value: undefined }, baseExpectedCall, { gas: '0xbcdx0.95' }) ) assert.equal(result, '0xabc16x1.5') }) it('should call ethQuery.estimateGas with a value of 0x0 and the expected data and to if passed a selectedToken', async () => { const result = await estimateGas(Object.assign({ data: 'mockData', selectedToken: { address: 'mockAddress' } }, baseMockParams)) assert.equal(baseMockParams.estimateGasMethod.callCount, 1) assert.deepEqual( baseMockParams.estimateGasMethod.getCall(0).args[0], Object.assign({}, baseExpectedCall, { gasPrice: undefined, value: '0x0', data: '0xa9059cbb104c', to: 'mockAddress', }) ) assert.equal(result, '0xabc16') }) it('should call ethQuery.estimateGas without a recipient if the recipient is empty and data passed', async () => { const data = 'mockData' const to = '' const result = await estimateGas({ ...baseMockParams, data, to }) assert.equal(baseMockParams.estimateGasMethod.callCount, 1) assert.deepEqual( baseMockParams.estimateGasMethod.getCall(0).args[0], { gasPrice: undefined, value: '0xff', data, from: baseExpectedCall.from, gas: baseExpectedCall.gas }, ) assert.equal(result, '0xabc16') }) it(`should return ${SIMPLE_GAS_COST} if ethQuery.getCode does not return '0x'`, async () => { assert.equal(baseMockParams.estimateGasMethod.callCount, 0) const result = await estimateGas(Object.assign({}, baseMockParams, { to: '0x123' })) assert.equal(result, SIMPLE_GAS_COST) }) it(`should return ${SIMPLE_GAS_COST} if not passed a selectedToken or truthy to address`, async () => { assert.equal(baseMockParams.estimateGasMethod.callCount, 0) const result = await estimateGas(Object.assign({}, baseMockParams, { to: null })) assert.equal(result, SIMPLE_GAS_COST) }) it(`should not return ${SIMPLE_GAS_COST} if passed a selectedToken`, async () => { assert.equal(baseMockParams.estimateGasMethod.callCount, 0) const result = await estimateGas(Object.assign({}, baseMockParams, { to: '0x123', selectedToken: { address: '' } })) assert.notEqual(result, SIMPLE_GAS_COST) }) it(`should return ${BASE_TOKEN_GAS_COST} if passed a selectedToken but no to address`, async () => { const result = await estimateGas(Object.assign({}, baseMockParams, { to: null, selectedToken: { address: '' } })) assert.equal(result, BASE_TOKEN_GAS_COST) }) it(`should return the adjusted blockGasLimit if it fails with a 'Transaction execution error.'`, async () => { const result = await estimateGas(Object.assign({}, baseMockParams, { to: 'isContract willFailBecauseOf:Transaction execution error.', })) assert.equal(result, '0x64x0.95') }) it(`should return the adjusted blockGasLimit if it fails with a 'gas required exceeds allowance or always failing transaction.'`, async () => { const result = await estimateGas(Object.assign({}, baseMockParams, { to: 'isContract willFailBecauseOf:gas required exceeds allowance or always failing transaction.', })) assert.equal(result, '0x64x0.95') }) it(`should reject other errors`, async () => { try { await estimateGas(Object.assign({}, baseMockParams, { to: 'isContract willFailBecauseOf:some other error', })) } catch (err) { assert.equal(err.message, 'some other error') } }) }) describe('estimateGasPriceFromRecentBlocks', () => { const ONE_GWEI_IN_WEI_HEX_PLUS_ONE = addCurrencies(ONE_GWEI_IN_WEI_HEX, '0x1', { aBase: 16, bBase: 16, toNumericBase: 'hex', }) const ONE_GWEI_IN_WEI_HEX_PLUS_TWO = addCurrencies(ONE_GWEI_IN_WEI_HEX, '0x2', { aBase: 16, bBase: 16, toNumericBase: 'hex', }) const ONE_GWEI_IN_WEI_HEX_MINUS_ONE = subtractCurrencies(ONE_GWEI_IN_WEI_HEX, '0x1', { aBase: 16, bBase: 16, toNumericBase: 'hex', }) it(`should return ${ONE_GWEI_IN_WEI_HEX} if recentBlocks is falsy`, () => { assert.equal(estimateGasPriceFromRecentBlocks(), ONE_GWEI_IN_WEI_HEX) }) it(`should return ${ONE_GWEI_IN_WEI_HEX} if recentBlocks is empty`, () => { assert.equal(estimateGasPriceFromRecentBlocks([]), ONE_GWEI_IN_WEI_HEX) }) it(`should estimate a block's gasPrice as ${ONE_GWEI_IN_WEI_HEX} if it has no gas prices`, () => { const mockRecentBlocks = [ { gasPrices: null }, { gasPrices: [ ONE_GWEI_IN_WEI_HEX_PLUS_ONE ] }, { gasPrices: [ ONE_GWEI_IN_WEI_HEX_MINUS_ONE ] }, ] assert.equal(estimateGasPriceFromRecentBlocks(mockRecentBlocks), ONE_GWEI_IN_WEI_HEX) }) it(`should estimate a block's gasPrice as ${ONE_GWEI_IN_WEI_HEX} if it has empty gas prices`, () => { const mockRecentBlocks = [ { gasPrices: [] }, { gasPrices: [ ONE_GWEI_IN_WEI_HEX_PLUS_ONE ] }, { gasPrices: [ ONE_GWEI_IN_WEI_HEX_MINUS_ONE ] }, ] assert.equal(estimateGasPriceFromRecentBlocks(mockRecentBlocks), ONE_GWEI_IN_WEI_HEX) }) it(`should return the middle value of all blocks lowest prices`, () => { const mockRecentBlocks = [ { gasPrices: [ ONE_GWEI_IN_WEI_HEX_PLUS_TWO ] }, { gasPrices: [ ONE_GWEI_IN_WEI_HEX_MINUS_ONE ] }, { gasPrices: [ ONE_GWEI_IN_WEI_HEX_PLUS_ONE ] }, ] assert.equal(estimateGasPriceFromRecentBlocks(mockRecentBlocks), ONE_GWEI_IN_WEI_HEX_PLUS_ONE) }) it(`should work if a block has multiple gas prices`, () => { const mockRecentBlocks = [ { gasPrices: [ '0x1', '0x2', '0x3', '0x4', '0x5' ] }, { gasPrices: [ '0x101', '0x100', '0x103', '0x104', '0x102' ] }, { gasPrices: [ '0x150', '0x50', '0x100', '0x200', '0x5' ] }, ] assert.equal(estimateGasPriceFromRecentBlocks(mockRecentBlocks), '0x5') }) }) describe('getToAddressForGasUpdate()', () => { it('should return empty string if all params are undefined or null', () => { assert.equal(getToAddressForGasUpdate(undefined, null), '') }) it('should return the first string that is not defined or null in lower case', () => { assert.equal(getToAddressForGasUpdate('A', null), 'a') assert.equal(getToAddressForGasUpdate(undefined, 'B'), 'b') }) }) describe('removeLeadingZeroes()', () => { it('should remove leading zeroes from int when user types', () => { assert.equal(removeLeadingZeroes('0'), '0') assert.equal(removeLeadingZeroes('1'), '1') assert.equal(removeLeadingZeroes('00'), '0') assert.equal(removeLeadingZeroes('01'), '1') }) it('should remove leading zeroes from int when user copy/paste', () => { assert.equal(removeLeadingZeroes('001'), '1') }) it('should remove leading zeroes from float when user types', () => { assert.equal(removeLeadingZeroes('0.'), '0.') assert.equal(removeLeadingZeroes('0.0'), '0.0') assert.equal(removeLeadingZeroes('0.00'), '0.00') assert.equal(removeLeadingZeroes('0.001'), '0.001') assert.equal(removeLeadingZeroes('0.10'), '0.10') }) it('should remove leading zeroes from float when user copy/paste', () => { assert.equal(removeLeadingZeroes('00.1'), '0.1') }) }) })