Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

10 restructure the wasm deploy tool to be used as cli tool #24

Draft
wants to merge 8 commits into
base: 22-add-experimental-test-command
Choose a base branch
from
69 changes: 66 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,77 @@
# wasm-deploy

Usage for the example project:
Usage:

1. Pull the Solidity source files from git: `npx ts-node src/index.ts pull nabla`
2. Deploy: `npx ts-node src/index.ts deploy nabla --network foucoco`
1. Install the `wasm-deploy` tool globally: `npm install -g wasm-deploy`

2. Initiate a project by moving to the desired folder, and running: `wasm-deploy init <project-name>`
This will create a local typescript project, and add `wasm-deploy` dependency to it.

3. Edit `config.json` in the root project folder.

4. Write deploy/tests scripts in corresponding project folders.

5. Deploy or Test:
- Deploy: `wasm-deploy deploy <project-name> --network <network>`
- Test: `wasm-deploy test <project-name> --network <network>`

Alternatively you can use the parameter `local` instead of `foucoco`. This expects that there is a local chain running on port `9944` – this is particularly useful to run together with the [foucoco-standalone](https://github.com/pendulum-chain/foucoco-standalone) node.

### Running with typescript
Alternatively, clone this repo and run the following commands:

```npx ts-node src/cli.ts <command> <project-name> ...```

# Required

```
brew install binaryen
```
# Solang Compiler Config
If contracts are not pre-compiled, solang is used to compile contracts and obtain the meta-data.

Run `wasm-deploy set-solang <path-to-solang>` to point to the local binary of the compiler.

# Project Config
## Contracts
Inside the config.json created in your project root folder, add the conctract information that will be used in deployments or testing.

Contracts can either be pulled from a remote repository (defined elsewhere in config.json), or added to the project precompiled.

Example pulling contract from remote repository:
```
"contracts": {
"MyContract": {
"repository": "MyContracts",
"path": "path/to/my_contract.sol"
},
...
```


Example using a pre-compiled version of the contract, including the metadata:
```
"contracts": {
"MyPcContract": {
"path": "/local/path/to/myPcContract.contract",
"isPrecompiled": true
},
...
```

Example using local contract:
```
"contracts": {
"MyContract": {
"path": "/local/path/to/myContract.sol",
},
...
```
Keep in mind that if the contract has dependencies, they must also be locally available.

# Example Project

## ERC20
Create a sample project with an ERC20 contract and tests:

`wasm-deploy init sample -e erc20`
12 changes: 9 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,19 @@
"name": "wasm-deploy",
"version": "1.0.0",
"description": "",
"main": "index.js",
"main": "./dist/index.js",
"scripts": {
"start": "ts-node src/index.ts"
"start": "ts-node index.ts",
"build": "npx tsc",
"postbuild": "node dist/projectTemplates/copySampleFiles.js",
"prepublishOnly": "npm run build"
},
"keywords": [],
"author": "",
"license": "ISC",
"bin": {
"wasm-deploy": "./dist/cli.js"
},
"devDependencies": {
"@tsconfig/node16": "^1.0.4",
"@types/blake2b": "^2.1.0",
Expand Down Expand Up @@ -37,4 +43,4 @@
"workspaces": [
"packages/api-solang"
]
}
}
4 changes: 3 additions & 1 deletion src/actions/compileContract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { runCommand } from "../utils/childProcess";
import { ContractSourcecodeId } from "../types";
import { ContractDeploymentState } from "../processScripts";
import { Project } from "../project";
import { getSolangPath } from "../utils/config";


