import ObservableStore from 'obs-store' import PendingBalanceCalculator from '../lib/pending-balance-calculator' import { BN } from 'ethereumjs-util' export default class BalanceController { /** * Controller responsible for storing and updating an account's balance. * * @typedef {Object} BalanceController * @param {Object} opts - Initialize various properties of the class. * @property {string} address A base 16 hex string. The account address which has the balance managed by this * BalanceController. * @property {AccountTracker} accountTracker Stores and updates the users accounts * for which this BalanceController manages balance. * @property {TransactionController} txController Stores, tracks and manages transactions. Here used to create a listener for * transaction updates. * @property {BlockTracker} blockTracker Tracks updates to blocks. On new blocks, this BalanceController updates its balance * @property {Object} store The store for the ethBalance * @property {string} store.ethBalance A base 16 hex string. The balance for the current account. * @property {PendingBalanceCalculator} balanceCalc Used to calculate the accounts balance with possible pending * transaction costs taken into account. * */ constructor (opts = {}) { this._validateParams(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() } /** * Updates the ethBalance property to the current pending balance * * @returns {Promise} - Promises undefined */ async updateBalance () { const balance = await this.balanceCalc.getBalance() this.store.updateState({ ethBalance: balance, }) } /** * Sets up listeners and subscriptions which should trigger an update of ethBalance. These updates include: * - when a transaction changes state to 'submitted', 'confirmed' or 'failed' * - when the current account changes (i.e. a new account is selected) * - when there is a block update * * @private * */ _registerUpdates () { const update = this.updateBalance.bind(this) this.txController.on('tx:status-update', (_, status) => { switch (status) { case 'submitted': case 'confirmed': case 'failed': update() return default: return } }) this.accountTracker.store.subscribe(update) this.blockTracker.on('latest', update) } /** * Gets the balance, as a base 16 hex string, of the account at this BalanceController's current address. * If the current account has no balance, returns undefined. * * @returns {Promise} - Promises a BN with a value equal to the balance of the current account, or undefined * if the current account has no balance * */ async _getBalance () { const { accounts } = this.accountTracker.store.getState() const entry = accounts[this.address] const balance = entry.balance return balance ? new BN(balance.substring(2), 16) : undefined } /** * Gets the pending transactions (i.e. those with a 'submitted' status). These are accessed from the * TransactionController passed to this BalanceController during construction. * * @private * @returns {Promise} - Promises an array of transaction objects. * */ async _getPendingTransactions () { const pending = this.txController.getFilteredTxList({ from: this.address, status: 'submitted', err: undefined, }) return pending } /** * Validates that the passed options have all required properties. * * @param {Object} opts - The options object to validate * @throws {string} Throw a custom error indicating that address, accountTracker, txController and blockTracker are * missing and at least one is required * */ _validateParams (opts) { const { address, accountTracker, txController, blockTracker } = opts if (!address || !accountTracker || !txController || !blockTracker) { const error = 'Cannot construct a balance checker without address, accountTracker, txController, and blockTracker.' throw new Error(error) } } }