Skip to content

Commit 7f7f772

Browse files
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
1 parent 35acdf8 commit 7f7f772

File tree

9 files changed

+220
-23
lines changed

9 files changed

+220
-23
lines changed
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
export const PRICE_CALCULATOR_ABI = [
2+
{
3+
inputs: [
4+
{ internalType: "address", name: "_token", type: "address" },
5+
{
6+
internalType: "uint256",
7+
name: "_pricePerMessagePerEpoch",
8+
type: "uint256"
9+
}
10+
],
11+
stateMutability: "nonpayable",
12+
type: "constructor"
13+
},
14+
{ inputs: [], name: "OnlyTokensAllowed", type: "error" },
15+
{
16+
anonymous: false,
17+
inputs: [
18+
{
19+
indexed: true,
20+
internalType: "address",
21+
name: "previousOwner",
22+
type: "address"
23+
},
24+
{
25+
indexed: true,
26+
internalType: "address",
27+
name: "newOwner",
28+
type: "address"
29+
}
30+
],
31+
name: "OwnershipTransferred",
32+
type: "event"
33+
},
34+
{
35+
inputs: [{ internalType: "uint32", name: "_rateLimit", type: "uint32" }],
36+
name: "calculate",
37+
outputs: [
38+
{ internalType: "address", name: "", type: "address" },
39+
{ internalType: "uint256", name: "", type: "uint256" }
40+
],
41+
stateMutability: "view",
42+
type: "function"
43+
},
44+
{
45+
inputs: [],
46+
name: "owner",
47+
outputs: [{ internalType: "address", name: "", type: "address" }],
48+
stateMutability: "view",
49+
type: "function"
50+
},
51+
{
52+
inputs: [],
53+
name: "pricePerMessagePerEpoch",
54+
outputs: [{ internalType: "uint256", name: "", type: "uint256" }],
55+
stateMutability: "view",
56+
type: "function"
57+
},
58+
{
59+
inputs: [],
60+
name: "renounceOwnership",
61+
outputs: [],
62+
stateMutability: "nonpayable",
63+
type: "function"
64+
},
65+
{
66+
inputs: [
67+
{ internalType: "address", name: "_token", type: "address" },
68+
{
69+
internalType: "uint256",
70+
name: "_pricePerMessagePerEpoch",
71+
type: "uint256"
72+
}
73+
],
74+
name: "setTokenAndPrice",
75+
outputs: [],
76+
stateMutability: "nonpayable",
77+
type: "function"
78+
},
79+
{
80+
inputs: [],
81+
name: "token",
82+
outputs: [{ internalType: "address", name: "", type: "address" }],
83+
stateMutability: "view",
84+
type: "function"
85+
},
86+
{
87+
inputs: [{ internalType: "address", name: "newOwner", type: "address" }],
88+
name: "transferOwnership",
89+
outputs: [],
90+
stateMutability: "nonpayable",
91+
type: "function"
92+
}
93+
];

packages/rln/src/contract/constants.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,18 @@
1-
import { RLN_ABI } from "./abi.js";
1+
import { PRICE_CALCULATOR_ABI } from "./abi/price_calculator.js";
2+
import { RLN_ABI } from "./abi/rln.js";
23

