UNPKG

@cryptkeeperzk/rlnjs

Version:

Client library for generating and using RLN ZK proofs, is a forked repo from main Rate-Limiting-Nullifier/rlnjs to make it work with Cryptkeeper Browser Extension using `@cryptkeeperzk/snarkjs` and `@cryptkeeperzk/ffjavascript`

212 lines (183 loc) 13.9 kB
import { Proof } from './types' import { ethers } from 'ethers' const erc20ABI = JSON.parse('[{"constant": true, "inputs": [], "name": "name", "outputs": [{"name": "", "type": "string"}], "payable": false, "stateMutability": "view", "type": "function"}, {"constant": false, "inputs": [{"name": "_spender", "type": "address"}, {"name": "_value", "type": "uint256"}], "name": "approve", "outputs": [{"name": "", "type": "bool"}], "payable": false, "stateMutability": "nonpayable", "type": "function"}, {"constant": true, "inputs": [], "name": "totalSupply", "outputs": [{"name": "", "type": "uint256"}], "payable": false, "stateMutability": "view", "type": "function"}, {"constant": false, "inputs": [{"name": "_from", "type": "address"}, {"name": "_to", "type": "address"}, {"name": "_value", "type": "uint256"}], "name": "transferFrom", "outputs": [{"name": "", "type": "bool"}], "payable": false, "stateMutability": "nonpayable", "type": "function"}, {"constant": true, "inputs": [], "name": "decimals", "outputs": [{"name": "", "type": "uint8"}], "payable": false, "stateMutability": "view", "type": "function"}, {"constant": true, "inputs": [{"name": "_owner", "type": "address"}], "name": "balanceOf", "outputs": [{"name": "balance", "type": "uint256"}], "payable": false, "stateMutability": "view", "type": "function"}, {"constant": true, "inputs": [], "name": "symbol", "outputs": [{"name": "", "type": "string"}], "payable": false, "stateMutability": "view", "type": "function"}, {"constant": false, "inputs": [{"name": "_to", "type": "address"}, {"name": "_value", "type": "uint256"}], "name": "transfer", "outputs": [{"name": "", "type": "bool"}], "payable": false, "stateMutability": "nonpayable", "type": "function"}, {"constant": true, "inputs": [{"name": "_owner", "type": "address"}, {"name": "_spender", "type": "address"}], "name": "allowance", "outputs": [{"name": "", "type": "uint256"}], "payable": false, "stateMutability": "view", "type": "function"}, {"payable": true, "stateMutability": "payable", "type": "fallback"}, {"anonymous": false, "inputs": [{"indexed": true, "name": "owner", "type": "address"}, {"indexed": true, "name": "spender", "type": "address"}, {"indexed": false, "name": "value", "type": "uint256"}], "name": "Approval", "type": "event"}, {"anonymous": false, "inputs": [{"indexed": true, "name": "from", "type": "address"}, {"indexed": true, "name": "to", "type": "address"}, {"indexed": false, "name": "value", "type": "uint256"}], "name": "Transfer", "type": "event"}]') // Ref: https://github.com/Rate-Limiting-Nullifier/rln-contracts/blob/572bc396b6eef6627ecb2f3ef871370b4c0710f3/src/RLN.sol#L10 export const rlnContractABI = JSON.parse('[{"inputs": [{"internalType": "uint256", "name": "minimalDeposit", "type": "uint256"}, {"internalType": "uint256", "name": "maximalRate", "type": "uint256"}, {"internalType": "uint256", "name": "depth", "type": "uint256"}, {"internalType": "uint8", "name": "feePercentage", "type": "uint8"}, {"internalType": "address", "name": "feeReceiver", "type": "address"}, {"internalType": "uint256", "name": "freezePeriod", "type": "uint256"}, {"internalType": "address", "name": "_token", "type": "address"}, {"internalType": "address", "name": "_verifier", "type": "address"}], "stateMutability": "nonpayable", "type": "constructor"}, {"anonymous": false, "inputs": [{"indexed": false, "internalType": "uint256", "name": "identityCommitment", "type": "uint256"}, {"indexed": false, "internalType": "uint256", "name": "messageLimit", "type": "uint256"}, {"indexed": false, "internalType": "uint256", "name": "index", "type": "uint256"}], "name": "MemberRegistered", "type": "event"}, {"anonymous": false, "inputs": [{"indexed": false, "internalType": "uint256", "name": "index", "type": "uint256"}, {"indexed": false, "internalType": "address", "name": "slasher", "type": "address"}], "name": "MemberSlashed", "type": "event"}, {"anonymous": false, "inputs": [{"indexed": false, "internalType": "uint256", "name": "index", "type": "uint256"}], "name": "MemberWithdrawn", "type": "event"}, {"inputs": [], "name": "FEE_PERCENTAGE", "outputs": [{"internalType": "uint8", "name": "", "type": "uint8"}], "stateMutability": "view", "type": "function"}, {"inputs": [], "name": "FEE_RECEIVER", "outputs": [{"internalType": "address", "name": "", "type": "address"}], "stateMutability": "view", "type": "function"}, {"inputs": [], "name": "FREEZE_PERIOD", "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], "stateMutability": "view", "type": "function"}, {"inputs": [], "name": "MAXIMAL_RATE", "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], "stateMutability": "view", "type": "function"}, {"inputs": [], "name": "MINIMAL_DEPOSIT", "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], "stateMutability": "view", "type": "function"}, {"inputs": [], "name": "SET_SIZE", "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], "stateMutability": "view", "type": "function"}, {"inputs": [], "name": "identityCommitmentIndex", "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], "stateMutability": "view", "type": "function"}, {"inputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], "name": "members", "outputs": [{"internalType": "address", "name": "userAddress", "type": "address"}, {"internalType": "uint256", "name": "messageLimit", "type": "uint256"}, {"internalType": "uint256", "name": "index", "type": "uint256"}], "stateMutability": "view", "type": "function"}, {"inputs": [{"internalType": "uint256", "name": "identityCommitment", "type": "uint256"}, {"internalType": "uint256", "name": "amount", "type": "uint256"}], "name": "register", "outputs": [], "stateMutability": "nonpayable", "type": "function"}, {"inputs": [{"internalType": "uint256", "name": "identityCommitment", "type": "uint256"}], "name": "release", "outputs": [], "stateMutability": "nonpayable", "type": "function"}, {"inputs": [{"internalType": "uint256", "name": "identityCommitment", "type": "uint256"}, {"internalType": "address", "name": "receiver", "type": "address"}, {"internalType": "uint256[8]", "name": "proof", "type": "uint256[8]"}], "name": "slash", "outputs": [], "stateMutability": "nonpayable", "type": "function"}, {"inputs": [], "name": "token", "outputs": [{"internalType": "contract IERC20", "name": "", "type": "address"}], "stateMutability": "view", "type": "function"}, {"inputs": [], "name": "verifier", "outputs": [{"internalType": "contract IVerifier", "name": "", "type": "address"}], "stateMutability": "view", "type": "function"}, {"inputs": [{"internalType": "uint256", "name": "identityCommitment", "type": "uint256"}, {"internalType": "uint256[8]", "name": "proof", "type": "uint256[8]"}], "name": "withdraw", "outputs": [], "stateMutability": "nonpayable", "type": "function"}, {"inputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], "name": "withdrawals", "outputs": [{"internalType": "uint256", "name": "blockNumber", "type": "uint256"}, {"internalType": "uint256", "name": "amount", "type": "uint256"}, {"internalType": "address", "name": "receiver", "type": "address"}], "stateMutability": "view", "type": "function"}]') type User = { userAddress: string, messageLimit: bigint, index: bigint, } type Withdrawal = { blockNumber: bigint, amount: bigint, receiver: string, } function proofToArray(proof: Proof) { // verifier.verifyProof( // [proof[0], proof[1]], // [[proof[2], proof[3]], [proof[4], proof[5]]], // [proof[6], proof[7]], // [identityCommitment, uint256(uint160(receiver))] // ); return [ BigInt(proof.pi_a[0]), BigInt(proof.pi_a[1]), BigInt(proof.pi_b[0][0]), BigInt(proof.pi_b[0][1]), BigInt(proof.pi_b[1][0]), BigInt(proof.pi_b[1][1]), BigInt(proof.pi_c[0]), BigInt(proof.pi_c[1]), ] } /** event MemberRegistered(uint256 identityCommitment, uint256 messageLimit, uint256 index); event MemberWithdrawn(uint256 index); event MemberSlashed(uint256 index, address slasher); */ export type EventMemberRegistered = { name: 'MemberRegistered', identityCommitment: bigint, messageLimit: bigint, index: bigint, } export type EventMemberWithdrawn = { name: 'MemberWithdrawn', index: bigint, } export type EventMemberSlashed = { name: 'MemberSlashed', index: bigint, slasher: string, } export class RLNContract { // Either a signer (with private key) or a provider (without private key and read-only) private provider: ethers.Provider private signer?: ethers.Signer private rlnContract: ethers.Contract private contractAtBlock: number constructor(args: { provider: ethers.Provider, signer?: ethers.Signer, contractAddress: string, contractAtBlock: number, }) { this.provider = args.provider this.signer = args.signer this.rlnContract = new ethers.Contract(args.contractAddress, rlnContractABI, this.getContractRunner()) this.contractAtBlock = args.contractAtBlock } private getContractRunner() { // If signer is given, use signer. Else, use provider. return this.signer || this.provider } async getTokenAddress() { return this.rlnContract.token() } async getSignerAddress() { if (this.signer === undefined) { throw new Error('Cannot get signer address if signer is not set') } return this.signer.getAddress() } async getLogs() { const rlnContractAddress = await this.rlnContract.getAddress() const currentBlockNumber = await this.provider.getBlockNumber() if (currentBlockNumber < this.contractAtBlock) { throw new Error('Current block number is lower than the block number at which the contract was deployed') } const logs = await this.provider.getLogs({ address: rlnContractAddress, fromBlock: this.contractAtBlock, toBlock: currentBlockNumber, }) const events = await Promise.all(logs.map(log => this.handleLog(log))) return events.filter(x => x !== undefined) as (EventMemberRegistered | EventMemberWithdrawn | EventMemberSlashed)[] } private async handleLog(log: ethers.Log): Promise<EventMemberRegistered | EventMemberWithdrawn | EventMemberSlashed | undefined> { const memberRegisteredFilter = this.rlnContract.filters.MemberRegistered() const memberWithdrawnFilter = this.rlnContract.filters.MemberWithdrawn() const memberSlashedFilter = this.rlnContract.filters.MemberSlashed() const memberRegisteredTopics: ethers.TopicFilter = await memberRegisteredFilter.getTopicFilter() const memberWithdrawnTopics: ethers.TopicFilter = await memberWithdrawnFilter.getTopicFilter() const memberSlashedTopics: ethers.TopicFilter = await memberSlashedFilter.getTopicFilter() if (log.topics[0] === memberRegisteredTopics[0]) { const decoded = this.rlnContract.interface.decodeEventLog(memberRegisteredFilter.fragment, log.data) return { name: 'MemberRegistered', identityCommitment: decoded.identityCommitment, messageLimit: decoded.messageLimit, index: decoded.index, } } else if (log.topics[0] === memberWithdrawnTopics[0]) { const decoded = this.rlnContract.interface.decodeEventLog(memberWithdrawnFilter.fragment, log.data) return { name: 'MemberWithdrawn', index: decoded.index, } } else if (log.topics[0] === memberSlashedTopics[0]) { const decoded = this.rlnContract.interface.decodeEventLog(memberSlashedFilter.fragment, log.data) return { name: 'MemberSlashed', index: decoded.index, slasher: decoded.slasher, } } else { // Just skip this log return undefined } } async register(identityCommitment: bigint, messageLimit: bigint): Promise<ethers.TransactionReceipt> { const rlnContractAddress = await this.rlnContract.getAddress() const pricePerMessageLimit = await this.rlnContract.MINIMAL_DEPOSIT() const amount = messageLimit * pricePerMessageLimit const tokenContract = new ethers.Contract( await this.getTokenAddress(), erc20ABI, this.getContractRunner(), ) const txApprove = await tokenContract.approve(rlnContractAddress, amount) await txApprove.wait() const txRegister = await this.rlnContract.register(identityCommitment, amount) const receipt = await txRegister.wait() return receipt } async getUser(identityCommitment: bigint): Promise<User> { const [ userAddress, messageLimit, index] = await this.rlnContract.members(identityCommitment) return { userAddress, messageLimit, index, } } async getWithdrawal(identityCommitment: bigint): Promise<Withdrawal> { const [ blockNumber, amount, receiver ] = await this.rlnContract.withdrawals(identityCommitment) return { blockNumber, amount, receiver, } } async withdraw(identityCommitment: bigint, proof: Proof): Promise<ethers.TransactionReceipt> { const proofArray = proofToArray(proof) const tx = await this.rlnContract.withdraw(identityCommitment, proofArray) const receipt = await tx.wait() return receipt } async release(identityCommitment: bigint): Promise<ethers.TransactionReceipt> { const tx = await this.rlnContract.release(identityCommitment) const receipt = await tx.wait() return receipt } async slash(identityCommitment: bigint, receiver: string, proof: Proof): Promise<ethers.TransactionReceipt> { const proofArray = proofToArray(proof) const tx = await this.rlnContract.slash(identityCommitment, receiver, proofArray) const receipt = await tx.wait() return receipt } async isRegistered(identityCommitment: bigint): Promise<boolean> { const user = await this.getUser(identityCommitment) return user.userAddress !== ethers.ZeroAddress } }