Skip to content

Commit

Permalink
feat: Add Chopsticks integration
Browse files Browse the repository at this point in the history
This PR are connected to [Ganache-like testing feature
#49](inkdevhub/swanky-node#49). The main
purpose of this issue is to make testing easier by implementing next
features:
> * block time stamp manipulation (can mine a block with arbitrary block
time stamp)
> * block number manipulation
> * taking a snapshot and retrieve later
> * Balance manipulation of accounts
> * Impersonate any account

All of them can already implemented in
[Chopsticks](https://github.com/AcalaNetwork/chopsticks). Below I will
explain how to use it.
Firstly, you should set up your swanky project with `swanky init` and
choose that you want to install swanky-node. Then you should use `swanky
node chopsticks init` to initiate chopsticks config, which will be
stored in the `./node/config/` folder. Then you start the node(`swanky
node start`) and fork it with chopsticks(`swanky node chopsticks
start`).
Now we have a testing node that has all the features that we need.
Chopsticks allow to use of different RPC calls. Full list of them you
can check
[here](https://acalanetwork.github.io/chopsticks/docs/chopsticks/README.html).
For example, you can do timestamp manipulation by `dev_timeTravel' RPC
call:
```TypeScript
import { WsProvider } from '@polkadot/rpc-provider'
const ws = new WsProvider(`ws://localhost:8000`)
await ws.send('dev_timeTravel', ['Jan 1, 2023'])
```
Or go back to any block by 'dev_setHead':
```TypeScript
import { WsProvider } from '@polkadot/rpc-provider'
const ws = new WsProvider(`ws://localhost:8000`)
await ws.send('dev_setHead', [1000000])
```

---------

Co-authored-by: Igor Papandinas <[email protected]>
  • Loading branch information
prxgr4mm3r and ipapandinas committed Mar 25, 2024
1 parent c196997 commit f926f6a
Show file tree
Hide file tree
Showing 11 changed files with 136 additions and 17 deletions.
39 changes: 39 additions & 0 deletions src/commands/node/chopsticks/init.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import path from "node:path";
import { SwankyCommand } from "../../../lib/swankyCommand.js";
import {
copyChopsticksTemplateFile,
getSwankyConfig,
getTemplates,
} from "../../../lib/index.js";
import { ConfigBuilder } from "../../../lib/config-builder.js";
import { SwankyConfig } from "../../../types/index.js";

export const chopsticksConfig = "dev.yml";

export class InitChopsticks extends SwankyCommand<typeof InitChopsticks> {
static description = "Initialize chopsticks config";

async run(): Promise<void> {
const localConfig = getSwankyConfig("local") as SwankyConfig;
const projectPath = path.resolve();

const chopsticksTemplatePath = getTemplates().chopsticksTemplatesPath;
const configPath = path.resolve(projectPath, "node", "config");

await this.spinner.runCommand(
() =>
copyChopsticksTemplateFile(chopsticksTemplatePath, configPath),
"Copying Chopsticks template files...",
);

await this.spinner.runCommand(async () => {
const newLocalConfig = new ConfigBuilder(localConfig)
.addChopsticks(path.resolve(projectPath, "node", "config", chopsticksConfig))
.build();
await this.storeConfig(newLocalConfig, "local");
}, "Updating Swanky configuration with Chopsticks settings...");

this.log("Chopsticks config initialized successfully");
}
}

40 changes: 40 additions & 0 deletions src/commands/node/chopsticks/start.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { Flags } from "@oclif/core";
import { execaCommand } from "execa";
import { SwankyCommand } from "../../../lib/swankyCommand.js";
import { ensureSwankyNodeInstalled } from "../../../lib/index.js";
import { pathExists } from "fs-extra/esm";
import { ConfigError, FileError } from "../../../lib/errors.js";
export class StartChopsticks extends SwankyCommand<typeof StartChopsticks> {
static description = "Start chopsticks";

static flags = {
"config": Flags.string({
description: "Path to the chopsticks config file",
})
}

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

ensureSwankyNodeInstalled(this.swankyConfig);

const chopsticksConfigPath = flags.config ?? this.swankyConfig.node.chopsticks?.configPath;

if(!chopsticksConfigPath) {
throw new ConfigError("Chopsticks config not set in swanky config. Please set it in swanky config or provide the path to the chopsticks config file using --config flag.");
}

if (!(await pathExists(chopsticksConfigPath))) {
throw new FileError(`Chopsticks config file not found at ${flags.config}`);
}

await execaCommand(
`npx @acala-network/chopsticks@latest --config=${chopsticksConfigPath}`,
{
stdio: "inherit",
}
);

this.log("Chopsticks started successfully.");
}
}
6 changes: 3 additions & 3 deletions src/commands/node/start.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Flags } from "@oclif/core";
import { execaCommand } from "execa";
import { SwankyCommand } from "../../lib/swankyCommand.js";
import semver from "semver";
import { ensureSwankyNodeInstalled } from "../../lib/index.js";
export class StartNode extends SwankyCommand<typeof StartNode> {
static description = "Start a local node";

Expand Down Expand Up @@ -29,9 +30,8 @@ export class StartNode extends SwankyCommand<typeof StartNode> {
async run(): Promise<void> {
const { flags } = await this.parse(StartNode);

if(this.swankyConfig.node.localPath === "") {
this.error("Swanky node is not installed. Please run `swanky node:install` first.");
}
ensureSwankyNodeInstalled(this.swankyConfig);

// Run persistent mode by default. non-persistent mode in case flag is provided.
// Non-Persistent mode (`--dev`) allows all CORS origin, without `--dev`, users need to specify origins by `--rpc-cors`.
await execaCommand(
Expand Down
32 changes: 18 additions & 14 deletions src/lib/command-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ export function getSwankyConfig(configType: "local" | "global"): SwankyConfig |
return config;
}


export function getSystemConfigDirectoryPath(): string {
const homeDir = userInfo().homedir;
const configPath = homeDir + `/${DEFAULT_CONFIG_FOLDER_NAME}`;
Expand Down Expand Up @@ -148,12 +147,14 @@ export async function generateTypes(contractName: string) {
);
}
export function ensureAccountIsSet(account: string | undefined, config: SwankyConfig) {
if(!account && config.defaultAccount === null) {
throw new ConfigError("No default account set. Please set one or provide an account alias with --account");
if (!account && config.defaultAccount === null) {
throw new ConfigError(
"No default account set. Please set one or provide an account alias with --account"
);
}
}

export function buildSwankyConfig() {
export function buildSwankyConfig() {
return {
node: {
localPath: "",
Expand All @@ -164,16 +165,16 @@ export function buildSwankyConfig() {
defaultAccount: DEFAULT_ACCOUNT,
accounts: [
{
"alias": "alice",
"mnemonic": "//Alice",
"isDev": true,
"address": "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY"
alias: "alice",
mnemonic: "//Alice",
isDev: true,
address: "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY",
},
{
"alias": "bob",
"mnemonic": "//Bob",
"isDev": true,
"address": "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty"
alias: "bob",
mnemonic: "//Bob",
isDev: true,
address: "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty",
},
],
networks: {
Expand Down Expand Up @@ -249,5 +250,8 @@ export function extractCargoDylintVersion() {
}

export function extractCargoContractVersion() {
return extractVersion("cargo contract -V", /cargo-contract-contract (\d+\.\d+\.\d+(?:-[\w.]+)?)(?:-unknown-[\w-]+)/);
}
return extractVersion(
"cargo contract -V",
/cargo-contract-contract (\d+\.\d+\.\d+(?:-[\w.]+)?)(?:-unknown-[\w-]+)/
);
}
9 changes: 9 additions & 0 deletions src/lib/config-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,15 @@ export class ConfigBuilder<T extends SwankySystemConfig | SwankyConfig> {
return this;
}

addChopsticks(path: string): ConfigBuilder<T> {
if("node" in this.config) {
this.config.node.chopsticks ={
configPath: path,
}
}
return this;
}

build(): T {
return this.config;
}
Expand Down
8 changes: 8 additions & 0 deletions src/lib/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { SwankyConfig } from "../index.js";
import { FileError } from "./errors.js";

export function ensureSwankyNodeInstalled(config: SwankyConfig) {
if (config.node.localPath === "") {
throw new FileError('Swanky node is not installed. Please run `swanky node:install` first.');
}
}
1 change: 1 addition & 0 deletions src/lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export * from "./account.js";
export * from "./command-utils.js";
export * from "./config.js";
export * as consts from "./consts.js";
export * from "./crypto.js";
export * from "./nodeInfo.js";
Expand Down
9 changes: 9 additions & 0 deletions src/lib/tasks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { zombienetConfig } from "../commands/zombienet/init.js";
import { readFileSync } from "fs";
import TOML from "@iarna/toml";
import { writeFileSync } from "node:fs";
import { chopsticksConfig } from "../commands/node/chopsticks/init.js";

export async function checkCliDependencies(spinner: Spinner) {
const dependencyList = [
Expand Down Expand Up @@ -218,6 +219,14 @@ export async function copyZombienetTemplateFile(templatePath: string, configPath
);
}

export async function copyChopsticksTemplateFile(templatePath: string, configPath: string) {
await ensureDir(configPath);
await copy(
path.resolve(templatePath, chopsticksConfig),
path.resolve(configPath, chopsticksConfig),
);
}

export async function downloadZombienetBinaries(binaries: string[], projectPath: string, swankyConfig: SwankyConfig, spinner: Spinner) {
const binPath = path.resolve(projectPath, "zombienet", "bin");
await ensureDir(binPath);
Expand Down
2 changes: 2 additions & 0 deletions src/lib/templates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export function getTemplates() {
const templatesPath = path.resolve(__dirname, "..", "templates");
const contractTemplatesPath = path.resolve(templatesPath, "contracts");
const zombienetTemplatesPath = path.resolve(templatesPath, "zombienet");
const chopsticksTemplatesPath = path.resolve(templatesPath, "chopsticks");
const fileList = readdirSync(contractTemplatesPath, {
withFileTypes: true,
});
Expand All @@ -21,5 +22,6 @@ export function getTemplates() {
contractTemplatesPath,
contractTemplatesList,
zombienetTemplatesPath,
chopsticksTemplatesPath,
};
}
4 changes: 4 additions & 0 deletions src/templates/chopsticks/dev.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
endpoint: ws://127.0.0.1:9944
mock-signature-host: true
block: 1
db: ./db.sqlite
3 changes: 3 additions & 0 deletions src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ export interface SwankyConfig extends SwankySystemConfig{
localPath: string;
supportedInk: string;
version: string;
chopsticks?: {
configPath: string;
};
};
contracts: Record<string, ContractData> | Record<string, never>;
zombienet?: ZombienetData;
Expand Down

0 comments on commit f926f6a

Please sign in to comment.