import assert from 'assert' import sinon from 'sinon' import proxyquire from 'proxyquire' import { BASE_TOKEN_GAS_COST, SIMPLE_GAS_COST, INSUFFICIENT_FUNDS_ERROR, INSUFFICIENT_TOKENS_ERROR, } from '../send.constants' 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, generateTokenTransferData, getAmountErrorObject, getGasFeeErrorObject, getToAddressForGasUpdate, calcTokenBalance, isBalanceSufficient, isTokenBalanceSufficient, removeLeadingZeroes, } = sendUtils describe('send utils', function () { describe('calcGasTotal()', function () { it('should call multiplyCurrencies with the correct params and return the multiplyCurrencies return', function () { 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()', function () { 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, sendToken: { address: '0x0' }, expectedResult: true, }, 'should return false if they are all the same': { balance: 1, prevBalance: 1, gasTotal: 1, prevGasTotal: 1, tokenBalance: 1, prevTokenBalance: 1, sendToken: { address: '0x0' }, expectedResult: false, }, } Object.entries(config).forEach(([description, obj]) => { it(description, function () { assert.equal(doesAmountErrorRequireUpdate(obj), obj.expectedResult) }) }) }) describe('generateTokenTransferData()', function () { it('should return undefined if not passed a send token', function () { assert.equal(generateTokenTransferData({ toAddress: 'mockAddress', amount: '0xa', sendToken: undefined }), undefined) }) it('should call abi.rawEncode with the correct params', function () { stubs.rawEncode.resetHistory() generateTokenTransferData({ toAddress: 'mockAddress', amount: 'ab', sendToken: { address: '0x0' } }) assert.deepEqual( stubs.rawEncode.getCall(0).args, [['address', 'uint256'], ['mockAddress', '0xab']], ) }) it('should return encoded token transfer data', function () { assert.equal( generateTokenTransferData({ toAddress: 'mockAddress', amount: '0xa', sendToken: { address: '0x0' } }), '0xa9059cbb104c', ) }) }) describe('getAmountErrorObject()', function () { const config = { 'should return insufficientFunds error if isBalanceSufficient returns false': { amount: 15, balance: 1, conversionRate: 3, gasTotal: 17, primaryCurrency: 'ABC', expectedResult: { amount: INSUFFICIENT_FUNDS_ERROR }, }, 'should not return insufficientFunds error if sendToken is truthy': { amount: '0x0', balance: 1, conversionRate: 3, gasTotal: 17, primaryCurrency: 'ABC', sendToken: { address: '0x0', symbol: 'DEF', decimals: 0 }, decimals: 0, tokenBalance: 'sometokenbalance', expectedResult: { amount: null }, }, 'should return insufficientTokens error if token is selected and isTokenBalanceSufficient returns false': { amount: '0x10', balance: 100, conversionRate: 3, decimals: 10, gasTotal: 17, primaryCurrency: 'ABC', sendToken: { address: '0x0' }, tokenBalance: 123, expectedResult: { amount: INSUFFICIENT_TOKENS_ERROR }, }, } Object.entries(config).forEach(([description, obj]) => { it(description, function () { assert.deepEqual(getAmountErrorObject(obj), obj.expectedResult) }) }) }) describe('getGasFeeErrorObject()', function () { const config = { 'should return insufficientFunds error if isBalanceSufficient returns false': { balance: 16, conversionRate: 3, gasTotal: 17, primaryCurrency: 'ABC', expectedResult: { gasFee: INSUFFICIENT_FUNDS_ERROR }, }, 'should return null error if isBalanceSufficient returns true': { balance: 16, conversionRate: 3, gasTotal: 15, primaryCurrency: 'ABC', expectedResult: { gasFee: null }, }, } Object.entries(config).forEach(([description, obj]) => { it(description, function () { assert.deepEqual(getGasFeeErrorObject(obj), obj.expectedResult) }) }) }) describe('calcTokenBalance()', function () { it('should return the calculated token balance', function () { assert.equal(calcTokenBalance({ sendToken: { address: '0x0', decimals: 11, }, usersToken: { balance: 20, }, }), 'calc:2011') }) }) describe('isBalanceSufficient()', function () { it('should correctly call addCurrencies and return the result of calling conversionGTE', function () { stubs.conversionGTE.resetHistory() const result = isBalanceSufficient({ amount: 15, 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: 3, fromCurrency: 'ABC', }, ], ) assert.equal(result, true) }) }) describe('isTokenBalanceSufficient()', function () { it('should correctly call conversionUtil and return the result of calling conversionGTE', function () { 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', function () { const baseMockParams = { blockGasLimit: '0x64', selectedAddress: 'mockAddress', to: '0xisContract', estimateGasMethod: sinon.stub().callsFake( ({ to }) => { if (typeof to === 'string' && to.match(/willFailBecauseOf:/)) { throw new Error(to.match(/:(.+)$/)[1]) } return { toString: (n) => `0xabc${n}` } }, ), } const baseExpectedCall = { from: 'mockAddress', gas: '0x64x0.95', to: '0xisContract', value: '0xff', } beforeEach(function () { global.eth = { getCode: sinon.stub().callsFake( (address) => Promise.resolve(address.match(/isContract/) ? 'not-0x' : '0x'), ), } }) afterEach(function () { baseMockParams.estimateGasMethod.resetHistory() global.eth.getCode.resetHistory() }) it('should call ethQuery.estimateGas with the expected params', async function () { 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 function () { 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 sendToken', async function () { const result = await estimateGas(Object.assign({ data: 'mockData', sendToken: { 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 function () { 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 function () { 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 sendToken or truthy to address`, async function () { 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 sendToken`, async function () { assert.equal(baseMockParams.estimateGasMethod.callCount, 0) const result = await estimateGas(Object.assign({}, baseMockParams, { to: '0x123', sendToken: { address: '0x0' } })) assert.notEqual(result, SIMPLE_GAS_COST) }) it(`should return ${BASE_TOKEN_GAS_COST} if passed a sendToken but no to address`, async function () { const result = await estimateGas(Object.assign({}, baseMockParams, { to: null, sendToken: { address: '0x0' } })) assert.equal(result, BASE_TOKEN_GAS_COST) }) it(`should return the adjusted blockGasLimit if it fails with a 'Transaction execution error.'`, async function () { 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 function () { 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 function () { try { await estimateGas(Object.assign({}, baseMockParams, { to: 'isContract willFailBecauseOf:some other error', })) } catch (err) { assert.equal(err.message, 'some other error') } }) }) describe('getToAddressForGasUpdate()', function () { it('should return empty string if all params are undefined or null', function () { assert.equal(getToAddressForGasUpdate(undefined, null), '') }) it('should return the first string that is not defined or null in lower case', function () { assert.equal(getToAddressForGasUpdate('A', null), 'a') assert.equal(getToAddressForGasUpdate(undefined, 'B'), 'b') }) }) describe('removeLeadingZeroes()', function () { it('should remove leading zeroes from int when user types', function () { 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', function () { assert.equal(removeLeadingZeroes('001'), '1') }) it('should remove leading zeroes from float when user types', function () { 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', function () { assert.equal(removeLeadingZeroes('00.1'), '0.1') }) }) })