feat(rln)!: generate contract types, migrate from ethers to viem (#2705)

* feat: use wagmi to generate contract types

* feat: migrate rln from ethers to viem

* fix: remove .gitmodules

* fix: update readme

* fix: refactor to use a single viem client object

* fix: update comments, tsconfig

* feat: remove membership event tracking

* fix: script name in package.json and readme

* fix: only allow linea sepolia

* fix: consolidate viem types, typed window

* fix: use viem to infer type of decoded event

* fix: use js for generate abi script

* feat: generate abi and build rln package as release condition

* fix: check that eth_requestAccounts returns an array

* fix: handle error messages

* fix: use https instead of git for cloning in script

* fix: add warning annotations for contract typings check

* fix: install deps for rln package before building

* fix: use pnpm when installing rln contracts

* fix: use workspace flag to run abi script

* fix: add ref to checkout action

* fix: include pnpm in ci
This commit is contained in:
Arseniy Klempner 2025-12-01 17:32:35 -08:00 committed by GitHub
parent 788f7e62c5
commit f2ad23ad43
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 2169 additions and 2158 deletions

View File

@ -104,6 +104,7 @@
"reactjs", "reactjs",
"recid", "recid",
"rlnrelay", "rlnrelay",
"rlnv",
"roadmap", "roadmap",
"sandboxed", "sandboxed",
"scanf", "scanf",
@ -132,7 +133,9 @@
"upgrader", "upgrader",
"vacp", "vacp",
"varint", "varint",
"viem",
"vkey", "vkey",
"wagmi",
"waku", "waku",
"wakuconnect", "wakuconnect",
"wakunode", "wakunode",

View File

@ -113,12 +113,44 @@ jobs:
node-version: ${{ env.NODE_JS }} node-version: ${{ env.NODE_JS }}
registry-url: "https://registry.npmjs.org" registry-url: "https://registry.npmjs.org"
- uses: pnpm/action-setup@v4
if: ${{ steps.release.outputs.releases_created }}
with:
version: 9
- run: npm install - run: npm install
if: ${{ steps.release.outputs.releases_created }} if: ${{ steps.release.outputs.releases_created }}
- run: npm run build - run: npm run build
if: ${{ steps.release.outputs.releases_created }} if: ${{ steps.release.outputs.releases_created }}
- name: Setup Foundry
if: ${{ steps.release.outputs.releases_created }}
uses: foundry-rs/foundry-toolchain@v1
with:
version: nightly
- name: Generate RLN contract ABIs
id: rln-abi
if: ${{ steps.release.outputs.releases_created }}
run: |
npm run setup:contract-abi -w @waku/rln || {
echo "::warning::Failed to generate contract ABIs, marking @waku/rln as private to skip publishing"
cd packages/rln
node -e "const fs = require('fs'); const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8')); pkg.private = true; fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2));"
echo "failed=true" >> $GITHUB_OUTPUT
}
- name: Rebuild with new ABIs
if: ${{ steps.release.outputs.releases_created && steps.rln-abi.outputs.failed != 'true' }}
run: |
npm install -w packages/rln
npm run build -w @waku/rln || {
echo "::warning::Failed to build @waku/rln, marking as private to skip publishing"
cd packages/rln
node -e "const fs = require('fs'); const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8')); pkg.private = true; fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2));"
}
- run: npm run publish - run: npm run publish
if: ${{ steps.release.outputs.releases_created }} if: ${{ steps.release.outputs.releases_created }}
env: env:

View File

@ -17,16 +17,46 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:
repository: waku-org/js-waku repository: waku-org/js-waku
ref: ${{ github.ref }}
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version: ${{ env.NODE_JS }} node-version: ${{ env.NODE_JS }}
registry-url: "https://registry.npmjs.org" registry-url: "https://registry.npmjs.org"
- uses: pnpm/action-setup@v4
with:
version: 9
- run: npm install - run: npm install
- run: npm run build - run: npm run build
- name: Setup Foundry
uses: foundry-rs/foundry-toolchain@v1
with:
version: nightly
- name: Generate RLN contract ABIs
id: rln-abi
run: |
npm run setup:contract-abi -w @waku/rln || {
echo "::warning::Failed to generate contract ABIs, marking @waku/rln as private to skip publishing"
cd packages/rln
node -e "const fs = require('fs'); const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8')); pkg.private = true; fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2));"
echo "failed=true" >> $GITHUB_OUTPUT
}
- name: Rebuild with new ABIs
if: steps.rln-abi.outputs.failed != 'true'
run: |
npm install -w packages/rln
npm run build -w @waku/rln || {
echo "::warning::Failed to build @waku/rln, marking as private to skip publishing"
cd packages/rln
node -e "const fs = require('fs'); const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8')); pkg.private = true; fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2));"
}
- run: npm run publish -- --tag next - run: npm run publish -- --tag next
env: env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_JS_WAKU_PUBLISH }} NODE_AUTH_TOKEN: ${{ secrets.NPM_JS_WAKU_PUBLISH }}

1
.gitignore vendored
View File

@ -20,3 +20,4 @@ packages/discovery/mock_local_storage
CLAUDE.md CLAUDE.md
.env .env
postgres-data/ postgres-data/
packages/rln/waku-rlnv2-contract/

1373
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -3,5 +3,10 @@ module.exports = {
tsconfigRootDir: __dirname, tsconfigRootDir: __dirname,
project: "./tsconfig.dev.json" project: "./tsconfig.dev.json"
}, },
ignorePatterns: ["src/resources/**/*"] ignorePatterns: ["src/resources/**/*"],
overrides: [
{
files: ["*.config.ts", "*.config.js"]
}
]
}; };

View File

@ -12,6 +12,18 @@ This package provides RLN functionality for the Waku protocol, enabling rate-lim
npm install @waku/rln npm install @waku/rln
``` ```
## Smart Contract Type Generation
We use `wagmi` to generate TypeScript bindings for interacting with the RLN smart contracts.
When changes are pushed to the `waku-rlnv2-contract` repository, run the following script to fetch and build the latest contracts and generate the TypeScript bindings:
```
npm run setup:contract-abi
```
Note that we commit/bundle the generated typings, so it's not necessary to run this script unless the contracts are updated.
## Usage ## Usage
```typescript ```typescript
@ -20,11 +32,6 @@ import { RLN } from '@waku/rln';
// Usage examples coming soon // Usage examples coming soon
``` ```
## Constants
- Implementation contract: 0xde2260ca49300357d5af4153cda0d18f7b3ea9b3
- Proxy contract: 0xb9cd878c90e49f797b4431fbf4fb333108cb90e6
## License ## License
MIT OR Apache-2.0 MIT OR Apache-2.0

View File

@ -0,0 +1,66 @@
import { execSync } from "child_process";
import { existsSync, rmSync } from "fs";
import { dirname, join } from "path";
import process from "process";
import { fileURLToPath } from "url";
// Get script directory (equivalent to BASH_SOURCE in bash)
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const CONTRACT_DIR = join(__dirname, "waku-rlnv2-contract");
const REPO_URL = "https://github.com/waku-org/waku-rlnv2-contract.git";
/**
* Execute a shell command and print output in real-time
* @param {string} command - The command to execute
* @param {object} options - Options for execSync
*/
function exec(command, options = {}) {
execSync(command, {
stdio: "inherit",
cwd: options.cwd || __dirname,
...options
});
}
async function main() {
try {
console.log("📦 Setting up waku-rlnv2-contract...");
// Remove existing directory if it exists
if (existsSync(CONTRACT_DIR)) {
console.log("🗑️ Removing existing waku-rlnv2-contract directory...");
rmSync(CONTRACT_DIR, { recursive: true, force: true });
}
// Clone the repository
console.log("📥 Cloning waku-rlnv2-contract...");
exec(`git clone ${REPO_URL} ${CONTRACT_DIR}`);
// Install dependencies
console.log("📦 Installing dependencies...");
exec("pnpm i", { cwd: CONTRACT_DIR });
// Build contracts with Foundry
console.log("🔨 Building contracts with Foundry...");
exec("forge build", { cwd: CONTRACT_DIR });
// Generate ABIs with wagmi
console.log("⚙️ Generating ABIs with wagmi...");
exec("npx wagmi generate");
console.log("✅ Contract ABIs generated successfully!");
} catch (error) {
console.log(
"❌ Error generating contract ABIs:",
error instanceof Error ? error.message : error
);
process.exit(1);
}
}
main().catch((error) => {
console.log(error);
process.exit(1);
});

View File

