commit
15b2823e54
@ -0,0 +1,70 @@ |
|||||||
|
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('tx:status-update', (txId, status) => { |
||||||
|
switch (status) { |
||||||
|
case 'submitted': |
||||||
|
case 'confirmed': |
||||||
|
case 'failed': |
||||||
|
update() |
||||||
|
return |
||||||
|
default: |
||||||
|
return |
||||||
|
} |
||||||
|
}) |
||||||
|
this.accountTracker.store.subscribe(update) |
||||||
|
this.blockTracker.on('block', update) |
||||||
|
} |
||||||
|
|
||||||
|
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 |
||||||
|
} |
||||||
|
|
||||||
|
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.accountTracker.store.getState().accounts) { |
||||||
|
this.balances[address].updateBalance() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
_initBalanceUpdating () { |
||||||
|
const store = this.accountTracker.store.getState() |
||||||
|
this.addAnyAccountsFromStore(store) |
||||||
|
this.accountTracker.store.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 |
@ -1,595 +0,0 @@ |
|||||||
const ethUtil = require('ethereumjs-util') |
|
||||||
const BN = ethUtil.BN |
|
||||||
const bip39 = require('bip39') |
|
||||||
const EventEmitter = require('events').EventEmitter |
|
||||||
const ObservableStore = require('obs-store') |
|
||||||
const filter = require('promise-filter') |
|
||||||
const encryptor = require('browser-passworder') |
|
||||||
const sigUtil = require('eth-sig-util') |
|
||||||
const normalizeAddress = sigUtil.normalize |
|
||||||
// Keyrings:
|
|
||||||
const SimpleKeyring = require('eth-simple-keyring') |
|
||||||
const HdKeyring = require('eth-hd-keyring') |
|
||||||
const keyringTypes = [ |
|
||||||
SimpleKeyring, |
|
||||||
HdKeyring, |
|
||||||
] |
|
||||||
|
|
||||||
class KeyringController extends EventEmitter { |
|
||||||
|
|
||||||
// PUBLIC METHODS
|
|
||||||
//
|
|
||||||
// THE FIRST SECTION OF METHODS ARE PUBLIC-FACING,
|
|
||||||
// MEANING THEY ARE USED BY CONSUMERS OF THIS CLASS.
|
|
||||||
//
|
|
||||||
// THEIR SURFACE AREA SHOULD BE CHANGED WITH GREAT CARE.
|
|
||||||
|
|
||||||
constructor (opts) { |
|
||||||
super() |
|
||||||
const initState = opts.initState || {} |
|
||||||
this.keyringTypes = keyringTypes |
|
||||||
this.store = new ObservableStore(initState) |
|
||||||
this.memStore = new ObservableStore({ |
|
||||||
isUnlocked: false, |
|
||||||
keyringTypes: this.keyringTypes.map(krt => krt.type), |
|
||||||
keyrings: [], |
|
||||||
identities: {}, |
|
||||||
}) |
|
||||||
this.ethStore = opts.ethStore |
|
||||||
this.encryptor = opts.encryptor || encryptor |
|
||||||
this.keyrings = [] |
|
||||||
this.getNetwork = opts.getNetwork |
|
||||||
} |
|
||||||
|
|
||||||
// Full Update
|
|
||||||
// returns Promise( @object state )
|
|
||||||
//
|
|
||||||
// Emits the `update` event and
|
|
||||||
// returns a Promise that resolves to the current state.
|
|
||||||
//
|
|
||||||
// Frequently used to end asynchronous chains in this class,
|
|
||||||
// indicating consumers can often either listen for updates,
|
|
||||||
// or accept a state-resolving promise to consume their results.
|
|
||||||
//
|
|
||||||
// Not all methods end with this, that might be a nice refactor.
|
|
||||||
fullUpdate () { |
|
||||||
this.emit('update') |
|
||||||
return Promise.resolve(this.memStore.getState()) |
|
||||||
} |
|
||||||
|
|
||||||
// Create New Vault And Keychain
|
|
||||||
// @string password - The password to encrypt the vault with
|
|
||||||
//
|
|
||||||
// returns Promise( @object state )
|
|
||||||
//
|
|
||||||
// Destroys any old encrypted storage,
|
|
||||||
// creates a new encrypted store with the given password,
|
|
||||||
// randomly creates a new HD wallet with 1 account,
|
|
||||||
// faucets that account on the testnet.
|
|
||||||
createNewVaultAndKeychain (password) { |
|
||||||
return this.persistAllKeyrings(password) |
|
||||||
.then(this.createFirstKeyTree.bind(this)) |
|
||||||
.then(this.fullUpdate.bind(this)) |
|
||||||
} |
|
||||||
|
|
||||||
// CreateNewVaultAndRestore
|
|
||||||
// @string password - The password to encrypt the vault with
|
|
||||||
// @string seed - The BIP44-compliant seed phrase.
|
|
||||||
//
|
|
||||||
// returns Promise( @object state )
|
|
||||||
//
|
|
||||||
// Destroys any old encrypted storage,
|
|
||||||
// creates a new encrypted store with the given password,
|
|
||||||
// creates a new HD wallet from the given seed with 1 account.
|
|
||||||
createNewVaultAndRestore (password, seed) { |
|
||||||
if (typeof password !== 'string') { |
|
||||||
return Promise.reject('Password must be text.') |
|
||||||
} |
|
||||||
|
|
||||||
if (!bip39.validateMnemonic(seed)) { |
|
||||||
return Promise.reject(new Error('Seed phrase is invalid.')) |
|
||||||
} |
|
||||||
|
|
||||||
this.clearKeyrings() |
|
||||||
|
|
||||||
return this.persistAllKeyrings(password) |
|
||||||
.then(() => { |
|
||||||
return this.addNewKeyring('HD Key Tree', { |
|
||||||
mnemonic: seed, |
|
||||||
numberOfAccounts: 1, |
|
||||||
}) |
|
||||||
}) |
|
||||||
.then((firstKeyring) => { |
|
||||||
return firstKeyring.getAccounts() |
|
||||||
}) |
|
||||||
.then((accounts) => { |
|
||||||
const firstAccount = accounts[0] |
|
||||||
if (!firstAccount) throw new Error('KeyringController - First Account not found.') |
|
||||||
const hexAccount = normalizeAddress(firstAccount) |
|
||||||
this.emit('newAccount', hexAccount) |
|
||||||
return this.setupAccounts(accounts) |
|
||||||
}) |
|
||||||
.then(this.persistAllKeyrings.bind(this, password)) |
|
||||||
.then(this.fullUpdate.bind(this)) |
|
||||||
} |
|
||||||
|
|
||||||
// Set Locked
|
|
||||||
// returns Promise( @object state )
|
|
||||||
//
|
|
||||||
// This method deallocates all secrets, and effectively locks metamask.
|
|
||||||
setLocked () { |
|
||||||
// set locked
|
|
||||||
this.password = null |
|
||||||
this.memStore.updateState({ isUnlocked: false }) |
|
||||||
// remove keyrings
|
|
||||||
this.keyrings = [] |
|
||||||
this._updateMemStoreKeyrings() |
|
||||||
return this.fullUpdate() |
|
||||||
} |
|
||||||
|
|
||||||
// Submit Password
|
|
||||||
// @string password
|
|
||||||
//
|
|
||||||
// returns Promise( @object state )
|
|
||||||
//
|
|
||||||
// Attempts to decrypt the current vault and load its keyrings
|
|
||||||
// into memory.
|
|
||||||
//
|
|
||||||
// Temporarily also migrates any old-style vaults first, as well.
|
|
||||||
// (Pre MetaMask 3.0.0)
|
|
||||||
submitPassword (password) { |
|
||||||
return this.unlockKeyrings(password) |
|
||||||
.then((keyrings) => { |
|
||||||
this.keyrings = keyrings |
|
||||||
return this.fullUpdate() |
|
||||||
}) |
|
||||||
} |
|
||||||
|
|
||||||
// Add New Keyring
|
|
||||||
// @string type
|
|
||||||
// @object opts
|
|
||||||
//
|
|
||||||
// returns Promise( @Keyring keyring )
|
|
||||||
//
|
|
||||||
// Adds a new Keyring of the given `type` to the vault
|
|
||||||
// and the current decrypted Keyrings array.
|
|
||||||
//
|
|
||||||
// All Keyring classes implement a unique `type` string,
|
|
||||||
// and this is used to retrieve them from the keyringTypes array.
|
|
||||||
addNewKeyring (type, opts) { |
|
||||||
const Keyring = this.getKeyringClassForType(type) |
|
||||||
const keyring = new Keyring(opts) |
|
||||||
return keyring.deserialize(opts) |
|
||||||
.then(() => { |
|
||||||
return keyring.getAccounts() |
|
||||||
}) |
|
||||||
.then((accounts) => { |
|
||||||
return this.checkForDuplicate(type, accounts) |
|
||||||
}) |
|
||||||
.then((checkedAccounts) => { |
|
||||||
this.keyrings.push(keyring) |
|
||||||
return this.setupAccounts(checkedAccounts) |
|
||||||
}) |
|
||||||
.then(() => this.persistAllKeyrings()) |
|
||||||
.then(() => this._updateMemStoreKeyrings()) |
|
||||||
.then(() => this.fullUpdate()) |
|
||||||
.then(() => { |
|
||||||
return keyring |
|
||||||
}) |
|
||||||
} |
|
||||||
|
|
||||||
// For now just checks for simple key pairs
|
|
||||||
// but in the future
|
|
||||||
// should possibly add HD and other types
|
|
||||||
//
|
|
||||||
checkForDuplicate (type, newAccount) { |
|
||||||
return this.getAccounts() |
|
||||||
.then((accounts) => { |
|
||||||
switch (type) { |
|
||||||
case 'Simple Key Pair': |
|
||||||
const isNotIncluded = !accounts.find((key) => key === newAccount[0] || key === ethUtil.stripHexPrefix(newAccount[0])) |
|
||||||
return (isNotIncluded) ? Promise.resolve(newAccount) : Promise.reject(new Error('The account you\'re are trying to import is a duplicate')) |
|
||||||
default: |
|
||||||
return Promise.resolve(newAccount) |
|
||||||
} |
|
||||||
}) |
|
||||||
} |
|
||||||
|
|
||||||
|
|
||||||
// Add New Account
|
|
||||||
// @number keyRingNum
|
|
||||||
//
|
|
||||||
// returns Promise( @object state )
|
|
||||||
//
|
|
||||||
// Calls the `addAccounts` method on the Keyring
|
|
||||||
// in the kryings array at index `keyringNum`,
|
|
||||||
// and then saves those changes.
|
|
||||||
addNewAccount (selectedKeyring) { |
|
||||||
return selectedKeyring.addAccounts(1) |
|
||||||
.then(this.setupAccounts.bind(this)) |
|
||||||
.then(this.persistAllKeyrings.bind(this)) |
|
||||||
.then(this._updateMemStoreKeyrings.bind(this)) |
|
||||||
.then(this.fullUpdate.bind(this)) |
|
||||||
} |
|
||||||
|
|
||||||
// Save Account Label
|
|
||||||
// @string account
|
|
||||||
// @string label
|
|
||||||
//
|
|
||||||
// returns Promise( @string label )
|
|
||||||
//
|
|
||||||
// Persists a nickname equal to `label` for the specified account.
|
|
||||||
saveAccountLabel (account, label) { |
|
||||||
try { |
|
||||||
const hexAddress = normalizeAddress(account) |
|
||||||
// update state on diskStore
|
|
||||||
const state = this.store.getState() |
|
||||||
const walletNicknames = state.walletNicknames || {} |
|
||||||
walletNicknames[hexAddress] = label |
|
||||||
this.store.updateState({ walletNicknames }) |
|
||||||
// update state on memStore
|
|
||||||
const identities = this.memStore.getState().identities |
|
||||||
identities[hexAddress].name = label |
|
||||||
this.memStore.updateState({ identities }) |
|
||||||
return Promise.resolve(label) |
|
||||||
} catch (err) { |
|
||||||
return Promise.reject(err) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// Export Account
|
|
||||||
// @string address
|
|
||||||
//
|
|
||||||
// returns Promise( @string privateKey )
|
|
||||||
//
|
|
||||||
// Requests the private key from the keyring controlling
|
|
||||||
// the specified address.
|
|
||||||
//
|
|
||||||
// Returns a Promise that may resolve with the private key string.
|
|
||||||
exportAccount (address) { |
|
||||||
try { |
|
||||||
return this.getKeyringForAccount(address) |
|
||||||
.then((keyring) => { |
|
||||||
return keyring.exportAccount(normalizeAddress(address)) |
|
||||||
}) |
|
||||||
} catch (e) { |
|
||||||
return Promise.reject(e) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
|
|
||||||
// SIGNING METHODS
|
|
||||||
//
|
|
||||||
// This method signs tx and returns a promise for
|
|
||||||
// TX Manager to update the state after signing
|
|
||||||
|
|
||||||
signTransaction (ethTx, _fromAddress) { |
|
||||||
const fromAddress = normalizeAddress(_fromAddress) |
|
||||||
return this.getKeyringForAccount(fromAddress) |
|
||||||
.then((keyring) => { |
|
||||||
return keyring.signTransaction(fromAddress, ethTx) |
|
||||||
}) |
|
||||||
} |
|
||||||
|
|
||||||
// Sign Message
|
|
||||||
// @object msgParams
|
|
||||||
//
|
|
||||||
// returns Promise(@buffer rawSig)
|
|
||||||
//
|
|
||||||
// Attempts to sign the provided @object msgParams.
|
|
||||||
signMessage (msgParams) { |
|
||||||
const address = normalizeAddress(msgParams.from) |
|
||||||
return this.getKeyringForAccount(address) |
|
||||||
.then((keyring) => { |
|
||||||
return keyring.signMessage(address, msgParams.data) |
|
||||||
}) |
|
||||||
} |
|
||||||
|
|
||||||
// Sign Personal Message
|
|
||||||
// @object msgParams
|
|
||||||
//
|
|
||||||
// returns Promise(@buffer rawSig)
|
|
||||||
//
|
|
||||||
// Attempts to sign the provided @object msgParams.
|
|
||||||
// Prefixes the hash before signing as per the new geth behavior.
|
|
||||||
signPersonalMessage (msgParams) { |
|
||||||
const address = normalizeAddress(msgParams.from) |
|
||||||
return this.getKeyringForAccount(address) |
|
||||||
.then((keyring) => { |
|
||||||
return keyring.signPersonalMessage(address, msgParams.data) |
|
||||||
}) |
|
||||||
} |
|
||||||
|
|
||||||
// PRIVATE METHODS
|
|
||||||
//
|
|
||||||
// THESE METHODS ARE ONLY USED INTERNALLY TO THE KEYRING-CONTROLLER
|
|
||||||
// AND SO MAY BE CHANGED MORE LIBERALLY THAN THE ABOVE METHODS.
|
|
||||||
|
|
||||||
// Create First Key Tree
|
|
||||||
// returns @Promise
|
|
||||||
//
|
|
||||||
// Clears the vault,
|
|
||||||
// creates a new one,
|
|
||||||
// creates a random new HD Keyring with 1 account,
|
|
||||||
// makes that account the selected account,
|
|
||||||
// faucets that account on testnet,
|
|
||||||
// puts the current seed words into the state tree.
|
|
||||||
createFirstKeyTree () { |
|
||||||
this.clearKeyrings() |
|
||||||
return this.addNewKeyring('HD Key Tree', { numberOfAccounts: 1 }) |
|
||||||
.then((keyring) => { |
|
||||||
return keyring.getAccounts() |
|
||||||
}) |
|
||||||
.then((accounts) => { |
|
||||||
const firstAccount = accounts[0] |
|
||||||
if (!firstAccount) throw new Error('KeyringController - No account found on keychain.') |
|
||||||
const hexAccount = normalizeAddress(firstAccount) |
|
||||||
this.emit('newAccount', hexAccount) |
|
||||||
this.emit('newVault', hexAccount) |
|
||||||
return this.setupAccounts(accounts) |
|
||||||
}) |
|
||||||
.then(this.persistAllKeyrings.bind(this)) |
|
||||||
} |
|
||||||
|
|
||||||
// Setup Accounts
|
|
||||||
// @array accounts
|
|
||||||
//
|
|
||||||
// returns @Promise(@object account)
|
|
||||||
//
|
|
||||||
// Initializes the provided account array
|
|
||||||
// Gives them numerically incremented nicknames,
|
|
||||||
// and adds them to the ethStore for regular balance checking.
|
|
||||||
setupAccounts (accounts) { |
|
||||||
return this.getAccounts() |
|
||||||
.then((loadedAccounts) => { |
|
||||||
const arr = accounts || loadedAccounts |
|
||||||
return Promise.all(arr.map((account) => { |
|
||||||
return this.getBalanceAndNickname(account) |
|
||||||
})) |
|
||||||
}) |
|
||||||
} |
|
||||||
|
|
||||||
// Get Balance And Nickname
|
|
||||||
// @string account
|
|
||||||
//
|
|
||||||
// returns Promise( @string label )
|
|
||||||
//
|
|
||||||
// Takes an account address and an iterator representing
|
|
||||||
// the current number of named accounts.
|
|
||||||
getBalanceAndNickname (account) { |
|
||||||
if (!account) { |
|
||||||
throw new Error('Problem loading account.') |
|
||||||
} |
|
||||||
const address = normalizeAddress(account) |
|
||||||
this.ethStore.addAccount(address) |
|
||||||
return this.createNickname(address) |
|
||||||
} |
|
||||||
|
|
||||||
// Create Nickname
|
|
||||||
// @string address
|
|
||||||
//
|
|
||||||
// returns Promise( @string label )
|
|
||||||
//
|
|
||||||
// Takes an address, and assigns it an incremented nickname, persisting it.
|
|
||||||
createNickname (address) { |
|
||||||
const hexAddress = normalizeAddress(address) |
|
||||||
const identities = this.memStore.getState().identities |
|
||||||
const currentIdentityCount = Object.keys(identities).length + 1 |
|
||||||
const nicknames = this.store.getState().walletNicknames || {} |
|
||||||
const existingNickname = nicknames[hexAddress] |
|
||||||
const name = existingNickname || `Account ${currentIdentityCount}` |
|
||||||
identities[hexAddress] = { |
|
||||||
address: hexAddress, |
|
||||||
name, |
|
||||||
} |
|
||||||
this.memStore.updateState({ identities }) |
|
||||||
return this.saveAccountLabel(hexAddress, name) |
|
||||||
} |
|
||||||
|
|
||||||
// Persist All Keyrings
|
|
||||||
// @password string
|
|
||||||
//
|
|
||||||
// returns Promise
|
|
||||||
//
|
|
||||||
// Iterates the current `keyrings` array,
|
|
||||||
// serializes each one into a serialized array,
|
|
||||||
// encrypts that array with the provided `password`,
|
|
||||||
// and persists that encrypted string to storage.
|
|
||||||
persistAllKeyrings (password = this.password) { |
|
||||||
if (typeof password === 'string') { |
|
||||||
this.password = password |
|
||||||
this.memStore.updateState({ isUnlocked: true }) |
|
||||||
} |
|
||||||
return Promise.all(this.keyrings.map((keyring) => { |
|
||||||
return Promise.all([keyring.type, keyring.serialize()]) |
|
||||||
.then((serializedKeyringArray) => { |
|
||||||
// Label the output values on each serialized Keyring:
|
|
||||||
return { |
|
||||||
type: serializedKeyringArray[0], |
|
||||||
data: serializedKeyringArray[1], |
|
||||||
} |
|
||||||
}) |
|
||||||
})) |
|
||||||
.then((serializedKeyrings) => { |
|
||||||
return this.encryptor.encrypt(this.password, serializedKeyrings) |
|
||||||
}) |
|
||||||
.then((encryptedString) => { |
|
||||||
this.store.updateState({ vault: encryptedString }) |
|
||||||
return true |
|
||||||
}) |
|
||||||
} |
|
||||||
|
|
||||||
// Unlock Keyrings
|
|
||||||
// @string password
|
|
||||||
//
|
|
||||||
// returns Promise( @array keyrings )
|
|
||||||
//
|
|
||||||
// Attempts to unlock the persisted encrypted storage,
|
|
||||||
// initializing the persisted keyrings to RAM.
|
|
||||||
unlockKeyrings (password) { |
|
||||||
const encryptedVault = this.store.getState().vault |
|
||||||
if (!encryptedVault) { |
|
||||||
throw new Error('Cannot unlock without a previous vault.') |
|
||||||
} |
|
||||||
|
|
||||||
return this.encryptor.decrypt(password, encryptedVault) |
|
||||||
.then((vault) => { |
|
||||||
this.password = password |
|
||||||
this.memStore.updateState({ isUnlocked: true }) |
|
||||||
vault.forEach(this.restoreKeyring.bind(this)) |
|
||||||
return this.keyrings |
|
||||||
}) |
|
||||||
} |
|
||||||
|
|
||||||
// Restore Keyring
|
|
||||||
// @object serialized
|
|
||||||
//
|
|
||||||
// returns Promise( @Keyring deserialized )
|
|
||||||
//
|
|
||||||
// Attempts to initialize a new keyring from the provided
|
|
||||||
// serialized payload.
|
|
||||||
//
|
|
||||||
// On success, returns the resulting @Keyring instance.
|
|
||||||
restoreKeyring (serialized) { |
|
||||||
const { type, data } = serialized |
|
||||||
|
|
||||||
const Keyring = this.getKeyringClassForType(type) |
|
||||||
const keyring = new Keyring() |
|
||||||
return keyring.deserialize(data) |
|
||||||
.then(() => { |
|
||||||
return keyring.getAccounts() |
|
||||||
}) |
|
||||||
.then((accounts) => { |
|
||||||
return this.setupAccounts(accounts) |
|
||||||
}) |
|
||||||
.then(() => { |
|
||||||
this.keyrings.push(keyring) |
|
||||||
this._updateMemStoreKeyrings() |
|
||||||
return keyring |
|
||||||
}) |
|
||||||
} |
|
||||||
|
|
||||||
// Get Keyring Class For Type
|
|
||||||
// @string type
|
|
||||||
//
|
|
||||||
// Returns @class Keyring
|
|
||||||
//
|
|
||||||
// Searches the current `keyringTypes` array
|
|
||||||
// for a Keyring class whose unique `type` property
|
|
||||||
// matches the provided `type`,
|
|
||||||
// returning it if it exists.
|
|
||||||
getKeyringClassForType (type) { |
|
||||||
return this.keyringTypes.find(kr => kr.type === type) |
|
||||||
} |
|
||||||
|
|
||||||
getKeyringsByType (type) { |
|
||||||
return this.keyrings.filter((keyring) => keyring.type === type) |
|
||||||
} |
|
||||||
|
|
||||||
// Get Accounts
|
|
||||||
// returns Promise( @Array[ @string accounts ] )
|
|
||||||
//
|
|
||||||
// Returns the public addresses of all current accounts
|
|
||||||
// managed by all currently unlocked keyrings.
|
|
||||||
getAccounts () { |
|
||||||
const keyrings = this.keyrings || [] |
|
||||||
return Promise.all(keyrings.map(kr => kr.getAccounts())) |
|
||||||
.then((keyringArrays) => { |
|
||||||
return keyringArrays.reduce((res, arr) => { |
|
||||||
return res.concat(arr) |
|
||||||
}, []) |
|
||||||
}) |
|
||||||
} |
|
||||||
|
|
||||||
// Get Keyring For Account
|
|
||||||
// @string address
|
|
||||||
//
|
|
||||||
// returns Promise(@Keyring keyring)
|
|
||||||
//
|
|
||||||
// Returns the currently initialized keyring that manages
|
|
||||||
// the specified `address` if one exists.
|
|
||||||
getKeyringForAccount (address) { |
|
||||||
const hexed = normalizeAddress(address) |
|
||||||
log.debug(`KeyringController - getKeyringForAccount: ${hexed}`) |
|
||||||
|
|
||||||
return Promise.all(this.keyrings.map((keyring) => { |
|
||||||
return Promise.all([ |
|
||||||
keyring, |
|
||||||
keyring.getAccounts(), |
|
||||||
]) |
|
||||||
})) |
|
||||||
.then(filter((candidate) => { |
|
||||||
const accounts = candidate[1].map(normalizeAddress) |
|
||||||
return accounts.includes(hexed) |
|
||||||
})) |
|
||||||
.then((winners) => { |
|
||||||
if (winners && winners.length > 0) { |
|
||||||
return winners[0][0] |
|
||||||
} else { |
|
||||||
throw new Error('No keyring found for the requested account.') |
|
||||||
} |
|
||||||
}) |
|
||||||
} |
|
||||||
|
|
||||||
// Display For Keyring
|
|
||||||
// @Keyring keyring
|
|
||||||
//
|
|
||||||
// returns Promise( @Object { type:String, accounts:Array } )
|
|
||||||
//
|
|
||||||
// Is used for adding the current keyrings to the state object.
|
|
||||||
displayForKeyring (keyring) { |
|
||||||
return keyring.getAccounts() |
|
||||||
.then((accounts) => { |
|
||||||
return { |
|
||||||
type: keyring.type, |
|
||||||
accounts: accounts, |
|
||||||
} |
|
||||||
}) |
|
||||||
} |
|
||||||
|
|
||||||
// Add Gas Buffer
|
|
||||||
// @string gas (as hexadecimal value)
|
|
||||||
//
|
|
||||||
// returns @string bufferedGas (as hexadecimal value)
|
|
||||||
//
|
|
||||||
// Adds a healthy buffer of gas to an initial gas estimate.
|
|
||||||
addGasBuffer (gas) { |
|
||||||
const gasBuffer = new BN('100000', 10) |
|
||||||
const bnGas = new BN(ethUtil.stripHexPrefix(gas), 16) |
|
||||||
const correct = bnGas.add(gasBuffer) |
|
||||||
return ethUtil.addHexPrefix(correct.toString(16)) |
|
||||||
} |
|
||||||
|
|
||||||
// Clear Keyrings
|
|
||||||
//
|
|
||||||
// Deallocates all currently managed keyrings and accounts.
|
|
||||||
// Used before initializing a new vault.
|
|
||||||
clearKeyrings () { |
|
||||||
let accounts |
|
||||||
try { |
|
||||||
accounts = Object.keys(this.ethStore.getState()) |
|
||||||
} catch (e) { |
|
||||||
accounts = [] |
|
||||||
} |
|
||||||
accounts.forEach((address) => { |
|
||||||
this.ethStore.removeAccount(address) |
|
||||||
}) |
|
||||||
|
|
||||||
// clear keyrings from memory
|
|
||||||
this.keyrings = [] |
|
||||||
this.memStore.updateState({ |
|
||||||
keyrings: [], |
|
||||||
identities: {}, |
|
||||||
}) |
|
||||||
} |
|
||||||
|
|
||||||
_updateMemStoreKeyrings () { |
|
||||||
Promise.all(this.keyrings.map(this.displayForKeyring)) |
|
||||||
.then((keyrings) => { |
|
||||||
this.memStore.updateState({ keyrings }) |
|
||||||
}) |
|
||||||
} |
|
||||||
|
|
||||||
} |
|
||||||
|
|
||||||
module.exports = KeyringController |
|
@ -0,0 +1,104 @@ |
|||||||
|
/* Account Tracker |
||||||
|
* |
||||||
|
* This module is responsible for tracking any number of accounts |
||||||
|
* and caching their current balances & transaction counts. |
||||||
|
* |
||||||
|
* It also tracks transaction hashes, and checks their inclusion status |
||||||
|
* on each new block. |
||||||
|
*/ |
||||||
|
|
||||||
|
const async = require('async') |
||||||
|
const EthQuery = require('eth-query') |
||||||
|
const ObservableStore = require('obs-store') |
||||||
|
const EventEmitter = require('events').EventEmitter |
||||||
|
function noop () {} |
||||||
|
|
||||||
|
|
||||||
|
class AccountTracker extends EventEmitter { |
||||||
|
|
||||||
|
constructor (opts = {}) { |
||||||
|
super() |
||||||
|
|
||||||
|
const initState = { |
||||||
|
accounts: {}, |
||||||
|
currentBlockGasLimit: '', |
||||||
|
} |
||||||
|
this.store = new ObservableStore(initState) |
||||||
|
|
||||||
|
this._provider = opts.provider |
||||||
|
this._query = new EthQuery(this._provider) |
||||||
|
this._blockTracker = opts.blockTracker |
||||||
|
// subscribe to latest block
|
||||||
|
this._blockTracker.on('block', this._updateForBlock.bind(this)) |
||||||
|
// blockTracker.currentBlock may be null
|
||||||
|
this._currentBlockNumber = this._blockTracker.currentBlock |
||||||
|
} |
||||||
|
|
||||||
|
//
|
||||||
|
// public
|
||||||
|
//
|
||||||
|
|
||||||
|
addAccount (address) { |
||||||
|
const accounts = this.store.getState().accounts |
||||||
|
accounts[address] = {} |
||||||
|
this.store.updateState({ accounts }) |
||||||
|
if (!this._currentBlockNumber) return |
||||||
|
this._updateAccount(address) |
||||||
|
} |
||||||
|
|
||||||
|
removeAccount (address) { |
||||||
|
const accounts = this.store.getState().accounts |
||||||
|
delete accounts[address] |
||||||
|
this.store.updateState({ accounts }) |
||||||
|
} |
||||||
|
|
||||||
|
//
|
||||||
|
// private
|
||||||
|
//
|
||||||
|
|
||||||
|
_updateForBlock (block) { |
||||||
|
this._currentBlockNumber = block.number |
||||||
|
const currentBlockGasLimit = block.gasLimit |
||||||
|
|
||||||
|
this.store.updateState({ currentBlockGasLimit }) |
||||||
|
|
||||||
|
async.parallel([ |
||||||
|
this._updateAccounts.bind(this), |
||||||
|
], (err) => { |
||||||
|
if (err) return console.error(err) |
||||||
|
this.emit('block', this.store.getState()) |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
_updateAccounts (cb = noop) { |
||||||
|
const accounts = this.store.getState().accounts |
||||||
|
const addresses = Object.keys(accounts) |
||||||
|
async.each(addresses, this._updateAccount.bind(this), cb) |
||||||
|
} |
||||||
|
|
||||||
|
_updateAccount (address, cb = noop) { |
||||||
|
this._getAccount(address, (err, result) => { |
||||||
|
if (err) return cb(err) |
||||||
|
result.address = address |
||||||
|
const accounts = this.store.getState().accounts |
||||||
|
// only populate if the entry is still present
|
||||||
|
if (accounts[address]) { |
||||||
|
accounts[address] = result |
||||||
|
this.store.updateState({ accounts }) |
||||||
|
} |
||||||
|
cb(null, result) |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
_getAccount (address, cb = noop) { |
||||||
|
const query = this._query |
||||||
|
async.parallel({ |
||||||
|
balance: query.getBalance.bind(query, address), |
||||||
|
nonce: query.getTransactionCount.bind(query, address), |
||||||
|
code: query.getCode.bind(query, address), |
||||||
|
}, cb) |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
module.exports = AccountTracker |
@ -1,138 +0,0 @@ |
|||||||
/* Ethereum Store |
|
||||||
* |
|
||||||
* This module is responsible for tracking any number of accounts |
|
||||||
* and caching their current balances & transaction counts. |
|
||||||
* |
|
||||||
* It also tracks transaction hashes, and checks their inclusion status |
|
||||||
* on each new block. |
|
||||||
*/ |
|
||||||
|
|
||||||
const async = require('async') |
|
||||||
const EthQuery = require('eth-query') |
|
||||||
const ObservableStore = require('obs-store') |
|
||||||
function noop () {} |
|
||||||
|
|
||||||
|
|
||||||
class EthereumStore extends ObservableStore { |
|
||||||
|
|
||||||
constructor (opts = {}) { |
|
||||||
super({ |
|
||||||
accounts: {}, |
|
||||||
transactions: {}, |
|
||||||
currentBlockNumber: '0', |
|
||||||
currentBlockHash: '', |
|
||||||
currentBlockGasLimit: '', |
|
||||||
}) |
|
||||||
this._provider = opts.provider |
|
||||||
this._query = new EthQuery(this._provider) |
|
||||||
this._blockTracker = opts.blockTracker |
|
||||||
// subscribe to latest block
|
|
||||||
this._blockTracker.on('block', this._updateForBlock.bind(this)) |
|
||||||
// blockTracker.currentBlock may be null
|
|
||||||
this._currentBlockNumber = this._blockTracker.currentBlock |
|
||||||
} |
|
||||||
|
|
||||||
//
|
|
||||||
// public
|
|
||||||
//
|
|
||||||
|
|
||||||
addAccount (address) { |
|
||||||
const accounts = this.getState().accounts |
|
||||||
accounts[address] = {} |
|
||||||
this.updateState({ accounts }) |
|
||||||
if (!this._currentBlockNumber) return |
|
||||||
this._updateAccount(address) |
|
||||||
} |
|
||||||
|
|
||||||
removeAccount (address) { |
|
||||||
const accounts = this.getState().accounts |
|
||||||
delete accounts[address] |
|
||||||
this.updateState({ accounts }) |
|
||||||
} |
|
||||||
|
|
||||||
addTransaction (txHash) { |
|
||||||
const transactions = this.getState().transactions |
|
||||||
transactions[txHash] = {} |
|
||||||
this.updateState({ transactions }) |
|
||||||
if (!this._currentBlockNumber) return |
|
||||||
this._updateTransaction(this._currentBlockNumber, txHash, noop) |
|
||||||
} |
|
||||||
|
|
||||||
removeTransaction (txHash) { |
|
||||||
const transactions = this.getState().transactions |
|
||||||
delete transactions[txHash] |
|
||||||
this.updateState({ transactions }) |
|
||||||
} |
|
||||||
|
|
||||||
|
|
||||||
//
|
|
||||||
// private
|
|
||||||
//
|
|
||||||
|
|
||||||
_updateForBlock (block) { |
|
||||||
const blockNumber = '0x' + block.number.toString('hex') |
|
||||||
this._currentBlockNumber = blockNumber |
|
||||||
this.updateState({ currentBlockNumber: parseInt(blockNumber) }) |
|
||||||
this.updateState({ currentBlockHash: `0x${block.hash.toString('hex')}`}) |
|
||||||
this.updateState({ currentBlockGasLimit: `0x${block.gasLimit.toString('hex')}` }) |
|
||||||
async.parallel([ |
|
||||||
this._updateAccounts.bind(this), |
|
||||||
this._updateTransactions.bind(this, blockNumber), |
|
||||||
], (err) => { |
|
||||||
if (err) return console.error(err) |
|
||||||
this.emit('block', this.getState()) |
|
||||||
}) |
|
||||||
} |
|
||||||
|
|
||||||
_updateAccounts (cb = noop) { |
|
||||||
const accounts = this.getState().accounts |
|
||||||
const addresses = Object.keys(accounts) |
|
||||||
async.each(addresses, this._updateAccount.bind(this), cb) |
|
||||||
} |
|
||||||
|
|
||||||
_updateAccount (address, cb = noop) { |
|
||||||
const accounts = this.getState().accounts |
|
||||||
this._getAccount(address, (err, result) => { |
|
||||||
if (err) return cb(err) |
|
||||||
result.address = address |
|
||||||
// only populate if the entry is still present
|
|
||||||
if (accounts[address]) { |
|
||||||
accounts[address] = result |
|
||||||
this.updateState({ accounts }) |
|
||||||
} |
|
||||||
cb(null, result) |
|
||||||
}) |
|
||||||
} |
|
||||||
|
|
||||||
_updateTransactions (block, cb = noop) { |
|
||||||
const transactions = this.getState().transactions |
|
||||||
const txHashes = Object.keys(transactions) |
|
||||||
async.each(txHashes, this._updateTransaction.bind(this, block), cb) |
|
||||||
} |
|
||||||
|
|
||||||
_updateTransaction (block, txHash, cb = noop) { |
|
||||||
// would use the block here to determine how many confirmations the tx has
|
|
||||||
const transactions = this.getState().transactions |
|
||||||
this._query.getTransaction(txHash, (err, result) => { |
|
||||||
if (err) return cb(err) |
|
||||||
// only populate if the entry is still present
|
|
||||||
if (transactions[txHash]) { |
|
||||||
transactions[txHash] = result |
|
||||||
this.updateState({ transactions }) |
|
||||||
} |
|
||||||
cb(null, result) |
|
||||||
}) |
|
||||||
} |
|
||||||
|
|
||||||
_getAccount (address, cb = noop) { |
|
||||||
const query = this._query |
|
||||||
async.parallel({ |
|
||||||
balance: query.getBalance.bind(query, address), |
|
||||||
nonce: query.getTransactionCount.bind(query, address), |
|
||||||
code: query.getCode.bind(query, address), |
|
||||||
}, cb) |
|
||||||
} |
|
||||||
|
|
||||||
} |
|
||||||
|
|
||||||
module.exports = EthereumStore |
|
@ -0,0 +1,31 @@ |
|||||||
|
module.exports = function createEventEmitterProxy(eventEmitter, listeners) { |
||||||
|
let target = eventEmitter |
||||||
|
const eventHandlers = listeners || {} |
||||||
|
const proxy = new Proxy({}, { |
||||||
|
get: (obj, name) => { |
||||||
|
// intercept listeners
|
||||||
|
if (name === 'on') return addListener |
||||||
|
if (name === 'setTarget') return setTarget |
||||||
|
if (name === 'proxyEventHandlers') return eventHandlers |
||||||
|
return target[name] |
||||||
|
}, |
||||||
|
set: (obj, name, value) => { |
||||||
|
target[name] = value |
||||||
|
return true |
||||||
|
}, |
||||||
|
}) |
||||||
|
function setTarget (eventEmitter) { |
||||||
|
target = eventEmitter |
||||||
|
// migrate listeners
|
||||||
|
Object.keys(eventHandlers).forEach((name) => { |
||||||
|
eventHandlers[name].forEach((handler) => target.on(name, handler)) |
||||||
|
}) |
||||||
|
} |
||||||
|
function addListener (name, handler) { |
||||||
|
if (!eventHandlers[name]) eventHandlers[name] = [] |
||||||
|
eventHandlers[name].push(handler) |
||||||
|
target.on(name, handler) |
||||||
|
} |
||||||
|
if (listeners) proxy.setTarget(eventEmitter) |
||||||
|
return proxy |
||||||
|
} |
@ -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,245 @@ |
|||||||
|
const extend = require('xtend') |
||||||
|
const EventEmitter = require('events') |
||||||
|
const ObservableStore = require('obs-store') |
||||||
|
const ethUtil = require('ethereumjs-util') |
||||||
|
const txStateHistoryHelper = require('./tx-state-history-helper') |
||||||
|
|
||||||
|
module.exports = class TransactionStateManger extends EventEmitter { |
||||||
|
constructor ({ initState, txHistoryLimit, getNetwork }) { |
||||||
|
super() |
||||||
|
|
||||||
|
this.store = new ObservableStore( |
||||||
|
extend({ |
||||||
|
transactions: [], |
||||||
|
}, initState)) |
||||||
|
this.txHistoryLimit = txHistoryLimit |
||||||
|
this.getNetwork = getNetwork |
||||||
|
} |
||||||
|
|
||||||
|
// Returns the number of txs for the current network.
|
||||||
|
getTxCount () { |
||||||
|
return this.getTxList().length |
||||||
|
} |
||||||
|
|
||||||
|
getTxList () { |
||||||
|
const network = this.getNetwork() |
||||||
|
const fullTxList = this.getFullTxList() |
||||||
|
return fullTxList.filter((txMeta) => txMeta.metamaskNetworkId === network) |
||||||
|
} |
||||||
|
|
||||||
|
getFullTxList () { |
||||||
|
return this.store.getState().transactions |
||||||
|
} |
||||||
|
|
||||||
|
// Returns the tx list
|
||||||
|
getUnapprovedTxList () { |
||||||
|
const txList = this.getTxsByMetaData('status', 'unapproved') |
||||||
|
return txList.reduce((result, tx) => { |
||||||
|
result[tx.id] = tx |
||||||
|
return result |
||||||
|
}, {}) |
||||||
|
} |
||||||
|
|
||||||
|
getPendingTransactions (address) { |
||||||
|
const opts = { status: 'submitted' } |
||||||
|
if (address) opts.from = address |
||||||
|
return this.getFilteredTxList(opts) |
||||||
|
} |
||||||
|
|
||||||
|
addTx (txMeta) { |
||||||
|
this.once(`${txMeta.id}:signed`, function (txId) { |
||||||
|
this.removeAllListeners(`${txMeta.id}:rejected`) |
||||||
|
}) |
||||||
|
this.once(`${txMeta.id}:rejected`, function (txId) { |
||||||
|
this.removeAllListeners(`${txMeta.id}:signed`) |
||||||
|
}) |
||||||
|
// initialize history
|
||||||
|
txMeta.history = [] |
||||||
|
// capture initial snapshot of txMeta for history
|
||||||
|
const snapshot = txStateHistoryHelper.snapshotFromTxMeta(txMeta) |
||||||
|
txMeta.history.push(snapshot) |
||||||
|
|
||||||
|
const transactions = this.getFullTxList() |
||||||
|
const txCount = this.getTxCount() |
||||||
|
const txHistoryLimit = this.txHistoryLimit |
||||||
|
|
||||||
|
// checks if the length of the tx history is
|
||||||
|
// longer then desired persistence limit
|
||||||
|
// and then if it is removes only confirmed
|
||||||
|
// or rejected tx's.
|
||||||
|
// not tx's that are pending or unapproved
|
||||||
|
if (txCount > txHistoryLimit - 1) { |
||||||
|
const index = transactions.findIndex((metaTx) => metaTx.status === 'confirmed' || metaTx.status === 'rejected') |
||||||
|
transactions.splice(index, 1) |
||||||
|
} |
||||||
|
transactions.push(txMeta) |
||||||
|
this._saveTxList(transactions) |
||||||
|
return txMeta |
||||||
|
} |
||||||
|
// gets tx by Id and returns it
|
||||||
|
getTx (txId) { |
||||||
|
const txMeta = this.getTxsByMetaData('id', txId)[0] |
||||||
|
return txMeta |
||||||
|
} |
||||||
|
|
||||||
|
updateTx (txMeta) { |
||||||
|
if (txMeta.txParams) { |
||||||
|
Object.keys(txMeta.txParams).forEach((key) => { |
||||||
|
let value = txMeta.txParams[key] |
||||||
|
if (typeof value !== 'string') console.error(`${key}: ${value} in txParams is not a string`) |
||||||
|
if (!ethUtil.isHexPrefixed(value)) console.error('is not hex prefixed, anything on txParams must be hex prefixed') |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
// create txMeta snapshot for history
|
||||||
|
const currentState = txStateHistoryHelper.snapshotFromTxMeta(txMeta) |
||||||
|
// recover previous tx state obj
|
||||||
|
const previousState = txStateHistoryHelper.replayHistory(txMeta.history) |
||||||
|
// generate history entry and add to history
|
||||||
|
const entry = txStateHistoryHelper.generateHistoryEntry(previousState, currentState) |
||||||
|
txMeta.history.push(entry) |
||||||
|
|
||||||
|
// commit txMeta to state
|
||||||
|
const txId = txMeta.id |
||||||
|
const txList = this.getFullTxList() |
||||||
|
const index = txList.findIndex(txData => txData.id === txId) |
||||||
|
txList[index] = txMeta |
||||||
|
this._saveTxList(txList) |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
// merges txParams obj onto txData.txParams
|
||||||
|
// use extend to ensure that all fields are filled
|
||||||
|
updateTxParams (txId, txParams) { |
||||||
|
const txMeta = this.getTx(txId) |
||||||
|
txMeta.txParams = extend(txMeta.txParams, txParams) |
||||||
|
this.updateTx(txMeta) |
||||||
|
} |
||||||
|
|
||||||
|
/* |
||||||
|
Takes an object of fields to search for eg: |
||||||
|
let thingsToLookFor = { |
||||||
|
to: '0x0..', |
||||||
|
from: '0x0..', |
||||||
|
status: 'signed', |
||||||
|
err: undefined, |
||||||
|
} |
||||||
|
and returns a list of tx with all |
||||||
|
options matching |
||||||
|
|
||||||
|
****************HINT**************** |
||||||
|
| `err: undefined` is like looking | |
||||||
|
| for a tx with no err | |
||||||
|
| so you can also search txs that | |
||||||
|
| dont have something as well by | |
||||||
|
| setting the value as undefined | |
||||||
|
************************************ |
||||||
|
|
||||||
|
this is for things like filtering a the tx list |
||||||
|
for only tx's from 1 account |
||||||
|
or for filltering for all txs from one account |
||||||
|
and that have been 'confirmed' |
||||||
|
*/ |
||||||
|
getFilteredTxList (opts, initialList) { |
||||||
|
let filteredTxList = initialList |
||||||
|
Object.keys(opts).forEach((key) => { |
||||||
|
filteredTxList = this.getTxsByMetaData(key, opts[key], filteredTxList) |
||||||
|
}) |
||||||
|
return filteredTxList |
||||||
|
} |
||||||
|
|
||||||
|
getTxsByMetaData (key, value, txList = this.getTxList()) { |
||||||
|
return txList.filter((txMeta) => { |
||||||
|
if (txMeta.txParams[key]) { |
||||||
|
return txMeta.txParams[key] === value |
||||||
|
} else { |
||||||
|
return txMeta[key] === value |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
// STATUS METHODS
|
||||||
|
// statuses:
|
||||||
|
// - `'unapproved'` the user has not responded
|
||||||
|
// - `'rejected'` the user has responded no!
|
||||||
|
// - `'approved'` the user has approved the tx
|
||||||
|
// - `'signed'` the tx is signed
|
||||||
|
// - `'submitted'` the tx is sent to a server
|
||||||
|
// - `'confirmed'` the tx has been included in a block.
|
||||||
|
// - `'failed'` the tx failed for some reason, included on tx data.
|
||||||
|
|
||||||
|
// get::set status
|
||||||
|
|
||||||
|
// should return the status of the tx.
|
||||||
|
getTxStatus (txId) { |
||||||
|
const txMeta = this.getTx(txId) |
||||||
|
return txMeta.status |
||||||
|
} |
||||||
|
|
||||||
|
// should update the status of the tx to 'rejected'.
|
||||||
|
setTxStatusRejected (txId) { |
||||||
|
this._setTxStatus(txId, 'rejected') |
||||||
|
} |
||||||
|
|
||||||
|
// should update the status of the tx to 'approved'.
|
||||||
|
setTxStatusApproved (txId) { |
||||||
|
this._setTxStatus(txId, 'approved') |
||||||
|
} |
||||||
|
|
||||||
|
// should update the status of the tx to 'signed'.
|
||||||
|
setTxStatusSigned (txId) { |
||||||
|
this._setTxStatus(txId, 'signed') |
||||||
|
} |
||||||
|
|
||||||
|
// should update the status of the tx to 'submitted'.
|
||||||
|
setTxStatusSubmitted (txId) { |
||||||
|
this._setTxStatus(txId, 'submitted') |
||||||
|
} |
||||||
|
|
||||||
|
// should update the status of the tx to 'confirmed'.
|
||||||
|
setTxStatusConfirmed (txId) { |
||||||
|
this._setTxStatus(txId, 'confirmed') |
||||||
|
} |
||||||
|
|
||||||
|
setTxStatusFailed (txId, err) { |
||||||
|
const txMeta = this.getTx(txId) |
||||||
|
txMeta.err = { |
||||||
|
message: err.toString(), |
||||||
|
stack: err.stack, |
||||||
|
} |
||||||
|
this.updateTx(txMeta) |
||||||
|
this._setTxStatus(txId, 'failed') |
||||||
|
} |
||||||
|
|
||||||
|
//
|
||||||
|
// PRIVATE METHODS
|
||||||
|
//
|
||||||
|
|
||||||
|
// Should find the tx in the tx list and
|
||||||
|
// update it.
|
||||||
|
// should set the status in txData
|
||||||
|
// - `'unapproved'` the user has not responded
|
||||||
|
// - `'rejected'` the user has responded no!
|
||||||
|
// - `'approved'` the user has approved the tx
|
||||||
|
// - `'signed'` the tx is signed
|
||||||
|
// - `'submitted'` the tx is sent to a server
|
||||||
|
// - `'confirmed'` the tx has been included in a block.
|
||||||
|
// - `'failed'` the tx failed for some reason, included on tx data.
|
||||||
|
_setTxStatus (txId, status) { |
||||||
|
const txMeta = this.getTx(txId) |
||||||
|
txMeta.status = status |
||||||
|
this.emit(`${txMeta.id}:${status}`, txId) |
||||||
|
this.emit(`tx:status-update`, txId, status) |
||||||
|
if (status === 'submitted' || status === 'rejected') { |
||||||
|
this.emit(`${txMeta.id}:finished`, txMeta) |
||||||
|
} |
||||||
|
this.updateTx(txMeta) |
||||||
|
this.emit('update:badge') |
||||||
|
} |
||||||
|
|
||||||
|
// Saves the new/updated txList.
|
||||||
|
// Function is intended only for internal use
|
||||||
|
_saveTxList (transactions) { |
||||||
|
this.store.updateState({ transactions }) |
||||||
|
} |
||||||
|
} |
@ -1,164 +0,0 @@ |
|||||||
const assert = require('assert') |
|
||||||
const KeyringController = require('../../app/scripts/keyring-controller') |
|
||||||
const configManagerGen = require('../lib/mock-config-manager') |
|
||||||
const ethUtil = require('ethereumjs-util') |
|
||||||
const BN = ethUtil.BN |
|
||||||
const mockEncryptor = require('../lib/mock-encryptor') |
|
||||||
const sinon = require('sinon') |
|
||||||
|
|
||||||
describe('KeyringController', function () { |
|
||||||
let keyringController |
|
||||||
const password = 'password123' |
|
||||||
const seedWords = 'puzzle seed penalty soldier say clay field arctic metal hen cage runway' |
|
||||||
const addresses = ['eF35cA8EbB9669A35c31b5F6f249A9941a812AC1'.toLowerCase()] |
|
||||||
const accounts = [] |
|
||||||
// let originalKeystore
|
|
||||||
|
|
||||||
beforeEach(function (done) { |
|
||||||
this.sinon = sinon.sandbox.create() |
|
||||||
window.localStorage = {} // Hacking localStorage support into JSDom
|
|
||||||
|
|
||||||
keyringController = new KeyringController({ |
|
||||||
configManager: configManagerGen(), |
|
||||||
txManager: { |
|
||||||
getTxList: () => [], |
|
||||||
getUnapprovedTxList: () => [], |
|
||||||
}, |
|
||||||
ethStore: { |
|
||||||
addAccount (acct) { accounts.push(ethUtil.addHexPrefix(acct)) }, |
|
||||||
}, |
|
||||||
encryptor: mockEncryptor, |
|
||||||
}) |
|
||||||
|
|
||||||
keyringController.createNewVaultAndKeychain(password) |
|
||||||
.then(function (newState) { |
|
||||||
newState |
|
||||||
done() |
|
||||||
}) |
|
||||||
.catch((err) => { |
|
||||||
done(err) |
|
||||||
}) |
|
||||||
}) |
|
||||||
|
|
||||||
afterEach(function () { |
|
||||||
// Cleanup mocks
|
|
||||||
this.sinon.restore() |
|
||||||
}) |
|
||||||
|
|
||||||
describe('#createNewVaultAndKeychain', function () { |
|
||||||
this.timeout(10000) |
|
||||||
|
|
||||||
it('should set a vault on the configManager', function (done) { |
|
||||||
keyringController.store.updateState({ vault: null }) |
|
||||||
assert(!keyringController.store.getState().vault, 'no previous vault') |
|
||||||
keyringController.createNewVaultAndKeychain(password) |
|
||||||
.then(() => { |
|
||||||
const vault = keyringController.store.getState().vault |
|
||||||
assert(vault, 'vault created') |
|
||||||
done() |
|
||||||
}) |
|
||||||
.catch((reason) => { |
|
||||||
done(reason) |
|
||||||
}) |
|
||||||
}) |
|
||||||
}) |
|
||||||
|
|
||||||
describe('#restoreKeyring', function () { |
|
||||||
it(`should pass a keyring's serialized data back to the correct type.`, function (done) { |
|
||||||
const mockSerialized = { |
|
||||||
type: 'HD Key Tree', |
|
||||||
data: { |
|
||||||
mnemonic: seedWords, |
|
||||||
numberOfAccounts: 1, |
|
||||||
}, |
|
||||||
} |
|
||||||
const mock = this.sinon.mock(keyringController) |
|
||||||
|
|
||||||
mock.expects('getBalanceAndNickname') |
|
||||||
.exactly(1) |
|
||||||
|
|
||||||
keyringController.restoreKeyring(mockSerialized) |
|
||||||
.then((keyring) => { |
|
||||||
assert.equal(keyring.wallets.length, 1, 'one wallet restored') |
|
||||||
return keyring.getAccounts() |
|
||||||
}) |
|
||||||
.then((accounts) => { |
|
||||||
assert.equal(accounts[0], addresses[0]) |
|
||||||
mock.verify() |
|
||||||
done() |
|
||||||
}) |
|
||||||
.catch((reason) => { |
|
||||||
done(reason) |
|
||||||
}) |
|
||||||
}) |
|
||||||
}) |
|
||||||
|
|
||||||
describe('#createNickname', function () { |
|
||||||
it('should add the address to the identities hash', function () { |
|
||||||
const fakeAddress = '0x12345678' |
|
||||||
keyringController.createNickname(fakeAddress) |
|
||||||
const identities = keyringController.memStore.getState().identities |
|
||||||
const identity = identities[fakeAddress] |
|
||||||
assert.equal(identity.address, fakeAddress) |
|
||||||
}) |
|
||||||
}) |
|
||||||
|
|
||||||
describe('#saveAccountLabel', function () { |
|
||||||
it('sets the nickname', function (done) { |
|
||||||
const account = addresses[0] |
|
||||||
var nick = 'Test nickname' |
|
||||||
const identities = keyringController.memStore.getState().identities |
|
||||||
identities[ethUtil.addHexPrefix(account)] = {} |
|
||||||
keyringController.memStore.updateState({ identities }) |
|
||||||
keyringController.saveAccountLabel(account, nick) |
|
||||||
.then((label) => { |
|
||||||
try { |
|
||||||
assert.equal(label, nick) |
|
||||||
const persisted = keyringController.store.getState().walletNicknames[account] |
|
||||||
assert.equal(persisted, nick) |
|
||||||
done() |
|
||||||
} catch (err) { |
|
||||||
done() |
|
||||||
} |
|
||||||
}) |
|
||||||
.catch((reason) => { |
|
||||||
done(reason) |
|
||||||
}) |
|
||||||
}) |
|
||||||
}) |
|
||||||
|
|
||||||
describe('#getAccounts', function () { |
|
||||||
it('returns the result of getAccounts for each keyring', function (done) { |
|
||||||
keyringController.keyrings = [ |
|
||||||
{ getAccounts () { return Promise.resolve([1, 2, 3]) } }, |
|
||||||
{ getAccounts () { return Promise.resolve([4, 5, 6]) } }, |
|
||||||
] |
|
||||||
|
|
||||||
keyringController.getAccounts() |
|
||||||
.then((result) => { |
|
||||||
assert.deepEqual(result, [1, 2, 3, 4, 5, 6]) |
|
||||||
done() |
|
||||||
}) |
|
||||||
}) |
|
||||||
}) |
|
||||||
|
|
||||||
describe('#addGasBuffer', function () { |
|
||||||
it('adds 100k gas buffer to estimates', function () { |
|
||||||
const gas = '0x04ee59' // Actual estimated gas example
|
|
||||||
const tooBigOutput = '0x80674f9' // Actual bad output
|
|
||||||
const bnGas = new BN(ethUtil.stripHexPrefix(gas), 16) |
|
||||||
const correctBuffer = new BN('100000', 10) |
|
||||||
const correct = bnGas.add(correctBuffer) |
|
||||||
|
|
||||||
// const tooBig = new BN(tooBigOutput, 16)
|
|
||||||
const result = keyringController.addGasBuffer(gas) |
|
||||||
const bnResult = new BN(ethUtil.stripHexPrefix(result), 16) |
|
||||||
|
|
||||||
assert.equal(result.indexOf('0x'), 0, 'included hex prefix') |
|
||||||
assert(bnResult.gt(bnGas), 'Estimate increased in value.') |
|
||||||
assert.equal(bnResult.sub(bnGas).toString(10), '100000', 'added 100k gas') |
|
||||||
assert.equal(result, '0x' + correct.toString(16), 'Added the right amount') |
|
||||||
assert.notEqual(result, tooBigOutput, 'not that bad estimate') |
|
||||||
}) |
|
||||||
}) |
|
||||||
}) |
|
@ -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, |
||||||
|
}) |
||||||
|
} |
||||||
|
|
@ -0,0 +1,241 @@ |
|||||||
|
const assert = require('assert') |
||||||
|
const clone = require('clone') |
||||||
|
const ObservableStore = require('obs-store') |
||||||
|
const TxStateManager = require('../../app/scripts/lib/tx-state-manager') |
||||||
|
const txStateHistoryHelper = require('../../app/scripts/lib/tx-state-history-helper') |
||||||
|
const noop = () => true |
||||||
|
|
||||||
|
describe('TransactionStateManger', function () { |
||||||
|
let txStateManager |
||||||
|
const currentNetworkId = 42 |
||||||
|
const otherNetworkId = 2 |
||||||
|
|
||||||
|
beforeEach(function () { |
||||||
|
txStateManager = new TxStateManager({ |
||||||
|
initState: { |
||||||
|
transactions: [], |
||||||
|
}, |
||||||
|
txHistoryLimit: 10, |
||||||
|
getNetwork: () => currentNetworkId |
||||||
|
}) |
||||||
|
}) |
||||||
|
|
||||||
|
describe('#setTxStatusSigned', function () { |
||||||
|
it('sets the tx status to signed', function () { |
||||||
|
let tx = { id: 1, status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} } |
||||||
|
txStateManager.addTx(tx, noop) |
||||||
|
txStateManager.setTxStatusSigned(1) |
||||||
|
let result = txStateManager.getTxList() |
||||||
|
assert.ok(Array.isArray(result)) |
||||||
|
assert.equal(result.length, 1) |
||||||
|
assert.equal(result[0].status, 'signed') |
||||||
|
}) |
||||||
|
|
||||||
|
it('should emit a signed event to signal the exciton of callback', (done) => { |
||||||
|
let tx = { id: 1, status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} } |
||||||
|
const noop = function () { |
||||||
|
assert(true, 'event listener has been triggered and noop executed') |
||||||
|
done() |
||||||
|
} |
||||||
|
txStateManager.addTx(tx) |
||||||
|
txStateManager.on('1:signed', noop) |
||||||
|
txStateManager.setTxStatusSigned(1) |
||||||
|
|
||||||
|
}) |
||||||
|
}) |
||||||
|
|
||||||
|
describe('#setTxStatusRejected', function () { |
||||||
|
it('sets the tx status to rejected', function () { |
||||||
|
let tx = { id: 1, status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} } |
||||||
|
txStateManager.addTx(tx) |
||||||
|
txStateManager.setTxStatusRejected(1) |
||||||
|
let result = txStateManager.getTxList() |
||||||
|
assert.ok(Array.isArray(result)) |
||||||
|
assert.equal(result.length, 1) |
||||||
|
assert.equal(result[0].status, 'rejected') |
||||||
|
}) |
||||||
|
|
||||||
|
it('should emit a rejected event to signal the exciton of callback', (done) => { |
||||||
|
let tx = { id: 1, status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} } |
||||||
|
txStateManager.addTx(tx) |
||||||
|
const noop = function (err, txId) { |
||||||
|
assert(true, 'event listener has been triggered and noop executed') |
||||||
|
done() |
||||||
|
} |
||||||
|
txStateManager.on('1:rejected', noop) |
||||||
|
txStateManager.setTxStatusRejected(1) |
||||||
|
}) |
||||||
|
}) |
||||||
|
|
||||||
|
describe('#getFullTxList', function () { |
||||||
|
it('when new should return empty array', function () { |
||||||
|
let result = txStateManager.getTxList() |
||||||
|
assert.ok(Array.isArray(result)) |
||||||
|
assert.equal(result.length, 0) |
||||||
|
}) |
||||||
|
}) |
||||||
|
|
||||||
|
describe('#getTxList', function () { |
||||||
|
it('when new should return empty array', function () { |
||||||
|
let result = txStateManager.getTxList() |
||||||
|
assert.ok(Array.isArray(result)) |
||||||
|
assert.equal(result.length, 0) |
||||||
|
}) |
||||||
|
}) |
||||||
|
|
||||||
|
describe('#addTx', function () { |
||||||
|
it('adds a tx returned in getTxList', function () { |
||||||
|
let tx = { id: 1, status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams: {} } |
||||||
|
txStateManager.addTx(tx, noop) |
||||||
|
let result = txStateManager.getTxList() |
||||||
|
assert.ok(Array.isArray(result)) |
||||||
|
assert.equal(result.length, 1) |
||||||
|
assert.equal(result[0].id, 1) |
||||||
|
}) |
||||||
|
|
||||||
|
it('does not override txs from other networks', function () { |
||||||
|
let tx = { id: 1, status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams: {} } |
||||||
|
let tx2 = { id: 2, status: 'confirmed', metamaskNetworkId: otherNetworkId, txParams: {} } |
||||||
|
txStateManager.addTx(tx, noop) |
||||||
|
txStateManager.addTx(tx2, noop) |
||||||
|
let result = txStateManager.getFullTxList() |
||||||
|
let result2 = txStateManager.getTxList() |
||||||
|
assert.equal(result.length, 2, 'txs were deleted') |
||||||
|
assert.equal(result2.length, 1, 'incorrect number of txs on network.') |
||||||
|
}) |
||||||
|
|
||||||
|
it('cuts off early txs beyond a limit', function () { |
||||||
|
const limit = txStateManager.txHistoryLimit |
||||||
|
for (let i = 0; i < limit + 1; i++) { |
||||||
|
const tx = { id: i, time: new Date(), status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams: {} } |
||||||
|
txStateManager.addTx(tx, noop) |
||||||
|
} |
||||||
|
let result = txStateManager.getTxList() |
||||||
|
assert.equal(result.length, limit, `limit of ${limit} txs enforced`) |
||||||
|
assert.equal(result[0].id, 1, 'early txs truncted') |
||||||
|
}) |
||||||
|
|
||||||
|
it('cuts off early txs beyond a limit whether or not it is confirmed or rejected', function () { |
||||||
|
const limit = txStateManager.txHistoryLimit |
||||||
|
for (let i = 0; i < limit + 1; i++) { |
||||||
|
const tx = { id: i, time: new Date(), status: 'rejected', metamaskNetworkId: currentNetworkId, txParams: {} } |
||||||
|
txStateManager.addTx(tx, noop) |
||||||
|
} |
||||||
|
let result = txStateManager.getTxList() |
||||||
|
assert.equal(result.length, limit, `limit of ${limit} txs enforced`) |
||||||
|
assert.equal(result[0].id, 1, 'early txs truncted') |
||||||
|
}) |
||||||
|
|
||||||
|
it('cuts off early txs beyond a limit but does not cut unapproved txs', function () { |
||||||
|
let unconfirmedTx = { id: 0, time: new Date(), status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} } |
||||||
|
txStateManager.addTx(unconfirmedTx, noop) |
||||||
|
const limit = txStateManager.txHistoryLimit |
||||||
|
for (let i = 1; i < limit + 1; i++) { |
||||||
|
const tx = { id: i, time: new Date(), status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams: {} } |
||||||
|
txStateManager.addTx(tx, noop) |
||||||
|
} |
||||||
|
let result = txStateManager.getTxList() |
||||||
|
assert.equal(result.length, limit, `limit of ${limit} txs enforced`) |
||||||
|
assert.equal(result[0].id, 0, 'first tx should still be there') |
||||||
|
assert.equal(result[0].status, 'unapproved', 'first tx should be unapproved') |
||||||
|
assert.equal(result[1].id, 2, 'early txs truncted') |
||||||
|
}) |
||||||
|
}) |
||||||
|
|
||||||
|
describe('#updateTx', function () { |
||||||
|
it('replaces the tx with the same id', function () { |
||||||
|
txStateManager.addTx({ id: '1', status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} }, noop) |
||||||
|
txStateManager.addTx({ id: '2', status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams: {} }, noop) |
||||||
|
const txMeta = txStateManager.getTx('1') |
||||||
|
txMeta.hash = 'foo' |
||||||
|
txStateManager.updateTx(txMeta) |
||||||
|
let result = txStateManager.getTx('1') |
||||||
|
assert.equal(result.hash, 'foo') |
||||||
|
}) |
||||||
|
|
||||||
|
it('updates gas price and adds history items', function () { |
||||||
|
const originalGasPrice = '0x01' |
||||||
|
const desiredGasPrice = '0x02' |
||||||
|
|
||||||
|
const txMeta = { |
||||||
|
id: '1', |
||||||
|
status: 'unapproved', |
||||||
|
metamaskNetworkId: currentNetworkId, |
||||||
|
txParams: { |
||||||
|
gasPrice: originalGasPrice, |
||||||
|
}, |
||||||
|
} |
||||||
|
|
||||||
|
const updatedMeta = clone(txMeta) |
||||||
|
|
||||||
|
txStateManager.addTx(txMeta) |
||||||
|
const updatedTx = txStateManager.getTx('1') |
||||||
|
// verify tx was initialized correctly
|
||||||
|
assert.equal(updatedTx.history.length, 1, 'one history item (initial)') |
||||||
|
assert.equal(Array.isArray(updatedTx.history[0]), false, 'first history item is initial state') |
||||||
|
assert.deepEqual(updatedTx.history[0], txStateHistoryHelper.snapshotFromTxMeta(updatedTx), 'first history item is initial state') |
||||||
|
// modify value and updateTx
|
||||||
|
updatedTx.txParams.gasPrice = desiredGasPrice |
||||||
|
txStateManager.updateTx(updatedTx) |
||||||
|
// check updated value
|
||||||
|
const result = txStateManager.getTx('1') |
||||||
|
assert.equal(result.txParams.gasPrice, desiredGasPrice, 'gas price updated') |
||||||
|
// validate history was updated
|
||||||
|
assert.equal(result.history.length, 2, 'two history items (initial + diff)') |
||||||
|
const expectedEntry = { op: 'replace', path: '/txParams/gasPrice', value: desiredGasPrice } |
||||||
|
assert.deepEqual(result.history[1], [expectedEntry], 'two history items (initial + diff)') |
||||||
|
}) |
||||||
|
}) |
||||||
|
|
||||||
|
describe('#getUnapprovedTxList', function () { |
||||||
|
it('returns unapproved txs in a hash', function () { |
||||||
|
txStateManager.addTx({ id: '1', status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} }, noop) |
||||||
|
txStateManager.addTx({ id: '2', status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams: {} }, noop) |
||||||
|
const result = txStateManager.getUnapprovedTxList() |
||||||
|
assert.equal(typeof result, 'object') |
||||||
|
assert.equal(result['1'].status, 'unapproved') |
||||||
|
assert.equal(result['2'], undefined) |
||||||
|
}) |
||||||
|
}) |
||||||
|
|
||||||
|
describe('#getTx', function () { |
||||||
|
it('returns a tx with the requested id', function () { |
||||||
|
txStateManager.addTx({ id: '1', status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} }, noop) |
||||||
|
txStateManager.addTx({ id: '2', status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams: {} }, noop) |
||||||
|
assert.equal(txStateManager.getTx('1').status, 'unapproved') |
||||||
|
assert.equal(txStateManager.getTx('2').status, 'confirmed') |
||||||
|
}) |
||||||
|
}) |
||||||
|
|
||||||
|
describe('#getFilteredTxList', function () { |
||||||
|
it('returns a tx with the requested data', function () { |
||||||
|
const txMetas = [ |
||||||
|
{ id: 0, status: 'unapproved', txParams: { from: '0xaa', to: '0xbb' }, metamaskNetworkId: currentNetworkId }, |
||||||
|
{ id: 1, status: 'unapproved', txParams: { from: '0xaa', to: '0xbb' }, metamaskNetworkId: currentNetworkId }, |
||||||
|
{ id: 2, status: 'unapproved', txParams: { from: '0xaa', to: '0xbb' }, metamaskNetworkId: currentNetworkId }, |
||||||
|
{ id: 3, status: 'unapproved', txParams: { from: '0xbb', to: '0xaa' }, metamaskNetworkId: currentNetworkId }, |
||||||
|
{ id: 4, status: 'unapproved', txParams: { from: '0xbb', to: '0xaa' }, metamaskNetworkId: currentNetworkId }, |
||||||
|
{ id: 5, status: 'confirmed', txParams: { from: '0xaa', to: '0xbb' }, metamaskNetworkId: currentNetworkId }, |
||||||
|
{ id: 6, status: 'confirmed', txParams: { from: '0xaa', to: '0xbb' }, metamaskNetworkId: currentNetworkId }, |
||||||
|
{ id: 7, status: 'confirmed', txParams: { from: '0xbb', to: '0xaa' }, metamaskNetworkId: currentNetworkId }, |
||||||
|
{ id: 8, status: 'confirmed', txParams: { from: '0xbb', to: '0xaa' }, metamaskNetworkId: currentNetworkId }, |
||||||
|
{ id: 9, status: 'confirmed', txParams: { from: '0xbb', to: '0xaa' }, metamaskNetworkId: currentNetworkId }, |
||||||
|
] |
||||||
|
txMetas.forEach((txMeta) => txStateManager.addTx(txMeta, noop)) |
||||||
|
let filterParams |
||||||
|
|
||||||
|
filterParams = { status: 'unapproved', from: '0xaa' } |
||||||
|
assert.equal(txStateManager.getFilteredTxList(filterParams).length, 3, `getFilteredTxList - ${JSON.stringify(filterParams)}`) |
||||||
|
filterParams = { status: 'unapproved', to: '0xaa' } |
||||||
|
assert.equal(txStateManager.getFilteredTxList(filterParams).length, 2, `getFilteredTxList - ${JSON.stringify(filterParams)}`) |
||||||
|
filterParams = { status: 'confirmed', from: '0xbb' } |
||||||
|
assert.equal(txStateManager.getFilteredTxList(filterParams).length, 3, `getFilteredTxList - ${JSON.stringify(filterParams)}`) |
||||||
|
filterParams = { status: 'confirmed' } |
||||||
|
assert.equal(txStateManager.getFilteredTxList(filterParams).length, 5, `getFilteredTxList - ${JSON.stringify(filterParams)}`) |
||||||
|
filterParams = { from: '0xaa' } |
||||||
|
assert.equal(txStateManager.getFilteredTxList(filterParams).length, 5, `getFilteredTxList - ${JSON.stringify(filterParams)}`) |
||||||
|
filterParams = { to: '0xaa' } |
||||||
|
assert.equal(txStateManager.getFilteredTxList(filterParams).length, 5, `getFilteredTxList - ${JSON.stringify(filterParams)}`) |
||||||
|
}) |
||||||
|
}) |
||||||
|
}) |
Loading…
Reference in new issue