Skip to content

Commit

Permalink
feat(ACI): Event decoding (#1006)
Browse files Browse the repository at this point in the history
* feat(ACI): Implement decoding of Events for `ContractInstance`
Add events decoding to build in ACI methods(exp: `cInstance.methods.emit.decodeEvents(eventsArray)`)
Add test's

* feat(ACI): Adjust event decoding guides

* fix(Lint): Fix linter error
  • Loading branch information
nduchak authored May 29, 2020
1 parent 51dfed9 commit 6b8e6fe
Show file tree
Hide file tree
Showing 4 changed files with 91 additions and 51 deletions.
8 changes: 7 additions & 1 deletion docs/guides/contract-events.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,14 @@ contract EventExample =
```
- Decode using ACI
```js
// Auto decode of events on contract call
const callRes = await contractIns.methods.emitEvents()
console.log(callRes.decodedEvents)
// 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',
Expand Down
49 changes: 49 additions & 0 deletions es/contract/aci/helpers.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as R from 'ramda'
import { unpackTx } from '../../tx/builder'
import { decodeEvents as unpackEvents, transform, transformDecodedData, validateArguments } from './transformation'

/**
* Get function schema from contract ACI object
Expand All @@ -8,6 +9,7 @@ import { unpackTx } from '../../tx/builder'
* @return {Object} function ACI
*/
export function getFunctionACI (aci, name) {
if (!aci) throw new Error('ACI required')
const fn = aci.functions.find(f => f.name === name)
if (!fn && name !== 'init') throw new Error(`Function ${name} doesn't exist in contract`)

Expand Down Expand Up @@ -49,6 +51,9 @@ export const buildContractMethods = (instance) => () => ({
const { opt, args } = parseArguments(aciArgs)(arguments)
if (name === 'init') return instance.deploy(args, opt)
return instance.call(name, args, { ...opt, callStatic: false })
},
decodeEvents (events) {
return instance.decodeEvents(name, events)
}
}
)
Expand Down Expand Up @@ -85,3 +90,47 @@ export const parseArguments = (aciArgs = []) => (args) => ({
})

export const unpackByteCode = (bytecode) => unpackTx(bytecode, false, 'cb').tx

/**
* Validated contract call arguments using contract ACI
* @function validateCallParams
* @rtype (aci: Object, params: Array) => Object
* @param {Object} aci Contract ACI
* @param {Array} params Contract call arguments
* @return Promise{Array} Object with validation errors
*/
export const prepareArgsForEncode = async (aci, params) => {
if (!aci || !aci.arguments) return params
// Validation
if (aci.arguments.length > params.length) {
throw new Error(`Function "${aci.name}" require ${aci.arguments.length} arguments of types [${aci.arguments.map(a => JSON.stringify(a.type))}] but get [${params.map(JSON.stringify)}]`)
}

validateArguments(aci, params)
const bindings = aci.bindings
// Cast argument from JS to Sophia type
return Promise.all(aci.arguments.map(async ({ type }, i) => transform(type, params[i], {
bindings
})))
}

export const decodeEvents = (events, fnACI) => {
if (!fnACI || !fnACI.event || !fnACI.event.length) return []

const eventsSchema = fnACI.event.map(e => {
const name = Object.keys(e)[0]
return { name, types: e[name] }
})
return unpackEvents(events, { schema: eventsSchema })
}

export const decodeCallResult = async (result, fnACI, opt) => {
return {
decodedResult: await transformDecodedData(
fnACI.returns,
await result.decode(),
{ ...opt, bindings: fnACI.bindings }
),
decodedEvents: decodeEvents(result.result.log, fnACI)
}
}
77 changes: 28 additions & 49 deletions es/contract/aci/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,43 +24,22 @@
*/

import * as R from 'ramda'
import { BigNumber } from 'bignumber.js'

import AsyncInit from '../../utils/async-init'
import semverSatisfies from '../../utils/semver-satisfies'
import {
validateArguments,
transform,
transformDecodedData,
decodeEvents
} from './transformation'
import { buildContractMethods, getFunctionACI } from './helpers'
buildContractMethods,
decodeCallResult,
decodeEvents,
getFunctionACI,
prepareArgsForEncode as prepareArgs
} from './helpers'
import { isAddressValid } from '../../utils/crypto'
import AsyncInit from '../../utils/async-init'
import { BigNumber } from 'bignumber.js'
import { COMPILER_LT_VERSION } from '../compiler'
import semverSatisfies from '../../utils/semver-satisfies'
import { AMOUNT, DEPOSIT, GAS, MIN_GAS_PRICE } from '../../tx/builder/schema'

/**
* Validated contract call arguments using contract ACI
* @function validateCallParams
* @rtype (aci: Object, params: Array) => Object
* @param {Object} aci Contract ACI
* @param {Array} params Contract call arguments
* @return Promise{Array} Object with validation errors
*/
export async function prepareArgsForEncode (aci, params) {
if (!aci || !aci.arguments) return params
// Validation
if (aci.arguments.length > params.length) {
throw new Error(`Function "${aci.name}" require ${aci.arguments.length} arguments of types [${aci.arguments.map(a => JSON.stringify(a.type))}] but get [${params.map(JSON.stringify)}]`)
}

validateArguments(aci, params)
const bindings = aci.bindings
// Cast argument from JS to Sophia type
return Promise.all(aci.arguments.map(async ({ type }, i) => transform(type, params[i], {
bindings
})))
}
// TODO remove when Breaking Changes release is coming
export const prepareArgsForEncode = prepareArgs