@ -43,7 +43,8 @@
"watch:build": "tsc -p tsconfig.json -w", "watch:build": "tsc -p tsconfig.json -w",
"watch:test": "mocha --watch", "watch:test": "mocha --watch",
"prepublish": "npm run build", "prepublish": "npm run build",
"reset-hard": "git clean -dfx -e .idea && git reset --hard && npm i && npm run build" "reset-hard": "git clean -dfx -e .idea && git reset --hard && npm i && npm run build",
"setup:contract-abi": "node generate_contract_abi.js"
}, },
"engines": { "engines": {
"node": ">=22" "node": ">=22"
@ -54,12 +55,13 @@
"@rollup/plugin-node-resolve": "^15.2.3", "@rollup/plugin-node-resolve": "^15.2.3",
"@types/chai": "^5.0.1", "@types/chai": "^5.0.1",
"@types/chai-spies": "^1.0.6", "@types/chai-spies": "^1.0.6",
"@waku/interfaces": "0.0.34",
"@types/deep-equal-in-any-order": "^1.0.4", "@types/deep-equal-in-any-order": "^1.0.4",
"@types/lodash": "^4.17.15", "@types/lodash": "^4.17.15",
"@types/sinon": "^17.0.3", "@types/sinon": "^17.0.3",
"@wagmi/cli": "^2.7.0",
"@waku/build-utils": "^1.0.0", "@waku/build-utils": "^1.0.0",
"@waku/message-encryption": "^0.0.38", "@waku/interfaces": "0.0.34",
"@waku/message-encryption": "^0.0.37",
"deep-equal-in-any-order": "^2.0.6", "deep-equal-in-any-order": "^2.0.6",
"fast-check": "^3.23.2", "fast-check": "^3.23.2",
"rollup-plugin-copy": "^3.5.0" "rollup-plugin-copy": "^3.5.0"
@ -76,18 +78,19 @@
], ],
"dependencies": { "dependencies": {
"@chainsafe/bls-keystore": "3.0.0", "@chainsafe/bls-keystore": "3.0.0",
"@noble/hashes": "^1.2.0",
"@wagmi/core": "^2.22.1",
"@waku/core": "^0.0.40", "@waku/core": "^0.0.40",
"@waku/utils": "^0.0.27", "@waku/utils": "^0.0.27",
"@noble/hashes": "^1.2.0",
"@waku/zerokit-rln-wasm": "^0.2.1", "@waku/zerokit-rln-wasm": "^0.2.1",
"ethereum-cryptography": "^3.1.0",
"ethers": "^5.7.2",
"lodash": "^4.17.21",
"uuid": "^11.0.5",
"chai": "^5.1.2", "chai": "^5.1.2",
"chai-as-promised": "^8.0.1", "chai-as-promised": "^8.0.1",
"chai-spies": "^1.1.0", "chai-spies": "^1.1.0",
"chai-subset": "^1.6.0", "chai-subset": "^1.6.0",
"sinon": "^19.0.2" "ethereum-cryptography": "^3.1.0",
"lodash": "^4.17.21",
"sinon": "^19.0.2",
"uuid": "^11.0.5",
"viem": "^2.38.4"
} }
} }

View File

@ -1,93 +0,0 @@
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"
}
];

View File

