mirror of
https://github.com/logos-messaging/js-rln.git
synced 2026-01-02 13:43:06 +00:00
Add RLN Contract abstraction (#43)
* implement rln contract abstraction, add basic tests, add usefull constants * remove test command * resolve simple comments * move to getter for members, add init method * fix naming * remove default signature message * use direct path to js file * try different karma config * try generic import * update test * address comments: rename const file, return prev regexp * remove test * bring back test file * fix mock approach * use any for type casting * use another approach for typecasting * update mocks * update mocked event * use correct value for mock * fix spy definition * add BigInt to MembershipKey * fix joining * use slice * remove accidentally commited junk * fix typo, use DataView for conversion, use BigInt directly Co-authored-by: weboko <anon@mail.com>
This commit is contained in:
parent
fa70837558
commit
d77370fbec
@ -3,14 +3,15 @@
|
||||
"$schema": "https://raw.githubusercontent.com/streetsidesoftware/cspell/master/cspell.schema.json",
|
||||
"language": "en",
|
||||
"words": [
|
||||
"Waku",
|
||||
"arrayify",
|
||||
"circom",
|
||||
"keypair",
|
||||
"merkle",
|
||||
"nwaku",
|
||||
"vkey",
|
||||
"zkey",
|
||||
"circom",
|
||||
"Waku",
|
||||
"zerokit",
|
||||
"nwaku"
|
||||
"zkey"
|
||||
],
|
||||
"flagWords": [],
|
||||
"ignorePaths": [
|
||||
|
||||
@ -6,6 +6,8 @@
|
||||
</head>
|
||||
<body>
|
||||
<p>Open the developer tools to see the generated proof and its validation</p>
|
||||
<script src="https://cdn.ethers.io/lib/ethers-5.6.umd.min.js" type="text/javascript">
|
||||
</script>
|
||||
<script src="./index.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -38,4 +38,21 @@ rln.create().then(async rlnInstance => {
|
||||
} catch (err) {
|
||||
console.log("Invalid proof")
|
||||
}
|
||||
});
|
||||
|
||||
const provider = new ethers.providers.Web3Provider(
|
||||
window.ethereum,
|
||||
"any"
|
||||
);
|
||||
|
||||
const DEFAULT_SIGNATURE_MESSAGE =
|
||||
"The signature of this message will be used to generate your RLN credentials. Anyone accessing it may send messages on your behalf, please only share with the RLN dApp";
|
||||
|
||||
const signer = provider.getSigner();
|
||||
const signature = await signer.signMessage(DEFAULT_SIGNATURE_MESSAGE);
|
||||
console.log(`Got signature: ${signature}`);
|
||||
|
||||
const contract = await rln.RLNContract.init(rlnInstance, {address: rln.GOERLI_CONTRACT.address, provider: signer });
|
||||
|
||||
const event = await contract.registerMember(rlnInstance, signature);
|
||||
console.log(`Registered as member with ${event}`);
|
||||
});
|
||||
|
||||
20994
example/package-lock.json
generated
20994
example/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
1294
package-lock.json
generated
1294
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -59,6 +59,7 @@
|
||||
"@size-limit/preset-big-lib": "^8.0.0",
|
||||
"@types/app-root-path": "^1.2.4",
|
||||
"@types/chai": "^4.2.15",
|
||||
"@types/chai-spies": "^1.0.3",
|
||||
"@types/debug": "^4.1.7",
|
||||
"@types/mocha": "^9.1.0",
|
||||
"@types/node": "^17.0.6",
|
||||
@ -69,6 +70,7 @@
|
||||
"@web/rollup-plugin-import-meta-assets": "^1.0.7",
|
||||
"app-root-path": "^3.0.0",
|
||||
"chai": "^4.3.4",
|
||||
"chai-spies": "^1.0.0",
|
||||
"cspell": "^5.14.0",
|
||||
"eslint": "^8.6.0",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
@ -125,6 +127,7 @@
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@waku/zerokit-rln-wasm": "^0.0.5"
|
||||
"@waku/zerokit-rln-wasm": "^0.0.5",
|
||||
"ethers": "^5.7.2"
|
||||
}
|
||||
}
|
||||
|
||||
0
runtime.js
Normal file
0
runtime.js
Normal file
14
src/constants.ts
Normal file
14
src/constants.ts
Normal file
@ -0,0 +1,14 @@
|
||||
export const RLN_ABI = [
|
||||
"function MEMBERSHIP_DEPOSIT() public view returns(uint256)",
|
||||
"function register(uint256 pubkey) external payable",
|
||||
"function withdraw(uint256 secret, uint256 _pubkeyIndex, address payable receiver) external",
|
||||
"event MemberRegistered(uint256 pubkey, uint256 index)",
|
||||
"event MemberWithdrawn(uint256 pubkey, uint256 index)",
|
||||
];
|
||||
|
||||
export const GOERLI_CONTRACT = {
|
||||
chainId: 5,
|
||||
startBlock: 7109391,
|
||||
address: "0x4252105670fe33d2947e8ead304969849e64f2a6",
|
||||
abi: RLN_ABI,
|
||||
};
|
||||
15
src/index.ts
15
src/index.ts
@ -1,6 +1,8 @@
|
||||
import { RLNDecoder, RLNEncoder } from "./codec.js";
|
||||
import type { Proof, RLNInstance } from "./rln.js";
|
||||
import { GOERLI_CONTRACT, RLN_ABI } from "./constants.js";
|
||||
import { Proof, RLNInstance } from "./rln.js";
|
||||
import { MembershipKey } from "./rln.js";
|
||||
import { RLNContract } from "./rln_contract.js";
|
||||
|
||||
// reexport the create function, dynamically imported from rln.ts
|
||||
export async function create(): Promise<RLNInstance> {
|
||||
@ -11,4 +13,13 @@ export async function create(): Promise<RLNInstance> {
|
||||
return await rlnModule.create();
|
||||
}
|
||||
|
||||
export { RLNInstance, MembershipKey, Proof, RLNEncoder, RLNDecoder };
|
||||
export {
|
||||
RLNInstance,
|
||||
MembershipKey,
|
||||
Proof,
|
||||
RLNEncoder,
|
||||
RLNDecoder,
|
||||
RLNContract,
|
||||
RLN_ABI,
|
||||
GOERLI_CONTRACT,
|
||||
};
|
||||
|
||||
16
src/rln.ts
16
src/rln.ts
@ -26,6 +26,16 @@ function concatenate(...input: Uint8Array[]): Uint8Array {
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms Uint8Array into BigInt
|
||||
* @param array: Uint8Array
|
||||
* @returns BigInt
|
||||
*/
|
||||
function buildBigIntFromUint8Array(array: Uint8Array): bigint {
|
||||
const dataView = new DataView(array.buffer);
|
||||
return dataView.getBigUint64(0, true);
|
||||
}
|
||||
|
||||
const stringEncoder = new TextEncoder();
|
||||
|
||||
const DEPTH = 20;
|
||||
@ -59,13 +69,15 @@ export async function create(): Promise<RLNInstance> {
|
||||
export class MembershipKey {
|
||||
constructor(
|
||||
public readonly IDKey: Uint8Array,
|
||||
public readonly IDCommitment: Uint8Array
|
||||
public readonly IDCommitment: Uint8Array,
|
||||
public readonly IDCommitmentBigInt: bigint
|
||||
) {}
|
||||
|
||||
static fromBytes(memKeys: Uint8Array): MembershipKey {
|
||||
const idKey = memKeys.subarray(0, 32);
|
||||
const idCommitment = memKeys.subarray(32);
|
||||
return new MembershipKey(idKey, idCommitment);
|
||||
const idCommitmentBigInt = buildBigIntFromUint8Array(idCommitment);
|
||||
return new MembershipKey(idKey, idCommitment, idCommitmentBigInt);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
62
src/rln_contract.spec.ts
Normal file
62
src/rln_contract.spec.ts
Normal file
@ -0,0 +1,62 @@
|
||||
import chai from "chai";
|
||||
import spies from "chai-spies";
|
||||
import * as ethers from "ethers";
|
||||
|
||||
import * as rln from "./index.js";
|
||||
|
||||
chai.use(spies);
|
||||
|
||||
describe("RLN Contract abstraction", () => {
|
||||
it("should be able to fetch members from events and store to rln instance", async () => {
|
||||
const rlnInstance = await rln.create();
|
||||
|
||||
rlnInstance.insertMember = () => undefined;
|
||||
const insertMemberSpy = chai.spy.on(rlnInstance, "insertMember");
|
||||
|
||||
const voidSigner = new ethers.VoidSigner(rln.GOERLI_CONTRACT.address);
|
||||
const rlnContract = new rln.RLNContract({
|
||||
address: rln.GOERLI_CONTRACT.address,
|
||||
provider: voidSigner,
|
||||
});
|
||||
|
||||
rlnContract["_contract"] = {
|
||||
queryFilter: () => Promise.resolve([mockEvent()]),
|
||||
} as unknown as ethers.Contract;
|
||||
|
||||
await rlnContract.fetchMembers(rlnInstance);
|
||||
|
||||
chai.expect(insertMemberSpy).to.have.been.called();
|
||||
});
|
||||
|
||||
it("should register a member by signature", async () => {
|
||||
const mockSignature =
|
||||
"0xdeb8a6b00a8e404deb1f52d3aa72ed7f60a2ff4484c737eedaef18a0aacb2dfb4d5d74ac39bb71fa358cf2eb390565a35b026cc6272f2010d4351e17670311c21c";
|
||||
|
||||
const rlnInstance = await rln.create();
|
||||
const voidSigner = new ethers.VoidSigner(rln.GOERLI_CONTRACT.address);
|
||||
const rlnContract = new rln.RLNContract({
|
||||
address: rln.GOERLI_CONTRACT.address,
|
||||
provider: voidSigner,
|
||||
});
|
||||
|
||||
rlnContract["_contract"] = {
|
||||
register: () =>
|
||||
Promise.resolve({ wait: () => Promise.resolve(undefined) }),
|
||||
MEMBERSHIP_DEPOSIT: () => Promise.resolve(1),
|
||||
} as unknown as ethers.Contract;
|
||||
const contractSpy = chai.spy.on(rlnContract["_contract"], "register");
|
||||
|
||||
await rlnContract.registerMember(rlnInstance, mockSignature);
|
||||
|
||||
chai.expect(contractSpy).to.have.been.called();
|
||||
});
|
||||
});
|
||||
|
||||
function mockEvent(): ethers.Event {
|
||||
return {
|
||||
args: {
|
||||
pubkey: "0x9e7d3f8f8c7a1d2bef96a2e8dbb8e7c1ea9a9ab78d6b3c6c3c",
|
||||
index: 1,
|
||||
},
|
||||
} as unknown as ethers.Event;
|
||||
}
|
||||
104
src/rln_contract.ts
Normal file
104
src/rln_contract.ts
Normal file
@ -0,0 +1,104 @@
|
||||
import { ethers } from "ethers";
|
||||
|
||||
import { RLN_ABI } from "./constants.js";
|
||||
import { RLNInstance } from "./rln.js";
|
||||
|
||||
type Member = {
|
||||
pubkey: string;
|
||||
index: number;
|
||||
};
|
||||
|
||||
type ContractOptions = {
|
||||
address: string;
|
||||
provider: ethers.Signer | ethers.providers.Provider;
|
||||
};
|
||||
|
||||
export class RLNContract {
|
||||
private _contract: ethers.Contract;
|
||||
private membersFilter: ethers.EventFilter;
|
||||
|
||||
private _members: Member[] = [];
|
||||
|
||||
public static async init(
|
||||
rlnInstance: RLNInstance,
|
||||
options: ContractOptions
|
||||
): Promise<RLNContract> {
|
||||
const rlnContract = new RLNContract(options);
|
||||
|
||||
await rlnContract.fetchMembers(rlnInstance);
|
||||
rlnContract.subscribeToMembers(rlnInstance);
|
||||
|
||||
return rlnContract;
|
||||
}
|
||||
|
||||
constructor({ address, provider }: ContractOptions) {
|
||||
this._contract = new ethers.Contract(address, RLN_ABI, provider);
|
||||
this.membersFilter = this.contract.filters.MemberRegistered();
|
||||
}
|
||||
|
||||
public get contract(): ethers.Contract {
|
||||
return this._contract;
|
||||
}
|
||||
|
||||
public get members(): Member[] {
|
||||
return this._members;
|
||||
}
|
||||
|
||||
public async fetchMembers(
|
||||
rlnInstance: RLNInstance,
|
||||
fromBlock?: number
|
||||
): Promise<void> {
|
||||
const registeredMemberEvents = await this.contract.queryFilter(
|
||||
this.membersFilter,
|
||||
fromBlock
|
||||
);
|
||||
|
||||
for (const event of registeredMemberEvents) {
|
||||
this.addMemberFromEvent(rlnInstance, event);
|
||||
}
|
||||
}
|
||||
|
||||
public subscribeToMembers(rlnInstance: RLNInstance): void {
|
||||
this.contract.on(this.membersFilter, (_pubkey, _index, event) =>
|
||||
this.addMemberFromEvent(rlnInstance, event)
|
||||
);
|
||||
}
|
||||
|
||||
private addMemberFromEvent(
|
||||
rlnInstance: RLNInstance,
|
||||
event: ethers.Event
|
||||
): void {
|
||||
if (!event.args) {
|
||||
return;
|
||||
}
|
||||
|
||||
const pubkey: string = event.args.pubkey;
|
||||
const index: number = event.args.index;
|
||||
|
||||
this.members.push({ index, pubkey });
|
||||
|
||||
const idCommitment = ethers.utils.zeroPad(
|
||||
ethers.utils.arrayify(pubkey),
|
||||
32
|
||||
);
|
||||
rlnInstance.insertMember(idCommitment);
|
||||
}
|
||||
|
||||
public async registerMember(
|
||||
rlnInstance: RLNInstance,
|
||||
signature: string
|
||||
): Promise<ethers.Event | undefined> {
|
||||
const membershipKey = await rlnInstance.generateSeededMembershipKey(
|
||||
signature
|
||||
);
|
||||
const depositValue = await this.contract.MEMBERSHIP_DEPOSIT();
|
||||
|
||||
const txRegisterResponse: ethers.ContractTransaction =
|
||||
await this.contract.register(membershipKey.IDCommitmentBigInt, {
|
||||
value: depositValue,
|
||||
});
|
||||
const txRegisterReceipt = await txRegisterResponse.wait();
|
||||
|
||||
return txRegisterReceipt?.events?.[0];
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user