mirror of
https://github.com/logos-messaging/js-waku.git
synced 2026-01-02 13:53:12 +00:00
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
This commit is contained in:
parent
35acdf8fa5
commit
7f7f772d93
93
packages/rln/src/contract/abi/price_calculator.ts
Normal file
93
packages/rln/src/contract/abi/price_calculator.ts
Normal file
@ -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"
|
||||
}
|
||||
];
|
||||
@ -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.
|
||||
|
||||
66
packages/rln/src/contract/price_calculator.spec.ts
Normal file
66
packages/rln/src/contract/price_calculator.spec.ts
Normal file
@ -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;
|
||||
});
|
||||
});
|
||||
@ -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 };
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<RLNContract> {
|
||||
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
|
||||
|
||||
@ -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
|
||||
});
|
||||
|
||||
|
||||
@ -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}`);
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
};
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user