diff --git a/es/contract/aci.js b/es/contract/aci.js index 7846c8a761..b3eb9ff022 100644 --- a/es/contract/aci.js +++ b/es/contract/aci.js @@ -22,11 +22,13 @@ * @export ContractACI * @example import ContractACI from '@aeternity/aepp-sdk/es/contract/aci' */ +import Joi from 'joi-browser' + import AsyncInit from '../utils/async-init' import { decode } from '../tx/builder/helpers' import { encodeBase58Check } from '../utils/crypto' import { toBytes } from '../utils/bytes' -import Joi from 'joi-browser' +import * as R from 'ramda' const SOPHIA_TYPES = [ 'int', @@ -45,6 +47,7 @@ function encodeAddress (address, prefix = 'ak') { const encodedAddress = encodeBase58Check(addressBuffer) return `${prefix}_${encodedAddress}` } + /** * Transform decoded data to JS type * @param aci @@ -279,6 +282,8 @@ function getFunctionACI (aci, name) { * @param {String} source Contract source code * @param {Object} [options] Options object * @param {Object} [options.aci] Contract ACI + * @param {Object} [options.contractAddress] Contract address + * @param {Object} [options.opt] Contract options * @return {ContractInstance} JS Contract API * @example * const contractIns = await client.getContractInstance(sourceCode) @@ -287,14 +292,30 @@ function getFunctionACI (aci, name) { * const callResult = await contractIns.call('setState', [123]) * const staticCallResult = await contractIns.call('setState', [123], { callStatic: true }) */ -async function getContractInstance (source, { aci, contractAddress } = {}) { +async function getContractInstance (source, { aci, contractAddress, opt } = {}) { aci = aci || await this.contractGetACI(source) + const defaultOptions = { + skipArgsConvert: false, + skipTransformDecoded: false, + callStatic: false, + deposit: 0, + gasPrice: 1000000000, // min gasPrice 1e9 + amount: 0, + gas: 1600000 - 21000, + top: null, // using for contract call static + waitMined: true, + verify: false + } const instance = { interface: aci.interface, aci: aci.encoded_aci.contract, source, compiled: null, - deployInfo: { address: contractAddress } + deployInfo: { address: contractAddress }, + options: R.merge(defaultOptions, opt), + setOptions (opt) { + this.options = R.merge(this.options, opt) + } } /** * Compile contract @@ -327,6 +348,21 @@ async function getContractInstance (source, { aci, contractAddress } = {}) { */ instance.call = call(this).bind(instance) + instance.methods = instance + .aci + .functions + .reduce( + (acc, { name }) => ({ + ...acc, + [name]: function () { + return name !== 'init' + ? instance.call(name, Object.values(arguments)) + : instance.deploy(Object.values(arguments)) + } + }), + {} + ) + return instance } @@ -351,42 +387,40 @@ function transformReturnType (returns) { } function call (self) { - return async function (fn, params = [], options = { - skipArgsConvert: false, - skipTransformDecoded: false, - callStatic: false - }) { + return async function (fn, params = [], options = {}) { + const opt = R.merge(this.options, options) const fnACI = getFunctionACI(this.aci, fn) if (!fn) throw new Error('Function name is required') if (!this.deployInfo.address) throw new Error('You need to deploy contract before calling!') - params = !options.skipArgsConvert ? await prepareArgsForEncode(fnACI, params) : params - const result = options.callStatic - ? await self.contractCallStatic(this.source, this.deployInfo.address, fn, params, { - top: options.top, - options + params = !opt.skipArgsConvert ? await prepareArgsForEncode(fnACI, params) : params + const result = opt.callStatic + ? await self.contractCallStatic(opt.source || this.source, this.deployInfo.address, fn, params, { + top: opt.top, + opt }) - : await self.contractCall(this.source, this.deployInfo.address, fn, params, options) + : await self.contractCall(opt.source || this.source, this.deployInfo.address, fn, params, opt) return { ...result, - decode: async (type, opt = {}) => + decode: async (type, decodeOptions = {}) => transformDecodedData( fnACI.returns, await self.contractDecodeData(type || transformReturnType(fnACI.returns), result.result.returnValue), - { ...options, ...opt } + { ...opt, ...decodeOptions } ) } } } function deploy (self) { - return async function (init = [], options = { skipArgsConvert: false }) { + return async function (init = [], options = {}) { + const opt = R.merge(this.options, options) const fnACI = getFunctionACI(this.aci, 'init') if (!this.compiled) await this.compile() - init = !options.skipArgsConvert ? await prepareArgsForEncode(fnACI, init) : init + init = !opt.skipArgsConvert ? await prepareArgsForEncode(fnACI, init) : init - const { owner, transaction, address, createdAt, result } = await self.contractDeploy(this.compiled, this.source, init, options) - this.deployInfo = { owner, transaction, address, createdAt, result } + const { owner, transaction, address, createdAt, result, rawTx } = await self.contractDeploy(this.compiled, opt.source || this.source, init, opt) + this.deployInfo = { owner, transaction, address, createdAt, result, rawTx } return this } } diff --git a/test/integration/contract.js b/test/integration/contract.js index 3a80d8e2bb..a335f01569 100644 --- a/test/integration/contract.js +++ b/test/integration/contract.js @@ -17,6 +17,7 @@ import { describe, it, before } from 'mocha' import { configure, plan, ready } from './' +import * as R from 'ramda' const identityContract = ` contract Identity = @@ -160,7 +161,7 @@ describe('Contract', function () { let contractObject it('Generate ACI object', async () => { - contractObject = await contract.getContractInstance(testContract) + contractObject = await contract.getContractInstance(testContract, { opt: { amount: 10000, ttl: 10 } }) contractObject.should.have.property('interface') contractObject.should.have.property('aci') contractObject.should.have.property('source') @@ -169,6 +170,10 @@ describe('Contract', function () { contractObject.should.have.property('compile') contractObject.should.have.property('call') contractObject.should.have.property('deploy') + contractObject.options.amount.should.be.equal(10000) + const functionsFromACI = contractObject.aci.functions.map(({ name }) => name) + const methods = Object.keys(contractObject.methods) + R.equals(methods, functionsFromACI).should.be.equal(true) }) it('Compile contract', async () => { await contractObject.compile() @@ -179,7 +184,7 @@ describe('Contract', function () { describe('Deploy contract', function () { it('Deploy contract before compile', async () => { contractObject.compiled = null - await contractObject.deploy(['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) }) @@ -188,120 +193,120 @@ describe('Contract', function () { describe('INT', function () { it('Invalid', async () => { try { - await contractObject.call('intFn', ['asd']) + await contractObject.methods.intFn('asd') } catch (e) { e.message.should.be.equal('"Argument" at position 0 fails because [Value "[asd]" at path: [0] not a number]') } }) it('Valid', async () => { - await contractObject.call('intFn', [1]) + await contractObject.methods.intFn(1) }) }) describe('STRING', function () { it('Invalid', async () => { try { - await contractObject.call('stringFn', [123]) + await contractObject.methods.stringFn(123) } catch (e) { e.message.should.be.equal('"Argument" at position 0 fails because [Value "123" at path: [0] not a string]') } }) it('Valid', async () => { - await contractObject.call('stringFn', ['string']) + await contractObject.methods.stringFn('string') }) }) describe('ADDRESS', function () { it('Invalid address', async () => { try { - await contractObject.call('addressFn', ['asdasasd']) + await contractObject.methods.addressFn('asdasasd') } catch (e) { e.message.should.be.equal('"Argument" at position 0 fails because ["0" must be a number, "0" with value "asdasasd" fails to match the required pattern: /^(ak_|ct_|ok_|oq_)/]') } }) it('Invalid address type', async () => { try { - await contractObject.call('addressFn', [333]) + await contractObject.methods.addressFn(333) } catch (e) { e.message.should.be.equal('"Argument" at position 0 fails because ["0" must be less than or equal to 0, Value "333" at path: [0] not a string]') } }) it('Empty address', async () => { - const result = await contractObject.call('emptyAddress') + const result = await contractObject.methods.emptyAddress() return result.decode().should.eventually.become(0) }) it('Return address', async () => { - const contractAddress = await (await contractObject - .call('contractAddress', ['ct_AUUhhVZ9de4SbeRk8ekos4vZJwMJohwW5X8KQjBMUVduUmoUh'])) + const contractAddress = await (await contractObject.methods + .contractAddress('ct_AUUhhVZ9de4SbeRk8ekos4vZJwMJohwW5X8KQjBMUVduUmoUh')) .decode(null, { addressPrefix: 'ct' }) - const accountAddress = await (await contractObject - .call('accountAddress', [await contract.address()])) + const accountAddress = await (await contractObject.methods + .accountAddress(await contract.address())) .decode(null, { addressPrefix: 'ak' }) contractAddress.should.be.equal('ct_AUUhhVZ9de4SbeRk8ekos4vZJwMJohwW5X8KQjBMUVduUmoUh') accountAddress.should.be.equal(await contract.address()) }) it('Valid', async () => { - await contractObject.call('addressFn', ['ak_2ct6nMwmRnyGX6jPhraFPedZ5bYp1GXqpvnAq5LXeL5TTPfFif']) + await contractObject.methods.addressFn('ak_2ct6nMwmRnyGX6jPhraFPedZ5bYp1GXqpvnAq5LXeL5TTPfFif') }) }) describe('TUPLE', function () { it('Invalid type', async () => { try { - await contractObject.call('tupleFn', ['asdasasd']) + await contractObject.methods.tupleFn('asdasasd') } catch (e) { e.message.should.be.equal('"Argument" at position 0 fails because [Value "[asdasasd]" at path: [0] not a array]') } }) it('Invalid tuple prop type', async () => { try { - await contractObject.call('tupleFn', [[1, 'string']]) + await contractObject.methods.tupleFn([1, 'string']) } catch (e) { e.message.should.be.equal('"Argument" at position 0 fails because ["[1,string]" at position 0 fails because [Value "1" at path: [0,0] not a string], "[1,string]" at position 1 fails because [Value "1" at path: [0,1] not a number]]') } }) it('Required tuple prop', async () => { try { - await contractObject.call('tupleFn', [[1]]) + await contractObject.methods.tupleFn([1]) } catch (e) { e.message.should.be.equal('"Argument" at position 0 fails because ["[1]" at position 0 fails because [Value "1" at path: [0,0] not a string], "[1]" does not contain 1 required value(s)]') } }) it('Wrong type in list inside tuple', async () => { try { - await contractObject.call('tupleWithList', [[[true], 1]]) + await contractObject.methods.tupleWithList([[true], 1]) } catch (e) { e.message.should.be.equal('"Argument" at position 0 fails because ["[true,1]" at position 0 fails because ["0" at position 0 fails because [Value "0" at path: [0,0,0] not a number]]]') } }) it('Wrong type in tuple inside tuple', async () => { try { - await contractObject.call('tupleInTupleFn', [[['str', 1], 1]]) + await contractObject.methods.tupleInTupleFn([['str', 1], 1]) } catch (e) { e.message.should.be.equal('"Argument" at position 0 fails because ["[str,1,1]" at position 0 fails because ["Tuple argument" at position 1 fails because [Value "1" at path: [0,0,1] not a string]]]') } }) it('Valid', async () => { - await contractObject.call('tupleFn', [['test', 1]]) + await contractObject.methods.tupleFn(['test', 1]) }) }) describe('LIST', function () { it('Invalid type', async () => { try { - await contractObject.call('listFn', ['asdasasd']) + await contractObject.methods.listFn('asdasasd') } catch (e) { e.message.should.be.equal('"Argument" at position 0 fails because [Value "[asdasasd]" at path: [0] not a array]') } }) it('Invalid list element type', async () => { try { - await contractObject.call('listFn', [[1, 'string']]) + await contractObject.methods.listFn([1, 'string']) } catch (e) { e.message.should.be.equal('"Argument" at position 0 fails because ["[1,string]" at position 1 fails because [Value "1" at path: [0,1] not a number]]') } }) it('Invalid list element type nested', async () => { try { - await contractObject.call('listInListFn', [[['childListWronmgElement'], 'parentListWrongElement']]) + await contractObject.methods.listInListFn([['childListWronmgElement'], 'parentListWrongElement']) } catch (e) { e.message.should.be.equal('"Argument" at position 0 fails because ["[childListWronmgElement,parentListWrongElement]" at position 0 fails because ["0" at position 0 fails because [Value "0" at path: [0,0,0] not a number]], "[childListWronmgElement,parentListWrongElement]" at position 1 fails because [Value "1" at path: [0,1] not a array]]') } @@ -315,7 +320,7 @@ describe('Contract', function () { [address, ['someStringV', 324]] ] ) - const result = await contractObject.call('mapFn', [mapArg]) + const result = await contractObject.methods.mapFn(mapArg) return result.decode().should.eventually.become(Array.from(mapArg.entries())) }) it('Map With Option Value', async () => { @@ -340,8 +345,8 @@ describe('Contract', function () { [address, ['someStringV', undefined]] ] ) - const resultWithSome = await contractObject.call('mapOptionFn', [mapArgWithSomeValue]) - const resultWithNone = await contractObject.call('mapOptionFn', [mapArgWithNoneValue]) + const resultWithSome = await contractObject.methods.mapOptionFn(mapArgWithSomeValue) + const resultWithNone = await contractObject.methods.mapOptionFn(mapArgWithNoneValue) const decodedSome = resultWithSome.decode() @@ -355,7 +360,7 @@ describe('Contract', function () { [address, ['someStringV', '324']] ] ) - const result = await contractObject.call('mapFn', [mapArg]) + const result = await contractObject.methods.mapFn(mapArg) mapArg.set(address, ['someStringV', 324]) return result.decode().should.eventually.become(Array.from(mapArg.entries())) }) @@ -365,29 +370,29 @@ describe('Contract', function () { [ [address, ['someStringV', 324]] ] - const result = await contractObject.call('mapFn', [mapArg]) + const result = await contractObject.methods.mapFn(mapArg) return result.decode().should.eventually.become(mapArg) }) }) describe('RECORD/STATE', function () { it('Valid Set Record (Cast from JS object)', async () => { - await contractObject.call('setRecord', [{ value: 'qwe', key: 1234, testOption: Promise.resolve('test') }]) - const state = await contractObject.call('getRecord', []) + await contractObject.methods.setRecord({ value: 'qwe', key: 1234, testOption: Promise.resolve('test') }) + const state = await contractObject.methods.getRecord() return state.decode().should.eventually.become({ value: 'qwe', key: 1234, testOption: 'test' }) }) it('Get Record(Convert to JS object)', async () => { - const result = await contractObject.call('getRecord', []) + const result = await contractObject.methods.getRecord() return result.decode().should.eventually.become({ value: 'qwe', key: 1234, testOption: 'test' }) }) it('Get Record With Option (Convert to JS object)', async () => { - await contractObject.call('setRecord', [{ key: 1234, value: 'qwe', testOption: Promise.resolve('resolved string') }]) - const result = await contractObject.call('getRecord', []) + await contractObject.methods.setRecord({ key: 1234, value: 'qwe', testOption: Promise.resolve('resolved string') }) + const result = await contractObject.methods.getRecord() return result.decode().should.eventually.become({ value: 'qwe', key: 1234, testOption: 'resolved string' }) }) it('Invalid value type', async () => { try { - await contractObject.call('setRecord', [{ value: 123, key: 'test' }]) + await contractObject.methods.setRecord({ value: 123, key: 'test' }) } catch (e) { e.message.should.be.equal('"Argument" at position 0 fails because [child "value" fails because [Value "123" at path: [0,value] not a string], child "key" fails because [Value "key" at path: [0,key] not a number]]') } @@ -395,23 +400,23 @@ describe('Contract', function () { }) describe('OPTION', function () { it('Set Some Option Value(Cast from JS value/Convert result to JS)', async () => { - const optionRes = await contractObject.call('intOption', [Promise.resolve(123)]) + const optionRes = await contractObject.methods.intOption(Promise.resolve(123)) return optionRes.decode().should.eventually.become(123) }) it('Set Some Option List Value(Cast from JS value/Convert result to JS)', async () => { - const optionRes = await contractObject.call('listOption', [Promise.resolve([[1, 'testString']])]) + const optionRes = await contractObject.methods.listOption(Promise.resolve([[1, 'testString']])) return optionRes.decode().should.eventually.become([[1, 'testString']]) }) it('Set None Option Value(Cast from JS value/Convert to JS)', async () => { - const optionRes = await contractObject.call('intOption', [Promise.reject(Error())]) + const optionRes = await contractObject.methods.intOption(Promise.reject(Error())) return optionRes.decode().should.eventually.become(undefined) }) it('Invalid option type', async () => { try { - await contractObject.call('intOption', [{ s: 2 }]) + await contractObject.methods.intOption({ s: 2 }) } catch (e) { e.message.should.be.equal('"Argument" at position 0 fails because [Value \'[[object Object]]\' at path: [0] not a Promise]') } @@ -420,21 +425,25 @@ describe('Contract', function () { }) describe('Call contract', function () { it('Call contract using using sophia type arguments', async () => { - const res = await contractObject.call('listFn', ['[ 1, 2 ]'], { skipArgsConvert: true }) + contractObject.setOptions({ skipArgsConvert: true }) + const res = await contractObject.methods.listFn('[ 1, 2 ]') + contractObject.setOptions({ skipArgsConvert: false }) return res.decode().should.eventually.become([1, 2]) }) it('Call contract using using js type arguments', async () => { - const res = await contractObject.call('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 () => { - const res = await contractObject.call('listFn', [[ 1, 2 ]], { skipTransformDecoded: true }) + contractObject.setOptions({ skipTransformDecoded: true }) + const res = await contractObject.methods.listFn([ 1, 2 ]) const decoded = await res.decode() const decodedJSON = '{"type":"list","value":[{"type":"word","value":1},{"type":"word","value":2}]}' + contractObject.setOptions({ skipTransformDecoded: false }) JSON.stringify(decoded).should.be.equal(decodedJSON) }) it('Call contract with contract type argument', async () => { - const result = await contractObject.call('approve', [0, 'ct_AUUhhVZ9de4SbeRk8ekos4vZJwMJohwW5X8KQjBMUVduUmoUh']) + const result = await contractObject.methods.approve(0, 'ct_AUUhhVZ9de4SbeRk8ekos4vZJwMJohwW5X8KQjBMUVduUmoUh') return result.decode().should.eventually.become(0) }) })