Skip to content

Commit

Permalink
feat(ACI): External contract integration (#1027)
Browse files Browse the repository at this point in the history
* feat(ACI): Proper resolving of remote contract type

* feat(Node): Retrieve transaction info only after transaction will be mind

* feat(ACI): Store external ACI. Adjust `getType` helper recursively going through external ACI. Adjust arguments/return type transformation.
Add Remote Contract address type recognition

* fix(ACI): Fix `isRemoteAddress` check

* feat(Test): Add type resolving tests for ACI remote Contract and extenrla typeDefs
  • Loading branch information
nduchak authored Jun 18, 2020
1 parent 2cb8cc2 commit a14d13a
Show file tree
Hide file tree
Showing 6 changed files with 65 additions and 28 deletions.
2 changes: 1 addition & 1 deletion .env
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
NODE_TAG=v5.5.4
COMPILER_TAG=v4.2.0
COMPILER_TAG=v4.3.2
2 changes: 1 addition & 1 deletion es/chain/node.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
22 changes: 13 additions & 9 deletions es/contract/aci/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 : []
}
}
Expand Down Expand Up @@ -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 })
}
Expand Down
7 changes: 4 additions & 3 deletions es/contract/aci/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
Expand Down Expand Up @@ -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')
Expand All @@ -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)
Expand Down
37 changes: 24 additions & 13 deletions es/contract/aci/transformation.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

/**
Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
23 changes: 22 additions & 1 deletion test/integration/contract.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down Expand Up @@ -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"
Expand All @@ -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
Expand Down Expand Up @@ -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')
})
})
})
})

0 comments on commit a14d13a

Please sign in to comment.