Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(Contract/ACI): Add ability to use contract with external deps #653

Merged
merged 1 commit into from
Sep 6, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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