@ -1,646 +0,0 @@
export const RLN_ABI = [
{ inputs: [], stateMutability: "nonpayable", type: "constructor" },
{
inputs: [
{ internalType: "uint256", name: "idCommitment", type: "uint256" }
],
name: "CannotEraseActiveMembership",
type: "error"
},
{ inputs: [], name: "CannotExceedMaxTotalRateLimit", type: "error" },
{
inputs: [
{ internalType: "uint256", name: "idCommitment", type: "uint256" }
],
name: "CannotExtendNonGracePeriodMembership",
type: "error"
},
{
inputs: [
{ internalType: "uint256", name: "idCommitment", type: "uint256" }
],
name: "InvalidIdCommitment",
type: "error"
},
{ inputs: [], name: "InvalidMembershipRateLimit", type: "error" },
{
inputs: [
{ internalType: "uint256", name: "startIndex", type: "uint256" },
{ internalType: "uint256", name: "endIndex", type: "uint256" }
],
name: "InvalidPaginationQuery",
type: "error"
},
{
inputs: [
{ internalType: "uint256", name: "idCommitment", type: "uint256" }
],
name: "MembershipDoesNotExist",
type: "error"
},
{
inputs: [
{ internalType: "uint256", name: "idCommitment", type: "uint256" }
],
name: "NonHolderCannotEraseGracePeriodMembership",
type: "error"
},
{
inputs: [
{ internalType: "uint256", name: "idCommitment", type: "uint256" }
],
name: "NonHolderCannotExtend",
type: "error"
},
{
anonymous: false,
inputs: [
{
indexed: false,
internalType: "address",
name: "previousAdmin",
type: "address"
},
{
indexed: false,
internalType: "address",
name: "newAdmin",
type: "address"
}
],
name: "AdminChanged",
type: "event"
},
{
anonymous: false,
inputs: [
{
indexed: true,
internalType: "address",
name: "beacon",
type: "address"
}
],
name: "BeaconUpgraded",
type: "event"
},
{
anonymous: false,
inputs: [
{ indexed: false, internalType: "uint8", name: "version", type: "uint8" }
],
name: "Initialized",
type: "event"
},
{
anonymous: false,
inputs: [
{
indexed: false,
internalType: "uint256",
name: "idCommitment",
type: "uint256"
},
{
indexed: false,
internalType: "uint32",
name: "membershipRateLimit",
type: "uint32"
},
{ indexed: false, internalType: "uint32", name: "index", type: "uint32" }
],
name: "MembershipErased",
type: "event"
},
{
anonymous: false,
inputs: [
{
indexed: false,
internalType: "uint256",
name: "idCommitment",
type: "uint256"
},
{
indexed: false,
internalType: "uint32",
name: "membershipRateLimit",
type: "uint32"
},
{ indexed: false, internalType: "uint32", name: "index", type: "uint32" }
],
name: "MembershipExpired",
type: "event"
},
{
anonymous: false,
inputs: [
{
indexed: false,
internalType: "uint256",
name: "idCommitment",
type: "uint256"
},
{
indexed: false,
internalType: "uint32",
name: "membershipRateLimit",
type: "uint32"
},
{ indexed: false, internalType: "uint32", name: "index", type: "uint32" },
{
indexed: false,
internalType: "uint256",
name: "newGracePeriodStartTimestamp",
type: "uint256"
}
],
name: "MembershipExtended",
type: "event"
},
{
anonymous: false,
inputs: [
{
indexed: false,
internalType: "uint256",
name: "idCommitment",
type: "uint256"
},
{
indexed: false,
internalType: "uint256",
name: "membershipRateLimit",
type: "uint256"
},
{ indexed: false, internalType: "uint32", name: "index", type: "uint32" }
],
name: "MembershipRegistered",
type: "event"
},
{
anonymous: false,
inputs: [
{
indexed: true,
internalType: "address",
name: "previousOwner",
type: "address"
},
{
indexed: true,
internalType: "address",
name: "newOwner",
type: "address"
}
],
name: "OwnershipTransferred",
type: "event"
},
{
anonymous: false,
inputs: [
{
indexed: true,
internalType: "address",
name: "implementation",
type: "address"
}
],
name: "Upgraded",
type: "event"
},
{
inputs: [],
name: "MAX_MEMBERSHIP_SET_SIZE",
outputs: [{ internalType: "uint32", name: "", type: "uint32" }],
stateMutability: "view",
type: "function"
},
{
inputs: [],
name: "MERKLE_TREE_DEPTH",
outputs: [{ internalType: "uint8", name: "", type: "uint8" }],
stateMutability: "view",
type: "function"
},
{
inputs: [],
name: "Q",
outputs: [{ internalType: "uint256", name: "", type: "uint256" }],
stateMutability: "view",
type: "function"
},
{
inputs: [],
name: "activeDurationForNewMemberships",
outputs: [{ internalType: "uint32", name: "", type: "uint32" }],
stateMutability: "view",
type: "function"
},
{
inputs: [],
name: "currentTotalRateLimit",
outputs: [{ internalType: "uint256", name: "", type: "uint256" }],
stateMutability: "view",
type: "function"
},
{
inputs: [],
name: "deployedBlockNumber",
outputs: [{ internalType: "uint32", name: "", type: "uint32" }],
stateMutability: "view",
type: "function"
},
{
inputs: [
{ internalType: "address", name: "holder", type: "address" },
{ internalType: "address", name: "token", type: "address" }
],
name: "depositsToWithdraw",
outputs: [{ internalType: "uint256", name: "balance", type: "uint256" }],
stateMutability: "view",
type: "function"
},
{
inputs: [
{ internalType: "uint256[]", name: "idCommitments", type: "uint256[]" }
],
name: "eraseMemberships",
outputs: [],
stateMutability: "nonpayable",
type: "function"
},
{
inputs: [
{ internalType: "uint256[]", name: "idCommitments", type: "uint256[]" },
{ internalType: "bool", name: "eraseFromMembershipSet", type: "bool" }
],
name: "eraseMemberships",
outputs: [],
stateMutability: "nonpayable",
type: "function"
},
{
inputs: [
{ internalType: "uint256[]", name: "idCommitments", type: "uint256[]" }
],
name: "extendMemberships",
outputs: [],
stateMutability: "nonpayable",
type: "function"
},
{
inputs: [
{ internalType: "uint256", name: "idCommitment", type: "uint256" }
],
name: "getMembershipInfo",
outputs: [
{ internalType: "uint32", name: "", type: "uint32" },
{ internalType: "uint32", name: "", type: "uint32" },
{ internalType: "uint256", name: "", type: "uint256" }
],
stateMutability: "view",
type: "function"
},
{
inputs: [{ internalType: "uint40", name: "index", type: "uint40" }],
name: "getMerkleProof",
outputs: [{ internalType: "uint256[20]", name: "", type: "uint256[20]" }],
stateMutability: "view",
type: "function"
},
{
inputs: [
{ internalType: "uint32", name: "startIndex", type: "uint32" },
{ internalType: "uint32", name: "endIndex", type: "uint32" }
],
name: "getRateCommitmentsInRangeBoundsInclusive",
outputs: [{ internalType: "uint256[]", name: "", type: "uint256[]" }],
stateMutability: "view",
type: "function"
},
{
inputs: [],
name: "gracePeriodDurationForNewMemberships",
outputs: [{ internalType: "uint32", name: "", type: "uint32" }],
stateMutability: "view",
type: "function"
},
{
inputs: [{ internalType: "uint256", name: "", type: "uint256" }],
name: "indicesOfLazilyErasedMemberships",
outputs: [{ internalType: "uint32", name: "", type: "uint32" }],
stateMutability: "view",
type: "function"
},
{
inputs: [
{ internalType: "address", name: "_priceCalculator", type: "address" },
{ internalType: "uint32", name: "_maxTotalRateLimit", type: "uint32" },
{
internalType: "uint32",
name: "_minMembershipRateLimit",
type: "uint32"
},
{
internalType: "uint32",
name: "_maxMembershipRateLimit",
type: "uint32"
},
{ internalType: "uint32", name: "_activeDuration", type: "uint32" },
{ internalType: "uint32", name: "_gracePeriod", type: "uint32" }
],
name: "initialize",
outputs: [],
stateMutability: "nonpayable",
type: "function"
},
{
inputs: [
{ internalType: "uint256", name: "_idCommitment", type: "uint256" }
],
name: "isExpired",
outputs: [{ internalType: "bool", name: "", type: "bool" }],
stateMutability: "view",
type: "function"
},
{
inputs: [
{ internalType: "uint256", name: "_idCommitment", type: "uint256" }
],
name: "isInGracePeriod",
outputs: [{ internalType: "bool", name: "", type: "bool" }],
stateMutability: "view",
type: "function"
},
{
inputs: [
{ internalType: "uint256", name: "idCommitment", type: "uint256" }
],
name: "isInMembershipSet",
outputs: [{ internalType: "bool", name: "", type: "bool" }],
stateMutability: "view",
type: "function"
},
{
inputs: [
{ internalType: "uint256", name: "idCommitment", type: "uint256" }
],
name: "isValidIdCommitment",
outputs: [{ internalType: "bool", name: "", type: "bool" }],
stateMutability: "pure",
type: "function"
},
{
inputs: [{ internalType: "uint32", name: "rateLimit", type: "uint32" }],
name: "isValidMembershipRateLimit",
outputs: [{ internalType: "bool", name: "", type: "bool" }],
stateMutability: "view",
type: "function"
},
{
inputs: [],
name: "maxMembershipRateLimit",
outputs: [{ internalType: "uint32", name: "", type: "uint32" }],
stateMutability: "view",
type: "function"
},
{
inputs: [],
name: "maxTotalRateLimit",
outputs: [{ internalType: "uint32", name: "", type: "uint32" }],
stateMutability: "view",
type: "function"
},
{
inputs: [
{ internalType: "uint256", name: "_idCommitment", type: "uint256" }
],
name: "membershipExpirationTimestamp",
outputs: [{ internalType: "uint256", name: "", type: "uint256" }],
stateMutability: "view",
type: "function"
},
{
inputs: [
{ internalType: "uint256", name: "idCommitment", type: "uint256" }
],
name: "memberships",
outputs: [
{ internalType: "uint256", name: "depositAmount", type: "uint256" },
{ internalType: "uint32", name: "activeDuration", type: "uint32" },
{
internalType: "uint256",
name: "gracePeriodStartTimestamp",
type: "uint256"
},
{ internalType: "uint32", name: "gracePeriodDuration", type: "uint32" },
{ internalType: "uint32", name: "rateLimit", type: "uint32" },
{ internalType: "uint32", name: "index", type: "uint32" },
{ internalType: "address", name: "holder", type: "address" },
{ internalType: "address", name: "token", type: "address" }
],
stateMutability: "view",
type: "function"
},
{
inputs: [],
name: "merkleTree",
outputs: [
{ internalType: "uint40", name: "maxIndex", type: "uint40" },
{ internalType: "uint40", name: "numberOfLeaves", type: "uint40" }
],
stateMutability: "view",
type: "function"
},
{
inputs: [],
name: "minMembershipRateLimit",
outputs: [{ internalType: "uint32", name: "", type: "uint32" }],
stateMutability: "view",
type: "function"
},
{
inputs: [],
name: "nextFreeIndex",
outputs: [{ internalType: "uint32", name: "", type: "uint32" }],
stateMutability: "view",
type: "function"
},
{
inputs: [],
name: "owner",
outputs: [{ internalType: "address", name: "", type: "address" }],
stateMutability: "view",
type: "function"
},
{
inputs: [],
name: "priceCalculator",
outputs: [
{ internalType: "contract IPriceCalculator", name: "", type: "address" }
],
stateMutability: "view",
type: "function"
},
{
inputs: [],
name: "proxiableUUID",
outputs: [{ internalType: "bytes32", name: "", type: "bytes32" }],
stateMutability: "view",
type: "function"
},
{
inputs: [
{ internalType: "uint256", name: "idCommitment", type: "uint256" },
{ internalType: "uint32", name: "rateLimit", type: "uint32" },
{
internalType: "uint256[]",
name: "idCommitmentsToErase",
type: "uint256[]"
}
],
name: "register",
outputs: [],
stateMutability: "nonpayable",
type: "function"
},
{
inputs: [
{ internalType: "address", name: "owner", type: "address" },
{ internalType: "uint256", name: "deadline", type: "uint256" },
{ internalType: "uint8", name: "v", type: "uint8" },
{ internalType: "bytes32", name: "r", type: "bytes32" },
{ internalType: "bytes32", name: "s", type: "bytes32" },
{ internalType: "uint256", name: "idCommitment", type: "uint256" },
{ internalType: "uint32", name: "rateLimit", type: "uint32" },
{
internalType: "uint256[]",
name: "idCommitmentsToErase",
type: "uint256[]"
}
],
name: "registerWithPermit",
outputs: [],
stateMutability: "nonpayable",
type: "function"
},
{
inputs: [],
name: "renounceOwnership",
outputs: [],
stateMutability: "nonpayable",
type: "function"
},
{
inputs: [],
name: "root",
outputs: [{ internalType: "uint256", name: "", type: "uint256" }],
stateMutability: "view",
type: "function"
},
{
inputs: [
{
internalType: "uint32",
name: "_activeDurationForNewMembership",
type: "uint32"
}
],
name: "setActiveDuration",
outputs: [],
stateMutability: "nonpayable",
type: "function"
},
{
inputs: [
{
internalType: "uint32",
name: "_gracePeriodDurationForNewMembership",
type: "uint32"
}
],
name: "setGracePeriodDuration",
outputs: [],
stateMutability: "nonpayable",
type: "function"
},
{
inputs: [
{
internalType: "uint32",
name: "_maxMembershipRateLimit",
type: "uint32"
}
],
name: "setMaxMembershipRateLimit",
outputs: [],
stateMutability: "nonpayable",
type: "function"
},
{
inputs: [
{ internalType: "uint32", name: "_maxTotalRateLimit", type: "uint32" }
],
name: "setMaxTotalRateLimit",
outputs: [],
stateMutability: "nonpayable",
type: "function"
},
{
inputs: [
{
internalType: "uint32",
name: "_minMembershipRateLimit",
type: "uint32"
}
],
name: "setMinMembershipRateLimit",
outputs: [],
stateMutability: "nonpayable",
type: "function"
},
{
inputs: [
{ internalType: "address", name: "_priceCalculator", type: "address" }
],
name: "setPriceCalculator",
outputs: [],
stateMutability: "nonpayable",
type: "function"
},
{
inputs: [{ internalType: "address", name: "newOwner", type: "address" }],
name: "transferOwnership",
outputs: [],
stateMutability: "nonpayable",
type: "function"
},
{
inputs: [
{ internalType: "address", name: "newImplementation", type: "address" }
],
name: "upgradeTo",
outputs: [],
stateMutability: "nonpayable",
type: "function"
},
{
inputs: [
{ internalType: "address", name: "newImplementation", type: "address" },
{ internalType: "bytes", name: "data", type: "bytes" }
],
name: "upgradeToAndCall",
outputs: [],
stateMutability: "payable",
type: "function"
},
{
inputs: [{ internalType: "address", name: "token", type: "address" }],
name: "withdraw",
outputs: [],
stateMutability: "nonpayable",
type: "function"
}
];

View File

