1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-11-22 01:47:00 +01:00

meta - transactions - docs yo!

This commit is contained in:
frankiebee 2018-04-19 11:29:26 -07:00
parent 943eea043c
commit eeb9390de8
9 changed files with 351 additions and 73 deletions

View File

@ -0,0 +1,92 @@
# Transaction Controller
Transaction Controller is an aggregate of sub-controllers and trackers
composing them in a way to be exposed to the metamask controller
- txStateManager
responsible for the state of a transaction and
storing the transaction
- pendingTxTracker
watching blocks for transactions to be include
and emitting confirmed events
- txGasUtil
gas calculations and safety buffering
- nonceTracker
calculating nonces
## flow digram of processing a transaction
![transaction-flow](../../../../docs/transaction-flow.png)
## txMeta's && txParams
A txMeta is the "meta" object it has all the random bits of info we need about a transaction on it. txParams are sacred every thing on txParams gets signed so it must
be a valid key and be hex prefixed except for the network number. Extra stuff must go on the txMeta!
Here is a txMeta too look at:
```js
txMeta = {
"id": 2828415030114568, // unique id for this txMeta used for look ups
"time": 1524094064821, // time of creation
"status": "confirmed",
"metamaskNetworkId": "1524091532133", //the network id for the transaction
"loadingDefaults": false, // used to tell the ui when we are done calculatyig gass defaults
"txParams": { // the txParams object
"from": "0x8acce2391c0d510a6c5e5d8f819a678f79b7e675",
"to": "0x8acce2391c0d510a6c5e5d8f819a678f79b7e675",
"value": "0x0",
"gasPrice": "0x3b9aca00",
"gas": "0x7b0c",
"nonce": "0x0"
},
"history": [{ //debug
"id": 2828415030114568,
"time": 1524094064821,
"status": "unapproved",
"metamaskNetworkId": "1524091532133",
"loadingDefaults": true,
"txParams": {
"from": "0x8acce2391c0d510a6c5e5d8f819a678f79b7e675",
"to": "0x8acce2391c0d510a6c5e5d8f819a678f79b7e675",
"value": "0x0"
}
},
[
{
"op": "add",
"path": "/txParams/gasPrice",
"value": "0x3b9aca00"
},
...], // I've removed most of history for this
"gasPriceSpecified": false, //weather or not the user/dapp has specified gasPrice
"gasLimitSpecified": false, //weather or not the user/dapp has specified gas
"estimatedGas": "5208",
"origin": "MetaMask", //debug
"nonceDetails": {
"params": {
"highestLocallyConfirmed": 0,
"highestSuggested": 0,
"nextNetworkNonce": 0
},
"local": {
"name": "local",
"nonce": 0,
"details": {
"startPoint": 0,
"highest": 0
}
},
"network": {
"name": "network",
"nonce": 0,
"details": {
"baseCount": 0
}
}
},
"rawTx": "0xf86980843b9aca00827b0c948acce2391c0d510a6c5e5d8f819a678f79b7e67580808602c5b5de66eea05c01a320b96ac730cb210ca56d2cb71fa360e1fc2c21fa5cf333687d18eb323fa02ed05987a6e5fd0f2459fcff80710b76b83b296454ad9a37594a0ccb4643ea90", // used for rebroadcast
"hash": "0xa45ba834b97c15e6ff4ed09badd04ecd5ce884b455eb60192cdc73bcc583972a",
"submittedTime": 1524094077902 // time of the attempt to submit the raw tx to the network, used in the ui to show the retry button
}
```

View File

