index.js (334 lines of code) (raw):
#!/usr/bin/env node
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
const { Command } = require('commander');
const { execFile } = require('child_process');
const path = require('path');
const fs = require('fs');
const os = require('os');
const { getResDBHome } = require('./config');
const logger = require('./logger');
const { spawn } = require('child_process');
const program = new Command();
async function ensureResDBHome() {
try {
const resDBHome = await getResDBHome();
if (!resDBHome) {
console.error(
'Error: ResDB_Home is not set. Please set the ResDB_Home environment variable or provide a config.yaml file.'
);
logger.error('ResDB_Home is not set.');
process.exit(1);
}
return resDBHome;
} catch (error) {
console.error(`Error: ${error.message}`);
logger.error(`Error: ${error.message}`);
process.exit(1);
}
}
// Define the path for the deployed contracts registry file
const registryFilePath = path.join(
os.homedir(),
'.rescontract_deployed_contracts.json'
);
// Function to load the deployed contracts registry from file
function loadDeployedContracts() {
if (fs.existsSync(registryFilePath)) {
const data = fs.readFileSync(registryFilePath, 'utf-8');
return new Map(JSON.parse(data));
}
return new Map();
}
// Function to save the deployed contracts registry to file
function saveDeployedContracts(registry) {
const data = JSON.stringify(Array.from(registry.entries()), null, 2);
fs.writeFileSync(registryFilePath, data, 'utf-8');
}
// Load the registry at the start
const deployedContracts = loadDeployedContracts();
function handleExecFile(command, args, options = {}, onData) {
return new Promise((resolve, reject) => {
const child = execFile(command, args, options);
child.stdout.on('data', (data) => {
process.stdout.write(data);
if (onData) onData(data);
});
child.stderr.on('data', (data) => {
process.stderr.write(data);
if (onData) onData(data);
});
child.on('error', (error) => {
logger.error(`Error spawning child process: ${error.message}`);
reject(error);
});
child.on('exit', (code) => {
if (code !== 0) {
logger.error(`Process exited with code ${code}`);
reject(new Error(`Process exited with code ${code}`));
} else {
resolve();
}
});
});
}
// Function to handle the process execution
function handleSpawnProcess(command, args, options = {}, onData) {
return new Promise((resolve, reject) => {
const child = spawn(command, args, options);
let output = '';
child.stdout.on('data', (data) => {
process.stdout.write(data); // Print to CLI
output += data.toString(); // Accumulate output
if (onData) onData(data.toString());
});
child.stderr.on('data', (data) => {
process.stderr.write(data); // Print errors to CLI
output += data.toString(); // Accumulate output
if (onData) onData(data.toString());
});
child.on('error', (error) => {
reject(error); // Handle spawn error
});
child.on('close', (code) => {
if (code !== 0) {
reject(new Error(`Process exited with code ${code}`));
} else {
resolve(output); // Return the full output
}
});
});
}
program
.name('rescontract')
.version('1.2.2')
.description('ResContract CLI - Manage smart contracts in ResilientDB');
program
.command('create')
.description('Create a new account')
.requiredOption('-c, --config <path>', 'Path to the config file')
.action(async (options) => {
try {
const configPath = options.config;
const resDBHome = await ensureResDBHome();
const commandPath = path.join(
resDBHome,
'bazel-bin',
'service',
'tools',
'contract',
'api_tools',
'contract_tools'
);
if (!fs.existsSync(configPath)) {
logger.error(`Config file not found at ${configPath}`);
console.error(`Error: Config file not found at ${configPath}`);
process.exit(1);
}
await handleExecFile(commandPath, ['create', '-c', configPath]);
} catch (error) {
logger.error(`Error executing create command: ${error.message}`);
console.error(`Error: ${error.message}`);
process.exit(1);
}
});
program
.command('compile')
.description('Compile a .sol file to a .json file')
.requiredOption('-s, --sol <path>', 'Path to the .sol file')
.requiredOption('-o, --output <name>', 'Name of the output .json file')
.action(async (options) => {
try {
const solPath = options.sol;
const outputName = options.output;
if (!fs.existsSync(solPath)) {
logger.error(`Solidity file not found at ${solPath}`);
console.error(`Error: Solidity file not found at ${solPath}`);
process.exit(1);
}
const command = 'solc';
const args = [
'--evm-version',
'homestead',
'--combined-json',
'bin,hashes',
'--pretty-json',
'--optimize',
solPath,
];
await new Promise((resolve, reject) => {
const child = execFile(command, args, (error, stdout, stderr) => {
if (error) {
logger.error(`Compilation failed: ${error.message}`);
console.error(`Error: Compilation failed: ${error.message}`);
reject(error);
return;
}
fs.writeFileSync(outputName, stdout);
logger.info(`Successfully compiled ${solPath} to ${outputName}`);
console.log(`Compiled successfully to ${outputName}`);
resolve();
});
});
} catch (error) {
logger.error(`Error executing compile command: ${error.message}`);
console.error(`Error: ${error.message}`);
process.exit(1);
}
});
program
.command('deploy')
.description('Deploy a smart contract')
.requiredOption('-c, --config <configPath>', 'Client configuration path')
.requiredOption('-p, --contract <contractPath>', 'Contract JSON file path')
.requiredOption('-n, --name <contractName>', 'Contract name')
.requiredOption(
'-a, --arguments <parameters>',
'Constructor parameters (comma-separated)'
)
.requiredOption('-m, --owner <ownerAddress>', 'Contract owner’s address')
.action(async (options) => {
try {
const {
config: configPath,
contract,
name,
arguments: args,
owner,
} = options;
const deploymentKey = `${owner}:${name}`;
if (deployedContracts.has(deploymentKey)) {
const existingContractAddress =
deployedContracts.get(deploymentKey).contractAddress;
console.error(
JSON.stringify({
error: `Contract "${name}" is already deployed by owner "${owner}" at address "${existingContractAddress}".`,
})
);
process.exit(1);
}
const resDBHome = await ensureResDBHome();
const commandPath = path.join(
resDBHome,
'bazel-bin',
'service',
'tools',
'contract',
'api_tools',
'contract_tools'
);
const argList = [
'deploy',
'-c',
configPath,
'-p',
contract,
'-n',
name,
'-a',
args,
'-m',
owner,
];
const output = await handleSpawnProcess(commandPath, argList);
const outputLines = output.split('\n');
let ownerAddress = '';
let contractAddress = '';
let contractName = '';
for (const line of outputLines) {
const content = line.replace(/^.*\] /, '').trim();
const ownerMatch = content.match(/owner_address:\s*"(.+)"$/);
const contractAddressMatch = content.match(/contract_address:\s*"(.+)"$/);
const contractNameMatch = content.match(/contract_name:\s*"(.+)"$/);
if (ownerMatch) {
ownerAddress = ownerMatch[1];
} else if (contractAddressMatch) {
contractAddress = contractAddressMatch[1];
} else if (contractNameMatch) {
contractName = contractNameMatch[1];
}
}
if (!ownerAddress || !contractAddress || !contractName) {
console.error(
JSON.stringify({
error: 'Failed to parse deployment output.',
})
);
process.exit(1);
}
deployedContracts.set(deploymentKey, {
ownerAddress: ownerAddress,
contractAddress: contractAddress,
contractName: contractName,
});
saveDeployedContracts(deployedContracts);
console.log(
JSON.stringify({
owner_address: ownerAddress,
contract_address: contractAddress,
contract_name: contractName,
})
);
} catch (error) {
console.error(
JSON.stringify({
error: error.message,
})
);
process.exit(1);
}
});
program
.command('add_address')
.description('Add an external address to the system')
.requiredOption('-c, --config <path>', 'Path to the config file')
.requiredOption('-e, --external-address <address>', 'External address to add')
.action(async (options) => {
try {
const configPath = options.config;
const externalAddress = options.externalAddress;
const resDBHome = await ensureResDBHome();
const commandPath = path.join(
resDBHome,
'bazel-bin',
'service',
'tools',
'contract',
'api_tools',
'contract_tools'
);
if (!fs.existsSync(configPath)) {
logger.error(`Config file not found at ${configPath}`);
console.error(`Error: Config file not found at ${configPath}`);
process.exit(1);
}
await handleExecFile(commandPath, [
'add_address',
'-c',
configPath,
'-e',
externalAddress,
]);
} catch (error) {
logger.error(`Error executing add_address command: ${error.message}`);
console.error(`Error: ${error.message}`);
process.exit(1);
}
});
// Optional commands to manage the registry
program
.command('list-deployments')
.description('List all deployed contracts')
.action(() => {
if (deployedContracts.size === 0) {
console.log('No contracts have been deployed yet.');
} else {
console.log('Deployed Contracts:');
for (const [key, value] of deployedContracts.entries()) {
console.log(`Owner: ${value.ownerAddress}`);
console.log(`Contract Name: ${value.contractName}`);
console.log(`Contract Address: ${value.contractAddress}`);
console.log('---');
}
}
});
program
.command('clear-registry')
.description('Clear the deployed contracts registry')
.action(() => {
try {
if (fs.existsSync(registryFilePath)) {
fs.unlinkSync(registryFilePath);
deployedContracts.clear();
console.log('Deployed contracts registry cleared.');
} else {
console.log('Deployed contracts registry is already empty.');
}
} catch (error) {
logger.error(`Error clearing registry: ${error.message}`);
console.error(`Error: ${error.message}`);
process.exit(1);
}
});
if (!process.argv.slice(2).length) {
program.outputHelp();
}
program.parse(process.argv);