@ -1,16 +1,15 @@
import { PRICE_CALCULATOR_ABI } from "./abi/price_calculator.js"; import { linearPriceCalculatorAbi, wakuRlnV2Abi } from "./wagmi/generated.js";
import { RLN_ABI } from "./abi/rln.js";
export const RLN_CONTRACT = { export const RLN_CONTRACT = {
chainId: 59141, chainId: 59141,
address: "0xb9cd878c90e49f797b4431fbf4fb333108cb90e6", address: "0xb9cd878c90e49f797b4431fbf4fb333108cb90e6",
abi: RLN_ABI abi: wakuRlnV2Abi
}; };
export const PRICE_CALCULATOR_CONTRACT = { export const PRICE_CALCULATOR_CONTRACT = {
chainId: 59141, chainId: 59141,
address: "0xBcfC0660Df69f53ab409F32bb18A3fb625fcE644", address: "0xBcfC0660Df69f53ab409F32bb18A3fb625fcE644",
abi: PRICE_CALCULATOR_ABI abi: linearPriceCalculatorAbi
}; };
/** /**

View File

@ -1,28 +1,39 @@
import { expect, use } from "chai"; import { expect, use } from "chai";
import chaiAsPromised from "chai-as-promised"; import chaiAsPromised from "chai-as-promised";
import { ethers } from "ethers";
import sinon from "sinon"; import sinon from "sinon";
import { RLNBaseContract } from "./rln_base_contract.js"; import { RLNBaseContract } from "./rln_base_contract.js";
use(chaiAsPromised); use(chaiAsPromised);
function createMockRLNBaseContract(provider: any): RLNBaseContract { function createMockRLNBaseContract(
mockContract: any,
mockRpcClient: any
): RLNBaseContract {
const dummy = Object.create(RLNBaseContract.prototype); const dummy = Object.create(RLNBaseContract.prototype);
dummy.contract = { provider }; dummy.contract = mockContract;
dummy.rpcClient = mockRpcClient;
return dummy as RLNBaseContract; return dummy as RLNBaseContract;
} }
describe("RLNBaseContract.getPriceForRateLimit (unit)", function () { describe("RLNBaseContract.getPriceForRateLimit (unit)", function () {
let provider: any; let mockContract: any;
let calculateStub: sinon.SinonStub; let mockRpcClient: any;
let mockContractFactory: any; let priceCalculatorReadStub: sinon.SinonStub;
let readContractStub: sinon.SinonStub;
beforeEach(() => { beforeEach(() => {
provider = {}; priceCalculatorReadStub = sinon.stub();
calculateStub = sinon.stub(); readContractStub = sinon.stub();
mockContractFactory = function () {
return { calculate: calculateStub }; mockContract = {
read: {
priceCalculator: priceCalculatorReadStub
}
};
mockRpcClient = {
readContract: readContractStub
}; };
}); });
@ -32,35 +43,53 @@ describe("RLNBaseContract.getPriceForRateLimit (unit)", function () {
it("returns token and price for valid calculate", async () => { it("returns token and price for valid calculate", async () => {
const fakeToken = "0x1234567890abcdef1234567890abcdef12345678"; const fakeToken = "0x1234567890abcdef1234567890abcdef12345678";
const fakePrice = ethers.BigNumber.from(42); const fakePrice = 42n;
calculateStub.resolves([fakeToken, fakePrice]); const priceCalculatorAddress = "0xabcdef1234567890abcdef1234567890abcdef12";
priceCalculatorReadStub.resolves(priceCalculatorAddress);
readContractStub.resolves([fakeToken, fakePrice]);
const rlnBase = createMockRLNBaseContract(mockContract, mockRpcClient);
const result = await rlnBase.getPriceForRateLimit(20);
const rlnBase = createMockRLNBaseContract(provider);
const result = await rlnBase.getPriceForRateLimit(20, mockContractFactory);
expect(result.token).to.equal(fakeToken); expect(result.token).to.equal(fakeToken);
expect(result.price).to.not.be.null; expect(result.price).to.equal(fakePrice);
if (result.price) { expect(priceCalculatorReadStub.calledOnce).to.be.true;
expect(result.price.eq(fakePrice)).to.be.true; expect(readContractStub.calledOnce).to.be.true;
}
expect(calculateStub.calledOnceWith(20)).to.be.true; const readContractCall = readContractStub.getCall(0);
expect(readContractCall.args[0]).to.deep.include({
address: priceCalculatorAddress,
functionName: "calculate",
args: [20]
});
}); });
it("throws if calculate throws", async () => { it("throws if calculate throws", async () => {
calculateStub.rejects(new Error("fail")); const priceCalculatorAddress = "0xabcdef1234567890abcdef1234567890abcdef12";
const rlnBase = createMockRLNBaseContract(provider); priceCalculatorReadStub.resolves(priceCalculatorAddress);
await expect( readContractStub.rejects(new Error("fail"));
rlnBase.getPriceForRateLimit(20, mockContractFactory)
).to.be.rejectedWith("fail"); const rlnBase = createMockRLNBaseContract(mockContract, mockRpcClient);
expect(calculateStub.calledOnceWith(20)).to.be.true; await expect(rlnBase.getPriceForRateLimit(20)).to.be.rejectedWith("fail");
expect(priceCalculatorReadStub.calledOnce).to.be.true;
expect(readContractStub.calledOnce).to.be.true;
}); });
it("throws if calculate returns malformed data", async () => { it("returns null values if calculate returns malformed data", async () => {
calculateStub.resolves([null, null]); const priceCalculatorAddress = "0xabcdef1234567890abcdef1234567890abcdef12";
priceCalculatorReadStub.resolves(priceCalculatorAddress);
readContractStub.resolves([null, null]);
const rlnBase = createMockRLNBaseContract(mockContract, mockRpcClient);
const result = await rlnBase.getPriceForRateLimit(20);
const rlnBase = createMockRLNBaseContract(provider);
const result = await rlnBase.getPriceForRateLimit(20, mockContractFactory);
expect(result.token).to.be.null; expect(result.token).to.be.null;
expect(result.price).to.be.null; expect(result.price).to.be.null;
expect(priceCalculatorReadStub.calledOnce).to.be.true;
expect(readContractStub.calledOnce).to.be.true;
}); });
}); });

View File

@ -1,92 +1,74 @@
import { Logger } from "@waku/utils"; import { Logger } from "@waku/utils";
import { ethers } from "ethers"; import {
type Address,
decodeEventLog,
getContract,
type GetContractReturnType,
type Hash,
type PublicClient,
type WalletClient
} from "viem";
import { IdentityCredential } from "../identity.js"; import { IdentityCredential } from "../identity.js";
import { DecryptedCredentials } from "../keystore/types.js"; import type { DecryptedCredentials } from "../keystore/types.js";
import type { RpcClient } from "../utils/index.js";
import { RLN_ABI } from "./abi/rln.js";
import { import {
DEFAULT_RATE_LIMIT, DEFAULT_RATE_LIMIT,
PRICE_CALCULATOR_CONTRACT, RATE_LIMIT_PARAMS,
RATE_LIMIT_PARAMS RLN_CONTRACT
} from "./constants.js"; } from "./constants.js";
import { import {
CustomQueryOptions,
FetchMembersOptions,
Member,
MembershipInfo, MembershipInfo,
MembershipRegisteredEvent,
MembershipState, MembershipState,
RLNContractInitOptions RLNContractOptions
} from "./types.js"; } from "./types.js";
import { iPriceCalculatorAbi, wakuRlnV2Abi } from "./wagmi/generated.js";
const log = new Logger("rln:contract:base"); const log = new Logger("rln:contract:base");
export class RLNBaseContract { export class RLNBaseContract {
public contract: ethers.Contract; public contract: GetContractReturnType<
private deployBlock: undefined | number; typeof wakuRlnV2Abi,
PublicClient | WalletClient
>;
public rpcClient: RpcClient;
private rateLimit: number; private rateLimit: number;
private minRateLimit?: number; private minRateLimit?: number;
private maxRateLimit?: number; private maxRateLimit?: number;
protected _members: Map<number, Member> = new Map();
private _membersFilter: ethers.EventFilter;
private _membershipErasedFilter: ethers.EventFilter;
private _membersExpiredFilter: ethers.EventFilter;
/** /**
* Private constructor for RLNBaseContract. Use static create() instead. * Private constructor for RLNBaseContract. Use static create() instead.
*/ */
protected constructor(options: RLNContractInitOptions) { protected constructor(options: RLNContractOptions) {
const { const { address, rpcClient, rateLimit = DEFAULT_RATE_LIMIT } = options;
address,
signer,
rateLimit = DEFAULT_RATE_LIMIT,
contract
} = options;
log.info("Initializing RLNBaseContract", { address, rateLimit }); log.info("Initializing RLNBaseContract", { address, rateLimit });
this.contract = contract || new ethers.Contract(address, RLN_ABI, signer); this.rpcClient = rpcClient;
this.contract = getContract({
address,
abi: wakuRlnV2Abi,
client: this.rpcClient
});
this.rateLimit = rateLimit; this.rateLimit = rateLimit;
try {
log.info("Setting up event filters");
// Initialize event filters
this._membersFilter = this.contract.filters.MembershipRegistered();
this._membershipErasedFilter = this.contract.filters.MembershipErased();
this._membersExpiredFilter = this.contract.filters.MembershipExpired();
log.info("Event filters initialized successfully");
} catch (error) {
log.error("Failed to initialize event filters", { error });
throw new Error(
"Failed to initialize event filters: " + (error as Error).message
);
}
// Initialize members and subscriptions
this.fetchMembers()
.then(() => {
this.subscribeToMembers();
})
.catch((error) => {
log.error("Failed to initialize members", { error });
});
} }
/** /**
* Static async factory to create and initialize RLNBaseContract * Static async factory to create and initialize RLNBaseContract
*/ */
public static async create( public static async create(
options: RLNContractInitOptions options: RLNContractOptions
): Promise<RLNBaseContract> { ): Promise<RLNBaseContract> {
const instance = new RLNBaseContract(options); const instance = new RLNBaseContract(options);
const [min, max] = await Promise.all([ const [min, max] = await Promise.all([
instance.contract.minMembershipRateLimit(), instance.contract.read.minMembershipRateLimit(),
instance.contract.maxMembershipRateLimit() instance.contract.read.maxMembershipRateLimit()
]); ]);
instance.minRateLimit = ethers.BigNumber.from(min).toNumber();
instance.maxRateLimit = ethers.BigNumber.from(max).toNumber(); instance.minRateLimit = min;
instance.maxRateLimit = max;
instance.validateRateLimit(instance.rateLimit); instance.validateRateLimit(instance.rateLimit);
return instance; return instance;
@ -106,13 +88,6 @@ export class RLNBaseContract {
return this.contract.address; return this.contract.address;
} }
/**
* Gets the contract provider
*/
public get provider(): ethers.providers.Provider {
return this.contract.provider;
}
/** /**
* Gets the minimum allowed rate limit (cached) * Gets the minimum allowed rate limit (cached)
*/ */
@ -136,8 +111,7 @@ export class RLNBaseContract {
* @returns Promise<number> The maximum total rate limit in messages per epoch * @returns Promise<number> The maximum total rate limit in messages per epoch
*/ */
public async getMaxTotalRateLimit(): Promise<number> { public async getMaxTotalRateLimit(): Promise<number> {
const maxTotalRate = await this.contract.maxTotalRateLimit(); return await this.contract.read.maxTotalRateLimit();
return maxTotalRate.toNumber();
} }
/** /**
@ -145,8 +119,7 @@ export class RLNBaseContract {
* @returns Promise<number> The current total rate limit usage in messages per epoch * @returns Promise<number> The current total rate limit usage in messages per epoch
*/ */
public async getCurrentTotalRateLimit(): Promise<number> { public async getCurrentTotalRateLimit(): Promise<number> {
const currentTotal = await this.contract.currentTotalRateLimit(); return Number(await this.contract.read.currentTotalRateLimit());
return currentTotal.toNumber();
} }
/** /**
@ -154,11 +127,10 @@ export class RLNBaseContract {
* @returns Promise<number> The remaining rate limit that can be allocated * @returns Promise<number> The remaining rate limit that can be allocated
*/ */
public async getRemainingTotalRateLimit(): Promise<number> { public async getRemainingTotalRateLimit(): Promise<number> {
const [maxTotal, currentTotal] = await Promise.all([ return (
this.contract.maxTotalRateLimit(), (await this.contract.read.maxTotalRateLimit()) -
this.contract.currentTotalRateLimit() Number(await this.contract.read.currentTotalRateLimit())
]); );
return Number(maxTotal) - Number(currentTotal);
} }
/** /**
@ -170,233 +142,35 @@ export class RLNBaseContract {
this.rateLimit = newRateLimit; this.rateLimit = newRateLimit;
} }
public get members(): Member[] { /**
const sortedMembers = Array.from(this._members.values()).sort( * Gets the Merkle tree root for RLN proof verification
(left, right) => left.index.toNumber() - right.index.toNumber() * @returns Promise<bigint> The Merkle tree root
); *
return sortedMembers; */
public async getMerkleRoot(): Promise<bigint> {
return this.contract.read.root();
} }
public async fetchMembers(options: FetchMembersOptions = {}): Promise<void> { /**
const registeredMemberEvents = await RLNBaseContract.queryFilter( * Gets the Merkle proof for a member at a given index
this.contract, * @param index The index of the member in the membership set
{ * @returns Promise<bigint[]> Array of 20 Merkle proof elements
fromBlock: this.deployBlock, *
...options, */
membersFilter: this.membersFilter public async getMerkleProof(index: number): Promise<readonly bigint[]> {
} return await this.contract.read.getMerkleProof([index]);
);
const removedMemberEvents = await RLNBaseContract.queryFilter(
this.contract,
{
fromBlock: this.deployBlock,
...options,
membersFilter: this.membershipErasedFilter
}
);
const expiredMemberEvents = await RLNBaseContract.queryFilter(
this.contract,
{
fromBlock: this.deployBlock,
...options,
membersFilter: this.membersExpiredFilter
}
);
const events = [
...registeredMemberEvents,
...removedMemberEvents,
...expiredMemberEvents
];
this.processEvents(events);
}
public static async queryFilter(
contract: ethers.Contract,
options: CustomQueryOptions
): Promise<ethers.Event[]> {
const FETCH_CHUNK = 5;
const BLOCK_RANGE = 3000;
const {
fromBlock,
membersFilter,
fetchRange = BLOCK_RANGE,
fetchChunks = FETCH_CHUNK
} = options;
if (fromBlock === undefined) {
return contract.queryFilter(membersFilter);
}
if (!contract.provider) {
throw Error("No provider found on the contract.");
}
const toBlock = await contract.provider.getBlockNumber();
if (toBlock - fromBlock < fetchRange) {
return contract.queryFilter(membersFilter, fromBlock, toBlock);
}
const events: ethers.Event[][] = [];
const chunks = RLNBaseContract.splitToChunks(
fromBlock,
toBlock,
fetchRange
);
for (const portion of RLNBaseContract.takeN<[number, number]>(
chunks,
fetchChunks
)) {
const promises = portion.map(([left, right]) =>
RLNBaseContract.ignoreErrors(
contract.queryFilter(membersFilter, left, right),
[]
)
);
const fetchedEvents = await Promise.all(promises);
events.push(fetchedEvents.flatMap((v) => v));
}
return events.flatMap((v) => v);
}
public processEvents(events: ethers.Event[]): void {
const toRemoveTable = new Map<number, number[]>();
const toInsertTable = new Map<number, ethers.Event[]>();
events.forEach((evt) => {
if (!evt.args) {
return;
}
if (
evt.event === "MembershipErased" ||
evt.event === "MembershipExpired"
) {
let index = evt.args.index;
if (!index) {
return;
}
if (typeof index === "number" || typeof index === "string") {
index = ethers.BigNumber.from(index);
}
const toRemoveVal = toRemoveTable.get(evt.blockNumber);
if (toRemoveVal != undefined) {
toRemoveVal.push(index.toNumber());
toRemoveTable.set(evt.blockNumber, toRemoveVal);
} else {
toRemoveTable.set(evt.blockNumber, [index.toNumber()]);
}
} else if (evt.event === "MembershipRegistered") {
let eventsPerBlock = toInsertTable.get(evt.blockNumber);
if (eventsPerBlock == undefined) {
eventsPerBlock = [];
}
eventsPerBlock.push(evt);
toInsertTable.set(evt.blockNumber, eventsPerBlock);
}
});
}
public static splitToChunks(
from: number,
to: number,
step: number
): Array<[number, number]> {
const chunks: Array<[number, number]> = [];
let left = from;
while (left < to) {
const right = left + step < to ? left + step : to;
chunks.push([left, right] as [number, number]);
left = right;
}
return chunks;
}
public static *takeN<T>(array: T[], size: number): Iterable<T[]> {
let start = 0;
while (start < array.length) {
const portion = array.slice(start, start + size);
yield portion;
start += size;
}
}
public static async ignoreErrors<T>(
promise: Promise<T>,
defaultValue: T
): Promise<T> {
try {
return await promise;
} catch (err: unknown) {
if (err instanceof Error) {
log.info(`Ignoring an error during query: ${err.message}`);
} else {
log.info(`Ignoring an unknown error during query`);
}
return defaultValue;
}
}
public subscribeToMembers(): void {
this.contract.on(
this.membersFilter,
(
_idCommitment: bigint,
_membershipRateLimit: ethers.BigNumber,
_index: ethers.BigNumber,
event: ethers.Event
) => {
this.processEvents([event]);
}
);
this.contract.on(
this.membershipErasedFilter,
(
_idCommitment: bigint,
_membershipRateLimit: ethers.BigNumber,
_index: ethers.BigNumber,
event: ethers.Event
) => {
this.processEvents([event]);
}
);
this.contract.on(
this.membersExpiredFilter,
(
_idCommitment: bigint,
_membershipRateLimit: ethers.BigNumber,
_index: ethers.BigNumber,
event: ethers.Event
) => {
this.processEvents([event]);
}
);
} }
public async getMembershipInfo( public async getMembershipInfo(
idCommitmentBigInt: bigint idCommitmentBigInt: bigint
): Promise<MembershipInfo | undefined> { ): Promise<MembershipInfo | undefined> {
try { try {
const membershipData = const membershipData = await this.contract.read.memberships([
await this.contract.memberships(idCommitmentBigInt); idCommitmentBigInt
const currentBlock = await this.contract.provider.getBlockNumber(); ]);
const currentBlock = await this.rpcClient.getBlockNumber();
const [ const [
depositAmount, depositAmount,
activeDuration, activeDuration,
@ -408,12 +182,13 @@ export class RLNBaseContract {
token token
] = membershipData; ] = membershipData;
const gracePeriodEnd = gracePeriodStartTimestamp.add(gracePeriodDuration); const gracePeriodEnd =
Number(gracePeriodStartTimestamp) + Number(gracePeriodDuration);
let state: MembershipState; let state: MembershipState;
if (currentBlock < gracePeriodStartTimestamp.toNumber()) { if (currentBlock < Number(gracePeriodStartTimestamp)) {
state = MembershipState.Active; state = MembershipState.Active;
} else if (currentBlock < gracePeriodEnd.toNumber()) { } else if (currentBlock < gracePeriodEnd) {
state = MembershipState.GracePeriod; state = MembershipState.GracePeriod;
} else { } else {
state = MembershipState.Expired; state = MembershipState.Expired;
@ -422,9 +197,9 @@ export class RLNBaseContract {
return { return {
index, index,
idCommitment: idCommitmentBigInt.toString(), idCommitment: idCommitmentBigInt.toString(),
rateLimit: Number(rateLimit), rateLimit: rateLimit,
startBlock: gracePeriodStartTimestamp.toNumber(), startBlock: Number(gracePeriodStartTimestamp),
endBlock: gracePeriodEnd.toNumber(), endBlock: gracePeriodEnd,
state, state,
depositAmount, depositAmount,
activeDuration, activeDuration,
@ -438,43 +213,87 @@ export class RLNBaseContract {
} }
} }
public async extendMembership( public async extendMembership(idCommitmentBigInt: bigint): Promise<Hash> {
idCommitmentBigInt: bigint if (!this.rpcClient.account) {
): Promise<ethers.ContractTransaction> { throw new Error(
const tx = await this.contract.extendMemberships([idCommitmentBigInt]); "Failed to extendMembership: no account set in wallet client"
await tx.wait(); );
return tx; }
try {
await this.contract.simulate.extendMemberships([[idCommitmentBigInt]], {
chain: this.rpcClient.chain,
account: this.rpcClient.account.address
});
} catch (err) {
if (err instanceof Error) {
throw new Error(
"Error simulating extending membership: " + err.message
);
} else {
throw new Error("Error simulating extending membership", {
cause: err
});
}
}
const hash = await this.contract.write.extendMemberships(
[[idCommitmentBigInt]],
{
account: this.rpcClient.account,
chain: this.rpcClient.chain
}
);
await this.rpcClient.waitForTransactionReceipt({ hash });
return hash;
} }
public async eraseMembership( public async eraseMembership(
idCommitmentBigInt: bigint, idCommitmentBigInt: bigint,
eraseFromMembershipSet: boolean = true eraseFromMembershipSet: boolean = true
): Promise<ethers.ContractTransaction> { ): Promise<Hash> {
if ( if (
!(await this.isExpired(idCommitmentBigInt)) || !(await this.isExpired(idCommitmentBigInt)) ||
!(await this.isInGracePeriod(idCommitmentBigInt)) !(await this.isInGracePeriod(idCommitmentBigInt))
) { ) {
throw new Error("Membership is not expired or in grace period"); throw new Error("Membership is not expired or in grace period");
} }
if (!this.rpcClient.account) {
throw new Error(
"Failed to eraseMembership: no account set in wallet client"
);
}
const estimatedGas = await this.contract.estimateGas[ try {
"eraseMemberships(uint256[],bool)" await this.contract.simulate.eraseMemberships(
]([idCommitmentBigInt], eraseFromMembershipSet); [[idCommitmentBigInt], eraseFromMembershipSet],
const gasLimit = estimatedGas.add(10000); {
chain: this.rpcClient.chain,
account: this.rpcClient.account.address
}
);
} catch (err) {
if (err instanceof Error) {
throw new Error("Error simulating eraseMemberships: " + err.message);
} else {
throw new Error("Error simulating eraseMemberships", { cause: err });
}
}
const tx = await this.contract["eraseMemberships(uint256[],bool)"]( const hash = await this.contract.write.eraseMemberships(
[idCommitmentBigInt], [[idCommitmentBigInt], eraseFromMembershipSet],
eraseFromMembershipSet, {
{ gasLimit } chain: this.rpcClient.chain,
account: this.rpcClient.account
}
); );
await tx.wait(); await this.rpcClient.waitForTransactionReceipt({ hash });
return tx; return hash;
} }
public async registerMembership( public async registerMembership(
idCommitmentBigInt: bigint, idCommitmentBigInt: bigint,
rateLimit: number = DEFAULT_RATE_LIMIT rateLimit: number = DEFAULT_RATE_LIMIT
): Promise<ethers.ContractTransaction> { ): Promise<Hash> {
if ( if (
rateLimit < RATE_LIMIT_PARAMS.MIN_RATE || rateLimit < RATE_LIMIT_PARAMS.MIN_RATE ||
rateLimit > RATE_LIMIT_PARAMS.MAX_RATE rateLimit > RATE_LIMIT_PARAMS.MAX_RATE
@ -483,21 +302,80 @@ export class RLNBaseContract {
`Rate limit must be between ${RATE_LIMIT_PARAMS.MIN_RATE} and ${RATE_LIMIT_PARAMS.MAX_RATE}` `Rate limit must be between ${RATE_LIMIT_PARAMS.MIN_RATE} and ${RATE_LIMIT_PARAMS.MAX_RATE}`
); );
} }
return this.contract.register(idCommitmentBigInt, rateLimit, []); if (!this.rpcClient.account) {
throw new Error(
"Failed to registerMembership: no account set in wallet client"
);
}
try {
await this.contract.simulate.register(
[idCommitmentBigInt, rateLimit, []],
{
chain: this.rpcClient.chain,
account: this.rpcClient.account.address
}
);
} catch (err) {
if (err instanceof Error) {
throw new Error("Error simulating register membership: " + err.message);
} else {
throw new Error("Error simulating register membership", { cause: err });
}
}
const hash = await this.contract.write.register(
[idCommitmentBigInt, rateLimit, []],
{
chain: this.rpcClient.chain,
account: this.rpcClient.account
}
);
await this.rpcClient.waitForTransactionReceipt({ hash });
return hash;
} }
public async withdraw(token: string, walletAddress: string): Promise<void> { /**
try { * Withdraw deposited tokens after membership is erased.
const tx = await this.contract.withdraw(token, walletAddress); * The smart contract validates that the sender is the holder of the membership,
await tx.wait(); * and will only send tokens to that address.
} catch (error) { * @param token - Token address to withdraw
log.error(`Error in withdraw: ${(error as Error).message}`); */
public async withdraw(token: string): Promise<Hash> {
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.rpcClient.chain,
account: this.rpcClient.account.address
});
} catch (err) {
if (err instanceof Error) {
throw new Error("Error simulating withdraw: " + err.message);
} else {
throw new Error("Error simulating withdraw", { cause: err });
}
}
const hash = await this.contract.write.withdraw([token as Address], {
chain: this.rpcClient.chain,
account: this.rpcClient.account
});
await this.rpcClient.waitForTransactionReceipt({ hash });
return hash;
} }
public async registerWithIdentity( public async registerWithIdentity(
identity: IdentityCredential identity: IdentityCredential
): Promise<DecryptedCredentials | undefined> { ): Promise<DecryptedCredentials | undefined> {
try { try {
if (!this.rpcClient.account) {
throw new Error(
"Failed to registerWithIdentity: no account set in wallet client"
);
}
log.info( log.info(
`Registering identity with rate limit: ${this.rateLimit} messages/epoch` `Registering identity with rate limit: ${this.rateLimit} messages/epoch`
); );
@ -520,62 +398,71 @@ export class RLNBaseContract {
); );
} }
const estimatedGas = await this.contract.estimateGas.register( await this.contract.simulate.register(
identity.IDCommitmentBigInt, [identity.IDCommitmentBigInt, this.rateLimit, []],
this.rateLimit, {
[] chain: this.rpcClient.chain,
account: this.rpcClient.account.address
}
); );
const gasLimit = estimatedGas.add(10000);
const txRegisterResponse: ethers.ContractTransaction = const hash: Hash = await this.contract.write.register(
await this.contract.register( [identity.IDCommitmentBigInt, this.rateLimit, []],
identity.IDCommitmentBigInt, {
this.rateLimit, chain: this.rpcClient.chain,
[], account: this.rpcClient.account
{ }
gasLimit );
}
);
const txRegisterReceipt = await txRegisterResponse.wait(); const txRegisterReceipt = await this.rpcClient.waitForTransactionReceipt({
hash
});
if (txRegisterReceipt.status === 0) { if (txRegisterReceipt.status === "reverted") {
throw new Error("Transaction failed on-chain"); throw new Error("Transaction failed on-chain");
} }
const memberRegistered = txRegisterReceipt.events?.find( // Parse MembershipRegistered event from logs
(event: ethers.Event) => event.event === "MembershipRegistered" const memberRegisteredLog = txRegisterReceipt.logs.find((log) => {
); try {
const decoded = decodeEventLog({
abi: wakuRlnV2Abi,
data: log.data,
topics: log.topics
});
return decoded.eventName === "MembershipRegistered";
} catch {
return false;
}
});
if (!memberRegistered || !memberRegistered.args) { if (!memberRegisteredLog) {
log.error( log.error(
"Failed to register membership: No MembershipRegistered event found" "Failed to register membership: No MembershipRegistered event found"
); );
return undefined; return undefined;
} }
const decodedData: MembershipRegisteredEvent = { // Decode the event
idCommitment: memberRegistered.args.idCommitment, const decoded = decodeEventLog({
membershipRateLimit: memberRegistered.args.membershipRateLimit, abi: wakuRlnV2Abi,
index: memberRegistered.args.index data: memberRegisteredLog.data,
}; topics: memberRegisteredLog.topics,
eventName: "MembershipRegistered"
});
log.info( log.info(
`Successfully registered membership with index ${decodedData.index} ` + `Successfully registered membership with index ${decoded.args.index} ` +
`and rate limit ${decodedData.membershipRateLimit}` `and rate limit ${decoded.args.membershipRateLimit}`
); );
const network = await this.contract.provider.getNetwork();
const address = this.contract.address;
const membershipId = Number(decodedData.index);
return { return {
identity, identity,
membership: { membership: {
address, address: this.contract.address,
treeIndex: membershipId, treeIndex: decoded.args.index,
chainId: network.chainId.toString(), chainId: String(RLN_CONTRACT.chainId),
rateLimit: decodedData.membershipRateLimit.toNumber() rateLimit: Number(decoded.args.membershipRateLimit)
} }
}; };
} catch (error) { } catch (error) {
@ -608,78 +495,6 @@ export class RLNBaseContract {
} }
} }
public async registerWithPermitAndErase(
identity: IdentityCredential,
permit: {
owner: string;
deadline: number;
v: number;
r: string;
s: string;
},
idCommitmentsToErase: string[]
): Promise<DecryptedCredentials | undefined> {
try {
log.info(
`Registering identity with permit and rate limit: ${this.rateLimit} messages/epoch`
);
const txRegisterResponse: ethers.ContractTransaction =
await this.contract.registerWithPermit(
permit.owner,
permit.deadline,
permit.v,
permit.r,
permit.s,
identity.IDCommitmentBigInt,
this.rateLimit,
idCommitmentsToErase.map((id) => ethers.BigNumber.from(id))
);
const txRegisterReceipt = await txRegisterResponse.wait();
const memberRegistered = txRegisterReceipt.events?.find(
(event: ethers.Event) => event.event === "MembershipRegistered"
);
if (!memberRegistered || !memberRegistered.args) {
log.error(
"Failed to register membership with permit: No MembershipRegistered event found"
);
return undefined;
}
const decodedData: MembershipRegisteredEvent = {
idCommitment: memberRegistered.args.idCommitment,
membershipRateLimit: memberRegistered.args.membershipRateLimit,
index: memberRegistered.args.index
};
log.info(
`Successfully registered membership with permit. Index: ${decodedData.index}, ` +
`Rate limit: ${decodedData.membershipRateLimit}, Erased ${idCommitmentsToErase.length} commitments`
);
const network = await this.contract.provider.getNetwork();
const address = this.contract.address;
const membershipId = Number(decodedData.index);
return {
identity,
membership: {
address,
treeIndex: membershipId,
chainId: network.chainId.toString(),
rateLimit: decodedData.membershipRateLimit.toNumber()
}
};
} catch (error) {
log.error(
`Error in registerWithPermitAndErase: ${(error as Error).message}`
);
return undefined;
}
}
/** /**
* Validates that the rate limit is within the allowed range (sync) * Validates that the rate limit is within the allowed range (sync)
* @throws Error if the rate limit is outside the allowed range * @throws Error if the rate limit is outside the allowed range
@ -695,50 +510,17 @@ export class RLNBaseContract {
} }
} }
private get membersFilter(): ethers.EventFilter { private async getMemberIndex(idCommitmentBigInt: bigint): Promise<number> {
if (!this._membersFilter) { // Current version of the contract has the index at position 5 in the membership struct
throw Error("Members filter was not initialized."); return (await this.contract.read.memberships([idCommitmentBigInt]))[5];
}
return this._membersFilter;
}
private get membershipErasedFilter(): ethers.EventFilter {
if (!this._membershipErasedFilter) {
throw Error("MembershipErased filter was not initialized.");
}
return this._membershipErasedFilter;
}
private get membersExpiredFilter(): ethers.EventFilter {
if (!this._membersExpiredFilter) {
throw Error("MembersExpired filter was not initialized.");
}
return this._membersExpiredFilter;
}
private async getMemberIndex(
idCommitmentBigInt: bigint
): Promise<ethers.BigNumber | undefined> {
try {
const events = await this.contract.queryFilter(
this.contract.filters.MembershipRegistered(idCommitmentBigInt)
);
if (events.length === 0) return undefined;
// Get the most recent registration event
const event = events[events.length - 1];
return event.args?.index;
} catch (error) {
return undefined;
}
} }
public async getMembershipStatus( public async getMembershipStatus(
idCommitment: bigint idCommitment: bigint
): Promise<"expired" | "grace" | "active"> { ): Promise<"expired" | "grace" | "active"> {
const [isExpired, isInGrace] = await Promise.all([ const [isExpired, isInGrace] = await Promise.all([
this.contract.isExpired(idCommitment), this.contract.read.isExpired([idCommitment]),
this.contract.isInGracePeriod(idCommitment) this.contract.read.isInGracePeriod([idCommitment])
]); ]);
if (isExpired) return "expired"; if (isExpired) return "expired";
@ -753,7 +535,7 @@ export class RLNBaseContract {
*/ */
public async isExpired(idCommitmentBigInt: bigint): Promise<boolean> { public async isExpired(idCommitmentBigInt: bigint): Promise<boolean> {
try { try {
return await this.contract.isExpired(idCommitmentBigInt); return await this.contract.read.isExpired([idCommitmentBigInt]);
} catch (error) { } catch (error) {
log.error("Error in isExpired:", error); log.error("Error in isExpired:", error);
return false; return false;
@ -767,7 +549,7 @@ export class RLNBaseContract {
*/ */
public async isInGracePeriod(idCommitmentBigInt: bigint): Promise<boolean> { public async isInGracePeriod(idCommitmentBigInt: bigint): Promise<boolean> {
try { try {
return await this.contract.isInGracePeriod(idCommitmentBigInt); return await this.contract.read.isInGracePeriod([idCommitmentBigInt]);
} catch (error) { } catch (error) {
log.error("Error in isInGracePeriod:", error); log.error("Error in isInGracePeriod:", error);
return false; return false;
@ -779,21 +561,18 @@ export class RLNBaseContract {
* @param rateLimit The rate limit to calculate the price for * @param rateLimit The rate limit to calculate the price for
* @param contractFactory Optional factory for creating the contract (for testing) * @param contractFactory Optional factory for creating the contract (for testing)
*/ */
public async getPriceForRateLimit( public async getPriceForRateLimit(rateLimit: number): Promise<{
rateLimit: number,
contractFactory?: typeof import("ethers").Contract
): Promise<{
token: string | null; token: string | null;
price: import("ethers").BigNumber | null; price: bigint | null;
}> { }> {
const provider = this.contract.provider; const address = await this.contract.read.priceCalculator();
const ContractCtor = contractFactory || ethers.Contract; const [token, price] = await this.rpcClient.readContract({
const priceCalculator = new ContractCtor( address,
PRICE_CALCULATOR_CONTRACT.address, abi: iPriceCalculatorAbi,
PRICE_CALCULATOR_CONTRACT.abi, functionName: "calculate",
provider args: [rateLimit]
); });
const [token, price] = await priceCalculator.calculate(rateLimit);
// Defensive: if token or price is null/undefined, return nulls // Defensive: if token or price is null/undefined, return nulls
if (!token || !price) { if (!token || !price) {
return { token: null, price: null }; return { token: null, price: null };

View File

@ -1,28 +1,22 @@
import { ethers } from "ethers"; import { Address } from "viem";
export interface CustomQueryOptions extends FetchMembersOptions { import { RpcClient } from "../utils/index.js";
membersFilter: ethers.EventFilter;
}
export type Member = { export type Member = {
idCommitment: string; idCommitment: string;
index: ethers.BigNumber; index: bigint;
}; };
export interface RLNContractOptions { export interface RLNContractOptions {
signer: ethers.Signer; rpcClient: RpcClient;
address: string; address: Address;
rateLimit?: number; rateLimit?: number;
} }
export interface RLNContractInitOptions extends RLNContractOptions {
contract?: ethers.Contract;
}
export interface MembershipRegisteredEvent { export interface MembershipRegisteredEvent {
idCommitment: string; idCommitment: string;
membershipRateLimit: ethers.BigNumber; membershipRateLimit: bigint;
index: ethers.BigNumber; index: bigint;
} }
export type FetchMembersOptions = { export type FetchMembersOptions = {
@ -32,13 +26,13 @@ export type FetchMembersOptions = {
}; };
export interface MembershipInfo { export interface MembershipInfo {
index: ethers.BigNumber; index: number;
idCommitment: string; idCommitment: string;
rateLimit: number; rateLimit: number;
startBlock: number; startBlock: number;
endBlock: number; endBlock: number;
state: MembershipState; state: MembershipState;
depositAmount: ethers.BigNumber; depositAmount: bigint;
activeDuration: number; activeDuration: number;
gracePeriodDuration: number; gracePeriodDuration: number;
holder: string; holder: string;

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,5 @@
import { Logger } from "@waku/utils"; import { Logger } from "@waku/utils";
import { ethers } from "ethers"; import { publicActions } from "viem";
import { RLN_CONTRACT } from "./contract/constants.js"; import { RLN_CONTRACT } from "./contract/constants.js";
import { RLNBaseContract } from "./contract/rln_base_contract.js"; import { RLNBaseContract } from "./contract/rln_base_contract.js";
@ -10,7 +10,7 @@ import type {
} from "./keystore/index.js"; } from "./keystore/index.js";
import { KeystoreEntity, Password } from "./keystore/types.js"; import { KeystoreEntity, Password } from "./keystore/types.js";
import { RegisterMembershipOptions, StartRLNOptions } from "./types.js"; import { RegisterMembershipOptions, StartRLNOptions } from "./types.js";
import { extractMetaMaskSigner } from "./utils/index.js"; import { createViemClientFromWindow, RpcClient } from "./utils/index.js";
import { Zerokit } from "./zerokit.js"; import { Zerokit } from "./zerokit.js";
const log = new Logger("rln:credentials"); const log = new Logger("rln:credentials");
@ -24,7 +24,7 @@ export class RLNCredentialsManager {
protected starting = false; protected starting = false;
public contract: undefined | RLNBaseContract; public contract: undefined | RLNBaseContract;
public signer: undefined | ethers.Signer; public rpcClient: undefined | RpcClient;
protected keystore = Keystore.create(); protected keystore = Keystore.create();
public credentials: undefined | DecryptedCredentials; public credentials: undefined | DecryptedCredentials;
@ -36,10 +36,6 @@ export class RLNCredentialsManager {
this.zerokit = zerokit; this.zerokit = zerokit;
} }
public get provider(): undefined | ethers.providers.Provider {
return this.contract?.provider;
}
public async start(options: StartRLNOptions = {}): Promise<void> { public async start(options: StartRLNOptions = {}): Promise<void> {
if (this.started || this.starting) { if (this.started || this.starting) {
log.info("RLNCredentialsManager already started or starting"); log.info("RLNCredentialsManager already started or starting");
@ -59,10 +55,8 @@ export class RLNCredentialsManager {
log.info("Credentials successfully decrypted"); log.info("Credentials successfully decrypted");
} }
const { signer, address, rateLimit } = await this.determineStartOptions( const { rpcClient, address, rateLimit } =
options, await this.determineStartOptions(options, credentials);
credentials
);
log.info(`Using contract address: ${address}`); log.info(`Using contract address: ${address}`);
@ -72,10 +66,10 @@ export class RLNCredentialsManager {
} }
this.credentials = credentials; this.credentials = credentials;
this.signer = signer!; this.rpcClient = rpcClient!;
this.contract = await RLNBaseContract.create({ this.contract = await RLNBaseContract.create({
address: address!, address: address! as `0x${string}`,
signer: signer!, rpcClient: this.rpcClient,
rateLimit: rateLimit ?? this.zerokit.rateLimit rateLimit: rateLimit ?? this.zerokit.rateLimit
}); });
@ -134,7 +128,7 @@ export class RLNCredentialsManager {
protected async determineStartOptions( protected async determineStartOptions(
options: StartRLNOptions, options: StartRLNOptions,
credentials: KeystoreEntity | undefined credentials: KeystoreEntity | undefined
): Promise<StartRLNOptions> { ): Promise<StartRLNOptions & { rpcClient: RpcClient }> {
let chainId = credentials?.membership.chainId; let chainId = credentials?.membership.chainId;
const address = const address =
credentials?.membership.address || credentials?.membership.address ||
@ -146,11 +140,14 @@ export class RLNCredentialsManager {
log.info(`Using Linea contract with chainId: ${chainId}`); log.info(`Using Linea contract with chainId: ${chainId}`);
} }
const signer = options.signer || (await extractMetaMaskSigner()); const rpcClient: RpcClient = options.walletClient
const currentChainId = await signer.getChainId(); ? options.walletClient.extend(publicActions)
: await createViemClientFromWindow();
const currentChainId = rpcClient.chain?.id;
log.info(`Current chain ID: ${currentChainId}`); log.info(`Current chain ID: ${currentChainId}`);
if (chainId && chainId !== currentChainId.toString()) { if (chainId && chainId !== currentChainId?.toString()) {
log.error( log.error(
`Chain ID mismatch: contract=${chainId}, current=${currentChainId}` `Chain ID mismatch: contract=${chainId}, current=${currentChainId}`
); );
@ -160,7 +157,7 @@ export class RLNCredentialsManager {
} }
return { return {
signer, rpcClient,
address address
}; };
} }
@ -206,9 +203,9 @@ export class RLNCredentialsManager {
protected async verifyCredentialsAgainstContract( protected async verifyCredentialsAgainstContract(
credentials: KeystoreEntity credentials: KeystoreEntity
): Promise<void> { ): Promise<void> {
if (!this.contract) { if (!this.contract || !this.rpcClient) {
throw Error( throw Error(
"Failed to verify chain coordinates: no contract initialized." "Failed to verify chain coordinates: no contract or viem client initialized."
); );
} }
@ -221,8 +218,7 @@ export class RLNCredentialsManager {
} }
const chainId = credentials.membership.chainId; const chainId = credentials.membership.chainId;
const network = await this.contract.provider.getNetwork(); const currentChainId = await this.rpcClient.getChainId();
const currentChainId = network.chainId;
if (chainId !== currentChainId.toString()) { if (chainId !== currentChainId.toString()) {
throw Error( throw Error(
`Failed to verify chain coordinates: credentials chainID=${chainId} is not equal to registryContract chainID=${currentChainId}` `Failed to verify chain coordinates: credentials chainID=${chainId} is not equal to registryContract chainID=${currentChainId}`

View File

@ -1,11 +1,10 @@
import { RLN_ABI } from "./contract/abi/rln.js";
import { RLN_CONTRACT } from "./contract/index.js"; import { RLN_CONTRACT } from "./contract/index.js";
import { RLNBaseContract } from "./contract/rln_base_contract.js"; import { RLNBaseContract } from "./contract/rln_base_contract.js";
import { createRLN } from "./create.js"; import { createRLN } from "./create.js";
import { IdentityCredential } from "./identity.js"; import { IdentityCredential } from "./identity.js";
import { Keystore } from "./keystore/index.js"; import { Keystore } from "./keystore/index.js";
import { RLNInstance } from "./rln.js"; import { RLNInstance } from "./rln.js";
import { extractMetaMaskSigner } from "./utils/index.js"; import { createViemClientFromWindow } from "./utils/index.js";
export { export {
RLNBaseContract, RLNBaseContract,
@ -14,10 +13,16 @@ export {
RLNInstance, RLNInstance,
IdentityCredential, IdentityCredential,
RLN_CONTRACT, RLN_CONTRACT,
extractMetaMaskSigner, createViemClientFromWindow
RLN_ABI
}; };
export {
wakuRlnV2Abi,
linearPriceCalculatorAbi,
iPriceCalculatorAbi,
membershipUpgradeableAbi
} from "./contract/wagmi/generated.js";
export type { export type {
DecryptedCredentials, DecryptedCredentials,
EncryptedCredentials, EncryptedCredentials,

View File

@ -1,4 +1,4 @@
import { ethers } from "ethers"; import { WalletClient } from "viem";
import { IdentityCredential } from "./identity.js"; import { IdentityCredential } from "./identity.js";
import { import {
@ -8,9 +8,9 @@ import {
export type StartRLNOptions = { export type StartRLNOptions = {
/** /**
* If not set - will extract MetaMask account and get signer from it. * If not set - will attempt to create from provider injected in window.
*/ */
signer?: ethers.Signer; walletClient?: WalletClient;
/** /**
* If not set - will use default SEPOLIA_CONTRACT address. * If not set - will use default SEPOLIA_CONTRACT address.
*/ */

View File

@ -1,4 +1,4 @@
export { extractMetaMaskSigner } from "./metamask.js"; export { createViemClientFromWindow, RpcClient } from "./rpcClient.js";
export { BytesUtils } from "./bytes.js"; export { BytesUtils } from "./bytes.js";
export { sha256, poseidonHash } from "./hash.js"; export { sha256, poseidonHash } from "./hash.js";
export { dateToEpoch, epochIntToBytes, epochBytesToInt } from "./epoch.js"; export { dateToEpoch, epochIntToBytes, epochBytesToInt } from "./epoch.js";

View File

@ -1,17 +0,0 @@
import { ethers } from "ethers";
export const extractMetaMaskSigner = async (): Promise<ethers.Signer> => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const ethereum = (window as any).ethereum;
if (!ethereum) {
throw Error(
"Missing or invalid Ethereum provider. Please install a Web3 wallet such as MetaMask."
);
}
await ethereum.request({ method: "eth_requestAccounts" });
const provider = new ethers.providers.Web3Provider(ethereum, "any");
return provider.getSigner();
};

View File

@ -0,0 +1,61 @@
import "viem/window";
import {
type Address,
createWalletClient,
custom,
PublicActions,
publicActions,
WalletClient
} from "viem";
import { lineaSepolia } from "viem/chains";
export type RpcClient = WalletClient & PublicActions;
/**
* Checks window for injected Ethereum provider, requests user to connect, and creates an RPC client object
* capable of performing both read and write operations on the blockchain.
*
* If the wallet is not connected to the Linea Sepolia network, it will attempt to switch to it.
* If the wallet does not have the Linea Sepolia network added, it will attempt to add it.
*/
export const createViemClientFromWindow = async (): Promise<RpcClient> => {
const ethereum = window.ethereum;
if (!ethereum) {
throw Error(
"Missing or invalid Ethereum provider. Please install a Web3 wallet such as MetaMask."
);
}
const accounts = await ethereum.request({ method: "eth_requestAccounts" });
if (!Array.isArray(accounts)) {
throw Error("Failed to get accounts");
}
const account = accounts[0] as Address;
const rpcClient: RpcClient = createWalletClient({
account: account as Address,
chain: lineaSepolia,
transport: custom(window.ethereum!)
}).extend(publicActions);
// Ensure wallet is connected to Linea Sepolia
try {
await rpcClient.switchChain({ id: lineaSepolia.id });
} catch (error: unknown) {
// This error code indicates that the chain has not been added to the wallet
if (
typeof error === "object" &&
error !== null &&
"code" in error &&
error.code === 4902
) {
await rpcClient.addChain({ chain: lineaSepolia });
await rpcClient.switchChain({ id: lineaSepolia.id });
} else {
throw error;
}
}
return rpcClient;
};

View File

@ -1,3 +1,4 @@
{ {
"extends": "../../tsconfig.dev" "extends": "../../tsconfig.dev",
"exclude": ["wagmi.config.ts"]
} }

View File

@ -6,5 +6,5 @@
"tsBuildInfoFile": "dist/.tsbuildinfo" "tsBuildInfoFile": "dist/.tsbuildinfo"
}, },
"include": ["src"], "include": ["src"],
"exclude": ["src/**/*.spec.ts", "src/test_utils"] "exclude": ["wagmi.config.ts", "src/**/*.spec.ts", "src/test_utils"]
} }

View File

@ -0,0 +1,18 @@
import { defineConfig } from "@wagmi/cli";
import { foundry } from "@wagmi/cli/plugins";
export default defineConfig({
out: "src/contract/wagmi/generated.ts",
plugins: [
foundry({
project: "./waku-rlnv2-contract",
artifacts: "out",
include: [
"WakuRlnV2.sol/**",
"Membership.sol/**",
"LinearPriceCalculator.sol/**",
"IPriceCalculator.sol/**"
]
})
]
});