3-
export const LINEA_CONTRACT = {
4+
export const RLN_CONTRACT = {
45
chainId: 59141,
56
address: "0xb9cd878c90e49f797b4431fbf4fb333108cb90e6",
67
abi: RLN_ABI
78
};
89

10+
export const PRICE_CALCULATOR_CONTRACT = {
11+
chainId: 59141,
12+
address: "0xBcfC0660Df69f53ab409F32bb18A3fb625fcE644",
13+
abi: PRICE_CALCULATOR_ABI
14+
};
15+
916
/**
1017
* Rate limit tiers (messages per epoch)
1118
* Each membership can specify a rate limit within these bounds.
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { expect, use } from "chai";
2+
import chaiAsPromised from "chai-as-promised";
3+
import { ethers } from "ethers";
4+
import sinon from "sinon";
5+
6+
import { RLNBaseContract } from "./rln_base_contract.js";
7+
8+
use(chaiAsPromised);
9+
10+
function createMockRLNBaseContract(provider: any): RLNBaseContract {
11+
const dummy = Object.create(RLNBaseContract.prototype);
12+
dummy.contract = { provider };
13+
return dummy as RLNBaseContract;
14+
}
15+
16+
describe("RLNBaseContract.getPriceForRateLimit (unit)", function () {
17+
let provider: any;
18+
let calculateStub: sinon.SinonStub;
19+
let mockContractFactory: any;
20+
21+
beforeEach(() => {
22+
provider = {};
23+
calculateStub = sinon.stub();
24+
mockContractFactory = function () {
25+
return { calculate: calculateStub };
26+
};
27+
});
28+
29+
afterEach(() => {
30+
sinon.restore();
31+
});
32+
33+
it("returns token and price for valid calculate", async () => {
34+
const fakeToken = "0x1234567890abcdef1234567890abcdef12345678";
35+
const fakePrice = ethers.BigNumber.from(42);
36+
calculateStub.resolves([fakeToken, fakePrice]);
37+
38+
const rlnBase = createMockRLNBaseContract(provider);
39+
const result = await rlnBase.getPriceForRateLimit(20, mockContractFactory);
40+
expect(result.token).to.equal(fakeToken);
41+
expect(result.price).to.not.be.null;
42+
if (result.price) {
43+
expect(result.price.eq(fakePrice)).to.be.true;
44+
}
45+
expect(calculateStub.calledOnceWith(20)).to.be.true;
46+
});
47+
48+
it("throws if calculate throws", async () => {
49+
calculateStub.rejects(new Error("fail"));
50+
51+
const rlnBase = createMockRLNBaseContract(provider);
52+
await expect(
53+
rlnBase.getPriceForRateLimit(20, mockContractFactory)
54+
).to.be.rejectedWith("fail");
55+
expect(calculateStub.calledOnceWith(20)).to.be.true;
56+
});
57+
58+
it("throws if calculate returns malformed data", async () => {
59+
calculateStub.resolves([null, null]);
60+
61+
const rlnBase = createMockRLNBaseContract(provider);
62+
const result = await rlnBase.getPriceForRateLimit(20, mockContractFactory);
63+
expect(result.token).to.be.null;
64+
expect(result.price).to.be.null;
65+
});
66+
});

packages/rln/src/contract/rln_base_contract.ts

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,12 @@ import { IdentityCredential } from "../identity.js";
55
import { DecryptedCredentials } from "../keystore/types.js";
66
import { BytesUtils } from "../utils/bytes.js";
77

8-
import { RLN_ABI } from "./abi.js";
9-
import { DEFAULT_RATE_LIMIT, RATE_LIMIT_PARAMS } from "./constants.js";
8+
import { RLN_ABI } from "./abi/rln.js";
9+
import {
10+
DEFAULT_RATE_LIMIT,
11+
PRICE_CALCULATOR_CONTRACT,
12+
RATE_LIMIT_PARAMS
13+
} from "./constants.js";
1014
import {
1115
CustomQueryOptions,
1216
FetchMembersOptions,
@@ -770,4 +774,31 @@ export class RLNBaseContract {
770774
return false;
771775
}
772776
}
777+
778+
/**
779+
* Calculates the price for a given rate limit using the PriceCalculator contract
780+
* @param rateLimit The rate limit to calculate the price for
781+
* @param contractFactory Optional factory for creating the contract (for testing)
782+
*/
783+
public async getPriceForRateLimit(
784+
rateLimit: number,
785+
contractFactory?: typeof import("ethers").Contract
786+
): Promise<{
787+
token: string | null;
788+
price: import("ethers").BigNumber | null;
789+
}> {
790+
const provider = this.contract.provider;
791+
const ContractCtor = contractFactory || ethers.Contract;
792+
const priceCalculator = new ContractCtor(
793+
PRICE_CALCULATOR_CONTRACT.address,
794+
PRICE_CALCULATOR_CONTRACT.abi,
795+
provider
796+
);
797+
const [token, price] = await priceCalculator.calculate(rateLimit);
798+
// Defensive: if token or price is null/undefined, return nulls
799+
if (!token || !price) {
800+
return { token: null, price: null };
801+
}
802+
return { token, price };
803+
}
773804
}

packages/rln/src/contract/test_setup.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import sinon from "sinon";
55
import { createRLN } from "../create.js";
66
import type { IdentityCredential } from "../identity.js";
77

8-
import { DEFAULT_RATE_LIMIT, LINEA_CONTRACT } from "./constants.js";
8+
import { DEFAULT_RATE_LIMIT, RLN_CONTRACT } from "./constants.js";
99
import { RLNContract } from "./rln_contract.js";
1010

