Skip to content

Commit

Permalink
feat: Add contract verifiable build (#125)
Browse files Browse the repository at this point in the history
Co-authored-by: Igor Papandinas <[email protected]>
  • Loading branch information
prxgr4mm3r and ipapandinas committed Mar 25, 2024
1 parent 1b0fc65 commit 91c23d9
Show file tree
Hide file tree
Showing 10 changed files with 1,836 additions and 1,343 deletions.
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`,
];
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");
}
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",
}),
};

async run(): Promise<void> {
const { args, flags } = await this.parse(VerifyContract);

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) {
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`);
}

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

0 comments on commit 91c23d9

Please sign in to comment.