From 298706557a8f2b73a87dfb10c81626ebf127cadb Mon Sep 17 00:00:00 2001 From: spypsy Date: Tue, 29 Aug 2023 12:28:45 +0100 Subject: [PATCH] test: CLI tests (#1786) Fixes #1450 --- yarn-project/aztec-cli/README.md | 4 +- yarn-project/aztec-cli/src/encoding.ts | 49 +++++-- yarn-project/aztec-cli/src/index.ts | 16 +-- yarn-project/aztec-cli/src/test/mocks.ts | 63 +++++++++ yarn-project/aztec-cli/src/test/utils.test.ts | 133 ++++++++++++++++++ yarn-project/aztec-cli/src/utils.ts | 1 + yarn-project/end-to-end/src/e2e_cli.test.ts | 130 +++++++++++++++-- 7 files changed, 361 insertions(+), 35 deletions(-) create mode 100644 yarn-project/aztec-cli/src/test/mocks.ts create mode 100644 yarn-project/aztec-cli/src/test/utils.test.ts diff --git a/yarn-project/aztec-cli/README.md b/yarn-project/aztec-cli/README.md index e8fb42516f2..69313b80911 100644 --- a/yarn-project/aztec-cli/README.md +++ b/yarn-project/aztec-cli/README.md @@ -324,7 +324,7 @@ Options: - `-c, --contract-abi `: The compiled contract's ABI in JSON format. You can also use one of Aztec's example contracts found in (@aztec/noir-contracts)[https://www.npmjs.com/package/@aztec/noir-contracts], e.g. PrivateTokenContractAbi. - `-ca, --contract-address
`: Address of the contract. - `-k, --private-key `: The sender's private key. -- `-u, --rpcUrl `: URL of the Aztec RPC. Default: `http://localhost:8080`. +- `-u, --rpc-url `: URL of the Aztec RPC. Default: `http://localhost:8080`. This command calls a function on an Aztec contract. It requires the contract's ABI, address, function name, and optionally, function arguments. The command executes the function call and displays the transaction details. @@ -352,7 +352,7 @@ Options: - `-c, --contract-abi `: The compiled contract's ABI in JSON format. You can also use one of Aztec's example contracts found in (@aztec/noir-contracts)[https://www.npmjs.com/package/@aztec/noir-contracts], e.g. PrivateTokenContractAbi. - `-ca, --contract-address
`: Address of the contract. - `-f, --from `: Public key of the transaction viewer. If empty, it will try to find an account in the RPC. -- `-u, --rpcUrl `: URL of the Aztec RPC. Default: `http://localhost:8080`. +- `-u, --rpc-url `: URL of the Aztec RPC. Default: `http://localhost:8080`. This command simulates the execution of a view function on a deployed contract without modifying the state. It requires the contract's ABI, address, function name, and optionally, function arguments. The command displays the result of the view function. diff --git a/yarn-project/aztec-cli/src/encoding.ts b/yarn-project/aztec-cli/src/encoding.ts index d1615ce818c..da1381c321c 100644 --- a/yarn-project/aztec-cli/src/encoding.ts +++ b/yarn-project/aztec-cli/src/encoding.ts @@ -29,24 +29,37 @@ export function parseStructString(str: string, abiType: StructType) { * @param abiType - The type as described by the contract's ABI. * @returns The encoded argument. */ -function encodeArg(arg: string, abiType: ABIType): any { +function encodeArg(arg: string, abiType: ABIType, name: string): any { const { kind } = abiType; if (kind === 'field' || kind === 'integer') { - return BigInt(arg); + let res: bigint; + try { + res = BigInt(arg); + } catch (err) { + throw new Error( + `Invalid value passed for ${name}. Could not parse ${arg} as a${kind === 'integer' ? 'n' : ''} ${kind}.`, + ); + } + return res; } else if (kind === 'boolean') { if (arg === 'true') return true; if (arg === 'false') return false; + else throw Error(`Invalid boolean value passed for ${name}: ${arg}.`); } else if (kind === 'array') { let arr; + const res = []; try { arr = JSON.parse(arg); - if (!Array.isArray(arr)) throw Error(); - for (let i = 0; i < abiType.length; i += 1) { - return encodeArg(arg[i], abiType.type); - } } catch { - throw new Error(`Unable to parse arg ${arg} as array`); + throw new Error(`Unable to parse arg ${arg} as array for ${name} parameter`); + } + if (!Array.isArray(arr)) throw Error(`Invalid argument ${arg} passed for array parameter ${name}.`); + if (arr.length !== abiType.length) + throw Error(`Invalid array length passed for ${name}. Expected ${abiType.length}, received ${arr.length}.`); + for (let i = 0; i < abiType.length; i += 1) { + res.push(encodeArg(arr[i], abiType.type, name)); } + return res; } else if (kind === 'struct') { // check if input is encoded long string if (arg.startsWith('0x')) { @@ -55,15 +68,18 @@ function encodeArg(arg: string, abiType: ABIType): any { let obj; try { obj = JSON.parse(arg); - if (Array.isArray(obj)) throw Error(); - const res = []; - for (const field of abiType.fields) { - res.push(encodeArg(obj[field.name], field.type)); - } - return res; } catch { throw new Error(`Unable to parse arg ${arg} as struct`); } + if (Array.isArray(obj)) throw Error(`Array passed for arg ${name}. Expected a struct.`); + const res = []; + for (const field of abiType.fields) { + // Remove field name from list as it's present + const arg = obj[field.name]; + if (!arg) throw Error(`Expected field ${field.name} not found in struct ${name}.`); + res.push(encodeArg(obj[field.name], field.type, field.name)); + } + return res; } } @@ -73,10 +89,13 @@ function encodeArg(arg: string, abiType: ABIType): any { * @returns The encoded array. */ export function encodeArgs(args: any[], params: ABIParameter[]) { + if (args.length !== params.length) { + throw new Error(`Invalid number of args provided. Expected: ${params.length}, received: ${args.length}`); + } return args .map((arg: any, index) => { - const paramType = params[index].type; - return encodeArg(arg, paramType); + const { type, name } = params[index]; + return encodeArg(arg, type, name); }) .flat(); } diff --git a/yarn-project/aztec-cli/src/index.ts b/yarn-project/aztec-cli/src/index.ts index ae3cdc24cc8..a72998d7c1a 100644 --- a/yarn-project/aztec-cli/src/index.ts +++ b/yarn-project/aztec-cli/src/index.ts @@ -129,7 +129,7 @@ export function getProgram(log: LogFn, debugLogger: DebugLogger): Command { .action(async options => { const client = await createCompatibleClient(options.rpcUrl, debugLogger); const privateKey = options.privateKey - ? new PrivateKey(Buffer.from(stripLeadingHex(options.privateKey), 'hex')) + ? PrivateKey.fromString(stripLeadingHex(options.privateKey)) : PrivateKey.random(); const account = getSchnorrAccount(client, privateKey, privateKey, accountCreationSalt); @@ -198,8 +198,8 @@ export function getProgram(log: LogFn, debugLogger: DebugLogger): Command { program .command('get-tx-receipt') - .argument('', 'A transaction hash to get the receipt for.') .description('Gets the receipt for the specified transaction hash.') + .argument('', 'A transaction hash to get the receipt for.') .option('-u, --rpc-url ', 'URL of the Aztec RPC', AZTEC_RPC_HOST || 'http://localhost:8080') .action(async (_txHash, options) => { const client = await createCompatibleClient(options.rpcUrl, debugLogger); @@ -361,7 +361,7 @@ export function getProgram(log: LogFn, debugLogger: DebugLogger): Command { ) .requiredOption('-ca, --contract-address
', 'Aztec address of the contract.') .option('-k, --private-key ', "The sender's private key.", PRIVATE_KEY) - .option('-u, --rpcUrl ', 'URL of the Aztec RPC', AZTEC_RPC_HOST || 'http://localhost:8080') + .option('-u, --rpc-url ', 'URL of the Aztec RPC', AZTEC_RPC_HOST || 'http://localhost:8080') .action(async (functionName, options) => { const { contractAddress, functionArgs, contractAbi } = await prepTx( @@ -379,7 +379,7 @@ export function getProgram(log: LogFn, debugLogger: DebugLogger): Command { ); } - const privateKey = new PrivateKey(Buffer.from(stripLeadingHex(options.privateKey), 'hex')); + const privateKey = PrivateKey.fromString(stripLeadingHex(options.privateKey)); const client = await createCompatibleClient(options.rpcUrl, debugLogger); const wallet = await getAccountWallets( @@ -391,7 +391,7 @@ export function getProgram(log: LogFn, debugLogger: DebugLogger): Command { ); const contract = await Contract.at(contractAddress, contractAbi, wallet); const tx = contract.methods[functionName](...functionArgs).send(); - await tx.isMined(); + await tx.wait(); log('\nTransaction has been mined'); const receipt = await tx.getReceipt(); log(`Transaction hash: ${(await tx.getTxHash()).toString()}`); @@ -413,7 +413,7 @@ export function getProgram(log: LogFn, debugLogger: DebugLogger): Command { ) .requiredOption('-ca, --contract-address
', 'Aztec address of the contract.') .option('-f, --from ', 'Public key of the TX viewer. If empty, will try to find account in RPC.') - .option('-u, --rpcUrl ', 'URL of the Aztec RPC', AZTEC_RPC_HOST || 'http://localhost:8080') + .option('-u, --rpc-url ', 'URL of the Aztec RPC', AZTEC_RPC_HOST || 'http://localhost:8080') .action(async (functionName, options) => { const { contractAddress, functionArgs, contractAbi } = await prepTx( options.contractAbi, @@ -431,7 +431,7 @@ export function getProgram(log: LogFn, debugLogger: DebugLogger): Command { const client = await createCompatibleClient(options.rpcUrl, debugLogger); const from = await getTxSender(client, options.from); const result = await client.viewTx(functionName, functionArgs, contractAddress, from); - log('\nView result: ', JsonStringify(result, true), '\n'); + log('\nView result: ', result, '\n'); }); // Helper for users to decode hex strings into structs if needed @@ -461,7 +461,7 @@ export function getProgram(log: LogFn, debugLogger: DebugLogger): Command { program .command('block-number') .description('Gets the current Aztec L2 block number.') - .option('-u, --rpcUrl ', 'URL of the Aztec RPC', AZTEC_RPC_HOST || 'http://localhost:8080') + .option('-u, --rpc-url ', 'URL of the Aztec RPC', AZTEC_RPC_HOST || 'http://localhost:8080') .action(async (options: any) => { const client = await createCompatibleClient(options.rpcUrl, debugLogger); const num = await client.getBlockNumber(); diff --git a/yarn-project/aztec-cli/src/test/mocks.ts b/yarn-project/aztec-cli/src/test/mocks.ts new file mode 100644 index 00000000000..80573e21c49 --- /dev/null +++ b/yarn-project/aztec-cli/src/test/mocks.ts @@ -0,0 +1,63 @@ +import { ABIParameterVisibility, ContractAbi, FunctionType } from '@aztec/foundation/abi'; + +export const mockContractAbi: ContractAbi = { + name: 'MockContract', + functions: [ + { + name: 'constructor', + functionType: FunctionType.SECRET, + isInternal: false, + parameters: [ + { + name: 'constructorParam1', + type: { + kind: 'field', + }, + visibility: ABIParameterVisibility.SECRET, + }, + ], + returnTypes: [], + bytecode: 'constructorBytecode', + }, + { + name: 'mockFunction', + functionType: FunctionType.SECRET, + isInternal: false, + parameters: [ + { + name: 'fieldParam', + type: { kind: 'field' }, + visibility: ABIParameterVisibility.SECRET, + }, + { + name: 'boolParam', + type: { kind: 'boolean' }, + visibility: ABIParameterVisibility.SECRET, + }, + { + name: 'integerParam', + type: { kind: 'integer', sign: 'signed', width: 32 }, + visibility: ABIParameterVisibility.SECRET, + }, + { + name: 'arrayParam', + type: { kind: 'array', length: 3, type: { kind: 'field' } }, + visibility: ABIParameterVisibility.SECRET, + }, + { + name: 'structParam', + type: { + kind: 'struct', + fields: [ + { name: 'subField1', type: { kind: 'field' } }, + { name: 'subField2', type: { kind: 'boolean' } }, + ], + }, + visibility: ABIParameterVisibility.SECRET, + }, + ], + returnTypes: [{ kind: 'boolean' }], + bytecode: 'mockBytecode', + }, + ], +}; diff --git a/yarn-project/aztec-cli/src/test/utils.test.ts b/yarn-project/aztec-cli/src/test/utils.test.ts new file mode 100644 index 00000000000..ec318b58488 --- /dev/null +++ b/yarn-project/aztec-cli/src/test/utils.test.ts @@ -0,0 +1,133 @@ +import { AztecAddress, Fr } from '@aztec/aztec.js'; +import { AztecRPC, CompleteAddress } from '@aztec/types'; + +import { MockProxy, mock } from 'jest-mock-extended'; + +import { encodeArgs } from '../encoding.js'; +import { getTxSender } from '../utils.js'; +import { mockContractAbi } from './mocks.js'; + +describe('CLI Utils', () => { + let client: MockProxy; + + // test values + const addr1 = AztecAddress.random(); + const addr2 = AztecAddress.random(); + const addr3 = AztecAddress.random(); + const fieldArray = [addr1.toString(), addr2.toString(), addr3.toString()]; + const num = 33; + const field = Fr.random(); + const struct = { + subField1: field.toString(), + subField2: 'true', + }; + beforeEach(() => { + client = mock(); + }); + it('Gets a txSender correctly or throw error', async () => { + // returns a parsed Aztec Address + const aztecAddress = AztecAddress.random(); + const result = await getTxSender(client, aztecAddress.toString()); + expect(client.getAccounts).toHaveBeenCalledTimes(0); + expect(result).toEqual(aztecAddress); + + // returns an address found in the aztec client + const completeAddress = await CompleteAddress.random(); + client.getAccounts.mockResolvedValueOnce([completeAddress]); + const resultWithoutString = await getTxSender(client); + expect(client.getAccounts).toHaveBeenCalled(); + expect(resultWithoutString).toEqual(completeAddress.address); + + // throws when invalid parameter passed + const errorAddr = 'foo'; + await expect( + (async () => { + await getTxSender(client, errorAddr); + })(), + ).rejects.toThrow(`Invalid option 'from' passed: ${errorAddr}`); + + // Throws error when no string is passed & no accounts found in RPC + client.getAccounts.mockResolvedValueOnce([]); + await expect( + (async () => { + await getTxSender(client); + })(), + ).rejects.toThrow('No accounts found in Aztec RPC instance.'); + }); + + it('Encodes args correctly', () => { + const args = [addr1.toString(), 'false', num.toString(), `${JSON.stringify(fieldArray)}`, JSON.stringify(struct)]; + const result = encodeArgs(args, mockContractAbi.functions[1].parameters); + const exp = [ + addr1.toBigInt(), + false, + 33n, + addr1.toBigInt(), + addr2.toBigInt(), + addr3.toBigInt(), + field.toBigInt(), + true, + ]; + expect(result).toEqual(exp); + }); + + it('Errors on invalid inputs', () => { + // invalid number of args + const args1 = [field.toString(), 'false']; + expect(() => encodeArgs(args1, mockContractAbi.functions[1].parameters)).toThrow( + 'Invalid number of args provided. Expected: 5, received: 2', + ); + + // invalid array length + const invalidArray = fieldArray.concat([Fr.random().toString()]); + const args2 = [ + addr1.toString(), + 'false', + num.toString(), + `${JSON.stringify(invalidArray)}`, + JSON.stringify(struct), + ]; + expect(() => encodeArgs(args2, mockContractAbi.functions[1].parameters)).toThrow( + 'Invalid array length passed for arrayParam. Expected 3, received 4.', + ); + + // invalid struct + const invalidStruct = { + subField1: Fr.random().toString(), + }; + const args3 = [ + addr1.toString(), + 'false', + num.toString(), + `${JSON.stringify(fieldArray)}`, + JSON.stringify(invalidStruct), + ]; + expect(() => encodeArgs(args3, mockContractAbi.functions[1].parameters)).toThrow( + 'Expected field subField2 not found in struct structParam.', + ); + + // invalid bool + const args4 = [ + addr1.toString(), + 'foo', + num.toString(), + `${JSON.stringify(fieldArray)}`, + JSON.stringify(invalidStruct), + ]; + expect(() => encodeArgs(args4, mockContractAbi.functions[1].parameters)).toThrow( + 'Invalid boolean value passed for boolParam: foo.', + ); + + // invalid field + const args5 = ['foo', 'false', num.toString(), `${JSON.stringify(fieldArray)}`, JSON.stringify(invalidStruct)]; + expect(() => encodeArgs(args5, mockContractAbi.functions[1].parameters)).toThrow( + 'Invalid value passed for fieldParam. Could not parse foo as a field.', + ); + + // invalid int + const args6 = [addr1.toString(), 'false', 'foo', `${JSON.stringify(fieldArray)}`, JSON.stringify(invalidStruct)]; + expect(() => encodeArgs(args6, mockContractAbi.functions[1].parameters)).toThrow( + 'Invalid value passed for integerParam. Could not parse foo as an integer.', + ); + }); +}); diff --git a/yarn-project/aztec-cli/src/utils.ts b/yarn-project/aztec-cli/src/utils.ts index a1fd71648b2..822152e96ba 100644 --- a/yarn-project/aztec-cli/src/utils.ts +++ b/yarn-project/aztec-cli/src/utils.ts @@ -8,6 +8,7 @@ import { mnemonicToAccount, privateKeyToAccount } from 'viem/accounts'; import { encodeArgs } from './encoding.js'; +export { createClient } from './client.js'; /** * Helper type to dynamically import contracts. */ diff --git a/yarn-project/end-to-end/src/e2e_cli.test.ts b/yarn-project/end-to-end/src/e2e_cli.test.ts index e793fc93c4e..f86d10daffe 100644 --- a/yarn-project/end-to-end/src/e2e_cli.test.ts +++ b/yarn-project/end-to-end/src/e2e_cli.test.ts @@ -4,7 +4,7 @@ import { startHttpRpcServer } from '@aztec/aztec-sandbox/http'; import { createDebugLogger } from '@aztec/aztec.js'; import { getProgram } from '@aztec/cli'; import { DebugLogger } from '@aztec/foundation/log'; -import { AztecRPC } from '@aztec/types'; +import { AztecRPC, CompleteAddress } from '@aztec/types'; import stringArgv from 'string-argv'; import { format } from 'util'; @@ -12,6 +12,8 @@ import { format } from 'util'; import { setup } from './fixtures/utils.js'; const HTTP_PORT = 9009; +const INITIAL_BALANCE = 33000; +const TRANSFER_BALANCE = 3000; // Spins up a new http server wrapping the set up rpc server, and tests cli commands against it describe('cli', () => { @@ -20,6 +22,9 @@ describe('cli', () => { let debug: DebugLogger; let aztecNode: AztecNodeService | undefined; let aztecRpcServer: AztecRPC; + let existingAccounts: CompleteAddress[]; + let contractAddress: AztecAddress; + let log: (...args: any[]) => void; // All logs emitted by the cli will be collected here, and reset between tests const logs: string[] = []; @@ -32,13 +37,17 @@ describe('cli', () => { ({ aztecNode, aztecRpcServer } = context); http = startHttpRpcServer(aztecRpcServer, deployL1ContractsValues, HTTP_PORT); debug(`HTTP RPC server started in port ${HTTP_PORT}`); - const log = (...args: any[]) => { + log = (...args: any[]) => { logs.push(format(...args)); debug(...args); }; - cli = getProgram(log, debug); }); + // in order to run the same command twice, we need to create a new CLI instance + const resetCli = () => { + cli = getProgram(log, debug); + }; + afterAll(async () => { http.close(); await aztecNode?.stop(); @@ -47,11 +56,17 @@ describe('cli', () => { beforeEach(() => { logs.splice(0); + resetCli(); }); // Run a command on the CLI - const run = (cmd: string) => - cli.parseAsync(stringArgv(cmd, 'node', 'dest/bin/index.js').concat(['--rpc-url', `http://localhost:${HTTP_PORT}`])); + const run = (cmd: string, addRpcUrl = true) => { + const args = stringArgv(cmd, 'node', 'dest/bin/index.js'); + if (addRpcUrl) { + args.push('--rpc-url', `http://localhost:${HTTP_PORT}`); + } + return cli.parseAsync(args); + }; // Returns first match across all logs collected so far const findInLogs = (regex: RegExp) => { @@ -61,14 +76,109 @@ describe('cli', () => { } }; - it('creates an account', async () => { - const accountsBefore = await aztecRpcServer.getAccounts(); + const findMultipleInLogs = (regex: RegExp) => { + const matches = []; + for (const log of logs) { + const match = regex.exec(log); + if (match) matches.push(match); + } + return matches; + }; + + const clearLogs = () => { + logs.splice(0); + }; + + it('creates & retrieves an account', async () => { + existingAccounts = await aztecRpcServer.getAccounts(); + debug('Create an account'); await run(`create-account`); - const newAddress = findInLogs(/Address:\s+(?
0x[a-fA-F0-9]+)/)?.groups?.address; - expect(newAddress).toBeDefined(); + const foundAddress = findInLogs(/Address:\s+(?
0x[a-fA-F0-9]+)/)?.groups?.address; + expect(foundAddress).toBeDefined(); + const newAddress = AztecAddress.fromString(foundAddress!); const accountsAfter = await aztecRpcServer.getAccounts(); - const expectedAccounts = [...accountsBefore.map(a => a.address), AztecAddress.fromString(newAddress!)]; + const expectedAccounts = [...existingAccounts.map(a => a.address), newAddress]; expect(accountsAfter.map(a => a.address)).toEqual(expectedAccounts); + const newCompleteAddress = accountsAfter[accountsAfter.length - 1]; + + // Test get-accounts + debug('Check that account was added to the list of accs in RPC'); + await run('get-accounts'); + const fetchedAddresses = findMultipleInLogs(/Address:\s+(?
0x[a-fA-F0-9]+)/); + const foundFetchedAddress = fetchedAddresses.find(match => match.groups?.address === newAddress.toString()); + expect(foundFetchedAddress).toBeDefined(); + + // Test get-account + debug('Check we can retrieve the specific account'); + clearLogs(); + await run(`get-account ${newAddress.toString()}`); + const fetchedAddress = findInLogs(/Public Key:\s+(?
0x[a-fA-F0-9]+)/)?.groups?.address; + expect(fetchedAddress).toEqual(newCompleteAddress.publicKey.toString()); + }); + + it('deploys a contract & sends transactions', async () => { + // generate a private key + debug('Create an account using a private key'); + await run('generate-private-key', false); + const privKey = findInLogs(/Private\sKey:\s+(?[a-fA-F0-9]+)/)?.groups?.privKey; + expect(privKey).toHaveLength(64); + await run(`create-account --private-key ${privKey}`); + const foundAddress = findInLogs(/Address:\s+(?
0x[a-fA-F0-9]+)/)?.groups?.address; + expect(foundAddress).toBeDefined(); + const ownerAddress = AztecAddress.fromString(foundAddress!); + + debug('Deploy Private Token Contract using created account.'); + await run(`deploy PrivateTokenContractAbi --args ${INITIAL_BALANCE} ${ownerAddress} --salt 0`); + const loggedAddress = findInLogs(/Contract\sdeployed\sat\s+(?
0x[a-fA-F0-9]+)/)?.groups?.address; + expect(loggedAddress).toBeDefined(); + contractAddress = AztecAddress.fromString(loggedAddress!); + + const deployedContract = await aztecRpcServer.getContractData(contractAddress); + expect(deployedContract?.contractAddress).toEqual(contractAddress); + + debug('Check contract can be found in returned address'); + await run(`check-deploy -ca ${loggedAddress}`); + const checkResult = findInLogs(/Contract\sfound\sat\s+(?
0x[a-fA-F0-9]+)/)?.groups?.address; + expect(checkResult).toEqual(deployedContract?.contractAddress.toString()); + + // clear logs + clearLogs(); + await run(`get-contract-data ${loggedAddress}`); + const contractDataAddress = findInLogs(/Address:\s+(?
0x[a-fA-F0-9]+)/)?.groups?.address; + expect(contractDataAddress).toEqual(deployedContract?.contractAddress.toString()); + + debug("Check owner's balance"); + await run( + `call getBalance --args ${ownerAddress} --contract-abi PrivateTokenContractAbi --contract-address ${contractAddress.toString()}`, + ); + const balance = findInLogs(/View\sresult:\s+(?\S+)/)?.groups?.data; + expect(balance!).toEqual(`${BigInt(INITIAL_BALANCE).toString()}n`); + + debug('Transfer some tokens'); + const existingAccounts = await aztecRpcServer.getAccounts(); + // ensure we pick a different acc + const receiver = existingAccounts.find(acc => acc.address.toString() !== ownerAddress.toString()); + + await run( + `send transfer --args ${TRANSFER_BALANCE} ${receiver?.address.toString()} --contract-address ${contractAddress.toString()} --contract-abi PrivateTokenContractAbi --private-key ${privKey}`, + ); + const txHash = findInLogs(/Transaction\shash:\s+(?\S+)/)?.groups?.txHash; + + debug('Check the transfer receipt'); + await run(`get-tx-receipt ${txHash}`); + const txResult = findInLogs(/Transaction receipt:\s*(?[\s\S]*?\})/)?.groups?.txHash; + const parsedResult = JSON.parse(txResult!); + expect(parsedResult.txHash).toEqual(txHash); + expect(parsedResult.status).toEqual('mined'); + debug("Check Receiver's balance"); + // Reset CLI as we're calling getBalance again + resetCli(); + clearLogs(); + await run( + `call getBalance --args ${receiver?.address.toString()} --contract-abi PrivateTokenContractAbi --contract-address ${contractAddress.toString()}`, + ); + const receiverBalance = findInLogs(/View\sresult:\s+(?\S+)/)?.groups?.data; + expect(receiverBalance).toEqual(`${BigInt(TRANSFER_BALANCE).toString()}n`); }); });