1111
export interface TestRLNInstance {
@@ -42,7 +42,7 @@ export async function initializeRLNContract(
4242
mockedRegistryContract: ethers.Contract
4343
): Promise<RLNContract> {
4444
const provider = new ethers.providers.JsonRpcProvider();
45-
const voidSigner = new ethers.VoidSigner(LINEA_CONTRACT.address, provider);
45+
const voidSigner = new ethers.VoidSigner(RLN_CONTRACT.address, provider);
4646

4747
const originalRegister = mockedRegistryContract.register;
4848
(mockedRegistryContract as any).register = function (...args: any[]) {
@@ -63,7 +63,7 @@ export async function initializeRLNContract(
6363
};
6464

6565
const contract = await RLNContract.init(rlnInstance, {
66-
address: LINEA_CONTRACT.address,
66+
address: RLN_CONTRACT.address,
6767
signer: voidSigner,
6868
rateLimit: DEFAULT_RATE_LIMIT,
6969
contract: mockedRegistryContract

packages/rln/src/contract/test_utils.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import sinon from "sinon";
55

66
import type { IdentityCredential } from "../identity.js";
77

8-
import { DEFAULT_RATE_LIMIT, LINEA_CONTRACT } from "./constants.js";
8+
import { DEFAULT_RATE_LIMIT, RLN_CONTRACT } from "./constants.js";
99

1010
export const mockRateLimits = {
1111
minRate: 20,
@@ -36,9 +36,9 @@ export function createMockProvider(): MockProvider {
3636

3737
export function createMockFilters(): MockFilters {
3838
return {
39-
MembershipRegistered: () => ({ address: LINEA_CONTRACT.address }),
40-
MembershipErased: () => ({ address: LINEA_CONTRACT.address }),
41-
MembershipExpired: () => ({ address: LINEA_CONTRACT.address })
39+
MembershipRegistered: () => ({ address: RLN_CONTRACT.address }),
40+
MembershipErased: () => ({ address: RLN_CONTRACT.address }),
41+
MembershipExpired: () => ({ address: RLN_CONTRACT.address })
4242
};
4343
}
4444

@@ -51,9 +51,9 @@ export function createMockRegistryContract(
5151
overrides: ContractOverrides = {}
5252
): ethers.Contract {
5353
const filters = {
54-
MembershipRegistered: () => ({ address: LINEA_CONTRACT.address }),
55-
MembershipErased: () => ({ address: LINEA_CONTRACT.address }),
56-
MembershipExpired: () => ({ address: LINEA_CONTRACT.address })
54+
MembershipRegistered: () => ({ address: RLN_CONTRACT.address }),
55+
MembershipErased: () => ({ address: RLN_CONTRACT.address }),
56+
MembershipExpired: () => ({ address: RLN_CONTRACT.address })
5757
};
5858

5959
const baseContract = {
@@ -89,7 +89,7 @@ export function createMockRegistryContract(
8989
format: () => {}
9090
})
9191
},
92-
address: LINEA_CONTRACT.address
92+
address: RLN_CONTRACT.address
9393
};
9494

9595
// Merge overrides while preserving filters
@@ -163,7 +163,7 @@ export function verifyRegistration(
163163
expect(decryptedCredentials).to.have.property("identity");
164164
expect(decryptedCredentials).to.have.property("membership");
165165
expect(decryptedCredentials.membership).to.include({
166-
address: LINEA_CONTRACT.address,
166+
address: RLN_CONTRACT.address,
167167
treeIndex: 1
168168
});
169169

packages/rln/src/credentials_manager.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { sha256 } from "@noble/hashes/sha2";
33
import { Logger } from "@waku/utils";
44
import { ethers } from "ethers";
55

6-
import { LINEA_CONTRACT, RLN_Q } from "./contract/constants.js";
6+
import { RLN_CONTRACT, RLN_Q } from "./contract/constants.js";
77
import { RLNBaseContract } from "./contract/rln_base_contract.js";
88
import { IdentityCredential } from "./identity.js";
99
import { Keystore } from "./keystore/index.js";
@@ -152,10 +152,10 @@ export class RLNCredentialsManager {
152152
const address =
153153
credentials?.membership.address ||
154154
options.address ||
155-
LINEA_CONTRACT.address;
155+
RLN_CONTRACT.address;
156156

157-
if (address === LINEA_CONTRACT.address) {
158-
chainId = LINEA_CONTRACT.chainId.toString();
157+
if (address === RLN_CONTRACT.address) {
158+
chainId = RLN_CONTRACT.chainId.toString();
159159
log.info(`Using Linea contract with chainId: ${chainId}`);
160160
}
161161

packages/rln/src/index.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { RLNDecoder, RLNEncoder } from "./codec.js";
2-
import { RLN_ABI } from "./contract/abi.js";
3-
import { LINEA_CONTRACT, RLNContract } from "./contract/index.js";
2+
import { RLN_ABI } from "./contract/abi/rln.js";
3+
import { RLN_CONTRACT, RLNContract } from "./contract/index.js";
44
import { RLNBaseContract } from "./contract/rln_base_contract.js";
55
import { createRLN } from "./create.js";
66
import { RLNCredentialsManager } from "./credentials_manager.js";
@@ -23,7 +23,7 @@ export {
2323
RLNDecoder,
2424
MerkleRootTracker,
2525
RLNContract,
26-
LINEA_CONTRACT,
26+
RLN_CONTRACT,
2727
extractMetaMaskSigner,
2828
RLN_ABI
2929
};

0 commit comments

Comments
 (0)