diff --git a/README.md b/README.md index d3b7d931..fddab214 100644 --- a/README.md +++ b/README.md @@ -592,6 +592,27 @@ rm ./examples/sample-key tradetrust deploy document-store "My Name" --network sepolia --key 0000000000000000000000000000000000000000000000000000000000000003 ``` +### Providing custom RPC URLs + +When interacting with the blockchain, you may want to connect to a different Ethereum provider than the default ones. All functions that interact with the blockchain support the `--rpc-url` option to specify a custom RPC endpoint: + +1. Using `--rpc-url` option where you provide the URL of your custom Ethereum RPC provider. +2. When `--rpc-url` is provided, it takes precedence over the default network provider. +3. This is useful for connecting to private networks, custom providers, or alternative public endpoints. + +Example: + +```bash +# Using custom RPC URL with document store deployment +tradetrust deploy document-store "My Name" --network sepolia --rpc-url https://custom-rpc.example.com + +# Using custom RPC URL with token registry operations +tradetrust deploy token-registry "My Registry" --network sepolia --rpc-url https://my-provider.com + +# Using custom RPC URL with document store operations +tradetrust document-store issue --address 0x1234... --hash 0xabcd... --network sepolia --rpc-url https://custom-endpoint.io +``` + ### Providing the Remarks and Encryption Key Enables users to attach encrypted remarks (up to 120 characters) to blockchain transactions. The encrypted remarks are stored immutably on the blockchain and can be viewed in the endorsement chain. This ensures secure and meaningful metadata is recorded alongside transactions. diff --git a/package-lock.json b/package-lock.json index acabfc36..4a18acf2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,11 +13,11 @@ "@snyk/protect": "^1.1196.0", "@tradetrust-tt/dnsprove": "^2.18.0", "@tradetrust-tt/document-store": "^4.1.1", - "@tradetrust-tt/token-registry": "^5.2.0", - "@tradetrust-tt/tradetrust": "^6.10.0", - "@tradetrust-tt/tradetrust-config": "^1.19.0", - "@tradetrust-tt/tt-verify": "^9.5.0", - "@trustvc/trustvc": "^1.7.0", + "@tradetrust-tt/token-registry": "^5.5.0", + "@tradetrust-tt/tradetrust": "^6.10.2", + "@tradetrust-tt/tradetrust-config": "^1.19.1", + "@tradetrust-tt/tt-verify": "^9.5.1", + "@trustvc/trustvc": "^1.7.4", "ajv": "^8.4.0", "ajv-formats": "^2.1.0", "chalk": "^4.1.2", @@ -7284,9 +7284,10 @@ "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==" }, "node_modules/@tradetrust-tt/token-registry": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/@tradetrust-tt/token-registry/-/token-registry-5.4.0.tgz", - "integrity": "sha512-J5IKJWPxHZXKzvpZBWCW8VoGaX5OJn5po5C4Edu2IRKT4z2zSW9qvZ9oZgUri8rog8XDY82z2O3S4PiQMlcfbQ==", + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@tradetrust-tt/token-registry/-/token-registry-5.5.0.tgz", + "integrity": "sha512-oiNI3L/zPxXPHaMltSPxZFaXH/Ej9MMy5eaiKXLOSiCxYwcqd7qe/NfFlvO1sR3OLASEnRNZxnWLl9yPY5+wnw==", + "license": "Apache-2.0", "dependencies": { "ethers": "^6.13.4" } @@ -7305,20 +7306,23 @@ }, "node_modules/@tradetrust-tt/token-registry-v5": { "name": "@tradetrust-tt/token-registry", - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/@tradetrust-tt/token-registry/-/token-registry-5.4.0.tgz", - "integrity": "sha512-J5IKJWPxHZXKzvpZBWCW8VoGaX5OJn5po5C4Edu2IRKT4z2zSW9qvZ9oZgUri8rog8XDY82z2O3S4PiQMlcfbQ==", + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@tradetrust-tt/token-registry/-/token-registry-5.5.0.tgz", + "integrity": "sha512-oiNI3L/zPxXPHaMltSPxZFaXH/Ej9MMy5eaiKXLOSiCxYwcqd7qe/NfFlvO1sR3OLASEnRNZxnWLl9yPY5+wnw==", + "license": "Apache-2.0", "dependencies": { "ethers": "^6.13.4" } }, "node_modules/@tradetrust-tt/tradetrust": { - "version": "6.10.1", - "resolved": "https://registry.npmjs.org/@tradetrust-tt/tradetrust/-/tradetrust-6.10.1.tgz", - "integrity": "sha512-Vk5TOlRKFbZ0qMitcYDcUGU5Nu5E5POHNlBnv61lrPoMq0gseIBaprgF/gIRC0Q4LVQLjCPOi1ufW11bDSpILQ==", + "version": "6.10.2", + "resolved": "https://registry.npmjs.org/@tradetrust-tt/tradetrust/-/tradetrust-6.10.2.tgz", + "integrity": "sha512-4zj4zlsrrQiUJQxvl4N8Pa4cLHtFtFIs0lMg6daP/gRJXIn1QWD0Kl4mQ5FsjuopeM7JOBX/xok22SmElEtT5w==", "hasInstallScript": true, + "license": "Apache-2.0", "dependencies": { "@govtechsg/jsonld": "^0.1.1", + "@trustvc/w3c-vc": "^1.2.17", "ajv": "^8.12.0", "ajv-formats": "^2.1.1", "cross-fetch": "^4.0.0", @@ -7337,9 +7341,9 @@ } }, "node_modules/@tradetrust-tt/tradetrust-config": { - "version": "1.19.0", - "resolved": "https://registry.npmjs.org/@tradetrust-tt/tradetrust-config/-/tradetrust-config-1.19.0.tgz", - "integrity": "sha512-wEwl1Ol2gvV5OfSRA7w4iqpxsKaNBs8N1qW9HYPOnT0Unn7lDb6Sse4L/7hJ7dxhDqtCj1JLG0Xpp0DUJ/BOyQ==", + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/@tradetrust-tt/tradetrust-config/-/tradetrust-config-1.19.1.tgz", + "integrity": "sha512-F6ZMa/nGEuV1nPuMTSkDq5sHW/OvqiR1/Qw6mrV2oOwQ2r43OFvWF7CnvPO24hcb8AverJtjjQEMEbE8c3LXyg==", "dependencies": { "ajv": "^8.12.0", "ajv-formats": "^2.1.1", @@ -7352,11 +7356,12 @@ } }, "node_modules/@tradetrust-tt/tradetrust-utils": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/@tradetrust-tt/tradetrust-utils/-/tradetrust-utils-2.4.0.tgz", - "integrity": "sha512-rbcKCcK1/rYiXhBpVzGxt6uTLwaZtTJPc4WWlWogOUtQA5DjSOOxAzNXOOSNIXnsXpxnBIFevncyz3ScK7Ot0w==", + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@tradetrust-tt/tradetrust-utils/-/tradetrust-utils-2.4.2.tgz", + "integrity": "sha512-RFCgCMQTadLhSF94syRKCxfHyn678RrUw4wkChL1tdZkcq/LM4dAnZuJOELUhsDlstoM6cVxiisEDGBSM3mB4g==", + "license": "Apache-2.0", "dependencies": { - "@tradetrust-tt/tt-verify": "^9.5.0", + "@tradetrust-tt/tt-verify": "^9.5.1", "dotenv": "^16.4.5", "ethers": "^5.8.0", "node-fetch": "^2.7.0" @@ -7370,13 +7375,14 @@ "license": "MIT" }, "node_modules/@tradetrust-tt/tt-verify": { - "version": "9.5.0", - "resolved": "https://registry.npmjs.org/@tradetrust-tt/tt-verify/-/tt-verify-9.5.0.tgz", - "integrity": "sha512-7r+P3QHtkVzINp+9eDcvVLK0VgmmfFv41ocSp8homN2iyTQC7Sp5fU/xqjRkxYVaCbTO/2uT9tfJQjWJsqQWXg==", + "version": "9.5.1", + "resolved": "https://registry.npmjs.org/@tradetrust-tt/tt-verify/-/tt-verify-9.5.1.tgz", + "integrity": "sha512-aPJ1yzGJlpa92iS6qkbS6/+Gfje006uVchgpXkr8249/diHRf/L/Qm19W92kHz7ZiloSJG8QKKU6i/bE4pQzGg==", + "license": "Apache-2.0", "dependencies": { "@tradetrust-tt/dnsprove": "^2.18.0", "@tradetrust-tt/document-store": "^4.1.1", - "@tradetrust-tt/token-registry": "^5.4.0", + "@tradetrust-tt/token-registry": "^5.5.0", "@tradetrust-tt/tradetrust": "^6.10.1", "axios": "^1.7.2", "debug": "^4.3.1", @@ -7395,17 +7401,18 @@ } }, "node_modules/@trustvc/trustvc": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@trustvc/trustvc/-/trustvc-1.7.0.tgz", - "integrity": "sha512-1spJLKIYyOCU87DiB/Kbsc0OghB6/SZXca+OmaFllrKEHxyT6rAJY2ihKBALIz3vifXY3Q94XZyDzjRxg4q7DQ==", + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/@trustvc/trustvc/-/trustvc-1.7.4.tgz", + "integrity": "sha512-hRyK7ryuUzWZceVEFEjvzbULImqLxZbmJhD7lXH97TKgFcEWSB6ro+MZu8rzYHNDoBgRCVEiu0041aVH/JVZmg==", + "license": "Apache-2.0", "dependencies": { - "@tradetrust-tt/dnsprove": "^2.17.0", + "@tradetrust-tt/dnsprove": "^2.18.0", "@tradetrust-tt/ethers-aws-kms-signer": "^2.1.4", "@tradetrust-tt/token-registry-v4": "npm:@tradetrust-tt/token-registry@^4.16.0", - "@tradetrust-tt/token-registry-v5": "npm:@tradetrust-tt/token-registry@^5.3.0", - "@tradetrust-tt/tradetrust": "^6.10.1", - "@tradetrust-tt/tradetrust-utils": "^2.3.2", - "@tradetrust-tt/tt-verify": "^9.4.0", + "@tradetrust-tt/token-registry-v5": "npm:@tradetrust-tt/token-registry@^5.5.0", + "@tradetrust-tt/tradetrust": "^6.10.2", + "@tradetrust-tt/tradetrust-utils": "^2.4.2", + "@tradetrust-tt/tt-verify": "^9.5.1", "@trustvc/w3c-context": "^1.2.13", "@trustvc/w3c-credential-status": "^1.2.13", "@trustvc/w3c-issuer": "^1.2.4", diff --git a/package.json b/package.json index ed68cb0c..a5893011 100644 --- a/package.json +++ b/package.json @@ -73,11 +73,11 @@ "@snyk/protect": "^1.1196.0", "@tradetrust-tt/dnsprove": "^2.18.0", "@tradetrust-tt/document-store": "^4.1.1", - "@tradetrust-tt/token-registry": "^5.2.0", - "@tradetrust-tt/tradetrust": "^6.10.0", - "@tradetrust-tt/tradetrust-config": "^1.19.0", - "@tradetrust-tt/tt-verify": "^9.5.0", - "@trustvc/trustvc": "^1.7.0", + "@tradetrust-tt/token-registry": "^5.5.0", + "@tradetrust-tt/tradetrust": "^6.10.2", + "@tradetrust-tt/tradetrust-config": "^1.19.1", + "@tradetrust-tt/tt-verify": "^9.5.1", + "@trustvc/trustvc": "^1.7.4", "ajv": "^8.4.0", "ajv-formats": "^2.1.0", "chalk": "^4.1.2", diff --git a/src/__tests__/unwrap.test.ts b/src/__tests__/unwrap.test.ts index 56276b35..a2eba138 100644 --- a/src/__tests__/unwrap.test.ts +++ b/src/__tests__/unwrap.test.ts @@ -6,8 +6,14 @@ import wrappedFileFixture1 from "./fixture/2.0/wrapped-example.1.json"; import unwrappedFileFixture1 from "./fixture/2.0/unwrapped-example.1.json"; import wrappedFileFixture2 from "./fixture/2.0/wrapped-example.2.json"; import unwrappedFileFixture2 from "./fixture/2.0/unwrapped-example.2.json"; - -jest.mock("fs"); +jest.mock("fs", () => { + return { + ...jest.requireActual("fs"), + readdir: jest.fn(), + lstatSync: jest.fn(), + writeFileSync: jest.fn(), + }; +}); describe("unwrap", () => { describe("unwrapIndividualDocuments", () => { diff --git a/src/__tests__/wrap.test.ts b/src/__tests__/wrap.test.ts index 98b63e4c..df34d60f 100644 --- a/src/__tests__/wrap.test.ts +++ b/src/__tests__/wrap.test.ts @@ -4,7 +4,14 @@ import { Output } from "../implementations/utils/disk"; import fs from "fs"; import { utils } from "@tradetrust-tt/tradetrust"; -jest.mock("fs"); +jest.mock("fs", () => { + return { + ...jest.requireActual("fs"), + readdir: jest.fn(), + lstatSync: jest.fn(), + writeFileSync: jest.fn(), + }; +}); describe("batchIssue", () => { describe("appendProofToDocuments", () => { diff --git a/src/commands/dns/txt-record/__test__/__snapshots__/get-astron.test.ts.snap b/src/commands/dns/txt-record/__test__/__snapshots__/get-astron.test.ts.snap index 835aac75..95ee8703 100644 --- a/src/commands/dns/txt-record/__test__/__snapshots__/get-astron.test.ts.snap +++ b/src/commands/dns/txt-record/__test__/__snapshots__/get-astron.test.ts.snap @@ -9,6 +9,13 @@ exports[`get should return dns-txt 1`] = ` "netId": "1338", "type": "openatts", }, + { + "addr": "0x94FD21A026E29E0686583b8be71Cb28a8ca1A8d4", + "dnssec": false, + "net": "ethereum", + "netId": "1338", + "type": "openatts", + }, { "addr": "0xc98d993271a997384889dd39c14cec0c1e0206c2", "dnssec": false, diff --git a/src/commands/shared.ts b/src/commands/shared.ts index 04149abb..954e3649 100644 --- a/src/commands/shared.ts +++ b/src/commands/shared.ts @@ -42,6 +42,15 @@ export const isWalletOption = (option: any): option is WalletOption => { return typeof option?.encryptedWalletPath === "string"; }; +export type RpcUrlOption = { + rpcUrl: string; +}; + +// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types +export const isRpcUrlOption = (option: any): option is RpcUrlOption => { + return typeof option?.rpcUrl === "string"; +}; + export type WalletOrSignerOption = Partial | Partial | Partial; export interface GasPriceScale { @@ -121,5 +130,11 @@ export const withAwsKmsSignerOption = (yargs: Argv): Argv => "AWS KMS key id. Example: arn:aws:kms:us-west-2:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab", }); +export const withRpcUrlOption = (yargs: Argv): Argv => + yargs.option("rpc-url", { + type: "string", + description: "Custom RPC URL to connect to. Example: https://mainnet.infura.io/v3/YOUR-PROJECT-ID", + }); + export const withNetworkAndWalletSignerOption = (yargs: Argv): Argv => - withNetworkOption(withAwsKmsSignerOption(withWalletOption(withPrivateKeyOption(yargs)))); + withNetworkOption(withRpcUrlOption(withAwsKmsSignerOption(withWalletOption(withPrivateKeyOption(yargs))))); diff --git a/src/implementations/deploy/document-store/document-store.test.ts b/src/implementations/deploy/document-store/document-store.test.ts index 20a04975..99eaa37d 100644 --- a/src/implementations/deploy/document-store/document-store.test.ts +++ b/src/implementations/deploy/document-store/document-store.test.ts @@ -1,6 +1,6 @@ import { deployDocumentStore } from "./document-store"; import { join } from "path"; -import { Wallet } from "ethers"; +import { Wallet, utils } from "ethers"; import { DocumentStoreFactory } from "@tradetrust-tt/document-store"; import { DeployDocumentStoreCommand } from "../../../commands/deploy/deploy.types"; @@ -104,5 +104,120 @@ describe("document-store", () => { const addr = await passedSigner.getAddress(); expect(mockedDeploy.mock.calls[0][1]).toStrictEqual(addr); }); + + describe("should use custom RPC URL", () => { + const createMockProvider = (chainId: number, name: string): any => ({ + getNetwork: jest.fn().mockResolvedValue({ chainId, name }), + getBalance: jest.fn(), + getTransactionCount: jest.fn(), + getGasPrice: jest.fn(), + getFeeData: jest.fn().mockResolvedValue({ + maxFeePerGas: utils.parseUnits("20", "gwei"), + maxPriorityFeePerGas: utils.parseUnits("2", "gwei"), + gasPrice: utils.parseUnits("20", "gwei"), + }), + estimateGas: jest.fn(), + call: jest.fn(), + sendTransaction: jest.fn(), + getBlock: jest.fn(), + getTransaction: jest.fn(), + getTransactionReceipt: jest.fn(), + getLogs: jest.fn(), + resolveName: jest.fn(), + lookupAddress: jest.fn(), + on: jest.fn(), + once: jest.fn(), + emit: jest.fn(), + listenerCount: jest.fn(), + listeners: jest.fn(), + off: jest.fn(), + removeAllListeners: jest.fn(), + addListener: jest.fn(), + removeListener: jest.fn(), + waitForTransaction: jest.fn(), + _isProvider: true, + }); + + it("should use custom RPC URL when provided with private key", async () => { + const customRpcUrl = "https://custom-rpc.example.com"; + const mockProvider = createMockProvider(11155111, "sepolia"); + + // eslint-disable-next-line @typescript-eslint/no-var-requires + const { ethers: ethersModule } = require("ethers"); + const jsonRpcProviderSpy = jest + .spyOn(ethersModule.providers, "JsonRpcProvider") + .mockImplementation(() => mockProvider); + + await deployDocumentStore({ + storeName: "Test", + network: "sepolia", + key: "0000000000000000000000000000000000000000000000000000000000000001", + rpcUrl: customRpcUrl, + dryRun: false, + maxPriorityFeePerGasScale: 1, + } as any); + + const passedSigner: Wallet = mockedDocumentStoreFactory.mock.calls[0][0]; + expect(passedSigner.privateKey).toBe("0x0000000000000000000000000000000000000000000000000000000000000001"); + // Verify JsonRpcProvider was called with the custom RPC URL + expect(jsonRpcProviderSpy).toHaveBeenCalledWith(customRpcUrl); + + jsonRpcProviderSpy.mockRestore(); + }); + + it("should use custom RPC URL when provided with environment variable key", async () => { + process.env.OA_PRIVATE_KEY = "0000000000000000000000000000000000000000000000000000000000000002"; + const customRpcUrl = "https://another-custom-rpc.example.com"; + const mockProvider = createMockProvider(11155111, "sepolia"); + + // eslint-disable-next-line @typescript-eslint/no-var-requires + const { ethers: ethersModule } = require("ethers"); + const jsonRpcProviderSpy = jest + .spyOn(ethersModule.providers, "JsonRpcProvider") + .mockImplementation(() => mockProvider); + + await deployDocumentStore({ + storeName: "Test", + network: "sepolia", + rpcUrl: customRpcUrl, + dryRun: false, + maxPriorityFeePerGasScale: 1, + } as any); + + const passedSigner: Wallet = mockedDocumentStoreFactory.mock.calls[0][0]; + expect(passedSigner.privateKey).toBe(`0x${process.env.OA_PRIVATE_KEY}`); + // Verify JsonRpcProvider was called with the custom RPC URL + expect(jsonRpcProviderSpy).toHaveBeenCalledWith(customRpcUrl); + + jsonRpcProviderSpy.mockRestore(); + }); + + it("should use custom RPC URL when provided with key file", async () => { + const customRpcUrl = "https://keyfile-custom-rpc.example.com"; + const mockProvider = createMockProvider(11155111, "sepolia"); + + // eslint-disable-next-line @typescript-eslint/no-var-requires + const { ethers: ethersModule } = require("ethers"); + const jsonRpcProviderSpy = jest + .spyOn(ethersModule.providers, "JsonRpcProvider") + .mockImplementation(() => mockProvider); + + await deployDocumentStore({ + storeName: "Test", + network: "sepolia", + keyFile: join(__dirname, "..", "..", "..", "..", "examples", "sample-key"), + rpcUrl: customRpcUrl, + dryRun: false, + maxPriorityFeePerGasScale: 1, + } as any); + + const passedSigner: Wallet = mockedDocumentStoreFactory.mock.calls[0][0]; + expect(passedSigner.privateKey).toBe("0x0000000000000000000000000000000000000000000000000000000000000003"); + // Verify JsonRpcProvider was called with the custom RPC URL + expect(jsonRpcProviderSpy).toHaveBeenCalledWith(customRpcUrl); + + jsonRpcProviderSpy.mockRestore(); + }); + }); }); }); diff --git a/src/implementations/utils/__tests__/wallet.test.ts b/src/implementations/utils/__tests__/wallet.test.ts index d6a491cf..990be78c 100644 --- a/src/implementations/utils/__tests__/wallet.test.ts +++ b/src/implementations/utils/__tests__/wallet.test.ts @@ -1,22 +1,77 @@ import { prompt } from "inquirer"; import path from "path"; import { getWalletOrSigner } from "../wallet"; +import { getSupportedNetwork } from "../../../common/networks"; + jest.mock("inquirer"); +jest.mock("../../../common/networks"); +jest.mock("ethers", () => ({ + ...jest.requireActual("ethers"), + providers: { + ...jest.requireActual("ethers").providers, + JsonRpcProvider: jest.fn(), + }, +})); // assigning the mock so that we get correct typing // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore const promptMock: jest.Mock = prompt; +const getSupportedNetworkMock = getSupportedNetwork as jest.MockedFunction; const privateKey = "0xcd27dc84c82c5814e7edac518edd5f263e7db7f25adb7a1afe13996a95583cf2"; const walletAddress = "0xB26B4941941C51a4885E5B7D3A1B861E54405f90"; +// Factory function to create mock providers with complete ethers.js provider interface +const createMockProvider = (chainId: number, name: string): any => ({ + getNetwork: jest.fn().mockResolvedValue({ chainId, name }), + getBalance: jest.fn(), + getTransactionCount: jest.fn(), + getGasPrice: jest.fn(), + estimateGas: jest.fn(), + call: jest.fn(), + sendTransaction: jest.fn(), + getBlock: jest.fn(), + getTransaction: jest.fn(), + getTransactionReceipt: jest.fn(), + getLogs: jest.fn(), + resolveName: jest.fn(), + lookupAddress: jest.fn(), + on: jest.fn(), + once: jest.fn(), + emit: jest.fn(), + listenerCount: jest.fn(), + listeners: jest.fn(), + off: jest.fn(), + removeAllListeners: jest.fn(), + addListener: jest.fn(), + removeListener: jest.fn(), + waitForTransaction: jest.fn(), + _isProvider: true, +}); + +// Mock provider for default network tests +const mockNetworkProvider = createMockProvider(11155111, "sepolia"); + describe("wallet", () => { // increase timeout because ethers is throttling jest.setTimeout(30000); + + beforeEach(() => { + // Mock the default network provider + getSupportedNetworkMock.mockReturnValue({ + provider: () => mockNetworkProvider as any, + networkId: 11155111, + networkName: "sepolia" as any, + explorer: "https://sepolia.etherscan.io", + currency: "ETH" as any, + }); + }); + afterEach(() => { delete process.env.OA_PRIVATE_KEY; promptMock.mockRestore(); + jest.clearAllMocks(); }); it("should return the wallet when providing the key using environment variable", async () => { process.env.OA_PRIVATE_KEY = privateKey; @@ -63,4 +118,86 @@ describe("wallet", () => { ) ); }); + + describe("custom RPC URL", () => { + const customRpcUrl = "https://custom-rpc.example.com"; + const mockCustomProvider = createMockProvider(1, "custom"); + + let JsonRpcProviderSpy: jest.SpyInstance; + + beforeEach(() => { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const { ethers } = require("ethers"); + JsonRpcProviderSpy = jest.spyOn(ethers.providers, "JsonRpcProvider").mockImplementation(() => mockCustomProvider); + }); + + afterEach(() => { + JsonRpcProviderSpy.mockRestore(); + }); + + it("should use custom RPC URL when provided with private key", async () => { + const wallet = await getWalletOrSigner({ + network: "sepolia", + key: privateKey, + rpcUrl: customRpcUrl, + }); + + expect(JsonRpcProviderSpy).toHaveBeenCalledWith(customRpcUrl); + await expect(wallet.getAddress()).resolves.toStrictEqual(walletAddress); + expect(wallet.privateKey).toStrictEqual(privateKey); + }); + + it("should use custom RPC URL when provided with environment variable key", async () => { + process.env.OA_PRIVATE_KEY = privateKey; + + const wallet = await getWalletOrSigner({ + network: "sepolia", + rpcUrl: customRpcUrl, + }); + + expect(JsonRpcProviderSpy).toHaveBeenCalledWith(customRpcUrl); + await expect(wallet.getAddress()).resolves.toStrictEqual(walletAddress); + expect(wallet.privateKey).toStrictEqual(privateKey); + }); + + it("should use custom RPC URL when provided with key file", async () => { + const wallet = await getWalletOrSigner({ + network: "sepolia", + keyFile: path.resolve(__dirname, "./key.file"), + rpcUrl: customRpcUrl, + }); + + expect(JsonRpcProviderSpy).toHaveBeenCalledWith(customRpcUrl); + await expect(wallet.getAddress()).resolves.toStrictEqual(walletAddress); + expect(wallet.privateKey).toStrictEqual(privateKey); + }); + + it("should use custom RPC URL when provided with encrypted wallet", async () => { + promptMock.mockReturnValue({ password: "password123" }); + + const wallet = await getWalletOrSigner({ + network: "sepolia", + encryptedWalletPath: path.resolve(__dirname, "./wallet.json"), + rpcUrl: customRpcUrl, + progress: () => void 0, + }); + + expect(JsonRpcProviderSpy).toHaveBeenCalledWith(customRpcUrl); + await expect(wallet.getAddress()).resolves.toStrictEqual(walletAddress); + expect(wallet.privateKey).toStrictEqual(privateKey); + }); + + it("should prioritize custom RPC URL over network setting", async () => { + const wallet = await getWalletOrSigner({ + network: "mainnet", // Different network + key: privateKey, + rpcUrl: customRpcUrl, + }); + + // Should use custom RPC URL instead of mainnet provider + expect(JsonRpcProviderSpy).toHaveBeenCalledWith(customRpcUrl); + await expect(wallet.getAddress()).resolves.toStrictEqual(walletAddress); + expect(wallet.privateKey).toStrictEqual(privateKey); + }); + }); }); diff --git a/src/implementations/utils/wallet.ts b/src/implementations/utils/wallet.ts index ee28e603..d77b1737 100644 --- a/src/implementations/utils/wallet.ts +++ b/src/implementations/utils/wallet.ts @@ -6,9 +6,11 @@ import { addAddressPrefix } from "../../utils"; import { isAwsKmsSignerOption, + isRpcUrlOption, isWalletOption, NetworkOption, PrivateKeyOption, + RpcUrlOption, WalletOrSignerOption, } from "../../commands/shared"; import { readFile } from "./disk"; @@ -43,10 +45,13 @@ export const getWalletOrSigner = async ({ network, progress = defaultProgress("Decrypting Wallet"), ...options -}: WalletOrSignerOption & Partial & { progress?: (progress: number) => void }): Promise< - Wallet | ConnectedSigner -> => { - const provider = getSupportedNetwork(network ?? "mainnet").provider(); +}: WalletOrSignerOption & + Partial & + Partial & { progress?: (progress: number) => void }): Promise => { + // Use custom RPC URL if provided, otherwise use the default network provider + const provider = isRpcUrlOption(options) + ? new ethers.providers.JsonRpcProvider(options.rpcUrl) + : getSupportedNetwork(network ?? "mainnet").provider(); if (isWalletOption(options)) { const { password } = await inquirer.prompt({ type: "password", name: "password", message: "Wallet password" });