export async function compileContract(
contractId: ContractSourcecodeId,
Expand Down Expand Up @@ -60,7 +62,7 @@ async function actuallyCompileContract(

updateContractStatus("compiling");
const solangResult = await runCommand([
"../clones/solang/target/release/solang",
getSolangPath(),
"compile",
"--no-strength-reduce", // temporary fix for https://github.com/hyperledger/solang/issues/1507
"--target",
Expand Down
44 changes: 44 additions & 0 deletions src/actions/compileScripts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { execSync } from 'child_process';
import * as fs from 'fs';
import * as path from 'path';

const folders = ['deploy', 'test', '.'];
export const distDir = 'dist';

export function compileInPlace(baseProjectDir: string): void {

execSync(`npm run build`, { cwd: process.cwd(), stdio: 'inherit' });

//Move the compiled files to the project dir
for (const dir of folders) {

moveCompiledFiles(path.join(distDir, dir), path.join(`./${baseProjectDir}`, dir));


}

fs.rmdirSync(distDir, { recursive: true });
}


function moveCompiledFiles(fromDir: string, toDir: string) {
if (fs.existsSync(fromDir)) {
const files = fs.readdirSync(fromDir);

for (const file of files) {

if (path.extname(file) === '.js') {
const from = path.join(fromDir, file);
const to = path.join(toDir, file);
fs.renameSync(from, to);
}
}
}

}






11 changes: 11 additions & 0 deletions src/cli.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#! /usr/bin/env node

const cryptoWaitReady = require("@polkadot/util-crypto");
const parseCommandLine = require("./commandLine");

async function main() {
await cryptoWaitReady.cryptoWaitReady();
parseCommandLine.parseCommandLine();
}

main().catch((error) => console.log(error));
19 changes: 19 additions & 0 deletions src/commandLine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { Command } from "commander";
import { deploy } from "./commands/deploy";
import { pull } from "./commands/pull";
import { runTestSuits } from "./commands/test";
import { initializeProject } from "./commands/init";
import { setSolangPath } from "./utils/config";

export function parseCommandLine() {
const program = new Command();
Expand Down Expand Up @@ -37,5 +39,22 @@ export function parseCommandLine() {
await runTestSuits({ projectFolder: str as string, network: options.network });
});

program
.command("init")
.description("Create the boilerplate for a new project to be used with the cli")
.argument("<project>", "project name")
.option('-e, --example <char>')
.action(async (str, options: Record<string, string>) => {
await initializeProject({ projectName: str as string, example: options.example });
});

program
.command("set-solang")
.description("Set the path to the solang compiler")
.argument("<path>", "solang path")
.action(async (str) => {
setSolangPath(str);
});

program.parse();
}
9 changes: 8 additions & 1 deletion src/commands/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import { Address, ArgumentType, ContractSourcecodeId, DeployedContractId, NamedA
import { connectToChain } from "../api/api";
import { createAnimatedTextContext } from "../utils/terminal";
import { processScripts } from "../processScripts";
import { initializeProject } from "../project";
import { initializeProject, isTypescript } from "../project";
import { compileInPlace } from "../actions/compileScripts";

export interface DeployOptions {
projectFolder: string;
Expand Down Expand Up @@ -55,6 +56,12 @@ export interface DeployScript {
}

export async function deploy(options: DeployOptions) {

//compile deploy scripts if we are NOT in typescript
if (!isTypescript()) {
compileInPlace(options.projectFolder);
}

const project = await initializeProject(options.projectFolder);

const networkName = options.network;
Expand Down
101 changes: 101 additions & 0 deletions src/commands/init.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import * as fs from 'fs';
import * as path from 'path';
import { execSync } from 'child_process';
import { distDir } from "../actions/compileScripts";
import { generateTsConfig } from '../projectTemplates/tsconfigTemplate';
import { generatePackageJson } from '../projectTemplates/packageJsonTemplate';
import { generateConfigJson } from '../projectTemplates/configJson';

export interface InitOptions {
projectName: string;
example: string;
}

export async function initializeProject({ projectName, example }: InitOptions): Promise<void> {

// Create and write tsconfig.json
let tsConfigPath = path.join(process.cwd(), 'tsconfig.json');
if (!fs.existsSync(tsConfigPath)) {
const tsConfigJson = generateTsConfig(distDir, projectName);
fs.writeFileSync(tsConfigPath, JSON.stringify(tsConfigJson, null, 2));
} else {
throw Error("tsconfig.json conflict, please ensure there is no project in directory")
}

// Create and write package.json
let packageConfigPath = path.join(process.cwd(), 'package.json');
if (!fs.existsSync(packageConfigPath)) {
const packageJson = generatePackageJson();
fs.writeFileSync(packageConfigPath, JSON.stringify(packageJson, null, 2));
} else {
throw Error("package.json conflict, please ensure there is no project in directory")
}

// Create project directory
const projectPath = path.join(process.cwd(), projectName);
fs.mkdirSync(projectPath, { recursive: true });

// Create subdirectories
fs.mkdirSync(path.join(projectPath, 'deploy'));
fs.mkdirSync(path.join(projectPath, 'test'));

//populate example files
let contractObject = {};
if (example) {
fs.mkdirSync(path.join(projectPath, 'contracts'));
contractObject = await populateExample(projectName, example);
}

fs.mkdirSync(path.join(process.cwd(), `./${projectName}/target`));

// Create and write config.json
const configJson = generateConfigJson(contractObject);
fs.writeFileSync(path.join(projectPath, 'config.json'), JSON.stringify(configJson, null, 2));




// Execute npm install in the project directory
execSync('npm install', { cwd: process.cwd(), stdio: 'inherit' });
execSync('npm install --save-dev @types/node', { cwd: process.cwd(), stdio: 'inherit' });

}

async function populateExample(projectName: string, exampleName: string): Promise<object> {

let rootDir = path.join(__dirname, '..');


switch (exampleName) {
case "erc20":
//copy required contracts
const contracts = ['ERC20.sol', 'SafeMath.sol'];
for (const contract of contracts) {
let sourcePath = path.join(rootDir, `projectTemplates/rawFiles/contractSamples/${contract}`);
let targetPath = path.join(process.cwd(), `./${projectName}/contracts/${contract}`);
fs.copyFileSync(sourcePath, targetPath);
}

//copy deploy sample script
let sourcePath = path.join(rootDir, 'projectTemplates/rawFiles/deploySamples/01_sample_deploy_erc20.ts');
let targetPath = path.join(process.cwd(), `./${projectName}/deploy/01_sample_deploy_erc20.ts`);
fs.copyFileSync(sourcePath, targetPath);

//copy test files
sourcePath = path.join(rootDir, 'projectTemplates/rawFiles/testSamples/sampleERC20Test.ts');
targetPath = path.join(process.cwd(), `./${projectName}/test/sampleERC20Test.ts`);
fs.copyFileSync(sourcePath, targetPath);

//contracts object that need to be declared in config.json
const contractsObj = {
"ERC20": {
"path": "./contracts/ERC20.sol"
}
};

return contractsObj

}

throw Error("example does not exists");
}
26 changes: 24 additions & 2 deletions src/commands/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,27 @@ import { readFile } from "node:fs/promises";

import { Address, ArgumentType, ContractSourcecodeId } from "../types";
import { ChainApi, connectToChain } from "../api/api";
import { Project, initializeProject } from "../project";
import { Project, initializeProject, isTypescript } from "../project";
import { StyledText, createAnimatedTextContext } from "../utils/terminal";
import { compileContract } from "../actions/compileContract";
import { toUnit } from "../utils/rationals";
import { SigningSubmitter, Submitter, getSubmitterAddress } from "../api/submitter";
import { PanicCode } from "@pendulum-chain/api-solang";
import { Codec } from "@polkadot/types-codec/types";
import { compileInPlace } from "../actions/compileScripts";

export interface RunTestSuitesOptions {
projectFolder: string;
network: string;
}

export async function runTestSuits(options: RunTestSuitesOptions) {

//compile tests scripts if we are NOT in typescript
if (!isTypescript()) {
compileInPlace(options.projectFolder);
}

const project = await initializeProject(options.projectFolder);

const networkName = options.network;
Expand Down Expand Up @@ -370,7 +377,22 @@ async function processTestScripts(
}

addStaticText([[{ text: "Process test suite " }, { text: testSuiteName, color: "cyan" }]], false);
let testSuiteInstance = await testSuite.default(environmentForTestSuite);

let testSuiteInstance: any;
try {
testSuiteInstance = await testSuite.default(environmentForTestSuite);
} catch (error) {
if (error instanceof TypeError && /is not a function/.test(error.message)) {

let attemptedContractInstance = error.message.split(" ")[0].replace("new", "").trim();

throw Error(`Contract ${attemptedContractInstance} does not exist in project nabla, please add it in the config.json file`)
} else {
throw error;
}

}

const tests = Object.keys(testSuiteInstance).filter((key) => key.startsWith("test") && key !== "setUp");

for (const test of tests) {
Expand Down
Loading