Skip to content

Commit

Permalink
feat(aci): use dry-run to estimate gas and get rich errors
Browse files Browse the repository at this point in the history
  • Loading branch information
davidyuk committed Jan 28, 2022
1 parent 3852130 commit bb6977d
Show file tree
Hide file tree
Showing 8 changed files with 97 additions and 24 deletions.
8 changes: 3 additions & 5 deletions docs/transaction-options.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ These options are common and can be provided to every tx-type:
- 2 different strategies to use in order to determine the next nonce, See option `strategy` to learn more.
- `strategy` (default: `max`)
- The strategy to obtain next nonce for an account via node API
- If set to `max`, then the greatest nonce seen in the account or currently in the transaction pool is incremented with 1 and returned.
- If set to `max`, then the greatest nonce seen in the account or currently in the transaction pool is incremented with 1 and returned.
If the strategy is set to `continuity`, then transactions in the mempool are checked if there are gaps - missing nonces that prevent transactions with greater nonces to get included
- `ttl` (default: `0`)
- Should be set if you want the transaction to be only valid until a certain block height is reached.
Expand All @@ -51,7 +51,7 @@ The following options are sepcific for each tx-type.
- To be used for providing `aettos` (or `AE` with respective denomination) to a contract related transaction.
- `denomination` (default: `aettos`)
- You can specify the denomination of the `amount` that will be provided to the contract related transaction.
- `gas` (default: `25000`)
- `gas`
- Max. amount of gas to be consumed by the transaction. Learn more on [How to estimate gas?](#how-to-estimate-gas)
- `gasPrice` (default: `1e9`)
- To increase chances to get your transaction included quickly you can use a higher gasPrice.
Expand Down Expand Up @@ -91,6 +91,4 @@ The following options are sepcific for each tx-type.

## How to estimate gas?
- As æpp developer, it is reasonable to estimate the gas consumption for a contract call using the dry-run feature of the node **once** and provide a specific offset (e.g. multiplied by 1.5 or 2) as default in the æpp to ensure that contract calls are mined. Depending on the logic of the contract the gas consumption of a specific contract call can vary and therefore you should monitor the gas consumption and increase the default for the respective contract call accordingly over time.
- The default `gas` value of `25000` should cover all trivial contract calls. In case transactions start running out of gas you should proceed the way described above and estimate the required gas using the dry-run feature.


- By default, SDK estimates `gas` using dry-run endpoint. This means an extra request that makes contract iterations slower, but it is more developer friendly (support of heavy requests without adjustments, and verbose error messages).
5 changes: 2 additions & 3 deletions src/ae/contract.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
import Ae from './'
import ContractCompilerHttp from '../contract/compiler'
import getContractInstance from '../contract/aci'
import { AMOUNT, DEPOSIT, GAS, MIN_GAS_PRICE } from '../tx/builder/schema'
import { AMOUNT, DEPOSIT, MIN_GAS_PRICE } from '../tx/builder/schema'
import { decode, produceNameId } from '../tx/builder/helpers'

/**
Expand Down Expand Up @@ -258,8 +258,7 @@ export default Ae.compose(ContractCompilerHttp, {
defaults: {
deposit: DEPOSIT,
gasPrice: MIN_GAS_PRICE,
amount: AMOUNT,
gas: GAS
amount: AMOUNT
}
}
}
Expand Down
19 changes: 17 additions & 2 deletions src/contract/aci.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
*/

import { Encoder as Calldata } from '@aeternity/aepp-calldata'
import { DRY_RUN_ACCOUNT, DEPOSIT } from '../tx/builder/schema'
import { DRY_RUN_ACCOUNT, DEPOSIT, GAS_MAX } from '../tx/builder/schema'
import TxObject from '../tx/tx-object'
import { decode } from '../tx/builder/helpers'
import {
Expand Down Expand Up @@ -166,6 +166,13 @@ export default async function getContractInstance ({
throw new NodeInvocationError(message)
}

const estimateGas = async (name, params, options) => {
const { result: { gasUsed } } =
await instance.call(name, params, { ...options, callStatic: true })
// taken from https://github.com/aeternity/aepp-sdk-js/issues/1286#issuecomment-977814771
return Math.floor(gasUsed * 1.25)
}

/**
* Deploy contract
* @alias module:@aeternity/aepp-sdk/es/contract/aci
Expand All @@ -183,6 +190,7 @@ export default async function getContractInstance ({
const ownerId = await this.address(opt)
const { tx, contractId } = await this.contractCreateTx({
...opt,
gas: opt.gas ?? await estimateGas('init', params, opt),
callData: instance.calldata.encode(instance._name, 'init', params),
code: instance.bytecode,
ownerId
Expand Down Expand Up @@ -249,6 +257,7 @@ export default async function getContractInstance ({
}
const txOpt = {
...opt,
gas: opt.gas ?? GAS_MAX,
callData,
nonce: opt.nonce ??
(opt.top && (await this.getAccount(callerId, { hash: opt.top })).nonce + 1)
Expand All @@ -261,7 +270,13 @@ export default async function getContractInstance ({
await handleCallError(callObj)
res = { ...dryRunOther, tx: TxObject({ tx }), result: callObj }
} else {
const tx = await this.contractCallTx({ ...opt, callerId, contractId, callData })
const tx = await this.contractCallTx({
...opt,
gas: opt.gas ?? await estimateGas(fn, params, opt),
callerId,
contractId,
callData
})
res = await sendAndProcess(tx, opt)
}
if (opt.waitMined || opt.callStatic) {
Expand Down
3 changes: 2 additions & 1 deletion src/contract/ga/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
* @example import { GeneralizeAccount } from '@aeternity/aepp-sdk'
*/
import Contract from '../../ae/contract'
import { TX_TYPE } from '../../tx/builder/schema'
import { TX_TYPE, GAS_ABOVE_AVERAGE } from '../../tx/builder/schema'
import { buildTx, unpackTx } from '../../tx/builder'
import { prepareGaParams } from './helpers'
import { hash } from '../../utils/crypto'
Expand Down Expand Up @@ -91,6 +91,7 @@ async function createGeneralizeAccount (authFnName, source, args = [], options =
const contract = await this.getContractInstance({ source })
await contract.compile()
const { tx, contractId } = await this.gaAttachTx({
gas: GAS_ABOVE_AVERAGE,
...opt,
ownerId,
code: contract.bytecode,
Expand Down
3 changes: 2 additions & 1 deletion src/tx/builder/schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ export const RESPONSE_TTL = { type: 'delta', value: 10 }
// # CONTRACT
export const DEPOSIT = 0
export const AMOUNT = 0
export const GAS = 25000
export const GAS_ABOVE_AVERAGE = 25000 // TODO: don't use this
export const GAS_MAX = 1600000 - 21000
export const MIN_GAS_PRICE = 1e9
export const MAX_AUTH_FUN_GAS = 50000
export const DRY_RUN_ACCOUNT = { pub: 'ak_11111111111111111111111111111111273Yts', amount: '100000000000000000000000000000000000' }
Expand Down
58 changes: 56 additions & 2 deletions test/integration/contract-aci.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ contract StateContract =
entrypoint init(value: string, key: int, testOption: option(string)) : state = { value = value, key = key, testOption = testOption }
entrypoint retrieve() : string*int = (state.value, state.key)
stateful entrypoint setKey(key: number) = put(state{key = key})
entrypoint remoteContract(a: RemoteI) : int = 1
entrypoint remoteArgs(a: RemoteI.test_record) : RemoteI.test_type = 1
Expand Down Expand Up @@ -126,6 +127,10 @@ contract StateContract =
Chain.event(AnotherEvent2(true, "This is not indexed", 1))
entrypoint chainTtlFn(t: Chain.ttl): Chain.ttl = t
stateful entrypoint recursion(t: string): string =
put(state{value = t})
recursion(t)
`
const filesystem = {
testLib: libContractSource
Expand All @@ -147,7 +152,12 @@ describe('Contract instance', function () {
})

it('generates by source code', async () => {
testContract = await sdk.getContractInstance({ source: testContractSource, filesystem, ttl: 0 })
sdk.Ae.defaults.testProperty = 'test'
testContract = await sdk.getContractInstance({
source: testContractSource, filesystem, ttl: 0, gas: 15000
})
delete sdk.Ae.defaults.testProperty
expect(testContract.options.testProperty).to.be.equal('test')
testContract.should.have.property('source')
testContract.should.have.property('bytecode')
testContract.should.have.property('deployInfo')
Expand All @@ -173,7 +183,7 @@ describe('Contract instance', function () {
it('deploys', async () => {
const deployInfo = await testContract.deploy(['test', 1, 'hahahaha'], { amount: 42 })
expect(deployInfo.address.startsWith('ct_')).to.equal(true)
expect(deployInfo.txData.tx.gas).to.be.equal(25000)
expect(deployInfo.txData.tx.gas).to.be.equal(15000)
expect(deployInfo.txData.tx.amount).to.be.equal(42)
expect(deployInfo.txData.gasUsed).to.be.equal(209)
expect(testContract.bytecode.startsWith('cb_')).to.be.equal(true)
Expand Down Expand Up @@ -293,6 +303,50 @@ describe('Contract instance', function () {
result.callerId.should.be.equal(onAccount)
})

describe('Gas', () => {
let contract

before(async () => {
contract = await sdk.getContractInstance({ source: testContractSource, filesystem })
})

it('estimates gas by default for contract deployments', async () => {
const { tx: { gas }, gasUsed } = (await contract.deploy(['test', 42])).txData
expect(gasUsed).to.be.equal(160)
expect(gas).to.be.equal(200)
})

it('overrides gas through getContractInstance options for contract deployments', async () => {
const ct = await sdk.getContractInstance({
source: testContractSource, filesystem, gas: 300
})
const { tx: { gas }, gasUsed } = (await ct.deploy(['test', 42])).txData
expect(gasUsed).to.be.equal(160)
expect(gas).to.be.equal(300)
})

it('estimates gas by default for contract calls', async () => {
const { tx: { gas }, gasUsed } = (await contract.methods.setKey(2)).txData
expect(gasUsed).to.be.equal(61)
expect(gas).to.be.equal(76)
})

it('overrides gas through options for contract calls', async () => {
const { tx: { gas }, gasUsed } = (await contract.methods.setKey(3, { gas: 100 })).txData
expect(gasUsed).to.be.equal(61)
expect(gas).to.be.equal(100)
})

it('runs out of gas with correct message', async () => {
await expect(contract.methods.setKey(42, { gas: 10, callStatic: true }))
.to.be.rejectedWith('Invocation failed: "Out of gas"')
await expect(contract.methods.setKey(42, { gas: 10 }))
.to.be.rejectedWith('Invocation failed')
await expect(contract.methods.recursion('infinite'))
.to.be.rejectedWith('Invocation failed: "Out of gas"')
})
})

describe('Events parsing', () => {
let remoteContract
let eventResult
Expand Down
9 changes: 4 additions & 5 deletions test/integration/contract.js
Original file line number Diff line number Diff line change
Expand Up @@ -233,11 +233,10 @@ describe('Contract', function () {

it('initializes contract state', async () => {
const data = 'Hello World!'
return sdk.contractCompile(stateContract)
.then(bytecode => bytecode.deploy([data]))
.then(deployed => deployed.call('retrieve'))
.then(result => result.decodedResult)
.should.eventually.become('Hello World!')
const bytecode = await sdk.contractCompile(stateContract)
const deployed = await bytecode.deploy([data])
const result = await deployed.call('retrieve')
expect(result.decodedResult).to.be.equal('Hello World!')
})

describe('Namespaces', () => {
Expand Down
16 changes: 11 additions & 5 deletions test/integration/transaction.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { describe, it, before } from 'mocha'
import { encodeBase58Check, encodeBase64Check, generateKeyPair, salt } from '../../src/utils/crypto'
import { getSdk } from './index'
import { commitmentHash, oracleQueryId } from '../../src/tx/builder/helpers'
import { GAS_MAX, MIN_GAS_PRICE } from '../../src/tx/builder/schema'
import { MemoryAccount } from '../../src'
import { AE_AMOUNT_FORMATS } from '../../src/utils/amount-formatter'
import { unpackTx } from '../../src/tx/builder'
Expand Down Expand Up @@ -52,8 +53,6 @@ contract Identity =
`
let contractId
const deposit = 4
const gasPrice = 1000000000
const gas = 1600000 - 21000 // MAX GAS

let _salt
let commitmentId
Expand Down Expand Up @@ -143,8 +142,8 @@ describe('Native Transaction', function () {
code: contract.bytecode,
deposit,
amount,
gas,
gasPrice,
gas: GAS_MAX,
gasPrice: MIN_GAS_PRICE,
callData: contract.calldata.encode('Identity', 'init', [])
}
const txFromAPI = await sdk.contractCreateTx(params)
Expand All @@ -161,7 +160,14 @@ describe('Native Transaction', function () {
const callData = contract.calldata.encode('Identity', 'getArg', [2])
const owner = await sdk.address()

const params = { callerId: owner, contractId, amount, gas, gasPrice, callData }
const params = {
callerId: owner,
contractId,
amount,
gas: GAS_MAX,
gasPrice: MIN_GAS_PRICE,
callData
}
const txFromAPI = await sdk.contractCallTx(params)
const nativeTx = await sdkNative.contractCallTx(params)
txFromAPI.should.be.equal(nativeTx)
Expand Down

0 comments on commit bb6977d

Please sign in to comment.