Skip to content

Commit

Permalink
feat(contract deploy)!: accept args and bytecode, custom descrPath
Browse files Browse the repository at this point in the history
  • Loading branch information
davidyuk committed Jan 28, 2022
1 parent 201a7e0 commit cb25bb7
Show file tree
Hide file tree
Showing 4 changed files with 100 additions and 80 deletions.
88 changes: 29 additions & 59 deletions src/actions/contract.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,12 @@

import fs from 'fs';
import path from 'path';
import { TxBuilderHelper } from '@aeternity/aepp-sdk';
import { initClient, initClientByWalletFile } from '../utils/cli';
import { print, printTransaction, printUnderscored } from '../utils/print';

const readFile = (filename) => fs.readFileSync(path.resolve(process.cwd(), filename), 'utf-8');
const resolve = (filename) => path.resolve(process.cwd(), filename);
const readFile = (filename, encoding = 'utf-8') => fs.readFileSync(resolve(filename), encoding);

// ## Function which compile your `source` code
export async function compile(filename, options) {
Expand All @@ -34,16 +36,17 @@ export async function compile(filename, options) {
}

function getContractParams({
descrPath, contractAddress, contractSource, contractAci,
descrPath, contractAddress, contractSource, contractBytecode, contractAci,
}, { dummySource } = {}) {
if (descrPath) {
if (descrPath && fs.existsSync(resolve(descrPath))) {
const { address, ...other } = JSON.parse(readFile(descrPath));
return { contractAddress: address, ...other };
}
return {
contractAddress,
// TODO: either remove calldata methods in cli or reconsider getContractInstance requirements
source: (contractSource && readFile(contractSource)) ?? (dummySource && 'invalid-source'),
bytecode: contractBytecode && TxBuilderHelper.encode(readFile(contractBytecode, null), 'cb'),
aci: contractAci && JSON.parse(readFile(contractAci)),
};
}
Expand All @@ -67,64 +70,31 @@ export async function decodeCallResult(fn, calldata, options) {
}
}

// ## Function which `deploy ` contract
export async function deploy(walletPath, contractPath, callData = '', options) {
const {
json, gas, gasPrice, ttl, nonce, fee,
} = options;
// Deploy a contract to the chain and create a deploy descriptor
// with the contract informations that can be use to invoke the contract
// later on.
// The generated descriptor will be created in the same folde of the contract
// source file. Multiple deploy of the same contract file will generate different
// deploy descriptor
if (callData.split('_')[0] !== 'cb') throw new Error('"callData" should be a string with "cb" prefix');
// ## Function which `deploy` contract
export async function deploy(walletPath, args, options) {
// Deploy a contract to the chain and create a deployment descriptor
// with the contract information that can be used to invoke the contract
// later on. The generated descriptor will be created in the same folder of the contract
// source file or at location provided in descrPath. Multiple deploy of the same contract
// file will generate different deploy descriptors.
const sdk = await initClientByWalletFile(walletPath, options);
const contractFile = readFile(contractPath);

const ownerId = await sdk.address();
const { bytecode: code } = await sdk.contractCompile(contractFile);
const opt = {
...sdk.Ae.defaults, gas, gasPrice, ttl, nonce, fee,
};

// Prepare contract create transaction
const { tx, contractId } = await sdk.contractCreateTx({
...opt,
callData,
code,
ownerId,
const descriptor = getContractParams(options);
const contract = await sdk.getContractInstance(descriptor);
const result = await contract.deploy(args, options);
Object.assign(descriptor, {
address: result.address,
bytecode: contract.bytecode,
});
// Broadcast transaction
const { hash } = await sdk.send(tx, opt);
const result = await sdk.getTxInfo(hash);

if (result.returnType === 'ok') {
const deployDescriptor = Object.freeze({
result,
owner: ownerId,
transaction: hash,
address: contractId,
createdAt: new Date(),
});
// Prepare contract descriptor
const descPath = `${contractPath.split('/').pop()}.deploy.${ownerId.slice(3)}.json`;
const descriptor = {
descPath,
source: contractFile,
bytecode: code,
...deployDescriptor,
};
fs.writeFileSync(descPath, JSON.stringify(descriptor));
if (json) print({ descPath, ...deployDescriptor });
else {
print('Contract was successfully deployed');
printUnderscored('Contract address', descriptor.address);
printUnderscored('Transaction hash', descriptor.transaction);
printUnderscored('Deploy descriptor', descriptor.descPath);
}
} else {
await this.handleCallError(result);
const filename = options.contractSource ?? options.contractBytecode;
options.descrPath ||= path
.resolve(process.cwd(), `${filename}.deploy.${result.address.slice(3)}.json`);
fs.writeFileSync(options.descrPath, JSON.stringify(descriptor, undefined, 2));
if (options.json) print({ ...result, descrPath: options.descrPath });
else {
print('Contract was successfully deployed');
printUnderscored('Contract address', result.address);
printUnderscored('Transaction hash', result.transaction);
printUnderscored('Deploy descriptor', options.descrPath);
}
}

Expand Down
9 changes: 7 additions & 2 deletions src/commands/contract.js
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,12 @@ export default () => {
//
// Example: `aecli contract deploy ./myWalletFile --password tstpass ./contractSourceCodeFile --gas 2222222`
program
.command('deploy <wallet_path> <contract_path> <callData>')
.command('deploy <wallet_path>')
.addArgument(callArgs)
.addOption(descriptorPathOption)
.addOption(contractSourceFilenameOption)
.option('--contractBytecode [contractBytecode]', 'Contract bytecode file name')
.addOption(contractAciFilenameOption)
.option('--networkId [networkId]', 'Network id (default: ae_mainnet)')
.option('-W, --no-waitMined', 'Force waiting until transaction will be mined')
.option('-P, --password [password]', 'Wallet Password')
Expand All @@ -138,7 +143,7 @@ export default () => {
.option('-T, --ttl [ttl]', 'Validity of the spend transaction in number of blocks (default forever)', SCHEMA.TX_TTL)
.option('-N, --nonce [nonce]', 'Override the nonce that the transaction is going to be sent with')
.description('Deploy a contract on the chain')
.action((walletPath, path, callData, ...args) => Contract.deploy(walletPath, path, callData, getCmdFromArguments(args)));
.action((walletPath, args, ...otherArgs) => Contract.deploy(walletPath, args, getCmdFromArguments(otherArgs)));

return program;
};
4 changes: 2 additions & 2 deletions src/utils/print.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export function print(msg, obj) {
}
if (obj) {
console.log(msg);
console.log(JSON.stringify(obj));
console.log(JsonStringifyBigInt(obj));
} else {
console.log(msg);
}
Expand All @@ -60,7 +60,7 @@ export function printUnderscored(key, val) {
print([
key,
'_'.repeat(WIDTH - key.length),
typeof val !== 'object' ? val : JSON.stringify(val),
typeof val !== 'object' ? val : JsonStringifyBigInt(val),
].join(' '));
}

Expand Down
79 changes: 62 additions & 17 deletions test/contract.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,23 +20,28 @@ import {
after, before, describe, it,
} from 'mocha';
import { expect } from 'chai';
import { TxBuilderHelper } from '@aeternity/aepp-sdk';
import { executeProgram, getSdk, WALLET_NAME } from './index';
import contractProgramFactory from '../src/commands/contract';

const executeContract = (args) => executeProgram(contractProgramFactory, args);

const testContractSource = `
@compiler >= 6
@compiler < 7
contract Identity =
entrypoint test(x : int, y: int) = x + y
record state = { z: int }
entrypoint init(_z: int) = { z = _z }
entrypoint test(x : int, y: int) = x + y + state.z
`;

describe('CLI Contract Module', () => {
const contractSourceFile = 'testContract';
const contractAciFile = 'testContractAci';
let deployDescriptorFile;
let sdk;
let contractBytecode;
let contractAddress;

before(async () => {
Expand All @@ -54,21 +59,61 @@ describe('CLI Contract Module', () => {

it('compiles contract', async () => {
const { bytecode } = await executeContract(['compile', contractSourceFile, '--json']);
contractBytecode = bytecode;
expect(bytecode).to.satisfy((b) => b.startsWith('cb_'));
});

it('Deploy Contract', async () => {
const { calldata } = await executeContract(['encode-calldata', '--contractSource', contractSourceFile, 'init', '--json']);
const res = await executeContract(['deploy', WALLET_NAME, '--password', 'test', contractSourceFile, calldata, '--json']);
const { result: { contractId }, transaction, descPath } = res;
deployDescriptorFile = descPath;
const [name, pref, add] = deployDescriptorFile.split('.');
contractAddress = contractId;
contractId.should.be.a('string');
transaction.should.be.a('string');
name.should.be.equal(contractSourceFile);
pref.should.be.equal('deploy');
add.should.be.equal((await sdk.address()).split('_')[1]);
describe('Deploy', () => {
it('deploys contract', async () => {
const { address, transaction, descrPath } = await executeContract([
'deploy',
WALLET_NAME, '--password', 'test',
'--contractSource', contractSourceFile,
'[3]',
'--json',
]);
deployDescriptorFile = descrPath;
const [name, pref, add] = deployDescriptorFile.split('.');
contractAddress = address;
address.should.be.a('string');
transaction.should.be.a('string');
name.should.satisfy((n) => n.endsWith(contractSourceFile));
pref.should.be.equal('deploy');
add.should.be.equal(address.split('_')[1]);
});

it('deploys contract with custom descrPath', async () => {
const descrPath = './testDescriptor.json';
await executeContract([
'deploy',
WALLET_NAME, '--password', 'test',
'--contractSource', contractSourceFile,
'--descrPath', descrPath,
'[3]',
'--json',
]);
expect(fs.existsSync(descrPath)).to.be.equal(true);
const descriptor = JSON.parse(fs.readFileSync(descrPath, 'utf-8'));
expect(descriptor.address).to.satisfy((b) => b.startsWith('ct_'));
expect(descriptor.bytecode).to.satisfy((b) => b.startsWith('cb_'));
expect(descriptor.source).to.satisfy((b) => b.includes('contract Identity'));
fs.unlinkSync(descrPath);
});

it('deploys contract by bytecode', async () => {
const contractBytecodeFile = './bytecode.bin';
fs.writeFileSync(contractBytecodeFile, TxBuilderHelper.decode(contractBytecode));
const { descrPath } = await executeContract([
'deploy',
WALLET_NAME, '--password', 'test',
'--contractAci', contractAciFile,
'--contractBytecode', contractBytecodeFile,
'[3]',
'--json',
]);
fs.unlinkSync(descrPath);
fs.unlinkSync(contractBytecodeFile);
});
});

describe('Call', () => {
Expand All @@ -81,7 +126,7 @@ describe('CLI Contract Module', () => {
'test', '[1, 2]',
]);
callResponse.result.returnValue.should.contain('cb_');
callResponse.decodedResult.should.be.equal('3');
callResponse.decodedResult.should.be.equal('6');
});

it('calls contract static', async () => {
Expand All @@ -94,7 +139,7 @@ describe('CLI Contract Module', () => {
'--callStatic',
]);
callResponse.result.returnValue.should.contain('cb_');
callResponse.decodedResult.should.equal('3');
callResponse.decodedResult.should.equal('6');
});

it('calls contract by contract source and address', async () => {
Expand All @@ -106,7 +151,7 @@ describe('CLI Contract Module', () => {
'--contractSource', contractSourceFile,
'test', '[1, 2]',
]);
callResponse.decodedResult.should.equal('3');
callResponse.decodedResult.should.equal('6');
});

it('calls contract by contract ACI and address', async () => {
Expand All @@ -118,7 +163,7 @@ describe('CLI Contract Module', () => {
'--contractAci', contractAciFile,
'test', '[1, 2]',
]);
callResponse.decodedResult.should.equal('3');
callResponse.decodedResult.should.equal('6');
});
});

Expand Down

0 comments on commit cb25bb7

Please sign in to comment.