UNPKG

51.5 kBJavaScriptView Raw
1/*
2 * ISC License (ISC)
3 * Copyright (c) 2018 aeternity developers
4 *
5 * Permission to use, copy, modify, and/or distribute this software for any
6 * purpose with or without fee is hereby granted, provided that the above
7 * copyright notice and this permission notice appear in all copies.
8 *
9 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
10 * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
11 * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
12 * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
13 * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
14 * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
15 * PERFORMANCE OF THIS SOFTWARE.
16 */
17import Compiler from '../../es/contract/compiler'
18import { describe, it, before } from 'mocha'
19import { BaseAe, getSdk, compilerUrl, publicKey } from './'
20import { decode } from '../../es/tx/builder/helpers'
21import * as R from 'ramda'
22import { randomName } from '../utils'
23import { decodeEvents, readType, SOPHIA_TYPES } from '../../es/contract/aci/transformation'
24import { hash, personalMessageToBinary } from '../../es/utils/crypto'
25import { getFunctionACI } from '../../es/contract/aci/helpers'
26
27const identityContract = `
28contract Identity =
29 entrypoint main(x : int) = x
30`
31
32const errorContract = `
33contract Identity =
34 payable stateful entrypoint main(x : address) = Chain.spend(x, 1000000000)
35`
36
37const stateContract = `
38contract StateContract =
39 record state = { value: string }
40 entrypoint init(value) : state = { value = value }
41 entrypoint retrieve() : string = state.value
42`
43const libContract = `
44namespace TestLib =
45 function sum(x: int, y: int) : int = x + y
46`
47const contractWithLib = `
48include "testLib"
49contract Voting =
50 entrypoint sumNumbers(x: int, y: int) : int = TestLib.sum(x, y)
51`
52const testContract = `
53namespace Test =
54 function double(x: int): int = x*2
55
56
57contract Voting =
58 type test_type = int
59 record state = { value: string, key: test_type, testOption: option(string) }
60 record test_record = { value: string, key: list(test_type) }
61 entrypoint test : () => int
62
63include "testLib"
64contract StateContract =
65 type number = int
66 record state = { value: string, key: number, testOption: option(string) }
67 record yesEr = { t: number}
68
69 datatype event = TheFirstEvent(int) | AnotherEvent(string, address) | AnotherEvent2(bool, string, int)
70 datatype dateUnit = Year | Month | Day
71 datatype one_or_both('a, 'b) = Left('a) | Right('b) | Both('a, 'b)
72
73 entrypoint init(value: string, key: int, testOption: option(string)) : state = { value = value, key = key, testOption = testOption }
74 entrypoint retrieve() : string*int = (state.value, state.key)
75
76 entrypoint remoteContract(a: Voting) : int = 1
77 entrypoint remoteArgs(a: Voting.test_record) : Voting.test_type = 1
78 entrypoint intFn(a: int) : int = a
79 payable entrypoint stringFn(a: string) : string = a
80 entrypoint boolFn(a: bool) : bool = a
81 entrypoint addressFn(a: address) : address = a
82 entrypoint contractAddress (ct: address) : address = ct
83 entrypoint accountAddress (ak: address) : address = ak
84
85 entrypoint tupleFn (a: string*int) : string*int = a
86 entrypoint tupleInTupleFn (a: (string*string)*int) : (string*string)*int = a
87 entrypoint tupleWithList (a: list(int)*int) : list(int)*int = a
88
89 entrypoint listFn(a: list(int)) : list(int) = a
90 entrypoint listInListFn(a: list(list(int))) : list(list(int)) = a
91
92 entrypoint mapFn(a: map(address, string*int)) : map(address, string*int) = a
93 entrypoint mapOptionFn(a: map(address, string*option(int))) : map(address, string*option(int)) = a
94
95 entrypoint getRecord() : state = state
96 stateful entrypoint setRecord(s: state) = put(s)
97
98 entrypoint intOption(s: option(int)) : option(int) = s
99 entrypoint listOption(s: option(list(int*string))) : option(list(int*string)) = s
100
101 entrypoint testFn(a: list(int), b: bool) : list(int)*bool = (a, b)
102 entrypoint approve(tx_id: int, voting_contract: Voting) : int = tx_id
103
104 entrypoint hashFn(s: hash): hash = s
105 entrypoint signatureFn(s: signature): signature = s
106 entrypoint bytesFn(s: bytes(32)): bytes(32) = s
107
108 entrypoint usingExternalLib(s: int): int = Test.double(s)
109
110 entrypoint datTypeFn(s: dateUnit): dateUnit = s
111 entrypoint datTypeGFn(x : one_or_both(int, string)) : int =
112 switch(x)
113 Left(x) => x
114 Right(_) => abort("asdasd")
115 Both(x, _) => x
116 stateful entrypoint emitEvents() : unit =
117 Chain.event(TheFirstEvent(42))
118 Chain.event(AnotherEvent("This is not indexed", Contract.address))
119 Chain.event(AnotherEvent2(true, "This is not indexed", 1))
120`
121const aensDelegationContract = `
122contract DelegateTest =
123 // Transactions
124 stateful payable entrypoint signedPreclaim(addr : address,
125 chash : hash,
126 sign : signature) : unit =
127 AENS.preclaim(addr, chash, signature = sign)
128 stateful entrypoint signedClaim(addr : address,
129 name : string,
130 salt : int,
131 name_fee : int,
132 sign : signature) : unit =
133 AENS.claim(addr, name, salt, name_fee, signature = sign)
134 stateful entrypoint signedTransfer(owner : address,
135 new_owner : address,
136 name : string,
137 sign : signature) : unit =
138 AENS.transfer(owner, new_owner, name, signature = sign)
139 stateful entrypoint signedRevoke(owner : address,
140 name : string,
141 sign : signature) : unit =
142 AENS.revoke(owner, name, signature = sign)`
143const oracleContract = `
144contract DelegateTest =
145 type fee = int
146 type ttl = Chain.ttl
147 stateful payable entrypoint signedRegisterOracle(acct : address,
148 sign : signature,
149 qfee : fee,
150 ttl : ttl) : oracle(string, string) =
151 Oracle.register(acct, qfee, ttl, signature = sign)
152 stateful payable entrypoint signedExtendOracle(o : oracle(string, string),
153 sign : signature, // Signed oracle address
154 ttl : ttl) : unit =
155 Oracle.extend(o, signature = sign, ttl)
156
157 payable stateful entrypoint createQuery(o : oracle(string, string),
158 q : string,
159 qfee : int,
160 qttl : Chain.ttl,
161 rttl : Chain.ttl) : oracle_query(string, string) =
162 require(qfee =< Call.value, "insufficient value for qfee")
163 require(Oracle.check(o), "oracle not valid")
164 Oracle.query(o, q, qfee, qttl, rttl)
165
166 entrypoint queryFee(o : oracle(string, int)) : int =
167 Oracle.query_fee(o)
168
169 stateful entrypoint respond(o : oracle(string, string),
170 q : oracle_query(string, string),
171 sign : signature, // Signed oracle query id + contract address
172 r : string) =
173 Oracle.respond(o, q, signature = sign, r)`
174const encodedNumberSix = 'cb_DA6sWJo='
175const signSource = `
176contract Sign =
177 entrypoint verify (msg: hash, pub: address, sig: signature): bool =
178 Crypto.verify_sig(msg, pub, sig)
179`
180const filesystem = {
181 testLib: libContract
182}
183
184describe('Contract', function () {
185 let contract
186 let bytecode
187 let deployed
188
189 before(async function () {
190 contract = await getSdk()
191 })
192 describe('Aens and Oracle operation delegation', () => {
193 let cInstance
194 let cInstanceOracle
195 before(async () => {
196 cInstance = await contract.getContractInstance(aensDelegationContract)
197 cInstanceOracle = await contract.getContractInstance(oracleContract)
198 await cInstance.deploy()
199 await cInstanceOracle.deploy()
200 })
201 it('Delegate AENS operations', async () => {
202 const name = randomName(15)
203 const contractAddress = cInstance.deployInfo.address
204 const nameFee = 20 * (10 ** 18) // 20 AE
205 const current = await contract.address()
206
207 // preclaim
208 const { salt: _salt } = await contract.aensPreclaim(name)
209 // @TODO enable after next HF
210 // const commitmentId = commitmentHash(name, _salt)
211 const preclaimSig = await contract.delegateNamePreclaimSignature(contractAddress)
212 console.log(`preclaimSig -> ${preclaimSig}`)
213 // const preclaim = await cInstance.methods.signedPreclaim(await contract.address(), commitmentId, preclaimSig)
214 // preclaim.result.returnType.should.be.equal('ok')
215 await contract.awaitHeight((await contract.height()) + 2)
216 // claim
217 const claimSig = await contract.delegateNameClaimSignature(contractAddress, name)
218 const claim = await cInstance.methods.signedClaim(await contract.address(), name, _salt, nameFee, claimSig)
219 claim.result.returnType.should.be.equal('ok')
220 await contract.awaitHeight((await contract.height()) + 2)
221
222 // transfer
223 const transferSig = await contract.delegateNameTransferSignature(contractAddress, name)
224 const onAccount = contract.addresses().find(acc => acc !== current)
225 const transfer = await cInstance.methods.signedTransfer(await contract.address(), onAccount, name, transferSig)
226 transfer.result.returnType.should.be.equal('ok')
227
228 await contract.awaitHeight((await contract.height()) + 2)
229 // revoke
230 const revokeSig = await contract.delegateNameRevokeSignature(contractAddress, name, { onAccount })
231 const revoke = await cInstance.methods.signedRevoke(onAccount, name, revokeSig)
232 revoke.result.returnType.should.be.equal('ok')
233
234 try {
235 await contract.aensQuery(name)
236 } catch (e) {
237 e.message.should.be.an('string')
238 }
239 })
240 it('Delegate Oracle operations', async () => {
241 const contractAddress = cInstanceOracle.deployInfo.address
242 const current = await contract.address()
243 const onAccount = contract.addresses().find(acc => acc !== current)
244 const qFee = 500000
245 const ttl = 'RelativeTTL(50)'
246 const oracleId = `ok_${onAccount.slice(3)}`
247
248 const oracleCreateSig = await contract.delegateOracleRegisterSignature(contractAddress, { onAccount })
249 const oracleRegister = await cInstanceOracle.methods.signedRegisterOracle(onAccount, oracleCreateSig, qFee, ttl, { onAccount })
250 oracleRegister.result.returnType.should.be.equal('ok')
251 const oracle = await contract.getOracleObject(oracleId)
252 oracle.id.should.be.equal(oracleId)
253
254 const oracleExtendSig = await contract.delegateOracleExtendSignature(contractAddress, { onAccount })
255 const queryExtend = await cInstanceOracle.methods.signedExtendOracle(oracleId, oracleExtendSig, ttl, { onAccount })
256 queryExtend.result.returnType.should.be.equal('ok')
257 const oracleExtended = await contract.getOracleObject(oracleId)
258 console.log(oracleExtended)
259 oracleExtended.ttl.should.be.equal(oracle.ttl + 50)
260
261 // TODO ask core about this
262 // // create query
263 // const q = 'Hello!'
264 // const newOracle = await contract.registerOracle('string', 'int', { onAccount, queryFee: qFee })
265 // const query = await cInstanceOracle.methods.createQuery(newOracle.id, q, 1000 + qFee, ttl, ttl, { onAccount, amount: 5 * qFee })
266 // query.should.be.an('object')
267 // const queryObject = await contract.getQueryObject(newOracle.id, query.decodedResult)
268 // queryObject.should.be.an('object')
269 // queryObject.decodedQuery.should.be.equal(q)
270 // console.log(queryObject)
271 //
272 // // respond to query
273 // const r = 'Hi!'
274 // const respondSig = await contract.delegateOracleRespondSignature(newOracle.id, queryObject.id, contractAddress, { onAccount })
275 // const response = await cInstanceOracle.methods.respond(newOracle.id, queryObject.id, respondSig, r, { onAccount })
276 // console.log(response)
277 // const queryObject2 = await contract.getQueryObject(newOracle.id, queryObject.id)
278 // console.log(queryObject2)
279 // queryObject2.decodedResponse.should.be.equal(r)
280 })
281 })
282 it('precompiled bytecode can be deployed', async () => {
283 const { version, consensusProtocolVersion } = contract.getNodeInfo()
284 console.log(`Node => ${version}, consensus => ${consensusProtocolVersion}, compiler => ${contract.compilerVersion}`)
285 const code = await contract.contractCompile(identityContract)
286 return contract.contractDeploy(code.bytecode, identityContract).should.eventually.have.property('address')
287 })
288 it('Verify message in Sophia', async () => {
289 const msg = personalMessageToBinary('Hello')
290 const msgHash = hash(msg)
291 const signature = await contract.sign(msgHash)
292 const signContract = await contract.getContractInstance(signSource)
293 await signContract.deploy()
294 const { decodedResult } = await signContract.methods.verify(msgHash, await contract.address(), signature)
295 decodedResult.should.be.equal(true)
296 })
297 it('compiles Sophia code', async () => {
298 bytecode = await contract.contractCompile(identityContract)
299 return bytecode.should.have.property('bytecode')
300 })
301
302 it('deploy static compiled contract', async () => {
303 const res = await bytecode.deployStatic([])
304 res.result.should.have.property('gasUsed')
305 res.result.should.have.property('returnType')
306 })
307
308 it('deploys compiled contracts', async () => {
309 deployed = await bytecode.deploy([])
310 return deployed.should.have.property('address')
311 })
312
313 it('Deploy/Call/Dry-run contract using callData', async () => {
314 const callArg = 1
315 const { bytecode } = await contract.contractCompile(identityContract)
316 const callDataDeploy = await contract.contractEncodeCall(identityContract, 'init', [])
317 const callDataCall = await contract.contractEncodeCall(identityContract, 'main', [callArg.toString()])
318
319 const deployStatic = await contract.contractCallStatic(identityContract, null, 'init', callDataDeploy, { bytecode })
320 deployStatic.result.should.have.property('gasUsed')
321 deployStatic.result.should.have.property('returnType')
322
323 const deployed = await contract.contractDeploy(bytecode, identityContract, callDataDeploy)
324 deployed.result.should.have.property('gasUsed')
325 deployed.result.should.have.property('returnType')
326 deployed.should.have.property('address')
327
328 const callStaticRes = await contract.contractCallStatic(identityContract, deployed.address, 'main', callDataCall)
329 callStaticRes.result.should.have.property('gasUsed')
330 callStaticRes.result.should.have.property('returnType')
331 const decodedCallStaticResult = await callStaticRes.decode()
332 decodedCallStaticResult.should.be.equal(callArg)
333
334 const callRes = await contract.contractCall(identityContract, deployed.address, 'main', callDataCall)
335 callRes.result.should.have.property('gasUsed')
336 callRes.result.should.have.property('returnType')
337 callRes.result.should.have.property('returnType')
338 const decodedCallResult = await callRes.decode()
339 decodedCallResult.should.be.equal(callArg)
340 })
341
342 it('Deploy and call contract on specific account', async () => {
343 const current = await contract.address()
344 const onAccount = contract.addresses().find(acc => acc !== current)
345
346 const deployed = await bytecode.deploy([], { onAccount })
347 deployed.result.callerId.should.be.equal(onAccount)
348 const callRes = await deployed.call('main', ['42'])
349 callRes.result.callerId.should.be.equal(onAccount)
350 const callStaticRes = await deployed.callStatic('main', ['42'])
351 callStaticRes.result.callerId.should.be.equal(onAccount)
352 })
353
354 it('Call-Static deploy transaction', async () => {
355 const compiled = bytecode.bytecode
356 const res = await contract.contractCallStatic(identityContract, null, 'init', [], { bytecode: compiled })
357 res.result.should.have.property('gasUsed')
358 res.result.should.have.property('returnType')
359 })
360
361 it('Call-Static deploy transaction on specific hash', async () => {
362 const { hash } = await contract.topBlock()
363 const compiled = bytecode.bytecode
364 const res = await contract.contractCallStatic(identityContract, null, 'init', [], { bytecode: compiled, top: hash })
365 res.result.should.have.property('gasUsed')
366 res.result.should.have.property('returnType')
367 })
368 it('Test handleError(Parse and check contract execution error)', async () => {
369 const code = await contract.contractCompile(errorContract)
370 const deployed = await code.deploy()
371 try {
372 await deployed.call('main', [await contract.address()])
373 } catch (e) {
374 e.message.indexOf('Invocation failed').should.not.be.equal(-1)
375 }
376 })
377
378 it('Dry-run without accounts', async () => {
379 const client = await BaseAe()
380 client.removeAccount(publicKey)
381 client.addresses().length.should.be.equal(0)
382 const address = await client.address().catch(e => false)
383 address.should.be.equal(false)
384 const { result } = await client.contractCallStatic(identityContract, deployed.address, 'main', ['42'])
385 result.callerId.should.be.equal(client.Ae.defaults.dryRunAccount.pub)
386 })
387
388 it('calls deployed contracts', async () => {
389 const result = await deployed.call('main', ['42'])
390 return result.decode().should.eventually.become(42)
391 })
392
393 it('call contract/deploy with `waitMined: false`', async () => {
394 const deployed = await bytecode.deploy([], { waitMined: false })
395 await contract.poll(deployed.transaction)
396 Boolean(deployed.result === undefined).should.be.equal(true)
397 Boolean(deployed.txData === undefined).should.be.equal(true)
398 const result = await deployed.call('main', ['42'], { waitMined: false, verify: false })
399 Boolean(result.result === undefined).should.be.equal(true)
400 Boolean(result.txData === undefined).should.be.equal(true)
401 await contract.poll(result.hash)
402 })
403
404 it('calls deployed contracts static', async () => {
405 const result = await deployed.callStatic('main', ['42'])
406 return result.decode().should.eventually.become(42)
407 })
408
409 it('initializes contract state', async () => {
410 const data = '"Hello World!"'
411 return contract.contractCompile(stateContract)
412 .then(bytecode => bytecode.deploy([data]))
413 .then(deployed => deployed.call('retrieve'))
414 .then(result => result.decode())
415 .catch(e => {
416 console.log(e)
417 throw e
418 })
419 .should.eventually.become('Hello World!')
420 })
421 describe('Namespaces', () => {
422 let deployed
423 it('Can compiler contract with external deps', async () => {
424 const filesystem = {
425 testLib: libContract
426 }
427 const compiled = await contract.contractCompile(contractWithLib, { filesystem })
428 compiled.should.have.property('bytecode')
429 })
430 it('Throw error when try to compile contract without providing external deps', async () => {
431 try {
432 await contract.contractCompile(contractWithLib)
433 } catch (e) {
434 e.message.indexOf('Couldn\'t find include file').should.not.be.equal(-1)
435 }
436 })
437 it('Can deploy contract with external deps', async () => {
438 const filesystem = {
439 testLib: libContract
440 }
441 const compiled = await contract.contractCompile(contractWithLib, { filesystem })
442 deployed = await compiled.deploy()
443 deployed.should.have.property('address')
444
445 const deployedStatic = await compiled.deployStatic([])
446 deployedStatic.result.should.have.property('gasUsed')
447 deployedStatic.result.should.have.property('returnType')
448
449 const encodedCallData = await compiled.encodeCall('sumNumbers', ['1', '2'])
450 encodedCallData.indexOf('cb_').should.not.be.equal(-1)
451 })
452 it('Can call contract with external deps', async () => {
453 const callResult = await deployed.call('sumNumbers', ['1', '2'])
454 const decoded = await callResult.decode()
455 decoded.should.be.equal(3)
456
457 const callStaticResult = await deployed.callStatic('sumNumbers', ['1', '2'])
458 const decoded2 = await callStaticResult.decode()
459 decoded2.should.be.equal(3)
460 })
461 })
462
463 describe('Sophia Compiler', function () {
464 let callData
465 let bytecode
466 it('Init un-compatible compiler version', async () => {
467 try {
468 // Init compiler
469 const compiler = await Compiler({ compilerUrl })
470 // Overwrite compiler version
471 compiler.compilerVersion = '1.0.0'
472 await compiler.checkCompatibility()
473 } catch (e) {
474 e.message.indexOf('Unsupported compiler version 1.0.0').should.not.be.equal(-1)
475 }
476 })
477 it('compile', async () => {
478 bytecode = await contract.compileContractAPI(identityContract)
479 const prefix = bytecode.slice(0, 2)
480 const isString = typeof bytecode === 'string'
481 prefix.should.be.equal('cb')
482 isString.should.be.equal(true)
483 })
484 it('Get FATE assembler', async () => {
485 const result = await contract.getFateAssembler(bytecode)
486 result.should.be.a('object')
487 const assembler = result['fate-assembler']
488 assembler.should.be.a('string')
489 })
490 it('Get compiler version from bytecode', async () => {
491 const version = await contract.getBytecodeCompilerVersion(bytecode)
492 console.log(version)
493 })
494 it('get contract ACI', async () => {
495 const aci = await contract.contractGetACI(identityContract)
496 aci.should.have.property('interface')
497 })
498 it('encode call-data', async () => {
499 callData = await contract.contractEncodeCallDataAPI(identityContract, 'init', [])
500 const prefix = callData.slice(0, 2)
501 const isString = typeof callData === 'string'
502 prefix.should.be.equal('cb')
503 isString.should.be.equal(true)
504 })
505 it('decode call result', async () => {
506 return contract.contractDecodeCallResultAPI(identityContract, 'main', encodedNumberSix, 'ok', { backend: 'fate' }).should.eventually.become(6)
507 })
508 it('Decode call-data using source', async () => {
509 const decodedCallData = await contract.contractDecodeCallDataBySourceAPI(identityContract, 'init', callData)
510 decodedCallData.arguments.should.be.an('array')
511 decodedCallData.arguments.length.should.be.equal(0)
512 decodedCallData.function.should.be.equal('init')
513 })
514 it('Decode call-data using bytecode', async () => {
515 const decodedCallData = await contract.contractDecodeCallDataByCodeAPI(bytecode, callData)
516 decodedCallData.arguments.should.be.an('array')
517 decodedCallData.arguments.length.should.be.equal(0)
518 decodedCallData.function.should.be.equal('init')
519 })
520 it('Decode data API', async () => {
521 const returnData = 'cb_bzvA9Af6'
522 return contract.contractDecodeDataAPI('string', returnData).catch(e => 1).should.eventually.become(1)
523 })
524 it('validate bytecode', async () => {
525 return contract.validateByteCodeAPI(bytecode, identityContract).should.eventually.become(true)
526 })
527 it('Use invalid compiler url', async () => {
528 try {
529 const cloned = R.clone(contract)
530 await cloned.setCompilerUrl('https://compiler.aepps.comas')
531 } catch (e) {
532 e.message.should.be.equal('Compiler do not respond')
533 }
534 })
535 })
536
537 describe('Contract ACI Interface', function () {
538 let contractObject
539 describe('Events parsing', async () => {
540 let cInstance
541 let eventResult
542 let decodedEventsWithoutACI
543 let decodedEventsUsingACI
544 let decodedEventsUsingBuildInMethod
545
546 before(async () => {
547 cInstance = await contract.getContractInstance(testContract, { filesystem })
548 await cInstance.deploy(['test', 1, 'some'])
549 eventResult = await cInstance.methods.emitEvents()
550 const { log } = await contract.tx(eventResult.hash)
551 decodedEventsWithoutACI = decodeEvents(log, { schema: events })
552 decodedEventsUsingACI = cInstance.decodeEvents('emitEvents', log)
553 decodedEventsUsingBuildInMethod = cInstance.methods.emitEvents.decodeEvents(log)
554 })
555 const events = [
556 { name: 'AnotherEvent2', types: [SOPHIA_TYPES.bool, SOPHIA_TYPES.string, SOPHIA_TYPES.int] },
557 { name: 'AnotherEvent', types: [SOPHIA_TYPES.string, SOPHIA_TYPES.address] },
558 { name: 'TheFirstEvent', types: [SOPHIA_TYPES.int] }
559 ]
560 const checkEvents = (event, schema) => {
561 schema.name.should.be.equal(event.name)
562 schema.types.forEach((t, tIndex) => {
563 const value = event.decoded[tIndex]
564 const isNumber = typeof value === 'string' || typeof value === 'number'
565 const v = typeof value === t // eslint-disable-line valid-typeof
566 switch (t) {
567 case SOPHIA_TYPES.address:
568 event.address.should.be.equal(`ct_${value}`)
569 break
570 case SOPHIA_TYPES.int:
571 isNumber.should.be.equal(true)
572 Number.isInteger(+value).should.be.equal(true)
573 break
574 case SOPHIA_TYPES.bool:
575 value.should.be.a('boolean')
576 break
577 default:
578 v.should.be.equal(true)
579 break
580 }
581 })
582 }
583 events
584 .forEach((el, i) => {
585 describe(`Correct parse of ${el.name}(${el.types})`, () => {
586 it('ACI call result', () => checkEvents(eventResult.decodedEvents[i], el))
587 it('ACI instance', () => checkEvents(decodedEventsUsingACI[i], el))
588 it('ACI instance methods', () => checkEvents(decodedEventsUsingBuildInMethod[i], el))
589 it('Without ACI', () => checkEvents(decodedEventsWithoutACI[i], el))
590 })
591 })
592 })
593
594 it('Generate ACI object', async () => {
595 contractObject = await contract.getContractInstance(testContract, { filesystem, opt: { ttl: 0 } })
596 contractObject.should.have.property('interface')
597 contractObject.should.have.property('aci')
598 contractObject.should.have.property('source')
599 contractObject.should.have.property('compiled')
600 contractObject.should.have.property('deployInfo')
601 contractObject.should.have.property('compile')
602 contractObject.should.have.property('call')
603 contractObject.should.have.property('deploy')
604 contractObject.options.ttl.should.be.equal(0)
605 contractObject.options.should.have.property('filesystem')
606 contractObject.options.filesystem.should.have.property('testLib')
607 const functionsFromACI = contractObject.aci.functions.map(({ name }) => name)
608 const methods = Object.keys(contractObject.methods)
609 R.equals(methods, functionsFromACI).should.be.equal(true)
610 })
611 it('Compile contract', async () => {
612 await contractObject.compile()
613 const isCompiled = contractObject.compiled.length && contractObject.compiled.slice(0, 3) === 'cb_'
614 isCompiled.should.be.equal(true)
615 })
616 it('Dry-run deploy fn', async () => {
617 const res = await contractObject.methods.init.get('123', 1, 'hahahaha')
618 res.result.should.have.property('gasUsed')
619 res.result.should.have.property('returnType')
620 })
621 it('Dry-run deploy fn on specific account', async () => {
622 const current = await contract.address()
623 const onAccount = contract.addresses().find(acc => acc !== current)
624 const { result } = await contractObject.methods.init.get('123', 1, 'hahahaha', { onAccount })
625 result.should.have.property('gasUsed')
626 result.should.have.property('returnType')
627 result.callerId.should.be.equal(onAccount)
628 })
629 it('Can deploy/call using AEVM', async () => {
630 await contractObject.compile({ backend: 'aevm' })
631 const deployStatic = await contractObject.methods.init.get('123', 1, 'hahahaha', { backend: 'aevm' })
632 deployStatic.should.be.an('object')
633 deployed = await contractObject.methods.init.send('123', 1, 'hahahaha', { backend: 'aevm' })
634 deployed.should.be.an('object')
635 const { result } = await contractObject.methods.intFn(123, { backend: 'aevm' })
636 result.should.have.property('gasUsed')
637 result.should.have.property('returnType')
638 await contractObject.compile()
639 })
640 it('Deploy contract before compile', async () => {
641 contractObject.compiled = null
642 await contractObject.methods.init('123', 1, 'hahahaha')
643 const isCompiled = contractObject.compiled.length && contractObject.compiled.slice(0, 3) === 'cb_'
644 isCompiled.should.be.equal(true)
645 })
646 it('Deploy/Call contract with { waitMined: false }', async () => {
647 const deployed = await contractObject.methods.init('123', 1, 'hahahaha', { waitMined: false })
648 await contract.poll(deployed.transaction)
649 Boolean(deployed.result === undefined).should.be.equal(true)
650 Boolean(deployed.txData === undefined).should.be.equal(true)
651 const result = await contractObject.methods.intFn.send(2, { waitMined: false })
652 Boolean(result.result === undefined).should.be.equal(true)
653 Boolean(result.txData === undefined).should.be.equal(true)
654 await contract.poll(result.hash)
655 })
656 it('Generate ACI object with corresponding bytecode', async () => {
657 await contract.getContractInstance(testContract, { contractAddress: contractObject.deployInfo.address, filesystem, opt: { ttl: 0 } })
658 })
659 it('Generate ACI object with not corresponding bytecode', async () => {
660 try {
661 await contract.getContractInstance(identityContract, { contractAddress: contractObject.deployInfo.address, opt: { ttl: 0 } })
662 } catch (e) {
663 e.message.should.be.equal('Contract source do not correspond to the contract bytecode deployed on the chain')
664 }
665 })
666 it('Generate ACI object with not corresponding bytecode and force this check', async () => {
667 await contract.getContractInstance(identityContract, { forceCodeCheck: true, contractAddress: contractObject.deployInfo.address, opt: { ttl: 0 } })
668 })
669 it('Throw error on creating contract instance with invalid contractAddress', async () => {
670 try {
671 await contract.getContractInstance(testContract, { filesystem, contractAddress: 'ct_asdasdasd', opt: { ttl: 0 } })
672 } catch (e) {
673 e.message.should.be.equal('Invalid contract address')
674 }
675 })
676 it('Throw error on creating contract instance with contract address which is not found on-chain or not active', async () => {
677 const contractAddress = 'ct_ptREMvyDbSh1d38t4WgYgac5oLsa2v9xwYFnG7eUWR8Er5cmT'
678 try {
679 await contract.getContractInstance(testContract, { filesystem, contractAddress, opt: { ttl: 0 } })
680 } catch (e) {
681 e.message.should.be.equal(`Contract with address ${contractAddress} not found on-chain or not active`)
682 }
683 })
684 it('Fail on paying to not payable function', async () => {
685 const amount = 100
686 try {
687 await contractObject.methods.intFn.send(1, { amount })
688 } catch (e) {
689 e.message.should.be.equal(`You try to pay "${amount}" to function "intFn" which is not payable. Only payable function can accept tokens`)
690 }
691 })
692 it('Can pay to payable function', async () => {
693 const contractBalance = await contract.balance(contractObject.deployInfo.address)
694 await contractObject.methods.stringFn.send('1', { amount: 100 })
695 const balanceAfter = await contract.balance(contractObject.deployInfo.address)
696 balanceAfter.should.be.equal(`${+contractBalance + 100}`)
697 })
698 it('Call contract on specific account', async () => {
699 const current = await contract.address()
700 const onAccount = contract.addresses().find(acc => acc !== current)
701 const { result } = await contractObject.methods.intFn('123', { onAccount })
702 result.callerId.should.be.equal(onAccount)
703 })
704 describe('Arguments Validation and Casting', function () {
705 describe('INT', function () {
706 it('Invalid', async () => {
707 try {
708 await contractObject.methods.intFn('asd')
709 } catch (e) {
710 e.message.should.be.equal('"Argument" at position 0 fails because [Value "[asd]" at path: [0] not a number]')
711 }
712 })
713 it('Valid', async () => {
714 const { decodedResult } = await contractObject.methods.intFn.get(1)
715 decodedResult.toString().should.be.equal('1')
716 })
717 })
718 describe('BOOL', function () {
719 it('Invalid', async () => {
720 try {
721 await contractObject.methods.boolFn({})
722 } catch (e) {
723 e.message.should.be.equal('"Argument" at position 0 fails because [Value "[[object Object]]" at path: [0] not a boolean]')
724 }
725 })
726 it('Valid', async () => {
727 const { decodedResult } = await contractObject.methods.boolFn.get(true)
728 decodedResult.should.be.equal(true)
729 })
730 })
731 describe('STRING', function () {
732 it('Invalid', async () => {
733 try {
734 await contractObject.methods.stringFn(123)
735 } catch (e) {
736 e.message.should.be.equal('"Argument" at position 0 fails because [Value "123" at path: [0] not a string]')
737 }
738 })
739 it('Valid', async () => {
740 const { decodedResult } = await contractObject.methods.stringFn('string')
741 decodedResult.should.be.equal('string')
742 })
743 })
744 describe('ADDRESS', function () {
745 it('Invalid address', async () => {
746 try {
747 await contractObject.methods.addressFn('asdasasd')
748 } catch (e) {
749 e.message.should.be.equal('"Argument" at position 0 fails because ["[asdasasd]" with value "asdasasd" fails to match the required pattern: /^(ak_|ct_|ok_|oq_)/]')
750 }
751 })
752 it('Invalid address type', async () => {
753 try {
754 await contractObject.methods.addressFn(333)
755 } catch (e) {
756 e.message.should.be.equal('"Argument" at position 0 fails because [Value "333" at path: [0] not a string]')
757 }
758 })
759 it('Return address', async () => {
760 const { decodedResult } = await contractObject.methods.accountAddress(await contract.address())
761 decodedResult.should.be.equal(await contract.address())
762 })
763 it('Valid', async () => {
764 const { decodedResult } = await contractObject.methods.addressFn('ak_2ct6nMwmRnyGX6jPhraFPedZ5bYp1GXqpvnAq5LXeL5TTPfFif')
765 decodedResult.should.be.equal('ak_2ct6nMwmRnyGX6jPhraFPedZ5bYp1GXqpvnAq5LXeL5TTPfFif')
766 })
767 })
768 describe('TUPLE', function () {
769 it('Invalid type', async () => {
770 try {
771 await contractObject.methods.tupleFn('asdasasd')
772 } catch (e) {
773 e.message.should.be.equal('"Argument" at position 0 fails because [Value "[asdasasd]" at path: [0] not a array]')
774 }
775 })
776 it('Invalid tuple prop type', async () => {
777 try {
778 await contractObject.methods.tupleFn([1, 'string'])
779 } catch (e) {
780 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]]')
781 }
782 })
783 it('Required tuple prop', async () => {
784 try {
785 await contractObject.methods.tupleFn([1])
786 } catch (e) {
787 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)]')
788 }
789 })
790 it('Wrong type in list inside tuple', async () => {
791 try {
792 await contractObject.methods.tupleWithList([[true], 1])
793 } catch (e) {
794 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]]]')
795 }
796 })
797 it('Wrong type in tuple inside tuple', async () => {
798 try {
799 await contractObject.methods.tupleInTupleFn([['str', 1], 1])
800 } catch (e) {
801 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]]]')
802 }
803 })
804 it('Valid', async () => {
805 const { decodedResult } = await contractObject.methods.tupleFn(['test', 1])
806 JSON.stringify(decodedResult).should.be.equal(JSON.stringify(['test', 1]))
807 })
808 })
809 describe('LIST', function () {
810 it('Invalid type', async () => {
811 try {
812 await contractObject.methods.listFn('asdasasd')
813 } catch (e) {
814 e.message.should.be.equal('"Argument" at position 0 fails because [Value "[asdasasd]" at path: [0] not a array]')
815 }
816 })
817 it('Invalid list element type', async () => {
818 try {
819 await contractObject.methods.listFn([1, 'string'])
820 } catch (e) {
821 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]]')
822 }
823 })
824 it('Invalid list element type nested', async () => {
825 try {
826 await contractObject.methods.listInListFn([['childListWronmgElement'], 'parentListWrongElement'])
827 } catch (e) {
828 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]]')
829 }
830 })
831 it('Valid', async () => {
832 const { decodedResult } = await contractObject.methods.listInListFn([[1, 2], [3, 4]])
833 JSON.stringify(decodedResult).should.be.equal(JSON.stringify([[1, 2], [3, 4]]))
834 })
835 })
836 describe('MAP', function () {
837 it('Valid', async () => {
838 const address = await contract.address()
839 const mapArg = new Map(
840 [
841 [address, ['someStringV', 324]]
842 ]
843 )
844 const objectFromMap = Array.from(mapArg.entries()).reduce((acc, [k, v]) => ({ ...acc, [k]: v }), {})
845 const { decodedResult } = await contractObject.methods.mapFn(objectFromMap)
846 JSON.stringify(decodedResult).should.be.equal(JSON.stringify(Array.from(mapArg.entries())))
847 })
848 it('Map With Option Value', async () => {
849 const address = await contract.address()
850 const mapArgWithSomeValue = new Map(
851 [
852 [address, ['someStringV', 123]]
853 ]
854 )
855 const mapArgWithNoneValue = new Map(
856 [
857 [address, ['someStringV', undefined]]
858 ]
859 )
860 const returnArgWithSomeValue = new Map(
861 [
862 [address, ['someStringV', 123]]
863 ]
864 )
865 const returnArgWithNoneValue = new Map(
866 [
867 [address, ['someStringV', undefined]]
868 ]
869 )
870 const resultWithSome = await contractObject.methods.mapOptionFn(mapArgWithSomeValue)
871 const resultWithNone = await contractObject.methods.mapOptionFn(mapArgWithNoneValue)
872
873 const decodedSome = resultWithSome.decodedResult
874
875 JSON.stringify(decodedSome).should.be.equal(JSON.stringify(Array.from(returnArgWithSomeValue.entries())))
876 JSON.stringify(resultWithNone.decodedResult).should.be.equal(JSON.stringify(Array.from(returnArgWithNoneValue.entries())))
877 })
878 it('Cast from string to int', async () => {
879 const address = await contract.address()
880 const mapArg = new Map(
881 [
882 [address, ['someStringV', '324']]
883 ]
884 )
885 const result = await contractObject.methods.mapFn(mapArg)
886 mapArg.set(address, ['someStringV', 324])
887 JSON.stringify(result.decodedResult).should.be.equal(JSON.stringify(Array.from(mapArg.entries())))
888 })
889 it('Cast from array to map', async () => {
890 const address = await contract.address()
891 const mapArg =
892 [
893 [address, ['someStringV', 324]]
894 ]
895 const { decodedResult } = await contractObject.methods.mapFn(mapArg)
896 JSON.stringify(decodedResult).should.be.equal(JSON.stringify(mapArg))
897 })
898 })
899 describe('RECORD/STATE', function () {
900 const objEq = (obj, obj2) => !Object.entries(obj).find(([key, val]) => JSON.stringify(obj2[key]) !== JSON.stringify(val))
901 it('Valid Set Record (Cast from JS object)', async () => {
902 await contractObject.methods.setRecord({ value: 'qwe', key: 1234, testOption: 'test' })
903 const state = await contractObject.methods.getRecord()
904
905 objEq(state.decodedResult, { value: 'qwe', key: 1234, testOption: 'test' }).should.be.equal(true)
906 })
907 it('Get Record(Convert to JS object)', async () => {
908 const result = await contractObject.methods.getRecord()
909 objEq(result.decodedResult, { value: 'qwe', key: 1234, testOption: 'test' }).should.be.equal(true)
910 })
911 it('Get Record With Option (Convert to JS object)', async () => {
912 await contractObject.methods.setRecord({ key: 1234, value: 'qwe', testOption: 'resolved string' })
913 const result = await contractObject.methods.getRecord()
914 objEq(result.decodedResult, { value: 'qwe', key: 1234, testOption: 'resolved string' }).should.be.equal(true)
915 })
916 it('Invalid value type', async () => {
917 try {
918 await contractObject.methods.setRecord({ value: 123, key: 'test' })
919 } catch (e) {
920 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]]')
921 }
922 })
923 })
924 describe('OPTION', function () {
925 it('Set Some Option Value(Cast from JS value/Convert result to JS)', async () => {
926 const optionRes = await contractObject.methods.intOption(123)
927
928 optionRes.decodedResult.should.be.equal(123)
929 })
930 it('Set Some Option List Value(Cast from JS value/Convert result to JS)', async () => {
931 const optionRes = await contractObject.methods.listOption([[1, 'testString']])
932
933 JSON.stringify(optionRes.decodedResult).should.be.equal(JSON.stringify([[1, 'testString']]))
934 })
935 it('Set None Option Value(Cast from JS value/Convert to JS)', async () => {
936 const optionRes = await contractObject.methods.intOption(undefined)
937 const isUndefined = optionRes.decodedResult === undefined
938 isUndefined.should.be.equal(true)
939 })
940 it('Invalid option type', async () => {
941 try {
942 await contractObject.methods.intOption('string')
943 } catch (e) {
944 e.message.should.be.equal('"Argument" at position 0 fails because [Value "[string]" at path: [0] not a number]')
945 }
946 })
947 })
948 describe('NAMESPACES', function () {
949 it('Use namespace in function body', async () => {
950 const res = await contractObject.methods.usingExternalLib(2)
951
952 res.decodedResult.should.be.equal(4)
953 })
954 })
955 describe('DATATYPE', function () {
956 it('Invalid type', async () => {
957 try {
958 await contractObject.methods.datTypeFn({})
959 } catch (e) {
960 e.message.should.be.equal('"Argument" at position 0 fails because ["0" must be a string, "value" must contain at least one of [Year, Month, Day]]')
961 }
962 })
963 it('Call generic datatype', async () => {
964 const res = await contractObject.methods.datTypeGFn({ Left: [2] })
965 res.decodedResult.should.be.equal(2)
966 })
967 it('Invalid arguments length', async () => {
968 try {
969 await contractObject.methods.datTypeGFn()
970 } catch (e) {
971 e.message.should.be.equal('Function "datTypeGFn" require 1 arguments of types [{"StateContract.one_or_both":["int","string"]}] but get []')
972 }
973 })
974 it('Invalid variant', async () => {
975 try {
976 await contractObject.methods.datTypeFn('asdcxz')
977 } catch (e) {
978 e.message.should.be.equal('"Argument" at position 0 fails because ["0" must be one of [Year, Month, Day], "0" must be an object]')
979 }
980 })
981 it('Valid', async () => {
982 const res = await contractObject.methods.datTypeFn('Year')
983 JSON.stringify(res.decodedResult).should.be.equal('"Year"')
984 })
985 })
986 describe('Hash', function () {
987 it('Invalid type', async () => {
988 try {
989 await contractObject.methods.hashFn({})
990 } catch (e) {
991 e.message.should.be.equal('The first argument must be of type string or an instance of Buffer, ArrayBuffer, or Array or an Array-like Object. Received an instance of Object')
992 }
993 })
994 it('Invalid length', async () => {
995 const address = await contract.address()
996 const decoded = Buffer.from(decode(address, 'ak').slice(1))
997 try {
998 await contractObject.methods.hashFn(decoded)
999 } catch (e) {
1000 const isSizeCheck = e.message.indexOf('not a 32 bytes') !== -1
1001 isSizeCheck.should.be.equal(true)
1002 }
1003 })
1004 it('Valid', async () => {
1005 const address = await contract.address()
1006 const decoded = decode(address, 'ak')
1007 const hashAsBuffer = await contractObject.methods.hashFn(decoded)
1008 const hashAsHex = await contractObject.methods.hashFn(decoded.toString('hex'))
1009 hashAsBuffer.decodedResult.should.be.equal(decoded.toString('hex'))
1010 hashAsHex.decodedResult.should.be.equal(decoded.toString('hex'))
1011 })
1012 })
1013 describe('Signature', function () {
1014 it('Invalid type', async () => {
1015 try {
1016 await contractObject.methods.signatureFn({})
1017 } catch (e) {
1018 e.message.should.be.equal('The first argument must be of type string or an instance of Buffer, ArrayBuffer, or Array or an Array-like Object. Received an instance of Object')
1019 }
1020 })
1021 it('Invalid length', async () => {
1022 const address = await contract.address()
1023 const decoded = decode(address, 'ak')
1024 try {
1025 await contractObject.methods.signatureFn(decoded)
1026 } catch (e) {
1027 const isSizeCheck = e.message.indexOf('not a 64 bytes') !== -1
1028 isSizeCheck.should.be.equal(true)
1029 }
1030 })
1031 it('Valid', async () => {
1032 const address = await contract.address()
1033 const decoded = decode(address, 'ak')
1034 const fakeSignature = Buffer.from(await contract.sign(decoded))
1035 const hashAsBuffer = await contractObject.methods.signatureFn(fakeSignature)
1036 const hashAsHex = await contractObject.methods.signatureFn(fakeSignature.toString('hex'))
1037 hashAsBuffer.decodedResult.should.be.equal(fakeSignature.toString('hex'))
1038 hashAsHex.decodedResult.should.be.equal(fakeSignature.toString('hex'))
1039 })
1040 })
1041 describe('Bytes', function () {
1042 it('Invalid type', async () => {
1043 try {
1044 await contractObject.methods.bytesFn({})
1045 } catch (e) {
1046 e.message.should.be.equal('The first argument must be of type string or an instance of Buffer, ArrayBuffer, or Array or an Array-like Object. Received an instance of Object')
1047 }
1048 })
1049 it('Invalid length', async () => {
1050 const address = await contract.address()
1051 const decoded = decode(address, 'ak')
1052 try {
1053 await contractObject.methods.bytesFn(Buffer.from([...decoded, 2]))
1054 } catch (e) {
1055 const isSizeCheck = e.message.indexOf('not a 32 bytes') !== -1
1056 isSizeCheck.should.be.equal(true)
1057 }
1058 })
1059 it('Valid', async () => {
1060 const address = await contract.address()
1061 const decoded = decode(address, 'ak')
1062 const hashAsBuffer = await contractObject.methods.bytesFn(decoded)
1063 const hashAsHex = await contractObject.methods.bytesFn(decoded.toString('hex'))
1064 hashAsBuffer.decodedResult.should.be.equal(decoded.toString('hex'))
1065 hashAsHex.decodedResult.should.be.equal(decoded.toString('hex'))
1066 })
1067 })
1068 })
1069 describe('Call contract', function () {
1070 it('Call contract using using sophia type arguments', async () => {
1071 contractObject.setOptions({ skipArgsConvert: true })
1072 const res = await contractObject.methods.listFn('[ 1, 2 ]')
1073 contractObject.setOptions({ skipArgsConvert: false })
1074 return res.decode().should.eventually.become([1, 2])
1075 })
1076 it('Call contract using using js type arguments', async () => {
1077 const res = await contractObject.methods.listFn([1, 2])
1078 return res.decode().should.eventually.become([1, 2])
1079 })
1080 it('Call contract using using js type arguments and skip result transform', async () => {
1081 contractObject.setOptions({ skipTransformDecoded: true })
1082 const res = await contractObject.methods.listFn([1, 2])
1083 const decoded = await res.decode()
1084 const decodedJSON = JSON.stringify([1, 2])
1085 contractObject.setOptions({ skipTransformDecoded: false })
1086 JSON.stringify(decoded).should.be.equal(decodedJSON)
1087 })
1088 it('Call contract with contract type argument', async () => {
1089 const result = await contractObject.methods.approve(0, 'ct_AUUhhVZ9de4SbeRk8ekos4vZJwMJohwW5X8KQjBMUVduUmoUh')
1090 return result.decode().should.eventually.become(0)
1091 })
1092 })
1093 describe('Type resolving', () => {
1094 let cInstance
1095 before(async () => {
1096 cInstance = await contract.getContractInstance(testContract, { filesystem })
1097 })
1098 it('Resolve remote contract type', async () => {
1099 const fnACI = getFunctionACI(cInstance.aci, 'remoteContract', { external: cInstance.externalAci })
1100 readType('Voting', { bindings: fnACI.bindings }).t.should.be.equal('address')
1101 })
1102 it('Resolve external contract type', async () => {
1103 const fnACI = getFunctionACI(cInstance.aci, 'remoteArgs', { external: cInstance.externalAci })
1104 readType(fnACI.arguments[0].type, { bindings: fnACI.bindings }).should.eql(JSON.parse('{"t":"record","generic":[{"name":"value","type":"string"},{"name":"key","type":{"list":["Voting.test_type"]}}]}'))
1105 readType(fnACI.returns, { bindings: fnACI.bindings }).t.should.be.equal('int')
1106 })
1107 })
1108 })
1109})