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

Add verify feature #194

Merged
merged 13 commits into from
Feb 12, 2024
6 changes: 4 additions & 2 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
{
"name": "swanky-env",
"image": "ghcr.io/swankyhub/swanky-cli/swanky-base:swanky3.1.0-beta.0_v2.1.0",

"image": "ghcr.io/inkdevhub/swanky-cli/swanky-base:swanky3.1.0-beta.0_v2.1.1",
"features": {
"ghcr.io/devcontainers/features/docker-in-docker:2.8.0": {}
},
// Mount the workspace volume
"mounts": ["source=${localWorkspaceFolder},target=/workspaces,type=bind,consistency=cached"],
"workspaceFolder": "/workspaces",
Expand Down
12 changes: 6 additions & 6 deletions base-image/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,12 @@ RUN curl -L https://github.com/swankyhub/swanky-cli/releases/download/v3.1.0-bet
# Install Rustup and Rust, additional components, packages, and verify the installations
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && \
/bin/bash -c "source $HOME/.cargo/env && \
rustup toolchain install nightly-2023-03-05 && \
rustup default nightly-2023-03-05 && \
rustup component add rust-src --toolchain nightly-2023-03-05 && \
rustup target add wasm32-unknown-unknown --toolchain nightly-2023-03-05 && \
cargo +stable install cargo-dylint dylint-link && \
cargo +stable install cargo-contract --force --version 4.0.0-alpha && \
rustup install 1.72 && \
rustup default 1.72 && \
rustup component add rust-src && \
rustup target add wasm32-unknown-unknown && \
cargo install cargo-dylint dylint-link && \
cargo install cargo-contract --version 4.0.0-rc.1 && \
rustc --version"

# Install Yarn 1.x
Expand Down
35 changes: 14 additions & 21 deletions src/commands/check/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Listr } from "listr2";
import { commandStdoutOrNull } from "../../lib/index.js";
import { commandStdoutOrNull, extractCargoContractVersion } from "../../lib/index.js";
import { SwankyConfig } from "../../types/index.js";
import { pathExistsSync, readJSON, writeJson } from "fs-extra/esm";
import { readFileSync } from "fs";
Expand Down Expand Up @@ -74,7 +74,7 @@ export default class Check extends SwankyCommand<typeof Check> {
{
title: "Check Rust",
task: async (ctx, task) => {
ctx.versions.tools.rust = (await commandStdoutOrNull("rustc --version"))?.match(/rustc (.*) \((.*)/)?.[1];
ctx.versions.tools.rust = commandStdoutOrNull("rustc --version")?.match(/rustc (.*) \((.*)/)?.[1];
if (!ctx.versions.tools.rust) {
throw new Error("Rust is not installed");
}
Expand All @@ -85,7 +85,7 @@ export default class Check extends SwankyCommand<typeof Check> {
{
title: "Check cargo",
task: async (ctx, task) => {
ctx.versions.tools.cargo = (await commandStdoutOrNull("cargo -V"))?.match(/cargo (.*) \((.*)/)?.[1];
ctx.versions.tools.cargo = commandStdoutOrNull("cargo -V")?.match(/cargo (.*) \((.*)/)?.[1];
if (!ctx.versions.tools.cargo) {
throw new Error("Cargo is not installed");
}
Expand All @@ -96,7 +96,7 @@ export default class Check extends SwankyCommand<typeof Check> {
{
title: "Check cargo nightly",
task: async (ctx, task) => {
ctx.versions.tools.cargoNightly = (await commandStdoutOrNull("cargo +nightly -V"))?.match(/cargo (.*)-nightly \((.*)/)?.[1];
ctx.versions.tools.cargoNightly = commandStdoutOrNull("cargo +nightly -V")?.match(/cargo (.*)-nightly \((.*)/)?.[1];
if (!ctx.versions.tools.cargoNightly) {
throw new Error("Cargo nightly is not installed");
}
Expand All @@ -107,7 +107,7 @@ export default class Check extends SwankyCommand<typeof Check> {
{
title: "Check cargo dylint",
task: async (ctx, task) => {
ctx.versions.tools.cargoDylint = (await commandStdoutOrNull("cargo dylint -V"))?.match(/cargo-dylint (.*)/)?.[1];
ctx.versions.tools.cargoDylint = commandStdoutOrNull("cargo dylint -V")?.match(/cargo-dylint (.*)/)?.[1];
if (!ctx.versions.tools.cargoDylint) {
throw new Warn("Cargo dylint is not installed");
}
Expand All @@ -118,19 +118,12 @@ export default class Check extends SwankyCommand<typeof Check> {
{
title: "Check cargo-contract",
task: async (ctx, task) => {
ctx.versions.tools.cargoContract = await commandStdoutOrNull("cargo contract -V");
if (!ctx.versions.tools.cargoContract) {
const cargoContractVersion = extractCargoContractVersion();
ctx.versions.tools.cargoContract = cargoContractVersion;
if (!cargoContractVersion) {
throw new Error("Cargo contract is not installed");
}

const regex = /cargo-contract-contract (\d+\.\d+\.\d+(?:-[\w.]+)?)(?:-unknown-[\w-]+)/;
const match = ctx.versions.tools.cargoContract.match(regex);
if (match?.[1]) {
ctx.versions.tools.cargoContract = match[1];
} else {
throw new Error("Cargo contract version not found");
}
task.title = `Check cargo-contract: ${ctx.versions.tools.cargoContract}`;
task.title = `Check cargo-contract: ${cargoContractVersion}`;
},
exitOnError: false,
},
Expand Down Expand Up @@ -161,7 +154,7 @@ export default class Check extends SwankyCommand<typeof Check> {
const cargoToml = TOML.parse(cargoTomlString);

const inkDependencies = Object.entries(cargoToml.dependencies)
.filter((dependency) => dependency[0].includes("ink"))
.filter(([depName]) => /^ink($|_)/.test(depName))
.map(([depName, depInfo]) => {
const dependency = depInfo as Dependency;
return [depName, dependency.version ?? dependency.tag];
Expand All @@ -177,12 +170,12 @@ export default class Check extends SwankyCommand<typeof Check> {
task: async (ctx) => {
const supportedInk = ctx.swankyConfig!.node.supportedInk;
const mismatched: Record<string, string> = {};
Object.entries(ctx.versions.contracts).forEach(([contract, inkPackages]) => {
Object.entries(inkPackages).forEach(([inkPackage, version]) => {
Object.entries(ctx.versions.contracts).forEach(([contract, inkDependencies]) => {
Object.entries(inkDependencies).forEach(([depName, version]) => {
if (semver.gt(version, supportedInk)) {
mismatched[
`${contract}-${inkPackage}`
] = `Version of ${inkPackage} (${version}) in ${chalk.yellowBright(contract)} is higher than supported ink version (${supportedInk}) in current Swanky node version (${swankyNodeVersion}). A Swanky node update can fix this warning.`;
`${contract}-${depName}`
] = `Version of ${depName} (${version}) in ${chalk.yellowBright(contract)} is higher than supported ink version (${supportedInk}) in current Swanky node version (${swankyNodeVersion}). A Swanky node update can fix this warning.`;
}

if (version.startsWith(">") || version.startsWith("<") || version.startsWith("^") || version.startsWith("~")) {
Expand Down
37 changes: 31 additions & 6 deletions src/commands/contract/compile.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Args, Flags } from "@oclif/core";
import path from "node:path";
import { storeArtifacts, Spinner, generateTypes } from "../../lib/index.js";
import { ensureCargoContractVersionCompatibility, extractCargoContractVersion, generateTypes, Spinner, storeArtifacts } from "../../lib/index.js";
import { spawn } from "node:child_process";
import { pathExists } from "fs-extra/esm";
import { SwankyCommand } from "../../lib/swankyCommand.js";
Expand All @@ -16,6 +16,11 @@ export class CompileContract extends SwankyCommand<typeof CompileContract> {
description:
"A production contract should always be build in `release` mode for building optimized wasm",
}),
verifiable: Flags.boolean({
default: false,
description:
"A production contract should be build in `verifiable` mode to deploy on a public network. Ensure Docker Engine is up and running.",
}),
all: Flags.boolean({
default: false,
char: "a",
Expand Down Expand Up @@ -49,7 +54,7 @@ export class CompileContract extends SwankyCommand<typeof CompileContract> {
const contractInfo = this.swankyConfig.contracts[contractName];
if (!contractInfo) {
throw new ConfigError(
`Cannot find contract info for ${contractName} contract in swanky.config.json`
`Cannot find contract info for ${contractName} contract in swanky.config.json`,
);
}
const contractPath = path.resolve("contracts", contractInfo.name);
Expand All @@ -65,11 +70,23 @@ export class CompileContract extends SwankyCommand<typeof CompileContract> {
"contract",
"build",
"--manifest-path",
`${contractPath}/Cargo.toml`,
`contracts/${contractName}/Cargo.toml`,
ipapandinas marked this conversation as resolved.
Show resolved Hide resolved
];
if (flags.release) {
if (flags.release && !flags.verifiable) {
compileArgs.push("--release");
}
if (flags.verifiable) {
const cargoContractVersion = extractCargoContractVersion();
if (cargoContractVersion === null)
throw new InputError(
`Cargo contract tool is required for verifiable mode. Please ensure it is installed.`
);

ensureCargoContractVersionCompatibility(cargoContractVersion, "4.0.0", [
"4.0.0-alpha",
]);
compileArgs.push("--verifiable");
ipapandinas marked this conversation as resolved.
Show resolved Hide resolved
}
const compile = spawn("cargo", compileArgs);
this.logger.info(`Running compile command: [${JSON.stringify(compile.spawnargs)}]`);
let outputBuffer = "";
Expand Down Expand Up @@ -100,7 +117,7 @@ export class CompileContract extends SwankyCommand<typeof CompileContract> {
});
},
`Compiling ${contractName} contract`,
`${contractName} Contract compiled successfully`
`${contractName} Contract compiled successfully`,
);

const artifactsPath = compilationResult as string;
Expand All @@ -112,8 +129,16 @@ export class CompileContract extends SwankyCommand<typeof CompileContract> {
await spinner.runCommand(
async () => await generateTypes(contractInfo.name),
`Generating ${contractName} contract ts types`,
`${contractName} contract's TS types Generated successfully`
`${contractName} contract's TS types Generated successfully`,
);

this.swankyConfig.contracts[contractName].build = {
timestamp: Date.now(),
artifactsPath,
isVerified: false,
};

await this.storeConfig();
}
}
}
122 changes: 122 additions & 0 deletions src/commands/contract/verify.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import { Args, Flags } from "@oclif/core";
import path from "node:path";
import { ensureCargoContractVersionCompatibility, extractCargoContractVersion, Spinner } from "../../lib/index.js";
import { pathExists } from "fs-extra/esm";
import { SwankyCommand } from "../../lib/swankyCommand.js";
import { ConfigError, InputError, ProcessError } from "../../lib/errors.js";
import { spawn } from "node:child_process";

export class VerifyContract extends SwankyCommand<typeof VerifyContract> {
static description = "Verify the smart contract(s) in your contracts directory";

static flags = {
all: Flags.boolean({
default: false,
char: "a",
description: "Set all to true to verify all contracts",
}),
};

static args = {
contractName: Args.string({
name: "contractName",
required: false,
default: "",
description: "Name of the contract to verify",
}),
};

ipapandinas marked this conversation as resolved.
Show resolved Hide resolved
async run(): Promise<void> {
const { args, flags } = await this.parse(VerifyContract);

ipapandinas marked this conversation as resolved.
Show resolved Hide resolved
const cargoContractVersion = extractCargoContractVersion();
if (cargoContractVersion === null)
throw new InputError(
`Cargo contract tool is required for verifiable mode. Please ensure it is installed.`
);

ensureCargoContractVersionCompatibility(cargoContractVersion, "4.0.0", [
"4.0.0-alpha",
]);

if (args.contractName === undefined && !flags.all) {
ipapandinas marked this conversation as resolved.
Show resolved Hide resolved
throw new InputError("No contracts were selected to verify", { winston: { stack: true } });
}

const contractNames = flags.all
? Object.keys(this.swankyConfig.contracts)
: [args.contractName];

const spinner = new Spinner();

for (const contractName of contractNames) {
this.logger.info(`Started compiling contract [${contractName}]`);
const contractInfo = this.swankyConfig.contracts[contractName];
if (!contractInfo) {
throw new ConfigError(
`Cannot find contract info for ${contractName} contract in swanky.config.json`
);
}
const contractPath = path.resolve("contracts", contractInfo.name);
this.logger.info(`"Looking for contract ${contractInfo.name} in path: [${contractPath}]`);
if (!(await pathExists(contractPath))) {
throw new InputError(`Contract folder not found at expected path`);
}

if(!contractInfo.build) {
throw new InputError(`Contract ${contractName} is not compiled. Please compile it first`);
}

pmikolajczyk41 marked this conversation as resolved.
Show resolved Hide resolved
await spinner.runCommand(
async () => {
return new Promise<boolean>((resolve, reject) => {
if(contractInfo.build!.isVerified) {
this.logger.info(`Contract ${contractName} is already verified`);
resolve(true);
}
const compileArgs = [
"contract",
"verify",
`artifacts/${contractName}/${contractName}.contract`,
"--manifest-path",
`contracts/${contractName}/Cargo.toml`,
];
const compile = spawn("cargo", compileArgs);
this.logger.info(`Running verify command: [${JSON.stringify(compile.spawnargs)}]`);
let outputBuffer = "";
let errorBuffer = "";

compile.stdout.on("data", (data) => {
outputBuffer += data.toString();
spinner.ora.clear();
});

compile.stderr.on("data", (data) => {
errorBuffer += data;
});

compile.on("exit", (code) => {
if (code === 0) {
const regex = /Successfully verified contract (.*) against reference contract (.*)/;
const match = outputBuffer.match(regex);
if (match) {
this.logger.info(`Contract ${contractName} verification done.`);
resolve(true);
}
} else {
reject(new ProcessError(errorBuffer));
}
});
});
},
`Verifying ${contractName} contract`,
`${contractName} Contract verified successfully`
);
contractInfo.build.isVerified = true;

this.swankyConfig.contracts[contractName] = contractInfo;

await this.storeConfig();
}
}
}
6 changes: 3 additions & 3 deletions src/lib/command-utils.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { execaCommand } from "execa";
import { execaCommand, execaCommandSync } from "execa";
import { copy, emptyDir, ensureDir, readJSON } from "fs-extra/esm";
import path from "node:path";
import { DEFAULT_NETWORK_URL, ARTIFACTS_PATH, TYPED_CONTRACTS_PATH } from "./consts.js";
import { SwankyConfig } from "../types/index.js";
import { ConfigError, FileError, InputError } from "./errors.js";

export async function commandStdoutOrNull(command: string): Promise<string | null> {
export function commandStdoutOrNull(command: string): string | null {
try {
const result = await execaCommand(command);
const result = execaCommandSync(command);
return result.stdout;
} catch {
return null;
Expand Down
2 changes: 1 addition & 1 deletion src/lib/contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export class Contract {
`Cannot read .contract bundle, path not found: ${check.missingPaths.toString()}`
);
}
return readJSON(path.resolve(this.artifactsPath, `${this.moduleName}.contract`));
return readJSON(path.resolve(this.artifactsPath, `${this.moduleName}.contract`), 'utf-8');
}

async getWasm(): Promise<Buffer> {
Expand Down
Loading
Loading