From 5c86086b9570b450e0fbea403808044fcfca52ff Mon Sep 17 00:00:00 2001 From: naz_dou Date: Fri, 6 Sep 2019 15:55:36 +0300 Subject: [PATCH] feat(Contract/ACI): Add ability to use contract with external deps(`include "someLib"`) --- es/ae/contract.js | 25 +++++++------ es/contract/aci/index.js | 12 +++--- es/contract/compiler.js | 19 ++++++---- test/integration/contract.js | 72 +++++++++++++++++++++++++++++++++--- 4 files changed, 98 insertions(+), 30 deletions(-) diff --git a/es/ae/contract.js b/es/ae/contract.js index 594c1a4ec1..31adca56cb 100644 --- a/es/ae/contract.js +++ b/es/ae/contract.js @@ -65,8 +65,8 @@ async function handleCallError (result) { * @param {Array} args Argument's for call * @return {Promise} */ -async function contractEncodeCall (source, name, args) { - return this.contractEncodeCallDataAPI(source, name, args) +async function contractEncodeCall (source, name, args, options) { + return this.contractEncodeCallDataAPI(source, name, args, options) } /** @@ -115,7 +115,7 @@ async function contractCallStatic (source, address, name, args = [], { top, opti : await this.address().catch(e => opt.dryRunAccount.pub) // Prepare call-data - const callData = await this.contractEncodeCall(source, name, args) + const callData = await this.contractEncodeCall(source, name, args, options) // Get block hash by height if (top && !isNaN(top)) { @@ -192,7 +192,7 @@ async function contractCall (source, address, name, args = [], options = {}) { const tx = await this.contractCallTx(R.merge(opt, { callerId: await this.address(opt), contractId: address, - callData: await this.contractEncodeCall(source, name, args) + callData: await this.contractEncodeCall(source, name, args, opt) })) const { hash, rawTx } = await this.send(tx, opt) @@ -203,7 +203,7 @@ async function contractCall (source, address, name, args = [], options = {}) { hash, rawTx, result, - decode: () => this.contractDecodeData(source, name, result.returnValue, result.returnType) + decode: () => this.contractDecodeData(source, name, result.returnValue, result.returnType, opt) } } else { await this.handleCallError(result) @@ -234,7 +234,7 @@ async function contractCall (source, address, name, args = [], options = {}) { */ async function contractDeploy (code, source, initState = [], options = {}) { const opt = R.merge(this.Ae.defaults, options) - const callData = await this.contractEncodeCall(source, 'init', initState) + const callData = await this.contractEncodeCall(source, 'init', initState, opt) const ownerId = await this.address(opt) const { tx, contractId } = await this.contractCreateTx(R.merge(opt, { @@ -253,8 +253,8 @@ async function contractDeploy (code, source, initState = [], options = {}) { transaction: hash, rawTx, 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, ...options.options } }), + 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) } }), createdAt: new Date() }) } else { @@ -279,11 +279,12 @@ async function contractDeploy (code, source, initState = [], options = {}) { * } */ async function contractCompile (source, options = {}) { - const bytecode = await this.compileContractAPI(source, options) + const opt = R.merge(this.Ae.defaults, options) + const bytecode = await this.compileContractAPI(source, opt) return Object.freeze(Object.assign({ - encodeCall: async (name, args) => this.contractEncodeCall(source, name, args), - deploy: async (init, options = {}) => this.contractDeploy(bytecode, source, init, options), - deployStatic: async (init, options = {}) => this.contractCallStatic(source, null, 'init', init, { bytecode, top: options.top, options }) + 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 })) } diff --git a/es/contract/aci/index.js b/es/contract/aci/index.js index 7b602f5a1d..18921128dc 100644 --- a/es/contract/aci/index.js +++ b/es/contract/aci/index.js @@ -60,6 +60,7 @@ async function prepareArgsForEncode (aci, params) { * @param {Object} [options] Options object * @param {Object} [options.aci] Contract ACI * @param {Object} [options.contractAddress] Contract address + * @param {Object} [options.filesystem] Contact source external deps * @param {Object} [options.opt] Contract options * @return {ContractInstance} JS Contract API * @example @@ -70,8 +71,8 @@ async function prepareArgsForEncode (aci, params) { * Also you can call contract like: await contractIns.methods.setState(123, options) * Then sdk decide to make on-chain or static call(dry-run API) transaction based on function is stateful or not */ -async function getContractInstance (source, { aci, contractAddress, opt } = {}) { - aci = aci || await this.contractGetACI(source) +async function getContractInstance (source, { aci, contractAddress, filesystem = {}, opt } = {}) { + aci = aci || await this.contractGetACI(source, { filesystem }) const defaultOptions = { skipArgsConvert: false, skipTransformDecoded: false, @@ -82,7 +83,8 @@ async function getContractInstance (source, { aci, contractAddress, opt } = {}) gas: 1600000 - 21000, top: null, // using for contract call static waitMined: true, - verify: false + verify: false, + filesystem } const instance = { interface: R.defaultTo(null, R.prop('interface', aci)), @@ -163,7 +165,7 @@ const call = ({ client, instance }) => async (fn, params = [], options = {}) => decodedResult: await transformDecodedData( fnACI.returns, await result.decode(), - { ...opt, compilerVersion: instance.compilerVersion, bindings: fnACI.bindings } + { ...opt, bindings: fnACI.bindings } ) } } @@ -190,7 +192,7 @@ const deploy = ({ client, instance }) => async (init = [], options = {}) => { } const compile = ({ client, instance }) => async () => { - const { bytecode } = await client.contractCompile(instance.source) + const { bytecode } = await client.contractCompile(instance.source, instance.options) instance.compiled = bytecode return instance.compiled } diff --git a/es/contract/compiler.js b/es/contract/compiler.js index e50d1f199b..c8f1b9c139 100644 --- a/es/contract/compiler.js +++ b/es/contract/compiler.js @@ -37,27 +37,28 @@ async function getCompilerVersion (options = {}) { async function contractEncodeCallDataAPI (source, name, args = [], options = {}) { this.isInit() - options = { ...this.compilerOptions, ...options } + options = this.prepareCompilerOption(options) return this.http .post('/encode-calldata', { source, function: name, arguments: args, options }, options) .then(({ calldata }) => calldata) } -async function contractDecodeCallDataByCodeAPI (bytecode, calldata, options = {}) { +async function contractDecodeCallDataByCodeAPI (bytecode, calldata, backend = this.compilerOptions.backend, options = {}) { this.isInit() return this.http - .post('/decode-calldata/bytecode', { bytecode, calldata }, options) + .post('/decode-calldata/bytecode', { bytecode, calldata, backend }, options) } async function contractDecodeCallDataBySourceAPI (source, fn, callData, options = {}) { this.isInit() + options = this.prepareCompilerOption(options) return this.http .post('/decode-calldata/source', { function: fn, source, calldata: callData }, options) } async function contractDecodeCallResultAPI (source, fn, callValue, callResult, options = {}) { this.isInit() - options = { ...this.compilerOptions, ...options } + options = this.prepareCompilerOption(options) return this.http .post('/decode-call-result', { function: fn, source, 'call-result': callResult, 'call-value': callValue, options }, options) } @@ -71,14 +72,14 @@ async function contractDecodeDataAPI (type, data, options = {}) { async function compileContractAPI (code, options = {}) { this.isInit() - options = { ...this.compilerOptions, ...options } + options = this.prepareCompilerOption(options) return this.http.post('/compile', { code, options }, options) .then(({ bytecode }) => bytecode) } async function contractGetACI (code, options = {}) { this.isInit() - options = { ...this.compilerOptions, ...options } + options = this.prepareCompilerOption(options) return this.http.post('/aci', { code, options }, options) } @@ -99,6 +100,9 @@ async function checkCompatibility ({ force = false, forceCompatibility = false } } } +function prepareCompilerOption (options = {}) { + return { ...this.compilerOptions, ...options, file_system: options.filesystem || {} } +} function isInit () { if (this.compilerVersion === null) throw Error('Compiler not defined') return true @@ -134,7 +138,8 @@ const ContractCompilerAPI = AsyncInit.compose(ContractBase, { setCompilerUrl, getCompilerVersion, isInit, - checkCompatibility + checkCompatibility, + prepareCompilerOption }, props: { compilerVersion: null, diff --git a/test/integration/contract.js b/test/integration/contract.js index 53098507af..83a95bdaa3 100644 --- a/test/integration/contract.js +++ b/test/integration/contract.js @@ -31,6 +31,15 @@ contract StateContract = entrypoint init(value) : state = { value = value } entrypoint retrieve() : string = state.value ` +const libContract = ` +namespace TestLib = + function sum(x: int, y: int) : int = x + y +` +const contractWithLib = ` +include "testLib" +contract Voting = + entrypoint sumNumbers(x: int, y: int) : int = TestLib.sum(x, y) +` const testContract = ` namespace Test = function double(x: int): int = x*2 @@ -39,6 +48,7 @@ namespace Test = contract Voting = entrypoint test() : int = 1 +include "testLib" contract StateContract = type number = int record state = { value: string, key: number, testOption: option(string) } @@ -169,6 +179,47 @@ describe('Contract', function () { }) .should.eventually.become('Hello World!') }) + describe('Namespaces', () => { + let deployed + it('Can compiler contract with external deps', async () => { + const filesystem = { + testLib: libContract + } + const compiled = await contract.contractCompile(contractWithLib, { filesystem }) + compiled.should.have.property('bytecode') + }) + it('Throw error when try to compile contract without providing external deps', async () => { + try { + await contract.contractCompile(contractWithLib) + } catch (e) { + e.message.indexOf('could not find include file').should.not.be.equal(-1) + } + }) + it('Can deploy contract with external deps', async () => { + const filesystem = { + testLib: libContract + } + const compiled = await contract.contractCompile(contractWithLib, { filesystem }) + deployed = await compiled.deploy() + deployed.should.have.property('address') + + const deployedStatic = await compiled.deployStatic([]) + deployedStatic.result.should.have.property('gasUsed') + deployedStatic.result.should.have.property('returnType') + + const encodedCallData = await compiled.encodeCall('sumNumbers', ['1', '2']) + encodedCallData.indexOf('cb_').should.not.be.equal(-1) + }) + it('Can call contract with external deps', async () => { + const callResult = await deployed.call('sumNumbers', ['1', '2']) + const decoded = await callResult.decode() + decoded.should.be.equal(3) + + const callStaticResult = await deployed.callStatic('sumNumbers', ['1', '2']) + const decoded2 = await callStaticResult.decode() + decoded2.should.be.equal(3) + }) + }) describe('Sophia Compiler', function () { it('compile', async () => { @@ -206,7 +257,10 @@ describe('Contract', function () { let contractObject it('Generate ACI object', async () => { - contractObject = await contract.getContractInstance(testContract, { opt: { ttl: 10 } }) + const filesystem = { + testLib: libContract + } + contractObject = await contract.getContractInstance(testContract, { filesystem, opt: { ttl: 10 } }) contractObject.should.have.property('interface') contractObject.should.have.property('aci') contractObject.should.have.property('source') @@ -216,6 +270,8 @@ describe('Contract', function () { contractObject.should.have.property('call') contractObject.should.have.property('deploy') contractObject.options.ttl.should.be.equal(10) + contractObject.options.should.have.property('filesystem') + contractObject.options.filesystem.should.have.property('testLib') const functionsFromACI = contractObject.aci.functions.map(({ name }) => name) const methods = Object.keys(contractObject.methods) R.equals(methods, functionsFromACI).should.be.equal(true) @@ -241,7 +297,7 @@ describe('Contract', function () { it('Deploy contract before compile', async () => { contractObject.compiled = null - await contractObject.methods.init('123', 1, Promise.resolve('hahahaha'), { }) + await contractObject.methods.init('123', 1, Promise.resolve('hahahaha'), {}) const isCompiled = contractObject.compiled.length && contractObject.compiled.slice(0, 3) === 'cb_' isCompiled.should.be.equal(true) }) @@ -459,7 +515,11 @@ describe('Contract', function () { objEq(result.decodedResult, { value: 'qwe', key: 1234, testOption: 'test' }).should.be.equal(true) }) it('Get Record With Option (Convert to JS object)', async () => { - await contractObject.methods.setRecord({ key: 1234, value: 'qwe', testOption: Promise.resolve('resolved string') }) + await contractObject.methods.setRecord({ + key: 1234, + value: 'qwe', + testOption: Promise.resolve('resolved string') + }) const result = await contractObject.methods.getRecord() objEq(result.decodedResult, { value: 'qwe', key: 1234, testOption: 'resolved string' }).should.be.equal(true) }) @@ -613,14 +673,14 @@ describe('Contract', function () { return res.decode().should.eventually.become([1, 2]) }) it('Call contract using using js type arguments', async () => { - const res = await contractObject.methods.listFn([ 1, 2 ]) + const res = await contractObject.methods.listFn([1, 2]) return res.decode().should.eventually.become([1, 2]) }) it('Call contract using using js type arguments and skip result transform', async () => { contractObject.setOptions({ skipTransformDecoded: true }) - const res = await contractObject.methods.listFn([ 1, 2 ]) + const res = await contractObject.methods.listFn([1, 2]) const decoded = await res.decode() - const decodedJSON = JSON.stringify([ 1, 2 ]) + const decodedJSON = JSON.stringify([1, 2]) contractObject.setOptions({ skipTransformDecoded: false }) JSON.stringify(decoded).should.be.equal(decodedJSON) })