Skip to content

Commit

Permalink
feat(Contract/ACI): Add ability to use contract with external deps(`i…
Browse files Browse the repository at this point in the history
…nclude "someLib"`) (#653)
  • Loading branch information
nduchak authored Sep 6, 2019
1 parent d5f1632 commit 9708b43
Show file tree
Hide file tree
Showing 4 changed files with 98 additions and 30 deletions.
25 changes: 13 additions & 12 deletions es/ae/contract.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,8 @@ async function handleCallError (result) {
* @param {Array} args Argument's for call
* @return {Promise<String>}
*/
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)
}

/**
Expand Down Expand Up @@ -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)) {
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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, {
Expand All @@ -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 {
Expand All @@ -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 }))
}

Expand Down
12 changes: 7 additions & 5 deletions es/contract/aci/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand All @@ -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)),
Expand Down Expand Up @@ -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 }
)
}
}
Expand All @@ -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
}
Expand Down
19 changes: 12 additions & 7 deletions es/contract/compiler.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand All @@ -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)
}

Expand All @@ -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
Expand Down Expand Up @@ -134,7 +138,8 @@ const ContractCompilerAPI = AsyncInit.compose(ContractBase, {
setCompilerUrl,
getCompilerVersion,
isInit,
checkCompatibility
checkCompatibility,
prepareCompilerOption
},
props: {
compilerVersion: null,
Expand Down
72 changes: 66 additions & 6 deletions test/integration/contract.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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) }
Expand Down Expand Up @@ -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 () => {
Expand Down Expand Up @@ -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')
Expand All @@ -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)
Expand All @@ -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)
})
Expand Down Expand Up @@ -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)
})
Expand Down Expand Up @@ -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)
})
Expand Down

0 comments on commit 9708b43

Please sign in to comment.