Skip to content

Commit

Permalink
feat(ACI): Add contract, address, record types argument/result …
Browse files Browse the repository at this point in the history
…transformation (#349)

* feat(ACI): Add contract type argument transformation

Allow pass contract address as sophia 'address' type

* feat(ACI): Add record type to result tranform.

* feat(ACI): Convert result of contract `record` type to js object

* feat(ACI): Transform js object type arguments to sophia `record` type

* fix(ACI Test): Remove logs

* feat(ACI): Improve address type transformation. Add prefix option. Add tests.
  • Loading branch information
nduchak authored Apr 18, 2019
1 parent 9026e46 commit 0599d7d
Show file tree
Hide file tree
Showing 2 changed files with 103 additions and 21 deletions.
87 changes: 66 additions & 21 deletions es/contract/aci.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
*/
import AsyncInit from '../utils/async-init'
import { decode } from '../tx/builder/helpers'
import { aeEncodeKey } from '../utils/crypto'
import { encodeBase58Check } from '../utils/crypto'
import { toBytes } from '../utils/bytes'

const SOPHIA_TYPES = [
Expand All @@ -34,7 +34,8 @@ const SOPHIA_TYPES = [
'address',
'bool',
'list',
'map'
'map',
'record'
].reduce((acc, type, i) => {
acc[type] = type
return acc
Expand All @@ -47,7 +48,12 @@ const SOPHIA_TYPES = [
* @return {string}
*/
function transform (type, value) {
const { t, generic } = readType(type)
let { t, generic } = readType(type)

// contract TestContract = ...
// fn(ct: TestContract)
if (typeof value === 'string' && value.slice(0, 2) === 'ct') t = SOPHIA_TYPES.address // Handle Contract address transformation

switch (t) {
case SOPHIA_TYPES.string:
return `"${value}"`
Expand All @@ -56,8 +62,18 @@ function transform (type, value) {
case SOPHIA_TYPES.tuple:
return `(${value.map((el, i) => transform(generic[i], el))})`
case SOPHIA_TYPES.address:
return `#${decode(value, 'ak').toString('hex')}`
return `#${decode(value).toString('hex')}`
case SOPHIA_TYPES.record:
return `{${generic.reduce(
(acc, { name, type }, i) => {
if (i !== 0) acc += ','
acc += `${name} = ${transform(type[0], value[name])}`
return acc
},
''
)}}`
}

return `${value}`
}

Expand Down Expand Up @@ -101,21 +117,29 @@ function validate (type, value) {
}
}

function encodeAddress (address, prefix = 'ak') {
const addressBuffer = Buffer.from(address, 'hex')
const encodedAddress = encodeBase58Check(addressBuffer)
return `${prefix}_${encodedAddress}`
}
/**
* Transform decoded data to JS type
* @param aci
* @param result
* @param transformDecodedData
* @return {*}
*/
function transformDecodedData (aci, result, { skipTransformDecoded = false } = {}) {
function transformDecodedData (aci, result, { skipTransformDecoded = false, addressPrefix = 'ak' } = {}) {
if (skipTransformDecoded) return result
const { t, generic } = readType(aci, true)

switch (t) {
case SOPHIA_TYPES.bool:
return !!result.value
case SOPHIA_TYPES.address:
return aeEncodeKey(toBytes(result.value, true))
return result.value === 0
? 0
: encodeAddress(toBytes(result.value, true), addressPrefix)
case SOPHIA_TYPES.map:
const [keyT, valueT] = generic
return result.value
Expand All @@ -132,6 +156,15 @@ function transformDecodedData (aci, result, { skipTransformDecoded = false } = {
return result.value.map(({ value }) => transformDecodedData(generic, { value }))
case SOPHIA_TYPES.tuple:
return result.value.map(({ value }, i) => { return transformDecodedData(generic[i], { value }) })
case SOPHIA_TYPES.record:
return result.value.reduce(
(acc, { name, value }, i) =>
({
...acc,
[generic[i].name]: transformDecodedData(generic[i].type, { value })
}),
{}
)
}
return result.value
}
Expand Down Expand Up @@ -229,24 +262,32 @@ async function getContractInstance (source, { aci, contractAddress } = {}) {
return instance
}

// @TODO Remove after compiler can decode using type from ACI
function transformReturnType (returns) {
if (typeof returns === 'string') return returns
if (typeof returns === 'object') {
const [[key, value]] = Object.entries(returns)
return `${key !== 'tuple' ? key : ''}(${value
.reduce(
(acc, el, i) => {
if (i !== 0) acc += ','
acc += transformReturnType(el)
return acc
},
'')
})`
try {
if (typeof returns === 'string') return returns
if (typeof returns === 'object') {
const [[key, value]] = Object.entries(returns)
return `${key !== 'tuple' && key !== 'record' ? key : ''}(${value
.reduce(
(acc, el, i) => {
if (i !== 0) acc += ','
acc += transformReturnType(key !== 'record' ? el : el.type[0])
return acc
},
'')})`
}
} catch (e) {
return null
}
}

function call (self) {
return async function (fn, params = [], options = { skipArgsConvert: false, skipTransformDecoded: false, callStatic: false }) {
return async function (fn, params = [], options = {
skipArgsConvert: false,
skipTransformDecoded: false,
callStatic: false
}) {
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!')
Expand All @@ -258,10 +299,14 @@ function call (self) {
options
})
: await self.contractCall(this.source, this.deployInfo.address, fn, params, options)
const returnType = await transformReturnType(fnACI.returns)
return {
...result,
decode: async () => transformDecodedData(fnACI.returns, await self.contractDecodeData(returnType, result.result.returnValue), options)
decode: async (type, opt = {}) =>
transformDecodedData(
fnACI.returns,
await self.contractDecodeData(type || transformReturnType(fnACI.returns), result.result.returnValue),
{ ...options, ...opt }
)
}
}
}
Expand Down
37 changes: 37 additions & 0 deletions test/integration/contract.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ contract StateContract =
public function retrieve() : string = state.value
`
const testContract = `
contract Voting =
public function test() : int = 1
contract StateContract =
record state = { value: string, key: int }
public function init(value: string, key: int) : state = { value = value, key = key }
Expand All @@ -38,6 +41,12 @@ contract StateContract =
public function boolFn(a: bool) : bool = a
public function listFn(a: list(int)) : list(int) = a
public function testFn(a: list(int), b: bool) : (list(int), bool) = (a, b)
public function approve(tx_id: int, voting_contract: Voting) : int = tx_id
public function getRecord() : state = state
public function setRecord(s: state) : state = s
public function emptyAddress() : address = #0
public function contractAddress (ct: address) : address = ct
public function accountAddress (ak: address) : address = ak
`

const encodedNumberSix = 'cb_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAaKNdnK'
Expand Down Expand Up @@ -198,6 +207,34 @@ describe('Contract', function () {
e.message.should.be.equal('Validation error: ["Argument index: 1, value: [1234] must be of type [bool]"]')
}
})
it('Call contract with contract type argument', async () => {
const result = await contractObject.call('approve', [0, 'ct_AUUhhVZ9de4SbeRk8ekos4vZJwMJohwW5X8KQjBMUVduUmoUh'])
return result.decode().should.eventually.become(0)
})
it('Call contract with return of record type', async () => {
const result = await contractObject.call('getRecord', [])
return result.decode().should.eventually.become({ value: 'blabla', key: 100 })
})
it('Call contract with argument of record type', async () => {
const result = await contractObject.call('setRecord', [{ value: 'qwe', key: 1234 }])
return result.decode().should.eventually.become({ value: 'qwe', key: 1234 })
})
it('Function return #0 as address', async () => {
const result = await contractObject.call('emptyAddress')
return result.decode().should.eventually.become(0)
})
it('Function return address', async () => {
const contractAddress = await (await contractObject
.call('contractAddress', ['ct_AUUhhVZ9de4SbeRk8ekos4vZJwMJohwW5X8KQjBMUVduUmoUh']))
.decode(null, { addressPrefix: 'ct' })

const accountAddress = await (await contractObject
.call('accountAddress', [await contract.address()]))
.decode(null, { addressPrefix: 'ak' })

contractAddress.should.be.equal('ct_AUUhhVZ9de4SbeRk8ekos4vZJwMJohwW5X8KQjBMUVduUmoUh')
accountAddress.should.be.equal(await contract.address())
})
})
})
})

0 comments on commit 0599d7d

Please sign in to comment.