diff --git a/packages/rln/src/contract/price_calculator.spec.ts b/packages/rln/src/contract/price_calculator.spec.ts index 4f6b414664..e2fa53b1ec 100644 --- a/packages/rln/src/contract/price_calculator.spec.ts +++ b/packages/rln/src/contract/price_calculator.spec.ts @@ -1,7 +1,6 @@ import { expect, use } from "chai"; import chaiAsPromised from "chai-as-promised"; import sinon from "sinon"; -import { PublicClient } from "viem"; import { RLNBaseContract } from "./rln_base_contract.js"; @@ -9,17 +8,17 @@ use(chaiAsPromised); function createMockRLNBaseContract( mockContract: any, - mockPublicClient: PublicClient + mockRpcClient: any ): RLNBaseContract { const dummy = Object.create(RLNBaseContract.prototype); dummy.contract = mockContract; - dummy.publicClient = mockPublicClient; + dummy.rpcClient = mockRpcClient; return dummy as RLNBaseContract; } describe("RLNBaseContract.getPriceForRateLimit (unit)", function () { let mockContract: any; - let mockPublicClient: any; + let mockRpcClient: any; let priceCalculatorReadStub: sinon.SinonStub; let readContractStub: sinon.SinonStub; @@ -33,7 +32,7 @@ describe("RLNBaseContract.getPriceForRateLimit (unit)", function () { } }; - mockPublicClient = { + mockRpcClient = { readContract: readContractStub }; }); @@ -50,7 +49,7 @@ describe("RLNBaseContract.getPriceForRateLimit (unit)", function () { priceCalculatorReadStub.resolves(priceCalculatorAddress); readContractStub.resolves([fakeToken, fakePrice]); - const rlnBase = createMockRLNBaseContract(mockContract, mockPublicClient); + const rlnBase = createMockRLNBaseContract(mockContract, mockRpcClient); const result = await rlnBase.getPriceForRateLimit(20); expect(result.token).to.equal(fakeToken); @@ -72,7 +71,7 @@ describe("RLNBaseContract.getPriceForRateLimit (unit)", function () { priceCalculatorReadStub.resolves(priceCalculatorAddress); readContractStub.rejects(new Error("fail")); - const rlnBase = createMockRLNBaseContract(mockContract, mockPublicClient); + const rlnBase = createMockRLNBaseContract(mockContract, mockRpcClient); await expect(rlnBase.getPriceForRateLimit(20)).to.be.rejectedWith("fail"); expect(priceCalculatorReadStub.calledOnce).to.be.true; @@ -85,7 +84,7 @@ describe("RLNBaseContract.getPriceForRateLimit (unit)", function () { priceCalculatorReadStub.resolves(priceCalculatorAddress); readContractStub.resolves([null, null]); - const rlnBase = createMockRLNBaseContract(mockContract, mockPublicClient); + const rlnBase = createMockRLNBaseContract(mockContract, mockRpcClient); const result = await rlnBase.getPriceForRateLimit(20); expect(result.token).to.be.null; diff --git a/packages/rln/src/contract/rln_base_contract.ts b/packages/rln/src/contract/rln_base_contract.ts index 211f2fba80..1f052933cf 100644 --- a/packages/rln/src/contract/rln_base_contract.ts +++ b/packages/rln/src/contract/rln_base_contract.ts @@ -6,6 +6,7 @@ import { GetContractEventsReturnType, GetContractReturnType, type Hash, + publicActions, PublicClient, WalletClient } from "viem"; @@ -38,8 +39,7 @@ export class RLNBaseContract { typeof wakuRlnV2Abi, PublicClient | WalletClient >; - public publicClient: PublicClient; - public walletClient: WalletClient; + public rpcClient: WalletClient & PublicClient; private deployBlock: undefined | number; private rateLimit: number; private minRateLimit?: number; @@ -51,21 +51,16 @@ export class RLNBaseContract { * Private constructor for RLNBaseContract. Use static create() instead. */ protected constructor(options: RLNContractOptions) { - const { - address, - publicClient, - walletClient, - rateLimit = DEFAULT_RATE_LIMIT - } = options; + const { address, rpcClient, rateLimit = DEFAULT_RATE_LIMIT } = options; log.info("Initializing RLNBaseContract", { address, rateLimit }); - this.publicClient = publicClient; - this.walletClient = walletClient; + this.rpcClient = rpcClient.extend(publicActions) as WalletClient & + PublicClient; this.contract = getContract({ address, abi: wakuRlnV2Abi, - client: { wallet: walletClient, public: publicClient } + client: this.rpcClient }); this.rateLimit = rateLimit; @@ -334,7 +329,7 @@ export class RLNBaseContract { idCommitmentBigInt ]); - const currentBlock = await this.publicClient.getBlockNumber(); + const currentBlock = await this.rpcClient.getBlockNumber(); const [ depositAmount, @@ -379,15 +374,15 @@ export class RLNBaseContract { } public async extendMembership(idCommitmentBigInt: bigint): Promise { - if (!this.walletClient.account) { + if (!this.rpcClient.account) { throw new Error( "Failed to extendMembership: no account set in wallet client" ); } try { await this.contract.simulate.extendMemberships([[idCommitmentBigInt]], { - chain: this.walletClient.chain, - account: this.walletClient.account!.address + chain: this.rpcClient.chain, + account: (this.rpcClient as WalletClient).account!.address }); } catch (err) { throw new Error("Simulating extending membership failed: " + err); @@ -395,12 +390,12 @@ export class RLNBaseContract { const hash = await this.contract.write.extendMemberships( [[idCommitmentBigInt]], { - account: this.walletClient.account!, - chain: this.walletClient.chain + account: this.rpcClient.account!, + chain: this.rpcClient.chain } ); - await this.publicClient.waitForTransactionReceipt({ hash }); + await this.rpcClient.waitForTransactionReceipt({ hash }); return hash; } @@ -414,7 +409,7 @@ export class RLNBaseContract { ) { throw new Error("Membership is not expired or in grace period"); } - if (!this.walletClient.account) { + if (!this.rpcClient.account) { throw new Error( "Failed to eraseMembership: no account set in wallet client" ); @@ -424,8 +419,8 @@ export class RLNBaseContract { await this.contract.simulate.eraseMemberships( [[idCommitmentBigInt], eraseFromMembershipSet], { - chain: this.walletClient.chain, - account: this.walletClient.account!.address + chain: this.rpcClient.chain, + account: (this.rpcClient as WalletClient).account!.address } ); } catch (err) { @@ -435,11 +430,11 @@ export class RLNBaseContract { const hash = await this.contract.write.eraseMemberships( [[idCommitmentBigInt], eraseFromMembershipSet], { - chain: this.walletClient.chain, - account: this.walletClient.account! + chain: this.rpcClient.chain, + account: this.rpcClient.account! } ); - await this.publicClient.waitForTransactionReceipt({ hash }); + await this.rpcClient.waitForTransactionReceipt({ hash }); return hash; } @@ -455,7 +450,7 @@ export class RLNBaseContract { `Rate limit must be between ${RATE_LIMIT_PARAMS.MIN_RATE} and ${RATE_LIMIT_PARAMS.MAX_RATE}` ); } - if (!this.walletClient.account) { + if (!this.rpcClient.account) { throw new Error( "Failed to registerMembership: no account set in wallet client" ); @@ -464,8 +459,8 @@ export class RLNBaseContract { await this.contract.simulate.register( [idCommitmentBigInt, rateLimit, []], { - chain: this.walletClient.chain, - account: this.walletClient.account!.address + chain: this.rpcClient.chain, + account: (this.rpcClient as WalletClient).account!.address } ); } catch (err) { @@ -475,11 +470,11 @@ export class RLNBaseContract { const hash = await this.contract.write.register( [idCommitmentBigInt, rateLimit, []], { - chain: this.walletClient.chain, - account: this.walletClient.account! + chain: this.rpcClient.chain, + account: this.rpcClient.account! } ); - await this.publicClient.waitForTransactionReceipt({ hash }); + await this.rpcClient.waitForTransactionReceipt({ hash }); return hash; } @@ -489,25 +484,25 @@ export class RLNBaseContract { * NOTE: Funds are sent to msg.sender (the walletClient's address) */ public async withdraw(token: string): Promise { - if (!this.walletClient.account) { + if (!this.rpcClient.account) { throw new Error("Failed to withdraw: no account set in wallet client"); } try { await this.contract.simulate.withdraw([token as Address], { - chain: this.walletClient.chain, - account: this.walletClient.account!.address + chain: this.rpcClient.chain, + account: (this.rpcClient as WalletClient).account!.address }); } catch (err) { throw new Error("Error simulating withdraw: " + err); } const hash = await this.contract.write.withdraw([token as Address], { - chain: this.walletClient.chain, - account: this.walletClient.account! + chain: this.rpcClient.chain, + account: this.rpcClient.account! }); - await this.publicClient.waitForTransactionReceipt({ hash }); + await this.rpcClient.waitForTransactionReceipt({ hash }); return hash; } public async registerWithIdentity( @@ -539,23 +534,22 @@ export class RLNBaseContract { await this.contract.simulate.register( [identity.IDCommitmentBigInt, this.rateLimit, []], { - chain: this.walletClient.chain, - account: this.walletClient.account!.address + chain: this.rpcClient.chain, + account: (this.rpcClient as WalletClient).account!.address } ); const hash: Hash = await this.contract.write.register( [identity.IDCommitmentBigInt, this.rateLimit, []], { - chain: this.walletClient.chain, - account: this.walletClient.account! + chain: this.rpcClient.chain, + account: this.rpcClient.account! } ); - const txRegisterReceipt = - await this.publicClient.waitForTransactionReceipt({ - hash - }); + const txRegisterReceipt = await this.rpcClient.waitForTransactionReceipt({ + hash + }); if (txRegisterReceipt.status === "reverted") { throw new Error("Transaction failed on-chain"); @@ -709,7 +703,7 @@ export class RLNBaseContract { price: bigint | null; }> { const address = await this.contract.read.priceCalculator(); - const [token, price] = await this.publicClient.readContract({ + const [token, price] = await this.rpcClient.readContract({ address, abi: iPriceCalculatorAbi, functionName: "calculate", diff --git a/packages/rln/src/contract/types.ts b/packages/rln/src/contract/types.ts index 5553f6ddbe..e07b09bb40 100644 --- a/packages/rln/src/contract/types.ts +++ b/packages/rln/src/contract/types.ts @@ -1,4 +1,4 @@ -import { Address, PublicClient, WalletClient } from "viem"; +import { Address, WalletClient } from "viem"; export type Member = { idCommitment: string; @@ -6,8 +6,7 @@ export type Member = { }; export interface RLNContractOptions { - publicClient: PublicClient; - walletClient: WalletClient; + rpcClient: WalletClient; address: Address; rateLimit?: number; } diff --git a/packages/rln/src/credentials_manager.ts b/packages/rln/src/credentials_manager.ts index d79202f4ad..993f4f89dc 100644 --- a/packages/rln/src/credentials_manager.ts +++ b/packages/rln/src/credentials_manager.ts @@ -1,5 +1,5 @@ import { Logger } from "@waku/utils"; -import { PublicClient, WalletClient } from "viem"; +import { publicActions, PublicClient, WalletClient } from "viem"; import { RLN_CONTRACT } from "./contract/constants.js"; import { RLNBaseContract } from "./contract/rln_base_contract.js"; @@ -10,7 +10,7 @@ import type { } from "./keystore/index.js"; import { KeystoreEntity, Password } from "./keystore/types.js"; import { RegisterMembershipOptions, StartRLNOptions } from "./types.js"; -import { createViemClientsFromWindow } from "./utils/index.js"; +import { createViemClientFromWindow } from "./utils/index.js"; import { Zerokit } from "./zerokit.js"; const log = new Logger("rln:credentials"); @@ -24,8 +24,7 @@ export class RLNCredentialsManager { protected starting = false; public contract: undefined | RLNBaseContract; - public walletClient: undefined | WalletClient; - public publicClient: undefined | PublicClient; + public rpcClient: undefined | (WalletClient & PublicClient); protected keystore = Keystore.create(); public credentials: undefined | DecryptedCredentials; @@ -56,7 +55,7 @@ export class RLNCredentialsManager { log.info("Credentials successfully decrypted"); } - const { walletClient, publicClient, address, rateLimit } = + const { rpcClient, address, rateLimit } = await this.determineStartOptions(options, credentials); log.info(`Using contract address: ${address}`); @@ -67,12 +66,10 @@ export class RLNCredentialsManager { } this.credentials = credentials; - this.walletClient = walletClient!; - this.publicClient = publicClient!; + this.rpcClient = rpcClient!; this.contract = await RLNBaseContract.create({ address: address! as `0x${string}`, - publicClient: publicClient!, - walletClient: walletClient!, + rpcClient: this.rpcClient, rateLimit: rateLimit ?? this.zerokit.rateLimit }); @@ -131,9 +128,7 @@ export class RLNCredentialsManager { protected async determineStartOptions( options: StartRLNOptions, credentials: KeystoreEntity | undefined - ): Promise< - StartRLNOptions & { walletClient: WalletClient; publicClient: PublicClient } - > { + ): Promise { let chainId = credentials?.membership.chainId; const address = credentials?.membership.address || @@ -145,21 +140,16 @@ export class RLNCredentialsManager { log.info(`Using Linea contract with chainId: ${chainId}`); } - const walletClient = options.walletClient; - const publicClient = options.publicClient; - - let clients: { walletClient: WalletClient; publicClient: PublicClient }; - - if (!walletClient || !publicClient) { - clients = await createViemClientsFromWindow(); - } else { - clients = { walletClient, publicClient }; + let rpcClient: (WalletClient & PublicClient) | undefined = + options.rpcClient?.extend(publicActions) as WalletClient & PublicClient; + if (!rpcClient) { + rpcClient = await createViemClientFromWindow(); } - const currentChainId = await clients.publicClient.getChainId(); + const currentChainId = rpcClient.chain?.id; log.info(`Current chain ID: ${currentChainId}`); - if (chainId && chainId !== currentChainId.toString()) { + if (chainId && chainId !== currentChainId?.toString()) { log.error( `Chain ID mismatch: contract=${chainId}, current=${currentChainId}` ); @@ -169,8 +159,7 @@ export class RLNCredentialsManager { } return { - walletClient: clients.walletClient, - publicClient: clients.publicClient, + rpcClient, address }; } @@ -216,9 +205,9 @@ export class RLNCredentialsManager { protected async verifyCredentialsAgainstContract( credentials: KeystoreEntity ): Promise { - if (!this.contract || !this.publicClient) { + if (!this.contract || !this.rpcClient) { throw Error( - "Failed to verify chain coordinates: no contract or publicClient initialized." + "Failed to verify chain coordinates: no contract or viem client initialized." ); } @@ -231,7 +220,7 @@ export class RLNCredentialsManager { } const chainId = credentials.membership.chainId; - const currentChainId = await this.publicClient.getChainId(); + const currentChainId = await this.rpcClient.getChainId(); if (chainId !== currentChainId.toString()) { throw Error( `Failed to verify chain coordinates: credentials chainID=${chainId} is not equal to registryContract chainID=${currentChainId}` diff --git a/packages/rln/src/index.ts b/packages/rln/src/index.ts index 9f1bee0338..304b41900c 100644 --- a/packages/rln/src/index.ts +++ b/packages/rln/src/index.ts @@ -4,7 +4,7 @@ import { createRLN } from "./create.js"; import { IdentityCredential } from "./identity.js"; import { Keystore } from "./keystore/index.js"; import { RLNInstance } from "./rln.js"; -import { createViemClientsFromWindow } from "./utils/index.js"; +import { createViemClientFromWindow } from "./utils/index.js"; export { RLNBaseContract, @@ -13,7 +13,7 @@ export { RLNInstance, IdentityCredential, RLN_CONTRACT, - createViemClientsFromWindow as extractMetaMaskSigner + createViemClientFromWindow }; // Export wagmi-generated ABIs diff --git a/packages/rln/src/types.ts b/packages/rln/src/types.ts index a95dace147..20a1d1af9f 100644 --- a/packages/rln/src/types.ts +++ b/packages/rln/src/types.ts @@ -1,4 +1,4 @@ -import { PublicClient, WalletClient } from "viem"; +import { WalletClient } from "viem"; import { IdentityCredential } from "./identity.js"; import { @@ -10,8 +10,7 @@ export type StartRLNOptions = { /** * If not set - will attempt to create from injected provider. */ - walletClient?: WalletClient; - publicClient?: PublicClient; + rpcClient?: WalletClient; /** * If not set - will use default SEPOLIA_CONTRACT address. */ diff --git a/packages/rln/src/utils/index.ts b/packages/rln/src/utils/index.ts index cac46beaf2..aa6a17a0d2 100644 --- a/packages/rln/src/utils/index.ts +++ b/packages/rln/src/utils/index.ts @@ -1,4 +1,4 @@ -export { createViemClientsFromWindow } from "./walletClient.js"; +export { createViemClientFromWindow } from "./walletClient.js"; export { BytesUtils } from "./bytes.js"; export { sha256, poseidonHash } from "./hash.js"; export { dateToEpoch, epochIntToBytes, epochBytesToInt } from "./epoch.js"; diff --git a/packages/rln/src/utils/walletClient.ts b/packages/rln/src/utils/walletClient.ts index b9b3d174f5..86cb947d8e 100644 --- a/packages/rln/src/utils/walletClient.ts +++ b/packages/rln/src/utils/walletClient.ts @@ -1,15 +1,15 @@ import { - createPublicClient, createWalletClient, custom, + publicActions, PublicClient, WalletClient } from "viem"; import { type Chain, lineaSepolia } from "viem/chains"; -export const createViemClientsFromWindow = async ( +export const createViemClientFromWindow = async ( chain: Chain = lineaSepolia -): Promise<{ walletClient: WalletClient; publicClient: PublicClient }> => { +): Promise => { // eslint-disable-next-line @typescript-eslint/no-explicit-any const ethereum = (window as any).ethereum; @@ -21,16 +21,11 @@ export const createViemClientsFromWindow = async ( const [account] = await ethereum.request({ method: "eth_requestAccounts" }); - const walletClient = createWalletClient({ + const rpcClient = createWalletClient({ account, chain, transport: custom(ethereum) - }); + }).extend(publicActions); - const publicClient = createPublicClient({ - chain, - transport: custom(ethereum) - }); - - return { walletClient, publicClient }; + return rpcClient; };