Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion src/commands/contracts/call.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { BaseAction } from "../../lib/actions/BaseAction";

export interface CallOptions {
args: any[];
rpc?: string;
}

export class CallAction extends BaseAction{
Expand All @@ -15,12 +16,15 @@ export class CallAction extends BaseAction{
contractAddress,
method,
args,
rpc,
}: {
contractAddress: string;
method: string;
args: any[];
rpc?: string;
}): Promise<void> {
const client = await this.getClient();
const client = await this.getClient(rpc);
await client.initializeConsensusSmartContract();
this.startSpinner(`Calling method ${method} on contract at ${contractAddress}...`);

const contractSchema = await client.getContractSchema(contractAddress);
Expand Down
21 changes: 13 additions & 8 deletions src/commands/contracts/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ import { buildSync } from "esbuild";
export interface DeployOptions {
contract?: string;
args?: any[];
rpc?: string;
}

export interface DeployScriptsOptions {
rpc?: string;
}

export class DeployAction extends BaseAction {
Expand All @@ -26,7 +31,7 @@ export class DeployAction extends BaseAction {
return fs.readFileSync(contractPath, "utf-8");
}

private async executeTsScript(filePath: string): Promise<void> {
private async executeTsScript(filePath: string, rpcUrl?: string): Promise<void> {
const outFile = filePath.replace(/\.ts$/, ".compiled.js");
this.startSpinner(`Transpiling TypeScript file: ${filePath}`);
try {
Expand All @@ -39,31 +44,31 @@ export class DeployAction extends BaseAction {
target: "es2020",
sourcemap: false,
});
await this.executeJsScript(filePath, outFile);
await this.executeJsScript(filePath, outFile, rpcUrl);
} catch (error) {
this.failSpinner(`Error executing: ${filePath}`, error);
} finally {
fs.unlinkSync(outFile);
}
}

private async executeJsScript(filePath: string, transpiledFilePath?: string): Promise<void> {
private async executeJsScript(filePath: string, transpiledFilePath?: string, rpcUrl?: string): Promise<void> {
this.startSpinner(`Executing file: ${filePath}`);
try {
const module = await import(pathToFileURL(transpiledFilePath || filePath).href);
if (!module.default || typeof module.default !== "function") {
this.failSpinner(`No "default" function found in: ${filePath}`);
return
}
const client = await this.getClient();
const client = await this.getClient(rpcUrl);
await module.default(client);
this.succeedSpinner(`Successfully executed: ${filePath}`);
} catch (error) {
this.failSpinner(`Error executing: ${filePath}`, error);
}
}

async deployScripts() {
async deployScripts(options?: DeployScriptsOptions) {
this.startSpinner("Searching for deploy scripts...");
if (!fs.existsSync(this.deployDir)) {
this.failSpinner("No deploy folder found.");
Expand Down Expand Up @@ -93,9 +98,9 @@ export class DeployAction extends BaseAction {
this.setSpinnerText(`Executing script: ${filePath}`);
try {
if (file.endsWith(".ts")) {
await this.executeTsScript(filePath);
await this.executeTsScript(filePath, options?.rpc);
} else {
await this.executeJsScript(filePath);
await this.executeJsScript(filePath, undefined, options?.rpc);
}
} catch (error) {
this.failSpinner(`Error executing script: ${filePath}`, error);
Expand All @@ -105,7 +110,7 @@ export class DeployAction extends BaseAction {

async deploy(options: DeployOptions): Promise<void> {
try {
const client = await this.getClient();
const client = await this.getClient(options.rpc);
this.startSpinner("Setting up the deployment environment...");
await client.initializeConsensusSmartContract();

Expand Down
8 changes: 5 additions & 3 deletions src/commands/contracts/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Command } from "commander";
import { DeployAction, DeployOptions } from "./deploy";
import { DeployAction, DeployOptions, DeployScriptsOptions } from "./deploy";
import { CallAction, CallOptions } from "./call";

function parseArg(value: string, previous: any[] = []): any[] {
Expand All @@ -15,20 +15,22 @@ export function initializeContractsCommands(program: Command) {
.command("deploy")
.description("Deploy intelligent contracts")
.option("--contract <contractPath>", "Path to the smart contract to deploy")
// .option("--network <networkName>", "Specify the network (e.g., testnet)", "localnet")
.option("--rpc <rpcUrl>", "RPC URL for the network")
.option("--args <args...>", "Positional arguments for the contract (space-separated, use quotes for multi-word arguments)", parseArg, [])
.action(async (options: DeployOptions) => {
const deployer = new DeployAction();
if(options.contract){
await deployer.deploy(options);
}else {
await deployer.deployScripts();
const deployScriptsOptions: DeployScriptsOptions = { rpc: options.rpc };
await deployer.deployScripts(deployScriptsOptions);
}
});

program
.command("call <contractAddress> <method>")
.description("Call a contract method")
.option("--rpc <rpcUrl>", "RPC URL for the network")
.option("--args <args...>", "Positional arguments for the method (space-separated, use quotes for multi-word arguments)", parseArg, [])
.action(async (contractAddress: string, method: string, options: CallOptions) => {
const caller = new CallAction();
Expand Down
4 changes: 2 additions & 2 deletions src/lib/actions/BaseAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,11 @@ export class BaseAction extends ConfigFileManager {
this.keypairManager = new KeypairManager();
}

protected async getClient(): Promise<GenLayerClient<typeof localnet>> {
protected async getClient(rpcUrl?: string): Promise<GenLayerClient<typeof localnet>> {
if (!this._genlayerClient) {
this._genlayerClient = createClient({
chain: localnet,
endpoint: process.env.VITE_JSON_RPC_SERVER_URL,
endpoint: rpcUrl,
account: createAccount(await this.getPrivateKey() as any),
});
}
Expand Down
52 changes: 52 additions & 0 deletions tests/actions/call.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ describe("CallAction", () => {
writeContract: vi.fn(),
waitForTransactionReceipt: vi.fn(),
getContractSchema: vi.fn(),
initializeConsensusSmartContract: vi.fn()
};

const mockPrivateKey = "mocked_private_key";
Expand Down Expand Up @@ -103,4 +104,55 @@ describe("CallAction", () => {

expect(callActions["failSpinner"]).toHaveBeenCalledWith("Error during write operation", expect.any(Error));
});

test("uses custom RPC URL when provided", async () => {
const options = { args: [1, 2, "Hello"], rpc: "https://custom-rpc-url.com" };
const mockResult = "mocked_result";

vi.mocked(mockClient.getContractSchema).mockResolvedValue({ methods: { getData: { readonly: true } } });
vi.mocked(mockClient.readContract).mockResolvedValue(mockResult);

await callActions.call({
contractAddress: "0xMockedContract",
method: "getData",
...options,
});

expect(createClient).toHaveBeenCalledWith(expect.objectContaining({
endpoint: "https://custom-rpc-url.com"
}));
expect(mockClient.readContract).toHaveBeenCalledWith({
address: "0xMockedContract",
functionName: "getData",
args: [1, 2, "Hello"],
});
expect(callActions["succeedSpinner"]).toHaveBeenCalledWith("Read operation successfully executed", "mocked_result");
});

test("uses custom RPC URL for write operations", async () => {
const options = { args: [42, "Update"], rpc: "https://custom-rpc-url.com" };
const mockHash = "0xMockedTransactionHash";
const mockReceipt = { status: "success" };

vi.mocked(mockClient.getContractSchema).mockResolvedValue({ methods: { updateData: { readonly: false } } });
vi.mocked(mockClient.writeContract).mockResolvedValue(mockHash);
vi.mocked(mockClient.waitForTransactionReceipt).mockResolvedValue(mockReceipt);

await callActions.call({
contractAddress: "0xMockedContract",
method: "updateData",
...options,
});

expect(createClient).toHaveBeenCalledWith(expect.objectContaining({
endpoint: "https://custom-rpc-url.com"
}));
expect(mockClient.writeContract).toHaveBeenCalledWith({
address: "0xMockedContract",
functionName: "updateData",
args: [42, "Update"],
value: 0n,
});
expect(callActions["succeedSpinner"]).toHaveBeenCalledWith("Write operation successfully executed", mockReceipt);
});
});
118 changes: 106 additions & 12 deletions tests/actions/deploy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,9 +143,9 @@ describe("DeployAction", () => {
await deployer.deployScripts();

expect(deployer["setSpinnerText"]).toHaveBeenCalledWith("Found 3 deploy scripts. Executing...");
expect(deployer["executeTsScript"]).toHaveBeenCalledWith(expect.stringMatching(/1_first.ts/));
expect(deployer["executeJsScript"]).toHaveBeenCalledWith(expect.stringMatching(/2_second.js/));
expect(deployer["executeTsScript"]).toHaveBeenCalledWith(expect.stringMatching(/10_last.ts/));
expect(deployer["executeTsScript"]).toHaveBeenCalledWith(expect.stringMatching(/1_first.ts/), undefined);
expect(deployer["executeJsScript"]).toHaveBeenCalledWith(expect.stringMatching(/2_second.js/), undefined, undefined);
expect(deployer["executeTsScript"]).toHaveBeenCalledWith(expect.stringMatching(/10_last.ts/), undefined);
});

test("executeTsScript transpiles and executes TypeScript", async () => {
Expand All @@ -168,7 +168,7 @@ describe("DeployAction", () => {
sourcemap: false,
});

expect(deployer["executeJsScript"]).toHaveBeenCalledWith(filePath, outFile);
expect(deployer["executeJsScript"]).toHaveBeenCalledWith(filePath, outFile, undefined);
expect(fs.unlinkSync).toHaveBeenCalledWith(outFile);
});

Expand All @@ -193,9 +193,9 @@ describe("DeployAction", () => {

await deployer.deployScripts();

expect(deployer["executeTsScript"]).toHaveBeenCalledWith(expect.stringContaining("1_first.ts"));
expect(deployer["executeJsScript"]).toHaveBeenCalledWith(expect.stringContaining("2_second.js"));
expect(deployer["executeTsScript"]).toHaveBeenCalledWith(expect.stringContaining("10_last.ts"));
expect(deployer["executeTsScript"]).toHaveBeenCalledWith(expect.stringContaining("1_first.ts"), undefined);
expect(deployer["executeJsScript"]).toHaveBeenCalledWith(expect.stringContaining("2_second.js"), undefined, undefined);
expect(deployer["executeTsScript"]).toHaveBeenCalledWith(expect.stringContaining("10_last.ts"), undefined);
});

test("deployScripts fails when no scripts are found", async () => {
Expand Down Expand Up @@ -257,11 +257,11 @@ describe("DeployAction", () => {

await deployer.deployScripts();

expect(deployer["executeTsScript"]).toHaveBeenCalledWith(expect.stringContaining("script.ts"));
expect(deployer["executeTsScript"]).toHaveBeenCalledWith(expect.stringContaining("2alpha_script.ts"));
expect(deployer["executeTsScript"]).toHaveBeenCalledWith(expect.stringContaining("3alpha_script.ts"));
expect(deployer["executeTsScript"]).toHaveBeenCalledWith(expect.stringContaining("blpha_script.ts"));
expect(deployer["executeTsScript"]).toHaveBeenCalledWith(expect.stringContaining("clpha_script.ts"));
expect(deployer["executeTsScript"]).toHaveBeenCalledWith(expect.stringContaining("script.ts"), undefined);
expect(deployer["executeTsScript"]).toHaveBeenCalledWith(expect.stringContaining("2alpha_script.ts"), undefined);
expect(deployer["executeTsScript"]).toHaveBeenCalledWith(expect.stringContaining("3alpha_script.ts"), undefined);
expect(deployer["executeTsScript"]).toHaveBeenCalledWith(expect.stringContaining("blpha_script.ts"), undefined);
expect(deployer["executeTsScript"]).toHaveBeenCalledWith(expect.stringContaining("clpha_script.ts"), undefined);
});

test("executeJsScript fails if module has no default export", async () => {
Expand Down Expand Up @@ -304,4 +304,98 @@ describe("DeployAction", () => {
error
);
});

test("deploys contract with rpc option", async () => {
const options: DeployOptions = {
contract: "/mocked/contract/path",
args: [1, 2, 3],
rpc: "https://custom-rpc-url.com"
};
const contractContent = "contract code";

vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.readFileSync).mockReturnValue(contractContent);
vi.mocked(mockClient.deployContract).mockResolvedValue("mocked_tx_hash");
vi.mocked(mockClient.waitForTransactionReceipt).mockResolvedValue({
data: { contract_address: "0xdasdsadasdasdada" },
});

await deployer.deploy(options);

expect(createClient).toHaveBeenCalledWith(expect.objectContaining({
endpoint: "https://custom-rpc-url.com"
}));
expect(fs.readFileSync).toHaveBeenCalledWith(options.contract, "utf-8");
expect(mockClient.deployContract).toHaveBeenCalledWith({
code: contractContent,
args: [1, 2, 3],
leaderOnly: false,
});
});

test("executeJsScript uses rpc url when provided", async () => {
const filePath = "/mocked/script.js";
const rpcUrl = "https://custom-rpc-url.com";
const mockFn = vi.fn();

vi.doMock(pathToFileURL(filePath).href, () => ({ default: mockFn }));

await deployer["executeJsScript"](filePath, undefined, rpcUrl);

expect(createClient).toHaveBeenCalledWith(expect.objectContaining({
endpoint: rpcUrl
}));
expect(mockFn).toHaveBeenCalledWith(mockClient);
expect(deployer["succeedSpinner"]).toHaveBeenCalledWith(`Successfully executed: ${filePath}`);
});

test("executeTsScript passes rpc url to executeJsScript", async () => {
const filePath = "/mocked/script.ts";
const outFile = "/mocked/script.compiled.js";
const rpcUrl = "https://custom-rpc-url.com";

vi.spyOn(deployer as any, "executeJsScript").mockResolvedValue(undefined);
vi.mocked(buildSync).mockImplementation((() => {}) as any);

await deployer["executeTsScript"](filePath, rpcUrl);

expect(deployer["startSpinner"]).toHaveBeenCalledWith(`Transpiling TypeScript file: ${filePath}`);
expect(buildSync).toHaveBeenCalledWith({
entryPoints: [filePath],
outfile: outFile,
bundle: false,
platform: "node",
format: "esm",
target: "es2020",
sourcemap: false,
});

expect(deployer["executeJsScript"]).toHaveBeenCalledWith(filePath, outFile, rpcUrl);
expect(fs.unlinkSync).toHaveBeenCalledWith(outFile);
});

test("deployScripts passes rpc url to script execution methods", async () => {
const rpcUrl = "https://custom-rpc-url.com";

vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.readdirSync).mockReturnValue([
"1_first.ts",
"2_second.js",
] as any);

vi.spyOn(deployer as any, "executeTsScript").mockResolvedValue(undefined);
vi.spyOn(deployer as any, "executeJsScript").mockResolvedValue(undefined);

await deployer.deployScripts({ rpc: rpcUrl });

expect(deployer["executeTsScript"]).toHaveBeenCalledWith(
expect.stringMatching(/1_first.ts/),
rpcUrl
);
expect(deployer["executeJsScript"]).toHaveBeenCalledWith(
expect.stringMatching(/2_second.js/),
undefined,
rpcUrl
);
});
});
7 changes: 5 additions & 2 deletions tests/commands/call.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,16 @@ describe("call command", () => {
"2",
"Hello",
"false",
"true"
"true",
"--rpc",
"https://custom-rpc-url.com"
]);
expect(CallAction).toHaveBeenCalledTimes(1);
expect(CallAction.prototype.call).toHaveBeenCalledWith({
contractAddress: "0xMockedContract",
method: "updateData",
args: [1, 2, "Hello", false, true]
args: [1, 2, "Hello", false, true],
rpc: "https://custom-rpc-url.com"
});
});

Expand Down
5 changes: 4 additions & 1 deletion tests/commands/deploy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,14 @@ describe("deploy command", () => {
"1",
"2",
"3",
"--rpc",
"https://custom-rpc-url.com"
]);
expect(DeployAction).toHaveBeenCalledTimes(1);
expect(DeployAction.prototype.deploy).toHaveBeenCalledWith({
contract: "./path/to/contract",
args: [1, 2, 3]
args: [1, 2, 3],
rpc: "https://custom-rpc-url.com"
});
});

Expand Down