Merge pull request #2094 from MetaMask/AddBalanceController
Calculate balance based on pending txsfeature/default_network_editable
commit
b1daa5ae26
@ -0,0 +1,61 @@ |
||||
const ObservableStore = require('obs-store') |
||||
const PendingBalanceCalculator = require('../lib/pending-balance-calculator') |
||||
const BN = require('ethereumjs-util').BN |
||||
|
||||
class BalanceController { |
||||
|
||||
constructor (opts = {}) { |
||||
const { address, accountTracker, txController, blockTracker } = opts |
||||
this.address = address |
||||
this.accountTracker = accountTracker |
||||
this.txController = txController |
||||
this.blockTracker = blockTracker |
||||
|
||||
const initState = { |
||||
ethBalance: undefined, |
||||
} |
||||
this.store = new ObservableStore(initState) |
||||
|
||||
this.balanceCalc = new PendingBalanceCalculator({ |
||||
getBalance: () => this._getBalance(), |
||||
getPendingTransactions: this._getPendingTransactions.bind(this), |
||||
}) |
||||
|
||||
this._registerUpdates() |
||||
} |
||||
|
||||
async updateBalance () { |
||||
const balance = await this.balanceCalc.getBalance() |
||||
this.store.updateState({ |
||||
ethBalance: balance, |
||||
}) |
||||
} |
||||
|
||||
_registerUpdates () { |
||||
const update = this.updateBalance.bind(this) |
||||
this.txController.on('submitted', update) |
||||
this.txController.on('confirmed', update) |
||||
this.txController.on('failed', update) |
||||
this.accountTracker.subscribe(update) |
||||
this.blockTracker.on('block', update) |
||||
} |
||||
|
||||
async _getBalance () { |
||||
const { accounts } = this.accountTracker.getState() |
||||
const entry = accounts[this.address] |
||||
const balance = entry.balance |
||||
return balance ? new BN(balance.substring(2), 16) : undefined |
||||
} |
||||
|
||||
async _getPendingTransactions () { |
||||
const pending = this.txController.getFilteredTxList({ |
||||
from: this.address, |
||||
status: 'submitted', |
||||
err: undefined, |
||||
}) |
||||
return pending |
||||
} |
||||
|
||||
} |
||||
|
||||
module.exports = BalanceController |
@ -0,0 +1,66 @@ |
||||
const ObservableStore = require('obs-store') |
||||
const extend = require('xtend') |
||||
const BalanceController = require('./balance') |
||||
|
||||
class ComputedbalancesController { |
||||
|
||||
constructor (opts = {}) { |
||||
const { accountTracker, txController, blockTracker } = opts |
||||
this.accountTracker = accountTracker |
||||
this.txController = txController |
||||
this.blockTracker = blockTracker |
||||
|
||||
const initState = extend({ |
||||
computedBalances: {}, |
||||
}, opts.initState) |
||||
this.store = new ObservableStore(initState) |
||||
this.balances = {} |
||||
|
||||
this._initBalanceUpdating() |
||||
} |
||||
|
||||
updateAllBalances () { |
||||
for (let address in this.balances) { |
||||
this.balances[address].updateBalance() |
||||
} |
||||
} |
||||
|
||||
_initBalanceUpdating () { |
||||
const store = this.accountTracker.getState() |
||||
this.addAnyAccountsFromStore(store) |
||||
this.accountTracker.subscribe(this.addAnyAccountsFromStore.bind(this)) |
||||
} |
||||
|
||||
addAnyAccountsFromStore(store) { |
||||
const balances = store.accounts |
||||
|
||||
for (let address in balances) { |
||||
this.trackAddressIfNotAlready(address) |
||||
} |
||||
} |
||||
|
||||
trackAddressIfNotAlready (address) { |
||||
const state = this.store.getState() |
||||
if (!(address in state.computedBalances)) { |
||||
this.trackAddress(address) |
||||
} |
||||
} |
||||
|
||||
trackAddress (address) { |
||||
let updater = new BalanceController({ |
||||
address, |
||||
accountTracker: this.accountTracker, |
||||
txController: this.txController, |
||||
blockTracker: this.blockTracker, |
||||
}) |
||||
updater.store.subscribe((accountBalance) => { |
||||
let newState = this.store.getState() |
||||
newState.computedBalances[address] = accountBalance |
||||
this.store.updateState(newState) |
||||
}) |
||||
this.balances[address] = updater |
||||
updater.updateBalance() |
||||
} |
||||
} |
||||
|
||||
module.exports = ComputedbalancesController |
@ -0,0 +1,51 @@ |
||||
const BN = require('ethereumjs-util').BN |
||||
const normalize = require('eth-sig-util').normalize |
||||
|
||||
class PendingBalanceCalculator { |
||||
|
||||
// Must be initialized with two functions:
|
||||
// getBalance => Returns a promise of a BN of the current balance in Wei
|
||||
// getPendingTransactions => Returns an array of TxMeta Objects,
|
||||
// which have txParams properties, which include value, gasPrice, and gas,
|
||||
// all in a base=16 hex format.
|
||||
constructor ({ getBalance, getPendingTransactions }) { |
||||
this.getPendingTransactions = getPendingTransactions |
||||
this.getNetworkBalance = getBalance |
||||
} |
||||
|
||||
async getBalance() { |
||||
const results = await Promise.all([ |
||||
this.getNetworkBalance(), |
||||
this.getPendingTransactions(), |
||||
]) |
||||
|
||||
const [ balance, pending ] = results |
||||
if (!balance) return undefined |
||||
|
||||
const pendingValue = pending.reduce((total, tx) => { |
||||
return total.add(this.calculateMaxCost(tx)) |
||||
}, new BN(0)) |
||||
|
||||
return `0x${balance.sub(pendingValue).toString(16)}` |
||||
} |
||||
|
||||
calculateMaxCost (tx) { |
||||
const txValue = tx.txParams.value |
||||
const value = this.hexToBn(txValue) |
||||
const gasPrice = this.hexToBn(tx.txParams.gasPrice) |
||||
|
||||
const gas = tx.txParams.gas |
||||
const gasLimit = tx.txParams.gasLimit |
||||
const gasLimitBn = this.hexToBn(gas || gasLimit) |
||||
|
||||
const gasCost = gasPrice.mul(gasLimitBn) |
||||
return value.add(gasCost) |
||||
} |
||||
|
||||
hexToBn (hex) { |
||||
return new BN(normalize(hex).substring(2), 16) |
||||
} |
||||
|
||||
} |
||||
|
||||
module.exports = PendingBalanceCalculator |
@ -0,0 +1,93 @@ |
||||
const assert = require('assert') |
||||
const PendingBalanceCalculator = require('../../app/scripts/lib/pending-balance-calculator') |
||||
const MockTxGen = require('../lib/mock-tx-gen') |
||||
const BN = require('ethereumjs-util').BN |
||||
let providerResultStub = {} |
||||
|
||||
const zeroBn = new BN(0) |
||||
const etherBn = new BN(String(1e18)) |
||||
const ether = '0x' + etherBn.toString(16) |
||||
|
||||
describe('PendingBalanceCalculator', function () { |
||||
let balanceCalculator |
||||
|
||||
describe('#calculateMaxCost(tx)', function () { |
||||
it('returns a BN for a given tx value', function () { |
||||
const txGen = new MockTxGen() |
||||
pendingTxs = txGen.generate({ |
||||
status: 'submitted', |
||||
txParams: { |
||||
value: ether, |
||||
gasPrice: '0x0', |
||||
gas: '0x0', |
||||
} |
||||
}, { count: 1 }) |
||||
|
||||
const balanceCalculator = generateBalanceCalcWith([], zeroBn) |
||||
const result = balanceCalculator.calculateMaxCost(pendingTxs[0]) |
||||
assert.equal(result.toString(), etherBn.toString(), 'computes one ether') |
||||
}) |
||||
|
||||
it('calculates gas costs as well', function () { |
||||
const txGen = new MockTxGen() |
||||
pendingTxs = txGen.generate({ |
||||
status: 'submitted', |
||||
txParams: { |
||||
value: '0x0', |
||||
gasPrice: '0x2', |
||||
gas: '0x3', |
||||
} |
||||
}, { count: 1 }) |
||||
|
||||
const balanceCalculator = generateBalanceCalcWith([], zeroBn) |
||||
const result = balanceCalculator.calculateMaxCost(pendingTxs[0]) |
||||
assert.equal(result.toString(), '6', 'computes 6 wei of gas') |
||||
}) |
||||
}) |
||||
|
||||
describe('if you have no pending txs and one ether', function () { |
||||
|
||||
beforeEach(function () { |
||||
balanceCalculator = generateBalanceCalcWith([], etherBn) |
||||
}) |
||||
|
||||
it('returns the network balance', async function () { |
||||
const result = await balanceCalculator.getBalance() |
||||
assert.equal(result, ether, `gave ${result} needed ${ether}`) |
||||
}) |
||||
}) |
||||
|
||||
describe('if you have a one ether pending tx and one ether', function () { |
||||
beforeEach(function () { |
||||
const txGen = new MockTxGen() |
||||
pendingTxs = txGen.generate({ |
||||
status: 'submitted', |
||||
txParams: { |
||||
value: ether, |
||||
gasPrice: '0x0', |
||||
gas: '0x0', |
||||
} |
||||
}, { count: 1 }) |
||||
|
||||
balanceCalculator = generateBalanceCalcWith(pendingTxs, etherBn) |
||||
}) |
||||
|
||||
it('returns the subtracted result', async function () { |
||||
const result = await balanceCalculator.getBalance() |
||||
assert.equal(result, '0x0', `gave ${result} needed '0x0'`) |
||||
return true |
||||
}) |
||||
|
||||
}) |
||||
}) |
||||
|
||||
function generateBalanceCalcWith (transactions, providerStub = zeroBn) { |
||||
const getPendingTransactions = async () => transactions |
||||
const getBalance = async () => providerStub |
||||
|
||||
return new PendingBalanceCalculator({ |
||||
getBalance, |
||||
getPendingTransactions, |
||||
}) |
||||
} |
||||
|
Loading…
Reference in new issue