@ -25,14 +25,15 @@ const txUtils = require('./lib/util')
@param {object} opts -
- initState, initial transaction list default is an empty array<br>
- networkStore, an observable store for network number<br>
- blockTracker,<br>
- provider,<br>
- signTransaction, function the signs an ethereumjs-tx<br>
- getGasPrice, optional gas price calculator<br>
- txHistoryLimit, number *optional* for limiting how many transactions are in state <br>
- preferencesStore,
@property {object} opts.initState initial transaction list default is an empty array
@property {Object} opts.networkStore an observable store for network number
@property {Object} opts.blockTracker
@property {Object} opts.provider
@property {Object} opts.signTransaction function the signs an ethereumjs-tx
@property {function} opts.getGasPrice optional gas price calculator
@property {function} opts.signTransaction ethTx signer that returns a rawTx
@property {number} opts.txHistoryLimit number *optional* for limiting how many transactions are in state
@property {Object} opts.preferencesStore
@class
*/
@ -50,12 +51,12 @@ class TransactionController extends EventEmitter {
this.query = new EthQuery(this.provider)
this.txGasUtil = new TxGasUtil(this.provider)
this._mapMethods()
this.txStateManager = new TransactionStateManager({
initState: opts.initState,
txHistoryLimit: opts.txHistoryLimit,
getNetwork: this.getNetwork.bind(this),
})
this._mapMethods()
this._onBootCleanUp()
this.store = this.txStateManager.store
@ -92,7 +93,10 @@ class TransactionController extends EventEmitter {
}
}
/** Adds a tx to the txlist */
/**
Adds a tx to the txlist
@emits ${txMeta.id}:unapproved
*/
addTx (txMeta) {
this.txStateManager.addTx(txMeta)
this.emit(`${txMeta.id}:unapproved`, txMeta)
@ -172,6 +176,7 @@ add a new unapproved transaction to the pipeline
async addTxGasDefaults (txMeta) {
const txParams = txMeta.txParams
// ensure value
txParams.value = txParams.value ? ethUtil.addHexPrefix(value) : '0x0',
txMeta.gasPriceSpecified = Boolean(txParams.gasPrice)
let gasPrice = txParams.gasPrice
if (!gasPrice) {
@ -412,4 +417,4 @@ add a new unapproved transaction to the pipeline
}
}
module.exports = TransactionController
module.exports = TransactionController

View File

@ -1,6 +1,6 @@
const jsonDiffer = require('fast-json-patch')
const clone = require('clone')
/** @module*/
module.exports = {
generateHistoryEntry,
replayHistory,
@ -8,7 +8,11 @@ module.exports = {
migrateFromSnapshotsToDiffs,
}
/**
converts non-initial history entries into diffs
@param longHistory {array}
@returns {array}
*/
function migrateFromSnapshotsToDiffs (longHistory) {
return (
longHistory
@ -20,6 +24,17 @@ function migrateFromSnapshotsToDiffs (longHistory) {
)
}
/**
generates an array of history objects sense the previous state.
The object has the keys opp(the operation preformed),
path(the key and if a nested object then each key will be seperated with a `/`)
value
with the first entry having the note
@param previousState {object} - the previous state of the object
@param newState {object} - the update object
@param note {string} - a optional note for the state change
@reurns {array}
*/
function generateHistoryEntry (previousState, newState, note) {
const entry = jsonDiffer.compare(previousState, newState)
// Add a note to the first op, since it breaks if we append it to the entry
@ -27,11 +42,19 @@ function generateHistoryEntry (previousState, newState, note) {
return entry
}
/**
Recovers previous txMeta state obj
@return {object}
*/
function replayHistory (_shortHistory) {
const shortHistory = clone(_shortHistory)
return shortHistory.reduce((val, entry) => jsonDiffer.applyPatch(val, entry).newDocument)
}
/**
@param txMeta {object}
@returns {object} a clone object of the txMeta with out history
*/
function snapshotFromTxMeta (txMeta) {
// create txMeta snapshot for history
const snapshot = clone(txMeta)

View File

@ -3,11 +3,15 @@ const {
isValidAddress,
} = require('ethereumjs-util')
/**
@module
*/
module.exports = {
normalizeTxParams,
validateTxParams,
validateFrom,
validateRecipient,
getFinalStates,
}
@ -16,22 +20,30 @@ const normalizers = {
from: from => addHexPrefix(from).toLowerCase(),
to: to => addHexPrefix(to).toLowerCase(),
nonce: nonce => addHexPrefix(nonce),
value: value => value ? addHexPrefix(value) : '0x0',
value: value => addHexPrefix(value),
data: data => addHexPrefix(data),
gas: gas => addHexPrefix(gas),
gasPrice: gasPrice => addHexPrefix(gasPrice),
}
/**
normalizes txParams
@param txParams {object}
@returns {object} normalized txParams
*/
function normalizeTxParams (txParams) {
// apply only keys in the normalizers
const normalizedTxParams = {}
for (let key in normalizers) {
for (const key in normalizers) {
if (txParams[key]) normalizedTxParams[key] = normalizers[key](txParams[key])
}
return normalizedTxParams
}
/**
validates txParams
@param txParams {object}
*/
function validateTxParams (txParams) {
validateFrom(txParams)
validateRecipient(txParams)
@ -47,11 +59,19 @@ function validateTxParams (txParams) {
}
}
/**
validates the from field in txParams
@param txParams {object}
*/
function validateFrom (txParams) {
if (!(typeof txParams.from === 'string')) throw new Error(`Invalid from address ${txParams.from} not a string`)
if (!isValidAddress(txParams.from)) throw new Error('Invalid from address')
}
/**
validates the to field in txParams
@param txParams {object}
*/
function validateRecipient (txParams) {
if (txParams.to === '0x' || txParams.to === null) {
if (txParams.data) {
@ -64,3 +84,16 @@ function validateRecipient (txParams) {
}
return txParams
}
/**
@returns an {array} of states that can be considered final
*/
function getFinalStates () {
return [
'rejected', // the user has responded no!
'confirmed', // the tx has been included in a block.
'failed', // the tx failed for some reason, included on tx data.
'dropped', // the tx nonce was already used
]
}

View File

@ -1,7 +1,15 @@
const EthQuery = require('ethjs-query')
const assert = require('assert')
const Mutex = require('await-semaphore').Mutex
/**
@param opts {object} -
@property {Object} opts.provider a ethereum provider
@property {function} opts.getPendingTransactions a function that returns an array of txMeta
whos status is `submitted`
@property {function} opts.getConfirmedTransactions a function that returns an array of txMeta
whos status is `confirmed`
@class
*/
class NonceTracker {
constructor ({ provider, getPendingTransactions, getConfirmedTransactions }) {
@ -12,6 +20,9 @@ class NonceTracker {
this.lockMap = {}
}
/**
@returns {object} with the key releaseLock (the gloabl mutex)
*/
async getGlobalLock () {
const globalMutex = this._lookupMutex('global')
// await global mutex free
@ -19,8 +30,19 @@ class NonceTracker {
return { releaseLock }
}
// releaseLock must be called
// releaseLock must be called after adding signed tx to pending transactions (or discarding)
/**
this will return an object with the `nextNonce` `nonceDetails` which is an
object with:
highestLocallyConfirmed (nonce),
highestSuggested (either the network nonce or the highestLocallyConfirmed nonce),
nextNetworkNonce (the nonce suggested by the network),
and the releaseLock
<br>note: releaseLock must be called after adding signed tx to pending transactions
(or discarding)<br>
@param address {string} the hex string for the address whos nonce we are calculating
@returns {object}
*/
async getNonceLock (address) {
// await global mutex free
await this._globalMutexFree()

View File

@ -8,10 +8,10 @@ const EthQuery = require('ethjs-query')
As well as continues broadcast while in the pending state
<br>
@param config {object} - non optional configuration object consists of:
<br>provider
<br>nonceTracker: see nonce tracker
<br>getPendingTransactions: a function for getting an array of transactions,
<br>publishTransaction: a async function for publishing raw transactions,
@property {Object} config.provider
@property {Object} config.nonceTracker see nonce tracker
@property {function} config.getPendingTransactions a function for getting an array of transactions,
@property {function} config.publishTransaction a async function for publishing raw transactions,
@class
@ -220,4 +220,4 @@ class PendingTransactionTracker extends EventEmitter {
}
}
module.exports = PendingTransactionTracker
module.exports = PendingTransactionTracker

View File

@ -11,6 +11,7 @@ const SIMPLE_GAS_COST = '0x5208' // Hex for 21000, cost of a simple send.
tx-utils are utility methods for Transaction manager
its passed ethquery
and used to do things like calculate gas of a tx.
@param provider {object}
*/
module.exports = class TxGasUtil {

View File

@ -1,22 +1,33 @@
const extend = require('xtend')
const EventEmitter = require('events')
const ObservableStore = require('obs-store')
const createId = require('../../lib/random-id')
const ethUtil = require('ethereumjs-util')
const txStateHistoryHelper = require('./lib/tx-state-history-helper')
// 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.
// - `'dropped'` the tx nonce was already used
module.exports = class TransactionStateManager extends EventEmitter {
const createId = require('../../lib/random-id')
const { getFinalStates } = require('./lib/util')
/**
TransactionStateManager is responsible for the state of a transaction and
storing the transaction
it also has some convenience methods for finding subsets of transactions
*
*STATUS METHODS
<br>statuses:
<br> - `'unapproved'` the user has not responded
<br> - `'rejected'` the user has responded no!
<br> - `'approved'` the user has approved the tx
<br> - `'signed'` the tx is signed
<br> - `'submitted'` the tx is sent to a server
<br> - `'confirmed'` the tx has been included in a block.
<br> - `'failed'` the tx failed for some reason, included on tx data.
<br> - `'dropped'` the tx nonce was already used
@param opts {object} -
@property {object} opts.initState with the key transaction {array}
@property {number} opts.txHistoryLimit limit for how many finished
transactions can hang around in state
@property {function} opts.getNetwork return network number
@class
*/
class TransactionStateManager extends EventEmitter {
constructor ({ initState, txHistoryLimit, getNetwork }) {
super()
@ -28,6 +39,10 @@ module.exports = class TransactionStateManager extends EventEmitter {
this.getNetwork = getNetwork
}
/**
@param opts {object} - the object to use when overwriting defaults
@returns {txMeta} the default txMeta object
*/
generateTxMeta (opts) {
return extend({
id: createId(),
@ -38,17 +53,25 @@ module.exports = class TransactionStateManager extends EventEmitter {
}, opts)
}
/**
@returns {array} of txMetas that have been filtered for only the current network
*/
getTxList () {
const network = this.getNetwork()
const fullTxList = this.getFullTxList()
return fullTxList.filter((txMeta) => txMeta.metamaskNetworkId === network)
}
/**
@returns {array} of all the txMetas in store
*/
getFullTxList () {
return this.store.getState().transactions
}
// Returns the tx list
/**
@returns {array} the tx list whos status is unapproved
*/
getUnapprovedTxList () {
const txList = this.getTxsByMetaData('status', 'unapproved')
return txList.reduce((result, tx) => {
@ -57,18 +80,35 @@ module.exports = class TransactionStateManager extends EventEmitter {
}, {})
}
/**
@param address {string} - hex prefixed address to sort the txMetas for [optional]
@returns {array} the tx list whos status is submitted
*/
getPendingTransactions (address) {
const opts = { status: 'submitted' }
if (address) opts.from = address
return this.getFilteredTxList(opts)
}
/**
@param address {string} - hex prefixed address to sort the txMetas for [optional]
@returns {array} the tx list whos status is confirmed
*/
getConfirmedTransactions (address) {
const opts = { status: 'confirmed' }
if (address) opts.from = address
return this.getFilteredTxList(opts)
}
/**
Adds the txMeta to the list of transactions in the store.
if the list is over txHistoryLimit it will remove a transaction that
is in its final state
it will allso add the key `history` to the txMeta with the snap shot of the original
object
@param txMeta {object}
@returns {object} the txMeta
*/
addTx (txMeta) {
this.once(`${txMeta.id}:signed`, function (txId) {
this.removeAllListeners(`${txMeta.id}:rejected`)
@ -93,7 +133,7 @@ module.exports = class TransactionStateManager extends EventEmitter {
// not tx's that are pending or unapproved
if (txCount > txHistoryLimit - 1) {
const index = transactions.findIndex((metaTx) => {
return this.getFinalStates().includes(metaTx.status)
return getFinalStates().includes(metaTx.status)
})
if (index !== -1) {
transactions.splice(index, 1)
@ -103,12 +143,21 @@ module.exports = class TransactionStateManager extends EventEmitter {
this._saveTxList(transactions)
return txMeta
}
// gets tx by Id and returns it
/**
@param txId {number}
@returns {object} the txMeta who matches the given id if none found
for the network returns undefined
*/
getTx (txId) {
const txMeta = this.getTxsByMetaData('id', txId)[0]
return txMeta
}
/**
updates the txMeta in the list and adds a history entry
@param txMeta {object} - the txMeta to update
@param note {string} - a not about the update for history
*/
updateTx (txMeta, note) {
// validate txParams
if (txMeta.txParams) {
@ -136,15 +185,22 @@ module.exports = class TransactionStateManager extends EventEmitter {
}
// merges txParams obj onto txData.txParams
// use extend to ensure that all fields are filled
/**
merges txParams obj onto txMeta.txParams
use extend to ensure that all fields are filled
@param txId {number} - the id of the txMeta
@param txParams {object} - the updated txParams
*/
updateTxParams (txId, txParams) {
const txMeta = this.getTx(txId)
txMeta.txParams = extend(txMeta.txParams, txParams)
this.updateTx(txMeta, `txStateManager#updateTxParams`)
}
// validates txParams members by type
/**
validates txParams members by type
@param txParams {object} - txParams to validate
*/
validateTxParams (txParams) {
Object.keys(txParams).forEach((key) => {
const value = txParams[key]
@ -161,17 +217,18 @@ module.exports = class TransactionStateManager extends EventEmitter {
})
}
/*
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
/**
@param opts {object} - an object of fields to search for eg:<br>
let <code>thingsToLookFor = {<br>
to: '0x0..',<br>
from: '0x0..',<br>
status: 'signed',<br>
err: undefined,<br>
}<br></code>
@returns a {array} of txMeta with all
options matching
*/
/*
****************HINT****************
| `err: undefined` is like looking |
| for a tx with no err |
@ -192,7 +249,14 @@ module.exports = class TransactionStateManager extends EventEmitter {
})
return filteredTxList
}
/**
@param key {string} - the key to check
@param value - the value your looking for
@param txList {array} - [optional] the list to search. default is the txList
from txStateManager#getTxList
@returns {array} a list of txMetas who matches the search params
*/
getTxsByMetaData (key, value, txList = this.getTxList()) {
return txList.filter((txMeta) => {
if (txMeta.txParams[key]) {
@ -205,33 +269,51 @@ module.exports = class TransactionStateManager extends EventEmitter {
// get::set status
// should return the status of the tx.
/**
@param txId {number} - the txMeta Id
@return {string} the status of the tx.
*/
getTxStatus (txId) {
const txMeta = this.getTx(txId)
return txMeta.status
}
// should update the status of the tx to 'rejected'.
/**
should update the status of the tx to 'rejected'.
@param txId {number} - the txMeta Id
*/
setTxStatusRejected (txId) {
this._setTxStatus(txId, 'rejected')
}
// should update the status of the tx to 'unapproved'.
/**
should update the status of the tx to 'unapproved'.
@param txId {number} - the txMeta Id
*/
setTxStatusUnapproved (txId) {
this._setTxStatus(txId, 'unapproved')
}
// should update the status of the tx to 'approved'.
/**
should update the status of the tx to 'approved'.
@param txId {number} - the txMeta Id
*/
setTxStatusApproved (txId) {
this._setTxStatus(txId, 'approved')
}
// should update the status of the tx to 'signed'.
/**
should update the status of the tx to 'signed'.
@param txId {number} - the txMeta Id
*/
setTxStatusSigned (txId) {
this._setTxStatus(txId, 'signed')
}
// should update the status of the tx to 'submitted'.
// and add a time stamp for when it was called
/**
should update the status of the tx to 'submitted'.
and add a time stamp for when it was called
@param txId {number} - the txMeta Id
*/
setTxStatusSubmitted (txId) {
const txMeta = this.getTx(txId)
txMeta.submittedTime = (new Date()).getTime()
@ -239,17 +321,29 @@ module.exports = class TransactionStateManager extends EventEmitter {
this._setTxStatus(txId, 'submitted')
}
// should update the status of the tx to 'confirmed'.
/**
should update the status of the tx to 'confirmed'.
@param txId {number} - the txMeta Id
*/
setTxStatusConfirmed (txId) {
this._setTxStatus(txId, 'confirmed')
}
// should update the status dropped
/**
should update the status of the tx to 'dropped'.
@param txId {number} - the txMeta Id
*/
setTxStatusDropped (txId) {
this._setTxStatus(txId, 'dropped')
}
/**
should update the status of the tx to 'failed'.
and put the error on the txMeta
@param txId {number} - the txMeta Id
@param err {erroObject} - error object
*/
setTxStatusFailed (txId, err) {
const txMeta = this.getTx(txId)
txMeta.err = {
@ -260,16 +354,11 @@ module.exports = class TransactionStateManager extends EventEmitter {
this._setTxStatus(txId, 'failed')
}
// returns an array of states that can be considered final
getFinalStates () {
return [
'rejected', // the user has responded no!
'confirmed', // the tx has been included in a block.
'failed', // the tx failed for some reason, included on tx data.
'dropped', // the tx nonce was already used
]
}
/**
Removes transaction from the given address for the current network
from the txList
@param address {string} - hex string of the from address on the txParams to remove
*/
wipeTransactions (address) {
// network only tx
const txs = this.getFullTxList()
@ -295,6 +384,14 @@ module.exports = class TransactionStateManager extends EventEmitter {
// - `'confirmed'` the tx has been included in a block.
// - `'failed'` the tx failed for some reason, included on tx data.
// - `'dropped'` the tx nonce was already used
/**
@param txId {number} - the txMeta Id
@param status {string} - the status to set on the txMeta
@emits tx:status-update - passes txId and status
@emits ${txMeta.id}:finished - if it is a finished state. Passes the txMeta
@emits update:badge
*/
_setTxStatus (txId, status) {
const txMeta = this.getTx(txId)
txMeta.status = status
@ -307,9 +404,14 @@ module.exports = class TransactionStateManager extends EventEmitter {
this.emit('update:badge')
}
// Saves the new/updated txList.
/**
Saves the new/updated txList.
@param transactions {array} - the list of transactions to save
*/
// Function is intended only for internal use
_saveTxList (transactions) {
this.store.updateState({ transactions })
}
}
module.exports = TransactionStateManager

BIN
docs/transaction-flow.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB