Skip to content

Commit 0599d7d

Browse files
authored
feat(ACI): Add contract, address, record types argument/result 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.
1 parent 9026e46 commit 0599d7d

File tree

2 files changed

+103
-21
lines changed

2 files changed

+103
-21
lines changed

es/contract/aci.js

Lines changed: 66 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
*/
2525
import AsyncInit from '../utils/async-init'
2626
import { decode } from '../tx/builder/helpers'
27-
import { aeEncodeKey } from '../utils/crypto'
27+
import { encodeBase58Check } from '../utils/crypto'
2828
import { toBytes } from '../utils/bytes'
2929

3030
const SOPHIA_TYPES = [
@@ -34,7 +34,8 @@ const SOPHIA_TYPES = [
3434
'address',
3535
'bool',
3636
'list',
37-
'map'
37+
'map',
38+
'record'
3839
].reduce((acc, type, i) => {
3940
acc[type] = type
4041
return acc
@@ -47,7 +48,12 @@ const SOPHIA_TYPES = [
4748
* @return {string}
4849
*/
4950
function transform (type, value) {
50-
const { t, generic } = readType(type)
51+
let { t, generic } = readType(type)
52+
53+
// contract TestContract = ...
54+
// fn(ct: TestContract)
55+
if (typeof value === 'string' && value.slice(0, 2) === 'ct') t = SOPHIA_TYPES.address // Handle Contract address transformation
56+
5157
switch (t) {
5258
case SOPHIA_TYPES.string:
5359
return `"${value}"`
@@ -56,8 +62,18 @@ function transform (type, value) {
5662
case SOPHIA_TYPES.tuple:
5763
return `(${value.map((el, i) => transform(generic[i], el))})`
5864
case SOPHIA_TYPES.address:
59-
return `#${decode(value, 'ak').toString('hex')}`
65+
return `#${decode(value).toString('hex')}`
66+
case SOPHIA_TYPES.record:
67+
return `{${generic.reduce(
68+
(acc, { name, type }, i) => {
69+
if (i !== 0) acc += ','
70+
acc += `${name} = ${transform(type[0], value[name])}`
71+
return acc
72+
},
73+
''
74+
)}}`
6075
}
76+
6177
return `${value}`
6278
}
6379

@@ -101,21 +117,29 @@ function validate (type, value) {
101117
}
102118
}
103119

120+
function encodeAddress (address, prefix = 'ak') {
121+
const addressBuffer = Buffer.from(address, 'hex')
122+
const encodedAddress = encodeBase58Check(addressBuffer)
123+
return `${prefix}_${encodedAddress}`
124+
}
104125
/**
105126
* Transform decoded data to JS type
106127
* @param aci
107128
* @param result
108129
* @param transformDecodedData
109130
* @return {*}
110131
*/
111-
function transformDecodedData (aci, result, { skipTransformDecoded = false } = {}) {
132+
function transformDecodedData (aci, result, { skipTransformDecoded = false, addressPrefix = 'ak' } = {}) {
112133
if (skipTransformDecoded) return result
113134
const { t, generic } = readType(aci, true)
135+
114136
switch (t) {
115137
case SOPHIA_TYPES.bool:
116138
return !!result.value
117139
case SOPHIA_TYPES.address:
118-
return aeEncodeKey(toBytes(result.value, true))
140+
return result.value === 0
141+
? 0
142+
: encodeAddress(toBytes(result.value, true), addressPrefix)
119143
case SOPHIA_TYPES.map:
120144
const [keyT, valueT] = generic
121145
return result.value
@@ -132,6 +156,15 @@ function transformDecodedData (aci, result, { skipTransformDecoded = false } = {
132156
return result.value.map(({ value }) => transformDecodedData(generic, { value }))
133157
case SOPHIA_TYPES.tuple:
134158
return result.value.map(({ value }, i) => { return transformDecodedData(generic[i], { value }) })
159+
case SOPHIA_TYPES.record:
160+
return result.value.reduce(
161+
(acc, { name, value }, i) =>
162+
({
163+
...acc,
164+
[generic[i].name]: transformDecodedData(generic[i].type, { value })
165+
}),
166+
{}
167+
)
135168
}
136169
return result.value
137170
}
@@ -229,24 +262,32 @@ async function getContractInstance (source, { aci, contractAddress } = {}) {
229262
return instance
230263
}
231264

265+
// @TODO Remove after compiler can decode using type from ACI
232266
function transformReturnType (returns) {
233-
if (typeof returns === 'string') return returns
234-
if (typeof returns === 'object') {
235-
const [[key, value]] = Object.entries(returns)
236-
return `${key !== 'tuple' ? key : ''}(${value
237-
.reduce(
238-
(acc, el, i) => {
239-
if (i !== 0) acc += ','
240-
acc += transformReturnType(el)
241-
return acc
242-
},
243-
'')
244-
})`
267+
try {
268+
if (typeof returns === 'string') return returns
269+
if (typeof returns === 'object') {
270+
const [[key, value]] = Object.entries(returns)
271+
return `${key !== 'tuple' && key !== 'record' ? key : ''}(${value
272+
.reduce(
273+
(acc, el, i) => {
274+
if (i !== 0) acc += ','
275+
acc += transformReturnType(key !== 'record' ? el : el.type[0])
276+
return acc
277+
},
278+
'')})`
279+
}
280+
} catch (e) {
281+
return null
245282
}
246283
}
247284

248285
function call (self) {
249-
return async function (fn, params = [], options = { skipArgsConvert: false, skipTransformDecoded: false, callStatic: false }) {
286+
return async function (fn, params = [], options = {
287+
skipArgsConvert: false,
288+
skipTransformDecoded: false,
289+
callStatic: false
290+
}) {
250291
const fnACI = getFunctionACI(this.aci, fn)
251292
if (!fn) throw new Error('Function name is required')
252293
if (!this.deployInfo.address) throw new Error('You need to deploy contract before calling!')
@@ -258,10 +299,14 @@ function call (self) {
258299
options
259300
})
260301
: await self.contractCall(this.source, this.deployInfo.address, fn, params, options)
261-
const returnType = await transformReturnType(fnACI.returns)
262302
return {
263303
...result,
264-
decode: async () => transformDecodedData(fnACI.returns, await self.contractDecodeData(returnType, result.result.returnValue), options)
304+
decode: async (type, opt = {}) =>
305+
transformDecodedData(
306+
fnACI.returns,
307+
await self.contractDecodeData(type || transformReturnType(fnACI.returns), result.result.returnValue),
308+
{ ...options, ...opt }
309+
)
265310
}
266311
}
267312
}

test/integration/contract.js

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ contract StateContract =
3030
public function retrieve() : string = state.value
3131
`
3232
const testContract = `
33+
contract Voting =
34+
public function test() : int = 1
35+
3336
contract StateContract =
3437
record state = { value: string, key: int }
3538
public function init(value: string, key: int) : state = { value = value, key = key }
@@ -38,6 +41,12 @@ contract StateContract =
3841
public function boolFn(a: bool) : bool = a
3942
public function listFn(a: list(int)) : list(int) = a
4043
public function testFn(a: list(int), b: bool) : (list(int), bool) = (a, b)
44+
public function approve(tx_id: int, voting_contract: Voting) : int = tx_id
45+
public function getRecord() : state = state
46+
public function setRecord(s: state) : state = s
47+
public function emptyAddress() : address = #0
48+
public function contractAddress (ct: address) : address = ct
49+
public function accountAddress (ak: address) : address = ak
4150
`
4251

4352
const encodedNumberSix = 'cb_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAaKNdnK'
@@ -198,6 +207,34 @@ describe('Contract', function () {
198207
e.message.should.be.equal('Validation error: ["Argument index: 1, value: [1234] must be of type [bool]"]')
199208
}
200209
})
210+
it('Call contract with contract type argument', async () => {
211+
const result = await contractObject.call('approve', [0, 'ct_AUUhhVZ9de4SbeRk8ekos4vZJwMJohwW5X8KQjBMUVduUmoUh'])
212+
return result.decode().should.eventually.become(0)
213+
})
214+
it('Call contract with return of record type', async () => {
215+
const result = await contractObject.call('getRecord', [])
216+
return result.decode().should.eventually.become({ value: 'blabla', key: 100 })
217+
})
218+
it('Call contract with argument of record type', async () => {
219+
const result = await contractObject.call('setRecord', [{ value: 'qwe', key: 1234 }])
220+
return result.decode().should.eventually.become({ value: 'qwe', key: 1234 })
221+
})
222+
it('Function return #0 as address', async () => {
223+
const result = await contractObject.call('emptyAddress')
224+
return result.decode().should.eventually.become(0)
225+
})
226+
it('Function return address', async () => {
227+
const contractAddress = await (await contractObject
228+
.call('contractAddress', ['ct_AUUhhVZ9de4SbeRk8ekos4vZJwMJohwW5X8KQjBMUVduUmoUh']))
229+
.decode(null, { addressPrefix: 'ct' })
230+
231+
const accountAddress = await (await contractObject
232+
.call('accountAddress', [await contract.address()]))
233+
.decode(null, { addressPrefix: 'ak' })
234+
235+
contractAddress.should.be.equal('ct_AUUhhVZ9de4SbeRk8ekos4vZJwMJohwW5X8KQjBMUVduUmoUh')
236+
accountAddress.should.be.equal(await contract.address())
237+
})
201238
})
202239
})
203240
})

0 commit comments

Comments
 (0)