aboutsummaryrefslogtreecommitdiffstats
path: root/app/scripts/lib/tx-gas-utils.js
blob: 829b4c42142b8c38d4ee60e74d8c359b043a3fe5 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
const EthQuery = require('ethjs-query')
const {
  hexToBn,
  BnMultiplyByFraction,
  bnToHex,
} = require('./util')
const { addHexPrefix, isValidAddress } = require('ethereumjs-util')
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.
*/

module.exports = class TxGasUtil {

  constructor (provider) {
    this.query = new EthQuery(provider)
  }

  async analyzeGasUsage (txMeta) {
    const block = await this.query.getBlockByNumber('latest', true)
    let estimatedGasHex
    try {
      estimatedGasHex = await this.estimateTxGas(txMeta, block.gasLimit)
    } catch (err) {
      const simulationFailed = (
        err.message.includes('Transaction execution error.') ||
        err.message.includes('gas required exceeds allowance or always failing transaction')
      )
      if (simulationFailed) {
        txMeta.simulationFails = true
        return txMeta
      }
    }
    this.setTxGas(txMeta, block.gasLimit, estimatedGasHex)
    return txMeta
  }

  async estimateTxGas (txMeta, blockGasLimitHex) {
    const txParams = txMeta.txParams

    // check if gasLimit is already specified
    txMeta.gasLimitSpecified = Boolean(txParams.gas)

    // if it is, use that value
    if (txMeta.gasLimitSpecified) {
      return txParams.gas
    }

    // if recipient has no code, gas is 21k max:
    const recipient = txParams.to
    const hasRecipient = Boolean(recipient)
    let code
    if (recipient) code = await this.query.getCode(recipient)

    if (hasRecipient && (!code || code === '0x')) {
      txParams.gas = SIMPLE_GAS_COST
      txMeta.simpleSend = true // Prevents buffer addition
      return SIMPLE_GAS_COST
    }

    // if not, fall back to block gasLimit
    const blockGasLimitBN = hexToBn(blockGasLimitHex)
    const saferGasLimitBN = BnMultiplyByFraction(blockGasLimitBN, 19, 20)
    txParams.gas = bnToHex(saferGasLimitBN)

    // run tx
    return await this.query.estimateGas(txParams)
  }

  setTxGas (txMeta, blockGasLimitHex, estimatedGasHex) {
    txMeta.estimatedGas = addHexPrefix(estimatedGasHex)
    const txParams = txMeta.txParams

    // if gasLimit was specified and doesnt OOG,
    // use original specified amount
    if (txMeta.gasLimitSpecified || txMeta.simpleSend) {
      txMeta.estimatedGas = txParams.gas
      return
    }
    // if gasLimit not originally specified,
    // try adding an additional gas buffer to our estimation for safety
    const recommendedGasHex = this.addGasBuffer(txMeta.estimatedGas, blockGasLimitHex)
    txParams.gas = recommendedGasHex
    return
  }

  addGasBuffer (initialGasLimitHex, blockGasLimitHex) {
    const initialGasLimitBn = hexToBn(initialGasLimitHex)
    const blockGasLimitBn = hexToBn(blockGasLimitHex)
    const upperGasLimitBn = blockGasLimitBn.muln(0.9)
    const bufferedGasLimitBn = initialGasLimitBn.muln(1.5)

    // if initialGasLimit is above blockGasLimit, dont modify it
    if (initialGasLimitBn.gt(upperGasLimitBn)) return bnToHex(initialGasLimitBn)
    // if bufferedGasLimit is below blockGasLimit, use bufferedGasLimit
    if (bufferedGasLimitBn.lt(upperGasLimitBn)) return bnToHex(bufferedGasLimitBn)
    // otherwise use blockGasLimit
    return bnToHex(upperGasLimitBn)
  }

  async validateTxParams (txParams) {
    this.validateFrom(txParams)
    this.validateRecipient(txParams)
    if ('value' in txParams) {
      const value = txParams.value.toString()
      if (value.includes('-')) {
        throw new Error(`Invalid transaction value of ${txParams.value} not a positive number.`)
      }

      if (value.includes('.')) {
        throw new Error(`Invalid transaction value of ${txParams.value} number must be in wei`)
      }
    }
  }

  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')
  }

  validateRecipient (txParams) {
    if (txParams.to === '0x' || txParams.to === null ) {
      if (txParams.data) {
        delete txParams.to
      } else {
        throw new Error('Invalid recipient address')
      }
    } else if ( txParams.to !== undefined && !isValidAddress(txParams.to) ) {
      throw new Error('Invalid recipient address')
    }
    return txParams
  }
}