From 7f7f772d9331075b57ad76eca6f803cd600c401e Mon Sep 17 00:00:00 2001 From: Danish Arora <35004822+danisharora099@users.noreply.github.com> Date: Wed, 16 Jul 2025 15:16:13 +0530 Subject: [PATCH] feat(rln): price calculator for rate limits (#2480) * chore: add ABI for PriceCalculator * chore: rename LINEA_CONTRACT to RLN_CONTRACT * chore: add price calculator & test * fix: import * chore: convert e2e test to unit * fix: test --- .../rln/src/contract/abi/price_calculator.ts | 93 +++++++++++++++++++ .../rln/src/contract/{abi.ts => abi/rln.ts} | 0 packages/rln/src/contract/constants.ts | 11 ++- .../rln/src/contract/price_calculator.spec.ts | 66 +++++++++++++ .../rln/src/contract/rln_base_contract.ts | 35 ++++++- packages/rln/src/contract/test_setup.ts | 6 +- packages/rln/src/contract/test_utils.ts | 18 ++-- packages/rln/src/credentials_manager.ts | 8 +- packages/rln/src/index.ts | 6 +- 9 files changed, 220 insertions(+), 23 deletions(-) create mode 100644 packages/rln/src/contract/abi/price_calculator.ts rename packages/rln/src/contract/{abi.ts => abi/rln.ts} (100%) create mode 100644 packages/rln/src/contract/price_calculator.spec.ts diff --git a/packages/rln/src/contract/abi/price_calculator.ts b/packages/rln/src/contract/abi/price_calculator.ts new file mode 100644 index 0000000000..8199e7764b --- /dev/null +++ b/packages/rln/src/contract/abi/price_calculator.ts @@ -0,0 +1,93 @@ +export const PRICE_CALCULATOR_ABI = [ + { + inputs: [ + { internalType: "address", name: "_token", type: "address" }, + { + internalType: "uint256", + name: "_pricePerMessagePerEpoch", + type: "uint256" + } + ], + stateMutability: "nonpayable", + type: "constructor" + }, + { inputs: [], name: "OnlyTokensAllowed", type: "error" }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "previousOwner", + type: "address" + }, + { + indexed: true, + internalType: "address", + name: "newOwner", + type: "address" + } + ], + name: "OwnershipTransferred", + type: "event" + }, + { + inputs: [{ internalType: "uint32", name: "_rateLimit", type: "uint32" }], + name: "calculate", + outputs: [ + { internalType: "address", name: "", type: "address" }, + { internalType: "uint256", name: "", type: "uint256" } + ], + stateMutability: "view", + type: "function" + }, + { + inputs: [], + name: "owner", + outputs: [{ internalType: "address", name: "", type: "address" }], + stateMutability: "view", + type: "function" + }, + { + inputs: [], + name: "pricePerMessagePerEpoch", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function" + }, + { + inputs: [], + name: "renounceOwnership", + outputs: [], + stateMutability: "nonpayable", + type: "function" + }, + { + inputs: [ + { internalType: "address", name: "_token", type: "address" }, + { + internalType: "uint256", + name: "_pricePerMessagePerEpoch", + type: "uint256" + } + ], + name: "setTokenAndPrice", + outputs: [], + stateMutability: "nonpayable", + type: "function" + }, + { + inputs: [], + name: "token", + outputs: [{ internalType: "address", name: "", type: "address" }], + stateMutability: "view", + type: "function" + }, + { + inputs: [{ internalType: "address", name: "newOwner", type: "address" }], + name: "transferOwnership", + outputs: [], + stateMutability: "nonpayable", + type: "function" + } +]; diff --git a/packages/rln/src/contract/abi.ts b/packages/rln/src/contract/abi/rln.ts similarity index 100% rename from packages/rln/src/contract/abi.ts rename to packages/rln/src/contract/abi/rln.ts diff --git a/packages/rln/src/contract/constants.ts b/packages/rln/src/contract/constants.ts index cbf71174f4..4808a88a14 100644 --- a/packages/rln/src/contract/constants.ts +++ b/packages/rln/src/contract/constants.ts @@ -1,11 +1,18 @@ -import { RLN_ABI } from "./abi.js"; +import { PRICE_CALCULATOR_ABI } from "./abi/price_calculator.js"; +import { RLN_ABI } from "./abi/rln.js"; -export const LINEA_CONTRACT = { +export const RLN_CONTRACT = { chainId: 59141, address: "0xb9cd878c90e49f797b4431fbf4fb333108cb90e6", abi: RLN_ABI }; +export const PRICE_CALCULATOR_CONTRACT = { + chainId: 59141, + address: "0xBcfC0660Df69f53ab409F32bb18A3fb625fcE644", + abi: PRICE_CALCULATOR_ABI +}; + /** * Rate limit tiers (messages per epoch) * Each membership can specify a rate limit within these bounds. diff --git a/packages/rln/src/contract/price_calculator.spec.ts b/packages/rln/src/contract/price_calculator.spec.ts new file mode 100644 index 0000000000..2b36585509 --- /dev/null +++ b/packages/rln/src/contract/price_calculator.spec.ts @@ -0,0 +1,66 @@ +import { expect, use } from "chai"; +import chaiAsPromised from "chai-as-promised"; +import { ethers } from "ethers"; +import sinon from "sinon"; + +import { RLNBaseContract } from "./rln_base_contract.js"; + +use(chaiAsPromised); + +function createMockRLNBaseContract(provider: any): RLNBaseContract { + const dummy = Object.create(RLNBaseContract.prototype); + dummy.contract = { provider }; + return dummy as RLNBaseContract; +} + +describe("RLNBaseContract.getPriceForRateLimit (unit)", function () { + let provider: any; + let calculateStub: sinon.SinonStub; + let mockContractFactory: any; + + beforeEach(() => { + provider = {}; + calculateStub = sinon.stub(); + mockContractFactory = function () { + return { calculate: calculateStub }; + }; + }); + + afterEach(() => { + sinon.restore(); + }); + + it("returns token and price for valid calculate", async () => { + const fakeToken = "0x1234567890abcdef1234567890abcdef12345678"; + const fakePrice = ethers.BigNumber.from(42); + calculateStub.resolves([fakeToken, fakePrice]); + + const rlnBase = createMockRLNBaseContract(provider); + const result = await rlnBase.getPriceForRateLimit(20, mockContractFactory); + expect(result.token).to.equal(fakeToken); + expect(result.price).to.not.be.null; + if (result.price) { + expect(result.price.eq(fakePrice)).to.be.true; + } + expect(calculateStub.calledOnceWith(20)).to.be.true; + }); + + it("throws if calculate throws", async () => { + calculateStub.rejects(new Error("fail")); + + const rlnBase = createMockRLNBaseContract(provider); + await expect( + rlnBase.getPriceForRateLimit(20, mockContractFactory) + ).to.be.rejectedWith("fail"); + expect(calculateStub.calledOnceWith(20)).to.be.true; + }); + + it("throws if calculate returns malformed data", async () => { + calculateStub.resolves([null, null]); + + const rlnBase = createMockRLNBaseContract(provider); + const result = await rlnBase.getPriceForRateLimit(20, mockContractFactory); + expect(result.token).to.be.null; + expect(result.price).to.be.null; + }); +}); diff --git a/packages/rln/src/contract/rln_base_contract.ts b/packages/rln/src/contract/rln_base_contract.ts index d4b8306cad..d6fdbf2ff5 100644 --- a/packages/rln/src/contract/rln_base_contract.ts +++ b/packages/rln/src/contract/rln_base_contract.ts @@ -5,8 +5,12 @@ import { IdentityCredential } from "../identity.js"; import { DecryptedCredentials } from "../keystore/types.js"; import { BytesUtils } from "../utils/bytes.js"; -import { RLN_ABI } from "./abi.js"; -import { DEFAULT_RATE_LIMIT, RATE_LIMIT_PARAMS } from "./constants.js"; +import { RLN_ABI } from "./abi/rln.js"; +import { + DEFAULT_RATE_LIMIT, + PRICE_CALCULATOR_CONTRACT, + RATE_LIMIT_PARAMS +} from "./constants.js"; import { CustomQueryOptions, FetchMembersOptions, @@ -770,4 +774,31 @@ export class RLNBaseContract { return false; } } + + /** + * Calculates the price for a given rate limit using the PriceCalculator contract + * @param rateLimit The rate limit to calculate the price for + * @param contractFactory Optional factory for creating the contract (for testing) + */ + public async getPriceForRateLimit( + rateLimit: number, + contractFactory?: typeof import("ethers").Contract + ): Promise<{ + token: string | null; + price: import("ethers").BigNumber | null; + }> { + const provider = this.contract.provider; + const ContractCtor = contractFactory || ethers.Contract; + const priceCalculator = new ContractCtor( + PRICE_CALCULATOR_CONTRACT.address, + PRICE_CALCULATOR_CONTRACT.abi, + provider + ); + const [token, price] = await priceCalculator.calculate(rateLimit); + // Defensive: if token or price is null/undefined, return nulls + if (!token || !price) { + return { token: null, price: null }; + } + return { token, price }; + } } diff --git a/packages/rln/src/contract/test_setup.ts b/packages/rln/src/contract/test_setup.ts index e097d780d0..b5da3f6af6 100644 --- a/packages/rln/src/contract/test_setup.ts +++ b/packages/rln/src/contract/test_setup.ts @@ -5,7 +5,7 @@ import sinon from "sinon"; import { createRLN } from "../create.js"; import type { IdentityCredential } from "../identity.js"; -import { DEFAULT_RATE_LIMIT, LINEA_CONTRACT } from "./constants.js"; +import { DEFAULT_RATE_LIMIT, RLN_CONTRACT } from "./constants.js"; import { RLNContract } from "./rln_contract.js"; export interface TestRLNInstance { @@ -42,7 +42,7 @@ export async function initializeRLNContract( mockedRegistryContract: ethers.Contract ): Promise { const provider = new ethers.providers.JsonRpcProvider(); - const voidSigner = new ethers.VoidSigner(LINEA_CONTRACT.address, provider); + const voidSigner = new ethers.VoidSigner(RLN_CONTRACT.address, provider); const originalRegister = mockedRegistryContract.register; (mockedRegistryContract as any).register = function (...args: any[]) { @@ -63,7 +63,7 @@ export async function initializeRLNContract( }; const contract = await RLNContract.init(rlnInstance, { - address: LINEA_CONTRACT.address, + address: RLN_CONTRACT.address, signer: voidSigner, rateLimit: DEFAULT_RATE_LIMIT, contract: mockedRegistryContract diff --git a/packages/rln/src/contract/test_utils.ts b/packages/rln/src/contract/test_utils.ts index 9b76999fbb..a2ac8bc403 100644 --- a/packages/rln/src/contract/test_utils.ts +++ b/packages/rln/src/contract/test_utils.ts @@ -5,7 +5,7 @@ import sinon from "sinon"; import type { IdentityCredential } from "../identity.js"; -import { DEFAULT_RATE_LIMIT, LINEA_CONTRACT } from "./constants.js"; +import { DEFAULT_RATE_LIMIT, RLN_CONTRACT } from "./constants.js"; export const mockRateLimits = { minRate: 20, @@ -36,9 +36,9 @@ export function createMockProvider(): MockProvider { export function createMockFilters(): MockFilters { return { - MembershipRegistered: () => ({ address: LINEA_CONTRACT.address }), - MembershipErased: () => ({ address: LINEA_CONTRACT.address }), - MembershipExpired: () => ({ address: LINEA_CONTRACT.address }) + MembershipRegistered: () => ({ address: RLN_CONTRACT.address }), + MembershipErased: () => ({ address: RLN_CONTRACT.address }), + MembershipExpired: () => ({ address: RLN_CONTRACT.address }) }; } @@ -51,9 +51,9 @@ export function createMockRegistryContract( overrides: ContractOverrides = {} ): ethers.Contract { const filters = { - MembershipRegistered: () => ({ address: LINEA_CONTRACT.address }), - MembershipErased: () => ({ address: LINEA_CONTRACT.address }), - MembershipExpired: () => ({ address: LINEA_CONTRACT.address }) + MembershipRegistered: () => ({ address: RLN_CONTRACT.address }), + MembershipErased: () => ({ address: RLN_CONTRACT.address }), + MembershipExpired: () => ({ address: RLN_CONTRACT.address }) }; const baseContract = { @@ -89,7 +89,7 @@ export function createMockRegistryContract( format: () => {} }) }, - address: LINEA_CONTRACT.address + address: RLN_CONTRACT.address }; // Merge overrides while preserving filters @@ -163,7 +163,7 @@ export function verifyRegistration( expect(decryptedCredentials).to.have.property("identity"); expect(decryptedCredentials).to.have.property("membership"); expect(decryptedCredentials.membership).to.include({ - address: LINEA_CONTRACT.address, + address: RLN_CONTRACT.address, treeIndex: 1 }); diff --git a/packages/rln/src/credentials_manager.ts b/packages/rln/src/credentials_manager.ts index 396f3a29ed..8eca23b466 100644 --- a/packages/rln/src/credentials_manager.ts +++ b/packages/rln/src/credentials_manager.ts @@ -3,7 +3,7 @@ import { sha256 } from "@noble/hashes/sha2"; import { Logger } from "@waku/utils"; import { ethers } from "ethers"; -import { LINEA_CONTRACT, RLN_Q } from "./contract/constants.js"; +import { RLN_CONTRACT, RLN_Q } from "./contract/constants.js"; import { RLNBaseContract } from "./contract/rln_base_contract.js"; import { IdentityCredential } from "./identity.js"; import { Keystore } from "./keystore/index.js"; @@ -152,10 +152,10 @@ export class RLNCredentialsManager { const address = credentials?.membership.address || options.address || - LINEA_CONTRACT.address; + RLN_CONTRACT.address; - if (address === LINEA_CONTRACT.address) { - chainId = LINEA_CONTRACT.chainId.toString(); + if (address === RLN_CONTRACT.address) { + chainId = RLN_CONTRACT.chainId.toString(); log.info(`Using Linea contract with chainId: ${chainId}`); } diff --git a/packages/rln/src/index.ts b/packages/rln/src/index.ts index 63ca597d15..0a07db7810 100644 --- a/packages/rln/src/index.ts +++ b/packages/rln/src/index.ts @@ -1,6 +1,6 @@ import { RLNDecoder, RLNEncoder } from "./codec.js"; -import { RLN_ABI } from "./contract/abi.js"; -import { LINEA_CONTRACT, RLNContract } from "./contract/index.js"; +import { RLN_ABI } from "./contract/abi/rln.js"; +import { RLN_CONTRACT, RLNContract } from "./contract/index.js"; import { RLNBaseContract } from "./contract/rln_base_contract.js"; import { createRLN } from "./create.js"; import { RLNCredentialsManager } from "./credentials_manager.js"; @@ -23,7 +23,7 @@ export { RLNDecoder, MerkleRootTracker, RLNContract, - LINEA_CONTRACT, + RLN_CONTRACT, extractMetaMaskSigner, RLN_ABI };