diff --git a/docs/guides/contract-events.md b/docs/guides/contract-events.md index 48df9922c0..bae6dc9cff 100644 --- a/docs/guides/contract-events.md +++ b/docs/guides/contract-events.md @@ -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', diff --git a/es/contract/aci/helpers.js b/es/contract/aci/helpers.js index 6ad7b34059..b37673d426 100644 --- a/es/contract/aci/helpers.js +++ b/es/contract/aci/helpers.js @@ -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 @@ -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`) @@ -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) } } ) @@ -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) + } +} diff --git a/es/contract/aci/index.js b/es/contract/aci/index.js index a377f5bbdd..e13555cc18 100644 --- a/es/contract/aci/index.js +++ b/es/contract/aci/index.js @@ -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 @@ -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 @@ -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) @@ -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, @@ -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, { @@ -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 diff --git a/test/integration/contract.js b/test/integration/contract.js index 65d3567b9b..8dbafd5d2e 100644 --- a/test/integration/contract.js +++ b/test/integration/contract.js @@ -538,6 +538,8 @@ describe('Contract', function () { let cInstance let eventResult let decodedEventsWithoutACI + let decodedEventsUsingACI + let decodedEventsUsingBuildInMethod before(async () => { cInstance = await contract.getContractInstance(testContract, { filesystem }) @@ -545,6 +547,8 @@ describe('Contract', function () { 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] }, @@ -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)) }) })