fix: refactor to use a single viem client object

This commit is contained in:
Arseniy Klempner 2025-11-11 17:23:42 -08:00
parent 64aef7b2a2
commit ad24c6c787
No known key found for this signature in database
GPG Key ID: 51653F18863BD24B
8 changed files with 76 additions and 101 deletions

View File

@ -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;

View File

@ -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<Hash> {
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<Hash> {
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",

View File

@ -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;
}

View File

@ -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<StartRLNOptions & { rpcClient: WalletClient & PublicClient }> {
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<void> {
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}`

View File

@ -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

View File

@ -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.
*/

View File

@ -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";

View File

@ -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<WalletClient & PublicClient> => {
// 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;
};