Skip to content

Commit

Permalink
Merge pull request #1109 from aeternity/feature/expose-tx-events
Browse files Browse the repository at this point in the history
Expose tx events in dry-run call
  • Loading branch information
davidyuk authored Mar 18, 2021
2 parents 3d5e4f2 + 37d39d6 commit 7f0a6fe
Show file tree
Hide file tree
Showing 6 changed files with 165 additions and 181 deletions.
175 changes: 84 additions & 91 deletions docs/guides/contract-events.md
Original file line number Diff line number Diff line change
@@ -1,99 +1,92 @@
# Contract Events

This guide describes the usage of [Sophia Events](https://github.com/aeternity/protocol/blob/master/contracts/sophia.md#events) using [Aeternity JS SDK](https://github.com/aeternity/aepp-sdk-js)

## Smart Contract
Here are code examples of decoding [Sophia Events](https://github.com/aeternity/aesophia/blob/lima/docs/sophia.md#events)
using SDK.

## SDK initialisation
```js
import { Universal, Node, MemoryAccount } from '@aeternity/aepp-sdk/es'
const eventContract = `SOURCE_HERE`
const node = await Node({ ... })
const account = MemoryAccount({ keypair })
const initParams = { accounts: [account], nodes: [{ name: 'test', instance: node }] }

const sdkInstance = await Universal({ ...initParams })

const contractIns = await sdkInstance.getContractInstance(eventContract)
```
contract EventExample =
type event = TheFirstEvent(int) | AnotherEvent(bool, int)

stateful entrypoint emitEvents () =>
Chain.emit(TheFirstEvent(42))
Chain.emit(AnotherEvent("This is not indexed", Contract.address))
## Decode using ACI
```js
// Auto decode of events on contract call
const callRes = await contractIns.methods.emitEvents()
// decode of events using contract instance
const decodedUsingInstance = contractIns.decodeEvents('emitEvents', callRes.result.log)
// decode of events using contract instance ACI methods
const decodedUsingInstanceMethods = contractIns.methods.emitEvents.decodeEvents(callRes.result.log)
// callRes.decodedEvents === decodedUsingInstance === decodedUsingInstanceMethods
console.log(callRes.decodedEvents || decodedUsingInstance || decodedUsingInstanceMethods)
/*
[
{ address: 'ct_N9s65ZMz9SUUKx2HDLCtxVNpEYrzzmYEuESdJwmbEsAo5TzxM',
data: 'cb_VGhpcyBpcyBub3QgaW5kZXhlZK+w140=',
topics:
[ '101640830366340000167918459210098337687948756568954742276612796897811614700269',
'21724616073664889730503604151713289093967432540957029082538744539361158114576' ],
name: 'AnotherEvent',
decoded:
[ 'This is not indexed',
'N9s65ZMz9SUUKx2HDLCtxVNpEYrzzmYEuESdJwmbEsAo5TzxM' ]
},
{ address: 'ct_N9s65ZMz9SUUKx2HDLCtxVNpEYrzzmYEuESdJwmbEsAo5TzxM',
data: 'cb_Xfbg4g==',
topics:
[ '25381774165057387707802602748622431964055296361151037811644748771109370239835',
42 ],
name: 'TheFirstEvent',
decoded: [ '42' ]
}
]
*/
```
## SDK usage
- Init SDK
```js
import { Universal, Node, MemoryAccount } from '@aeternity/aepp-sdk/es'
const eventContract = `SOURCE_HERE`
const node = await Node({ ... })
const account = MemoryAccount({ keypair })
const initParams = { accounts: [account], nodes: [{ name: 'test', instance: node }] }

const sdkInstance = await Universal({ ...initParams })

const contractIns = await sdkInstance.getContractInstance(eventContract)
```
- Decode using ACI
```js
// Auto decode of events on contract call
const callRes = await contractIns.methods.emitEvents()
// decode of events using contract instance
const decodedUsingInstance = contractIns.decodeEvents('emitEvents', callRes.result.log)
// decode of events using contract instance ACI methods
const decodedUsingInstanceMethods = contractIns.methods.emitEvents.decodeEvents(callRes.result.log)
// callRes.decodedEvents === decodedUsingInstance === decodedUsingInstanceMethods
console.log(callRes.decodedEvents || decodedUsingInstance || decodedUsingInstanceMethods)
/*
[
{ address: 'ct_N9s65ZMz9SUUKx2HDLCtxVNpEYrzzmYEuESdJwmbEsAo5TzxM',
data: 'cb_VGhpcyBpcyBub3QgaW5kZXhlZK+w140=',
topics:
[ '101640830366340000167918459210098337687948756568954742276612796897811614700269',
'21724616073664889730503604151713289093967432540957029082538744539361158114576' ],
name: 'AnotherEvent',
decoded:
[ 'This is not indexed',
'N9s65ZMz9SUUKx2HDLCtxVNpEYrzzmYEuESdJwmbEsAo5TzxM' ]
},
{ address: 'ct_N9s65ZMz9SUUKx2HDLCtxVNpEYrzzmYEuESdJwmbEsAo5TzxM',
data: 'cb_Xfbg4g==',
topics:
[ '25381774165057387707802602748622431964055296361151037811644748771109370239835',
42 ],
name: 'TheFirstEvent',
decoded: [ '42' ]
}
]
*/
```
- Decode without ACI
```js
import { decodeEvents, SOPHIA_TYPES } from '@aeternity/aepp-sdk/es/contract/aci/transformation'

const txHash = 'tx_asdad2d23...'
const tx = await sdkInstance.tx(txHash)
const eventsSchema = [
{ name: 'TheFirstEvent', types: [SOPHIA_TYPES.int] },
{ name: 'AnotherEvent', types: [SOPHIA_TYPES.string, SOPHIA_TYPES.address] },
]
const decodedEvents = decodeEvents(tx.log, { schema: eventsSchema })
console.log(decodedEvents)
/*
[
{ address: 'ct_N9s65ZMz9SUUKx2HDLCtxVNpEYrzzmYEuESdJwmbEsAo5TzxM',
data: 'cb_VGhpcyBpcyBub3QgaW5kZXhlZK+w140=',
topics:
[ '101640830366340000167918459210098337687948756568954742276612796897811614700269',
'21724616073664889730503604151713289093967432540957029082538744539361158114576' ],
name: 'AnotherEvent',
decoded:
[ 'This is not indexed',
'N9s65ZMz9SUUKx2HDLCtxVNpEYrzzmYEuESdJwmbEsAo5TzxM' ]
},
{ address: 'ct_N9s65ZMz9SUUKx2HDLCtxVNpEYrzzmYEuESdJwmbEsAo5TzxM',
data: 'cb_Xfbg4g==',
topics:
[ '25381774165057387707802602748622431964055296361151037811644748771109370239835',
42 ],
name: 'TheFirstEvent',
decoded: [ '42' ]
}
]
*/
```
## Decode without ACI
```js
import { decodeEvents, SOPHIA_TYPES } from '@aeternity/aepp-sdk/es/contract/aci/transformation'

const txHash = 'tx_asdad2d23...'
const tx = await sdkInstance.tx(txHash)

const eventsSchema = [
{ name: 'TheFirstEvent', types: [SOPHIA_TYPES.int] },
{ name: 'AnotherEvent', types: [SOPHIA_TYPES.string, SOPHIA_TYPES.address] },
]
const decodedEvents = decodeEvents(tx.log, { schema: eventsSchema })
console.log(decodedEvents)
/*
[
{ address: 'ct_N9s65ZMz9SUUKx2HDLCtxVNpEYrzzmYEuESdJwmbEsAo5TzxM',
data: 'cb_VGhpcyBpcyBub3QgaW5kZXhlZK+w140=',
topics:
[ '101640830366340000167918459210098337687948756568954742276612796897811614700269',
'21724616073664889730503604151713289093967432540957029082538744539361158114576' ],
name: 'AnotherEvent',
decoded:
[ 'This is not indexed',
'N9s65ZMz9SUUKx2HDLCtxVNpEYrzzmYEuESdJwmbEsAo5TzxM' ]
},
{ address: 'ct_N9s65ZMz9SUUKx2HDLCtxVNpEYrzzmYEuESdJwmbEsAo5TzxM',
data: 'cb_Xfbg4g==',
topics:
[ '25381774165057387707802602748622431964055296361151037811644748771109370239835',
42 ],
name: 'TheFirstEvent',
decoded: [ '42' ]
}
]
*/
```

# Related Link
- [Sophia Events](https://github.com/aeternity/protocol/blob/master/contracts/sophia.md#events)
- [Sophia Events Explained](https://github.com/aeternity/protocol/blob/master/contracts/sophia_explained.md)
- [Sophia Events](https://github.com/aeternity/aesophia/blob/lima/docs/sophia.md#events)
- [Sophia Events Explained](https://github.com/aeternity/protocol/blob/master/contracts/events.md)
125 changes: 50 additions & 75 deletions es/ae/contract.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ import { isBase64 } from '../utils/string'
import ContractCompilerAPI from '../contract/compiler'
import ContractBase from '../contract'
import ContractACI from '../contract/aci'
import BigNumber from 'bignumber.js'
import NodePool from '../node-pool'
import { AMOUNT, DEPOSIT, DRY_RUN_ACCOUNT, GAS, MIN_GAS_PRICE } from '../tx/builder/schema'
import { decode, produceNameId } from '../tx/builder/helpers'
Expand All @@ -49,7 +48,7 @@ async function sendAndProcess (tx, options) {
const result = await this.getTxInfo(txData.hash)

if (result.returnType !== 'ok') {
await this.handleCallError({ result, tx: TxObject({ tx: txData.rawTx }), rawTx: txData.rawTx })
await this._handleCallError(result, txData.rawTx)
}

return { hash: txData.hash, tx: TxObject({ tx: txData.rawTx }), result, txData, rawTx: txData.rawTx }
Expand All @@ -58,22 +57,23 @@ async function sendAndProcess (tx, options) {
/**
* Handle contract call error
* @function
* @private
* @alias module:@aeternity/aepp-sdk/es/ae/contract
* @category async
* @param {Object} result call result object
* @param {Object} tx Unpacked transaction
* @param {String} rawTx Raw transaction
* @throws Error Decoded error
* @return {Promise<void>}
*/
async function handleCallError ({ result, tx, rawTx }) {
async function _handleCallError (result, rawTx) {
const error = Buffer.from(result.returnValue).toString()
const decodedError = isBase64(error.slice(3))
? Buffer.from(error.slice(3), 'base64').toString()
: await this.contractDecodeDataAPI('string', error)
throw Object.assign(
new Error(`Invocation failed: ${error}. Decoded: ${decodedError}`), {
...result,
tx,
tx: TxObject({ tx: rawTx }),
error,
rawTx,
decodedError
Expand Down Expand Up @@ -126,76 +126,52 @@ async function contractDecodeData (source, fn, callValue, callResult, options) {
* @param {String} address Contract address
* @param {String} name Name of function to call
* @param {Array|String} args Argument's or callData for call/deploy transaction
* @param {Object} [options={}] Options
* @param {String} [options.top] Block hash on which you want to call contract
* @param {Object} [options]
* @param {Number|String} [options.top] Block height or hash on which you want to call contract
* @param {String} [options.bytecode] Block hash on which you want to call contract
* @param {Object} [options.options] Transaction options (fee, ttl, gas, amount, deposit)
* @param {Object} [options.options.filesystem] Contract external namespaces map
* @param {Object} [options.filesystem] Contract external namespaces map
* @return {Promise<Object>} Result object
* @example
* const callResult = await client.contractCallStatic(source, address, fnName, args = [], { top, options = {} })
* const callResult = await client.contractCallStatic(source, address, fnName, args)
* {
* result: TX_DATA,
* decode: (type) => Decode call result
* }
*/
async function contractCallStatic (source, address, name, args = [], { top, options = {}, bytecode } = {}) {
const opt = R.merge(this.Ae.defaults, options)
const callerId = opt.onAccount
? await this.address(opt)
: await this.address().catch(e => opt.dryRunAccount.pub)

// Prepare call-data
const callData = Array.isArray(args) ? await this.contractEncodeCall(source, name, args, opt) : args

// Get block hash by height
if (top && !isNaN(top)) {
top = (await this.getKeyBlock(top)).hash
async function contractCallStatic (source, address, name, args = [], options = {}) {
const callerId = await this.address(options).catch(() => DRY_RUN_ACCOUNT.pub)
if (typeof options.top === 'number') {
options.top = (await this.getKeyBlock(options.top)).hash
}
// Prepare nonce
const nonce = top ? (await this.getAccount(callerId, { hash: top })).nonce + 1 : undefined
if (name === 'init') {
// Prepare deploy transaction
const { tx } = await this.contractCreateTx(R.merge(opt, {
callData,
code: bytecode,
ownerId: callerId,
nonce
}))
return this.dryRunContractTx(tx, callerId, source, name, { ...opt, top })
} else {
// Prepare `call` transaction
const tx = await this.contractCallTx(R.merge(opt, {
callerId,
contractId: await this.resolveName(address, 'ct', { resolveByNode: true }),
callData,
nonce
}))
return this.dryRunContractTx(tx, callerId, source, name, { ...opt, top })
const txOpt = {
...this.Ae.defaults,
...options,
callData: Array.isArray(args) ? await this.contractEncodeCall(source, name, args, options) : args,
nonce: options.top && (await this.getAccount(callerId, { hash: options.top })).nonce + 1
}
}
const tx = name === 'init'
? (await this.contractCreateTx({
...txOpt,
code: options.bytecode,
ownerId: callerId
})).tx
: await this.contractCallTx({
...txOpt,
callerId,
contractId: await this.resolveName(address, 'ct', { resolveByNode: true })
})

async function dryRunContractTx (tx, callerId, source, name, opt = {}) {
const { top } = opt
// Resolve Account for Dry-run
const dryRunAmount = BigNumber(opt.dryRunAccount.amount).gt(BigNumber(opt.amount || 0)) ? opt.dryRunAccount.amount : opt.amount
const dryRunAccount = {
amount: dryRunAmount,
pubKey: callerId
}
// Dry-run
const [{ result: status, callObj, reason }] = (await this.txDryRun([tx], [dryRunAccount], top)).results
const { callObj, ...dryRunOther } = await this.txDryRun(tx, callerId, options)

// Process response
if (status !== 'ok') throw Object.assign(new Error('Dry run error, ' + reason), { tx: TxObject({ tx }), dryRunParams: { accounts: [dryRunAccount], top } })
const { returnType, returnValue } = callObj
if (returnType !== 'ok') {
await this.handleCallError({ result: callObj, tx: TxObject({ tx }) })
await this._handleCallError(callObj, tx)
}
return {
...dryRunOther,
tx: TxObject({ tx }),
result: callObj,
decode: () => this.contractDecodeData(source, name, returnValue, returnType, opt)
decode: () => this.contractDecodeData(source, name, returnValue, returnType, options)
}
}

Expand Down Expand Up @@ -261,7 +237,7 @@ async function contractCall (source, address, name, argsOrCallData = [], options
* }
*/
async function contractDeploy (code, source, initState = [], options = {}) {
const opt = R.merge(this.Ae.defaults, options)
const opt = { ...this.Ae.defaults, ...options }
const callData = Array.isArray(initState) ? await this.contractEncodeCall(source, 'init', initState, opt) : initState
const ownerId = await this.address(opt)

Expand All @@ -279,11 +255,10 @@ async function contractDeploy (code, source, initState = [], options = {}) {
rawTx,
txData,
address: contractId,
call: async (name, args = [], options = {}) => this.contractCall(source, contractId, name, args, R.merge(opt, options)),
callStatic: async (name, args = [], options = {}) => this.contractCallStatic(source, contractId, name, args, {
...options,
options: { onAccount: opt.onAccount, ...R.merge(opt, options.options) }
}),
call: (name, args, options) =>
this.contractCall(source, contractId, name, args, { ...opt, ...options }),
callStatic: (name, args, options) =>
this.contractCallStatic(source, contractId, name, args, { ...opt, ...options }),
createdAt: new Date()
})
}
Expand All @@ -307,17 +282,18 @@ async function contractDeploy (code, source, initState = [], options = {}) {
* }
*/
async function contractCompile (source, options = {}) {
const opt = R.merge(this.Ae.defaults, options)
const opt = { ...this.Ae.defaults, ...options }
const bytecode = await this.compileContractAPI(source, options)
return Object.freeze(Object.assign({
encodeCall: async (name, args) => this.contractEncodeCall(source, name, args, R.merge(opt, options)),
deploy: async (init, options = {}) => this.contractDeploy(bytecode, source, init, R.merge(opt, options)),
deployStatic: async (init, options = {}) => this.contractCallStatic(source, null, 'init', init, {
bytecode,
top: options.top,
options: R.merge(opt, options)
})
}, { bytecode }))
return Object.freeze({
encodeCall: (name, args) => this.contractEncodeCall(source, name, args, opt),
deploy: (init, options) => this.contractDeploy(bytecode, source, init, { ...opt, ...options }),
deployStatic: (init, options) => this.contractCallStatic(source, null, 'init', init, {
...opt,
...options,
bytecode
}),
bytecode
})
}

/**
Expand Down Expand Up @@ -475,8 +451,7 @@ export const ContractAPI = Ae.compose(ContractBase, ContractACI, {
contractCall,
contractEncodeCall,
contractDecodeData,
dryRunContractTx,
handleCallError,
_handleCallError,
// Delegation for contract
// AENS
delegateSignatureCommon,
Expand Down
Loading

0 comments on commit 7f0a6fe

Please sign in to comment.