Skip to content

Commit

Permalink
feat: 'contracts new' command
Browse files Browse the repository at this point in the history
Add 'contract new' command, which verifies a project workspace, and adds
a new smart contract package in the workspace
  • Loading branch information
eliasmpw authored and aelesbao committed Aug 29, 2023
1 parent 3aaa062 commit bb1fda2
Show file tree
Hide file tree
Showing 30 changed files with 556 additions and 129 deletions.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@
"ow": "^0.28.2",
"promisify-child-process": "^4.1.1",
"prompts": "^2.4.2",
"terminal-link": "^2.1.1"
"terminal-link": "^2.1.1",
"toml": "^3.0.0"
},
"devDependencies": {
"@oclif/test": "^2.3.15",
Expand Down
14 changes: 14 additions & 0 deletions src/arguments/contract.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Args } from '@oclif/core';

import { sanitizeDirName } from '@/utils/sanitize';

const ContractArgumentDescription = 'Contract name';

/**
* Contract name argument
*/
export const contractNameRequired = Args.string({
required: true,
parse: async val => sanitizeDirName(val),
description: ContractArgumentDescription,
});
58 changes: 58 additions & 0 deletions src/commands/contracts/new.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { Flags } from '@oclif/core';

import { BaseCommand } from '@/lib/base';
import { darkGreen, green } from '@/utils/style';
import { contractNameRequired } from '@/arguments/contract';
import { templateWithPrompt } from '@/flags/template';
import { Config } from '@/domain/Config';
import { DEFAULT } from '@/config';