/**
* Generate contract ACI object with predefined js methods for contract usage - can be used for creating a reference to already deployed contracts
Expand Down Expand Up @@ -152,6 +131,15 @@ async function getContractInstance (source, { aci, contractAddress, filesystem =
* @return {Object} CallResult
*/
instance.call = call({ client: this, instance })
/**
* Decode Events
* @alias module:@aeternity/aepp-sdk/es/contract/aci
* @rtype (fn: String, events: Array) => DecodedEvents: Array
* @param {String} fn Function name
* @param {Array} events Array of encoded events(callRes.result.log)
* @return {Object} DecodedEvents
*/
instance.decodeEvents = eventDecode({ instance })

/**
* Generate proto function based on contract function using Contract ACI schema
Expand All @@ -165,21 +153,10 @@ async function getContractInstance (source, { aci, contractAddress, filesystem =
return instance
}

const decodeCallResult = async (result, fnACI, opt) => {
const eventsSchema = fnACI.event.map(e => {
const name = Object.keys(e)[0]
return { name, types: e[name] }
})

return {
decodedResult: await transformDecodedData(
fnACI.returns,
await result.decode(),
{ ...opt, bindings: fnACI.bindings }
),
decodedEvents: decodeEvents(result.result.log, { ...opt, schema: eventsSchema })
}
const eventDecode = ({ instance }) => (fn, events) => {
return decodeEvents(events, getFunctionACI(instance.aci, fn))
}

const call = ({ client, instance }) => async (fn, params = [], options = {}) => {
const opt = R.merge(instance.options, options)
const fnACI = getFunctionACI(instance.aci, fn)
Expand All @@ -191,7 +168,7 @@ const call = ({ client, instance }) => async (fn, params = [], options = {}) =>
BigNumber(opt.amount).gt(0) &&
(Object.prototype.hasOwnProperty.call(fnACI, 'payable') && !fnACI.payable)
) throw new Error(`You try to pay "${opt.amount}" to function "${fn}" which is not payable. Only payable function can accept tokens`)
params = !opt.skipArgsConvert ? await prepareArgsForEncode(fnACI, params) : params
params = !opt.skipArgsConvert ? await prepareArgs(fnACI, params) : params
const result = opt.callStatic
? await client.contractCallStatic(source, instance.deployInfo.address, fn, params, {
top: opt.top,
Expand All @@ -210,7 +187,7 @@ const deploy = ({ client, instance }) => async (init = [], options = {}) => {
const source = opt.source || instance.source

if (!instance.compiled) await instance.compile(opt)
init = !opt.skipArgsConvert ? await prepareArgsForEncode(fnACI, init) : init
init = !opt.skipArgsConvert ? await prepareArgs(fnACI, init) : init

if (opt.callStatic) {
return client.contractCallStatic(source, null, 'init', init, {
Expand Down Expand Up @@ -240,8 +217,10 @@ const compile = ({ client, instance }) => async (options = {}) => {
* @return {Object} Contract compiler instance
* @example ContractACI()
*/
export default AsyncInit.compose({

export const ContractACI = AsyncInit.compose({
methods: {
getContractInstance
}
})
export default ContractACI
8 changes: 7 additions & 1 deletion test/integration/contract.js
Original file line number Diff line number Diff line change
Expand Up @@ -538,13 +538,17 @@ describe('Contract', function () {
let cInstance
let eventResult
let decodedEventsWithoutACI
let decodedEventsUsingACI
let decodedEventsUsingBuildInMethod

before(async () => {
cInstance = await contract.getContractInstance(testContract, { filesystem })
await cInstance.deploy(['test', 1, 'some'])
eventResult = await cInstance.methods.emitEvents()
const { log } = await contract.tx(eventResult.hash)
decodedEventsWithoutACI = decodeEvents(log, { schema: events })
decodedEventsUsingACI = cInstance.decodeEvents('emitEvents', log)
decodedEventsUsingBuildInMethod = cInstance.methods.emitEvents.decodeEvents(log)
})
const events = [
{ name: 'AnotherEvent2', types: [SOPHIA_TYPES.bool, SOPHIA_TYPES.string, SOPHIA_TYPES.int] },
Expand Down Expand Up @@ -579,7 +583,9 @@ describe('Contract', function () {
events
.forEach((el, i) => {
describe(`Correct parse of ${el.name}(${el.types})`, () => {
it('ACI', () => checkEvents(eventResult.decodedEvents[i], el))
it('ACI call result', () => checkEvents(eventResult.decodedEvents[i], el))
it('ACI instance', () => checkEvents(decodedEventsUsingACI[i], el))
it('ACI instance methods', () => checkEvents(decodedEventsUsingBuildInMethod[i], el))
it('Without ACI', () => checkEvents(decodedEventsWithoutACI[i], el))
})
})
Expand Down

0 comments on commit 6b8e6fe

Please sign in to comment.