diff --git a/src/commands/contracts/call.ts b/src/commands/contracts/call.ts index d0b095cb..c4a11c51 100644 --- a/src/commands/contracts/call.ts +++ b/src/commands/contracts/call.ts @@ -4,6 +4,7 @@ import { BaseAction } from "../../lib/actions/BaseAction"; export interface CallOptions { args: any[]; + rpc?: string; } export class CallAction extends BaseAction{ @@ -15,12 +16,15 @@ export class CallAction extends BaseAction{ contractAddress, method, args, + rpc, }: { contractAddress: string; method: string; args: any[]; + rpc?: string; }): Promise { - 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); diff --git a/src/commands/contracts/deploy.ts b/src/commands/contracts/deploy.ts index d2971955..527a8739 100644 --- a/src/commands/contracts/deploy.ts +++ b/src/commands/contracts/deploy.ts @@ -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 { @@ -26,7 +31,7 @@ export class DeployAction extends BaseAction { return fs.readFileSync(contractPath, "utf-8"); } - private async executeTsScript(filePath: string): Promise { + private async executeTsScript(filePath: string, rpcUrl?: string): Promise { const outFile = filePath.replace(/\.ts$/, ".compiled.js"); this.startSpinner(`Transpiling TypeScript file: ${filePath}`); try { @@ -39,7 +44,7 @@ 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 { @@ -47,7 +52,7 @@ export class DeployAction extends BaseAction { } } - private async executeJsScript(filePath: string, transpiledFilePath?: string): Promise { + private async executeJsScript(filePath: string, transpiledFilePath?: string, rpcUrl?: string): Promise { this.startSpinner(`Executing file: ${filePath}`); try { const module = await import(pathToFileURL(transpiledFilePath || filePath).href); @@ -55,7 +60,7 @@ export class DeployAction extends BaseAction { 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) { @@ -63,7 +68,7 @@ export class DeployAction extends BaseAction { } } - async deployScripts() { + async deployScripts(options?: DeployScriptsOptions) { this.startSpinner("Searching for deploy scripts..."); if (!fs.existsSync(this.deployDir)) { this.failSpinner("No deploy folder found."); @@ -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); @@ -105,7 +110,7 @@ export class DeployAction extends BaseAction { async deploy(options: DeployOptions): Promise { try { - const client = await this.getClient(); + const client = await this.getClient(options.rpc); this.startSpinner("Setting up the deployment environment..."); await client.initializeConsensusSmartContract(); diff --git a/src/commands/contracts/index.ts b/src/commands/contracts/index.ts index 776b3a36..f746942e 100644 --- a/src/commands/contracts/index.ts +++ b/src/commands/contracts/index.ts @@ -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[] { @@ -15,20 +15,22 @@ export function initializeContractsCommands(program: Command) { .command("deploy") .description("Deploy intelligent contracts") .option("--contract ", "Path to the smart contract to deploy") - // .option("--network ", "Specify the network (e.g., testnet)", "localnet") + .option("--rpc ", "RPC URL for the network") .option("--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 ") .description("Call a contract method") + .option("--rpc ", "RPC URL for the network") .option("--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(); diff --git a/src/lib/actions/BaseAction.ts b/src/lib/actions/BaseAction.ts index 2ce4a211..b60d3edb 100644 --- a/src/lib/actions/BaseAction.ts +++ b/src/lib/actions/BaseAction.ts @@ -18,11 +18,11 @@ export class BaseAction extends ConfigFileManager { this.keypairManager = new KeypairManager(); } - protected async getClient(): Promise> { + protected async getClient(rpcUrl?: string): Promise> { 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), }); } diff --git a/tests/actions/call.test.ts b/tests/actions/call.test.ts index 1f7a10ca..bc7e62cf 100644 --- a/tests/actions/call.test.ts +++ b/tests/actions/call.test.ts @@ -11,6 +11,7 @@ describe("CallAction", () => { writeContract: vi.fn(), waitForTransactionReceipt: vi.fn(), getContractSchema: vi.fn(), + initializeConsensusSmartContract: vi.fn() }; const mockPrivateKey = "mocked_private_key"; @@ -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); + }); }); \ No newline at end of file diff --git a/tests/actions/deploy.test.ts b/tests/actions/deploy.test.ts index 9df3ccc6..61a03707 100644 --- a/tests/actions/deploy.test.ts +++ b/tests/actions/deploy.test.ts @@ -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 () => { @@ -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); }); @@ -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 () => { @@ -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 () => { @@ -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 + ); + }); }); diff --git a/tests/commands/call.test.ts b/tests/commands/call.test.ts index e3f135c5..f6733692 100644 --- a/tests/commands/call.test.ts +++ b/tests/commands/call.test.ts @@ -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" }); }); diff --git a/tests/commands/deploy.test.ts b/tests/commands/deploy.test.ts index b33ebd1d..853fe326 100644 --- a/tests/commands/deploy.test.ts +++ b/tests/commands/deploy.test.ts @@ -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" }); });