/**
* Command 'contracts new'
* Initializes a new smart contract from a template
*/
export default class ContractsNew extends BaseCommand<typeof ContractsNew> {
static summary = 'Scaffolds a new Smart Contract from a template';
static args = {
contract: contractNameRequired,
};

static flags = {
template: templateWithPrompt(),
};

/**
* Override init function to show message before prompts are displayed
*/
public async init(): Promise<void> {
// Need to parse early to display the contract name on the starting message
const { args } = await this.parse({
args: this.ctor.args,
// Override template flag on this early parse, to avoid early prompting
flags: { ...this.ctor.flags, template: Flags.string() },
});

this.log(`Creating new contract ${args.contract}...\n`);

await super.init();
}

/**
* Runs the command.
*
* @returns Empty promise
*/
public async run(): Promise<void> {
const config = await Config.open();

await config.contractsInstance.assertValidWorkspace();

await config.contractsInstance.new(this.args.contract!, this.flags.template || DEFAULT.Template);

this.success(
`${darkGreen('Contract')} ${green(this.args.contract)} ${darkGreen('created from template')} ${green(
this.flags.template || DEFAULT.Template
)}`
);
}
}
1 change: 1 addition & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export const DEFAULT = {
ChainId: 'constantine-1',
ConfigFileName: 'modulor.json',
ContractsRelativePath: './contracts',
ContractsCargoWorkspace: 'contracts/*',
ChainsRelativePath: './.modulor/chains',
DeploymentsRelativePath: './.modulor/deployments',
ChainFileExtension: '.json',
Expand Down
36 changes: 29 additions & 7 deletions src/domain/Cargo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ import _ from 'lodash';
import path from 'node:path';
import { ChildProcessPromise, PromisifySpawnOptions, spawn } from 'promisify-child-process';

import { BuildParams, GenerateParams, Metadata, ProjectMetadata } from '@/types/Cargo';
import { BuildParams, CargoProjectMetadata, GenerateParams, Metadata } from '@/types/Cargo';
import { ConsoleError } from '@/types/ConsoleError';
import { ErrorCodes } from '@/exceptions/ErrorCodes';
import { bold, red } from '@/utils/style';

/**
* Facade Class for the cargo shell command
Expand Down Expand Up @@ -80,9 +83,9 @@ export class Cargo {
* If ran from a workspace, it will return the first package in the workspace.
* If ran from a package folder, it will return the current package metadata.
*
* @returns Object of type {@link ProjectMetadata}
* @returns Promise containing object of type {@link ProjectMetadata}
*/
async projectMetadata(): Promise<ProjectMetadata> {
async projectMetadata(): Promise<CargoProjectMetadata> {
const { packages = [], target_directory: targetDirectory, workspace_root: workspaceRoot } = await this.metadata();
const currentManifestPath = await this.locateProject();

Expand All @@ -98,19 +101,19 @@ export class Cargo {
} = findPackageInPath(currentManifestPath) || firstPackageInWorkspace(currentManifestPath) || {};

if (!name || !version) {
throw new Error('Failed to resolve project metadata');
throw new CargoMetadataError(this.workingDir);
}

const id = `${name} ${version}`;
const isWorkspace = path.dirname(manifestPath || '') !== workspaceRoot;
const label = `${name}-${version}`;
const root = path.dirname(manifestPath || '');
const wasmFileName = `${name.replace(/-/g, '_')}.wasm`;
const wasm = {
fileName: wasmFileName,
filePath: path.join(targetDirectory, Cargo.WasmTarget, 'release', wasmFileName),
optimizedFilePath: path.join(workspaceRoot, 'artifacts', wasmFileName),
};

return { id, name, version, wasm, workspaceRoot, isWorkspace };
return { name, label, version, wasm, root, workspaceRoot };
}

/**
Expand All @@ -130,3 +133,22 @@ export class Cargo {
});
}
}

/**
* Error when project metadata can't be resolved
*/
export class CargoMetadataError extends ConsoleError {
/**
* @param workingDir - Optional - Path from where the project metadata was attempted to load
*/
constructor(public workingDir?: string) {
super(ErrorCodes.CARGO_METADATA_ERROR);
}

/**
* {@inheritDoc ConsoleError.toConsoleString}
*/
toConsoleString(): string {
return `${red('Failed to resolve project metadata')}${this.workingDir ? bold(` from ${this.workingDir}`) : ''}`;
}
}
21 changes: 8 additions & 13 deletions src/domain/ChainRegistry.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import ow from 'ow';
import path from 'node:path';
import fs from 'node:fs/promises';

import { getWorkspaceRoot } from '@/utils/paths';
import { DEFAULT } from '@/config';
import { ChainRegistrySpec, CosmosChain, cosmosChainValidator } from '@/types/Chain';
import { BuiltInChains } from '@/services/BuiltInChains';
import { readFilesFromDirectory, writeFileWithDir } from '@/utils/filesystem';
import { fileExists, readFilesFromDirectory, writeFileWithDir } from '@/utils/filesystem';
import { FileAlreadyExistsError, InvalidFormatError } from '@/exceptions';
import { bold, red, yellow } from '@/utils/style';
import { ConsoleError } from '@/types/ConsoleError';
Expand Down Expand Up @@ -48,10 +47,11 @@ export class ChainRegistry extends ChainRegistrySpec {
* Initializes the Chain Registry, by loading the built-in chains and reading the imported chain files.
*
* @param chainsDirectory - Optional - Path to the directory where the imported chains are
* @param workingDir - Optional - Path of the working directory
* @returns Promise containing a {@link ChainRegistry} instance
*/
static async init(chainsRelativePath?: string): Promise<ChainRegistry> {
const directoryPath = await this.getDirectoryPath(chainsRelativePath);
static async init(workingDir?: string): Promise<ChainRegistry> {
const directoryPath = await this.getDirectoryPath(workingDir);

let filesRead: Record<string, string> = {};

Expand Down Expand Up @@ -104,11 +104,11 @@ export class ChainRegistry extends ChainRegistrySpec {
/**
* Get the absolute path of the imported chains directory
*
* @param chainsRelativePath - Optional - Relative path of the chains directory
* @param workingDir - Optional - Path of the working directory
* @returns Promise containing the absolute path of the chains directory
*/
static async getDirectoryPath(chainsRelativePath?: string): Promise<string> {
return path.join(await getWorkspaceRoot(), chainsRelativePath || DEFAULT.ChainsRelativePath);
static async getDirectoryPath(workingDir?: string): Promise<string> {
return path.join(await getWorkspaceRoot(workingDir), DEFAULT.ChainsRelativePath);
}

/**
Expand Down Expand Up @@ -150,12 +150,7 @@ export class ChainRegistry extends ChainRegistrySpec {
*/
async fileExists(chainId: string): Promise<boolean> {
const chainPath = await this.getFilePath(chainId);
try {
await fs.access(chainPath);
return true;
} catch {
return false;
}
return fileExists(chainPath);
}

/**
Expand Down
58 changes: 45 additions & 13 deletions src/domain/Config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { InvalidFormatError } from '@/exceptions';
import { DeploymentWithChain } from '@/types/Deployment';
import { Contract } from '@/types/Contract';
import { Deployments } from './Deployments';
import { sanitizeDirName } from '@/utils/sanitize';

/**
* Manages the config file of the project and creates instances of ChainRegistry, Deployments and Contracts
Expand All @@ -26,23 +27,34 @@ export class Config {
private _contractsPath: string;
private _contracts: Contracts;
private _deployments: Deployments;
private _workspaceRoot: string;
private _configPath: string;

/**
* @param name - Name of the project
* @param chainId - Active/selected chain id in the project
* @param contractsPath - Absolute path of the contract files in the project
* @param contractsPath - Path of the contract files in the project
* @param contracts - Array containing all the contracts
* @param deployments - Array containing all the deployments
* @param workspaceRoot - Absolute path of the project's workspace root
* @param configPath - Absolute path of the project's config file
*/
// eslint-disable-next-line max-params
constructor(name: string, chainId: string, contractsPath: string, contracts: Contracts, deployments: Deployments, configPath: string) {
constructor(
name: string,
chainId: string,
contractsPath: string,
contracts: Contracts,
deployments: Deployments,
workspaceRoot: string,
configPath: string
) {
this._name = name;
this._chainId = chainId;
this._contractsPath = contractsPath;
this._contracts = contracts;
this._deployments = deployments;
this._workspaceRoot = workspaceRoot;
this._configPath = configPath;
}

Expand All @@ -54,10 +66,22 @@ export class Config {
return this._chainId;
}

get workspaceRoot(): string {
return this._workspaceRoot;
}

get contractsPath(): string {
return this._contractsPath;
}

get contractsInstance(): Contracts {
return this._contracts;
}

get deploymentsInstance(): Deployments {
return this._deployments;
}

get contracts(): Contract[] {
return this._contracts.listContracts();
}
Expand All @@ -74,10 +98,20 @@ export class Config {
* @returns Promise containing an instance of {@link Config}
*/
static async init(data: ConfigData, workingDir?: string): Promise<Config> {
const workspaceRoot = await getWorkspaceRoot(workingDir);
const configPath = await this.getFilePath(workingDir);
const contracts = await Contracts.open();
const deployments = await Deployments.open();
return new Config(data.name, data.chainId, data.contractsPath || DEFAULT.ContractsRelativePath, contracts, deployments, configPath);
const deployments = await Deployments.open(workingDir);
const contracts = await Contracts.open(workingDir, data.contractsPath);

return new Config(
data.name,
data.chainId,
data.contractsPath || DEFAULT.ContractsRelativePath,
contracts,
deployments,
workspaceRoot,
configPath
);
}

/**
Expand Down Expand Up @@ -124,9 +158,9 @@ export class Config {
}

// Get Workspace root
const directory = workingDir || (await getWorkspaceRoot());
const directory = await getWorkspaceRoot(workingDir);
// Get name of Workspace root directory
const name = path.basename(directory).replace(' ', '-');
const name = sanitizeDirName(path.basename(directory));

// Create config file
const configFile = await Config.init(
Expand All @@ -138,6 +172,7 @@ export class Config {
);
await configFile.write();

console.log(configFile._contractsPath)
return configFile;
}

Expand All @@ -148,7 +183,7 @@ export class Config {
* @returns Promise containing the absolute path of the config file
*/
static async getFilePath(workingDir?: string): Promise<string> {
const workspaceRoot = workingDir || (await getWorkspaceRoot());
const workspaceRoot = await getWorkspaceRoot(workingDir);

return path.join(workspaceRoot, DEFAULT.ConfigFileName);
}
Expand Down Expand Up @@ -223,8 +258,7 @@ export class Config {
let contractsStatus = '';

if (withContracts) {
const contracts = await Contracts.open(this._contractsPath);
contractsStatus = `\n\n${await contracts.prettyPrint()}`;
contractsStatus = `\n\n${await this.contractsInstance.prettyPrint()}`;
}

return `${bold('Project: ')}${this._name}\n${bold('Selected chain: ')}${this._chainId}` + contractsStatus;
Expand All @@ -236,11 +270,9 @@ export class Config {
* @returns Instance of {@link ConfigDataWithContracts}
*/
async dataWithContracts(): Promise<ConfigDataWithContracts> {
const contracts = await Contracts.open(this._contractsPath);

return {
...this.toConfigData(),
contracts: contracts.listContracts(),
contracts: this.contractsInstance.listContracts(),
};
}
}
Loading

0 comments on commit bb1fda2

Please sign in to comment.