diff --git a/.env b/.env index 428c633e53..235b02e557 100644 --- a/.env +++ b/.env @@ -1,2 +1,2 @@ NODE_TAG=v5.5.4 -COMPILER_TAG=v4.2.0 +COMPILER_TAG=v4.3.2 diff --git a/es/chain/node.js b/es/chain/node.js index c3123c3538..f5abaad238 100644 --- a/es/chain/node.js +++ b/es/chain/node.js @@ -101,7 +101,7 @@ async function getBalance (address, { height, hash, format = AE_AMOUNT_FORMATS.A async function tx (hash, info = true) { const tx = await this.api.getTransactionByHash(hash) - if (['ContractCreateTx', 'ContractCallTx', 'ChannelForceProgressTx'].includes(tx.tx.type) && info) { + if (['ContractCreateTx', 'ContractCallTx', 'ChannelForceProgressTx'].includes(tx.tx.type) && info && tx.blockHeight !== -1) { try { return { ...tx, ...await this.getTxInfo(hash) } } catch (e) { diff --git a/es/contract/aci/helpers.js b/es/contract/aci/helpers.js index b37673d426..0e9cfa52b4 100644 --- a/es/contract/aci/helpers.js +++ b/es/contract/aci/helpers.js @@ -6,20 +6,24 @@ import { decodeEvents as unpackEvents, transform, transformDecodedData, validate * Get function schema from contract ACI object * @param {Object} aci Contract ACI object * @param {String} name Function name + * @param external * @return {Object} function ACI */ -export function getFunctionACI (aci, name) { +export function getFunctionACI (aci, name, { external }) { 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`) return { ...fn, - bindings: { - state: aci.state, - typedef: aci.type_defs, - contractName: aci.name - }, + bindings: [ + { + state: aci.state, + type_defs: aci.type_defs, + name: aci.name + }, + ...external.map(R.pick(['state', 'type_defs', 'name'])) + ], event: aci.event ? aci.event.variant : [] } } @@ -64,18 +68,18 @@ export const buildContractMethods = (instance) => () => ({ ...instance.aci ? { init: Object.assign( function () { - const { arguments: aciArgs } = getFunctionACI(instance.aci, 'init') + const { arguments: aciArgs } = getFunctionACI(instance.aci, 'init', { external: instance.externalAci }) const { opt, args } = parseArguments(aciArgs)(arguments) return instance.deploy(args, opt) }, { get () { - const { arguments: aciArgs } = getFunctionACI(instance.aci, 'init') + const { arguments: aciArgs } = getFunctionACI(instance.aci, 'init', { external: instance.externalAci }) const { opt, args } = parseArguments(aciArgs)(arguments) return instance.deploy(args, { ...opt, callStatic: true }) }, send () { - const { arguments: aciArgs } = getFunctionACI(instance.aci, 'init') + const { arguments: aciArgs } = getFunctionACI(instance.aci, 'init', { external: instance.externalAci }) const { opt, args } = parseArguments(aciArgs)(arguments) return instance.deploy(args, { ...opt, callStatic: false }) } diff --git a/es/contract/aci/index.js b/es/contract/aci/index.js index f34aec080a..ffd8061a8f 100644 --- a/es/contract/aci/index.js +++ b/es/contract/aci/index.js @@ -79,6 +79,7 @@ async function getContractInstance (source, { aci, contractAddress, filesystem = const instance = { interface: R.defaultTo(null, R.prop('interface', aci)), aci: R.defaultTo(null, R.path(['encoded_aci', 'contract'], aci)), + externalAci: aci.external_encoded_aci ? aci.external_encoded_aci.map(a => a.contract || a.namespace) : [], source, compiled: null, deployInfo: { address: contractAddress }, @@ -155,12 +156,12 @@ async function getContractInstance (source, { aci, contractAddress, filesystem = } const eventDecode = ({ instance }) => (fn, events) => { - return decodeEvents(events, getFunctionACI(instance.aci, fn)) + return decodeEvents(events, getFunctionACI(instance.aci, fn, { external: instance.externalAci })) } const call = ({ client, instance }) => async (fn, params = [], options = {}) => { const opt = R.merge(instance.options, options) - const fnACI = getFunctionACI(instance.aci, fn) + const fnACI = getFunctionACI(instance.aci, fn, { external: instance.externalAci }) const source = opt.source || instance.source if (!fn) throw new Error('Function name is required') @@ -184,7 +185,7 @@ const call = ({ client, instance }) => async (fn, params = [], options = {}) => const deploy = ({ client, instance }) => async (init = [], options = {}) => { const opt = R.merge(instance.options, options) - const fnACI = getFunctionACI(instance.aci, 'init') + const fnACI = getFunctionACI(instance.aci, 'init', { external: instance.externalAci }) const source = opt.source || instance.source if (!instance.compiled) await instance.compile(opt) diff --git a/es/contract/aci/transformation.js b/es/contract/aci/transformation.js index 56e7dd6d24..8cf25dd793 100644 --- a/es/contract/aci/transformation.js +++ b/es/contract/aci/transformation.js @@ -116,16 +116,32 @@ export function injectVars (t, aciType) { * @return {Object} */ export function linkTypeDefs (t, bindings) { - const [, typeDef] = typeof t === 'object' ? Object.keys(t)[0].split('.') : t.split('.') + const [root, typeDef] = typeof t === 'object' ? Object.keys(t)[0].split('.') : t.split('.') + const contractTypeDefs = bindings.find(c => c.name === root) const aciType = [ - ...bindings.typedef, - { name: 'state', typedef: bindings.state, vars: [] } + ...contractTypeDefs.type_defs, + { name: 'state', typedef: contractTypeDefs.state, vars: [] } ].find(({ name }) => name === typeDef) if (aciType.vars.length) { aciType.typedef = injectVars(t, aciType) } + return isTypedDefOrState(aciType.typedef, bindings) ? linkTypeDefs(aciType.typedef, bindings) : aciType.typedef +} + +const isTypedDefOrState = (t, bindings) => { + if (!['string', 'object'].includes(typeof t)) return false + + t = typeof t === 'object' ? Object.keys(t)[0] : t + const [root, ...path] = t.split('.') + // Remote Contract Address + if (!path.length) return false + return bindings.map(c => c.name).includes(root) +} - return aciType.typedef +const isRemoteAddress = (t) => { + if (typeof t !== 'string') return false + const [root, ...path] = t.split('.') + return !path.length && !Object.values(SOPHIA_TYPES).includes(root) } /** @@ -137,14 +153,10 @@ export function linkTypeDefs (t, bindings) { export function readType (type, { bindings } = {}) { let [t] = Array.isArray(type) ? type : [type] + // If remote address + if (isRemoteAddress(t)) return { t: SOPHIA_TYPES.address } // Link State and typeDef - if ( - (typeof t === 'string' && t.indexOf(bindings.contractName) !== -1) || - (typeof t === 'object' && Object.keys(t)[0] && Object.keys(t)[0].indexOf(bindings.contractName) !== -1) - ) { - t = linkTypeDefs(t, bindings) - } - + if (isTypedDefOrState(t, bindings)) t = linkTypeDefs(t, bindings) // Map, Tuple, List, Record, Bytes if (typeof t === 'object') { const [[baseType, generic]] = Object.entries(t) @@ -312,9 +324,8 @@ export function transformDecodedData (aci, result, { skipTransformDecoded = fals * @return {Object} JoiSchema */ export function prepareSchema (type, { bindings } = {}) { - let { t, generic } = readType(type, { bindings }) + const { t, generic } = readType(type, { bindings }) - if (!Object.values(SOPHIA_TYPES).includes(t)) t = SOPHIA_TYPES.address // Handle Contract address transformation switch (t) { case SOPHIA_TYPES.int: return Joi.number().error(getJoiErrorMsg) diff --git a/test/integration/contract.js b/test/integration/contract.js index 8dbafd5d2e..63fb2d5716 100644 --- a/test/integration/contract.js +++ b/test/integration/contract.js @@ -21,8 +21,9 @@ import { decode } from '../../es/tx/builder/helpers' import * as R from 'ramda' import { randomName } from './aens' -import { decodeEvents, SOPHIA_TYPES } from '../../es/contract/aci/transformation' +import { decodeEvents, readType, SOPHIA_TYPES } from '../../es/contract/aci/transformation' import { hash, personalMessageToBinary } from '../../es/utils/crypto' +import { getFunctionACI } from '../../es/contract/aci/helpers' const identityContract = ` contract Identity = @@ -55,6 +56,9 @@ namespace Test = contract Voting = + type test_type = int + record state = { value: string, key: test_type, testOption: option(string) } + record test_record = { value: string, key: list(test_type) } entrypoint test : () => int include "testLib" @@ -70,6 +74,8 @@ 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) + entrypoint remoteContract(a: Voting) : int = 1 + entrypoint remoteArgs(a: Voting.test_record) : Voting.test_type = 1 entrypoint intFn(a: int) : int = a payable entrypoint stringFn(a: string) : string = a entrypoint boolFn(a: bool) : bool = a @@ -1090,5 +1096,20 @@ describe('Contract', function () { return result.decode().should.eventually.become(0) }) }) + describe('Type resolving', () => { + let cInstance + before(async () => { + cInstance = await contract.getContractInstance(testContract, { filesystem }) + }) + it('Resolve remote contract type', async () => { + const fnACI = getFunctionACI(cInstance.aci, 'remoteContract', { external: cInstance.externalAci }) + readType('Voting', { bindings: fnACI.bindings }).t.should.be.equal('address') + }) + it('Resolve external contract type', async () => { + const fnACI = getFunctionACI(cInstance.aci, 'remoteArgs', { external: cInstance.externalAci }) + readType(fnACI.arguments[0].type, { bindings: fnACI.bindings }).should.eql(JSON.parse('{"t":"record","generic":[{"name":"value","type":"string"},{"name":"key","type":{"list":["Voting.test_type"]}}]}')) + readType(fnACI.returns, { bindings: fnACI.bindings }).t.should.be.equal('int') + }) + }) }) })