mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-11-24 19:10:22 +01:00
528 lines
18 KiB
JavaScript
528 lines
18 KiB
JavaScript
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,
|
|
} from '../send.constants'
|
|
const {
|
|
addCurrencies,
|
|
subtractCurrencies,
|
|
} = require('../../../conversion-util')
|
|
|
|
const {
|
|
INSUFFICIENT_FUNDS_ERROR,
|
|
INSUFFICIENT_TOKENS_ERROR,
|
|
} = require('../send.constants')
|
|
|
|
const stubs = {
|
|
addCurrencies: sinon.stub().callsFake((a, b, obj) => {
|
|
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, obj) => 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', {
|
|
'../../conversion-util': {
|
|
addCurrencies: stubs.addCurrencies,
|
|
conversionUtil: stubs.conversionUtil,
|
|
conversionGTE: stubs.conversionGTE,
|
|
multiplyCurrencies: stubs.multiplyCurrencies,
|
|
conversionGreaterThan: stubs.conversionGreaterThan,
|
|
conversionLessThan: stubs.conversionLessThan,
|
|
},
|
|
'../../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: 'dec',
|
|
},
|
|
{
|
|
value: 'calc:1610',
|
|
fromNumericBase: 'dec',
|
|
},
|
|
]
|
|
)
|
|
|
|
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',
|
|
}
|
|
|
|
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: undefined, 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')
|
|
})
|
|
})
|
|
})
|