mirror of
https://github.com/logos-messaging/logos-delivery-js.git
synced 2026-05-18 05:19:36 +00:00
236 lines
6.5 KiB
TypeScript
236 lines
6.5 KiB
TypeScript
import { hmac } from "@noble/hashes/hmac";
|
|
import { sha256 } from "@noble/hashes/sha256";
|
|
import { Logger } from "@waku/utils";
|
|
import { ethers } from "ethers";
|
|
|
|
import { SEPOLIA_CONTRACT } from "./contract/constants.js";
|
|
import { RLNLightContract } from "./contract/rln_light_contract.js";
|
|
import { IdentityCredential } from "./identity.js";
|
|
import { Keystore } from "./keystore/index.js";
|
|
import type {
|
|
DecryptedCredentials,
|
|
EncryptedCredentials
|
|
} from "./keystore/index.js";
|
|
import { KeystoreEntity, Password } from "./keystore/types.js";
|
|
import {
|
|
buildBigIntFromUint8Array,
|
|
extractMetaMaskSigner
|
|
} from "./utils/index.js";
|
|
|
|
const log = new Logger("waku:rln");
|
|
|
|
/**
|
|
* Create an instance of RLN
|
|
* @returns RLNInstance
|
|
*/
|
|
export async function create(): Promise<RLNLightInstance> {
|
|
try {
|
|
return new RLNLightInstance();
|
|
} catch (error) {
|
|
log.error("Failed to initialize RLN:", error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
type StartRLNOptions = {
|
|
/**
|
|
* If not set - will extract MetaMask account and get signer from it.
|
|
*/
|
|
signer?: ethers.Signer;
|
|
/**
|
|
* If not set - will use default SEPOLIA_CONTRACT address.
|
|
*/
|
|
address?: string;
|
|
/**
|
|
* Credentials to use for generating proofs and connecting to the contract and network.
|
|
* If provided used for validating the network chainId and connecting to registry contract.
|
|
*/
|
|
credentials?: EncryptedCredentials | DecryptedCredentials;
|
|
/**
|
|
* Rate limit for the member.
|
|
*/
|
|
rateLimit?: number;
|
|
};
|
|
|
|
type RegisterMembershipOptions =
|
|
| { signature: string }
|
|
| { identity: IdentityCredential };
|
|
|
|
export class RLNLightInstance {
|
|
private started = false;
|
|
private starting = false;
|
|
|
|
private _contract: undefined | RLNLightContract;
|
|
private _signer: undefined | ethers.Signer;
|
|
|
|
private keystore = Keystore.create();
|
|
private _credentials: undefined | DecryptedCredentials;
|
|
|
|
public constructor() {}
|
|
|
|
public get contract(): undefined | RLNLightContract {
|
|
return this._contract;
|
|
}
|
|
|
|
public get signer(): undefined | ethers.Signer {
|
|
return this._signer;
|
|
}
|
|
|
|
public async start(options: StartRLNOptions = {}): Promise<void> {
|
|
if (this.started || this.starting) {
|
|
return;
|
|
}
|
|
|
|
this.starting = true;
|
|
|
|
try {
|
|
const { credentials, keystore } =
|
|
await RLNLightInstance.decryptCredentialsIfNeeded(options.credentials);
|
|
const { signer, address, rateLimit } = await this.determineStartOptions(
|
|
options,
|
|
credentials
|
|
);
|
|
|
|
if (keystore) {
|
|
this.keystore = keystore;
|
|
}
|
|
|
|
this._credentials = credentials;
|
|
this._signer = signer!;
|
|
this._contract = await RLNLightContract.init({
|
|
address: address!,
|
|
signer: signer!,
|
|
rateLimit: rateLimit
|
|
});
|
|
this.started = true;
|
|
} finally {
|
|
this.starting = false;
|
|
}
|
|
}
|
|
|
|
public get credentials(): DecryptedCredentials | undefined {
|
|
return this._credentials;
|
|
}
|
|
|
|
private async determineStartOptions(
|
|
options: StartRLNOptions,
|
|
credentials: KeystoreEntity | undefined
|
|
): Promise<StartRLNOptions> {
|
|
let chainId = credentials?.membership.chainId;
|
|
const address =
|
|
credentials?.membership.address ||
|
|
options.address ||
|
|
SEPOLIA_CONTRACT.address;
|
|
|
|
if (address === SEPOLIA_CONTRACT.address) {
|
|
chainId = SEPOLIA_CONTRACT.chainId;
|
|
}
|
|
|
|
const signer = options.signer || (await extractMetaMaskSigner());
|
|
const currentChainId = await signer.getChainId();
|
|
|
|
if (chainId && chainId !== currentChainId) {
|
|
throw Error(
|
|
`Failed to start RLN contract, chain ID of contract is different from current one: contract-${chainId}, current network-${currentChainId}`
|
|
);
|
|
}
|
|
|
|
return {
|
|
signer,
|
|
address
|
|
};
|
|
}
|
|
|
|
private static async decryptCredentialsIfNeeded(
|
|
credentials?: EncryptedCredentials | DecryptedCredentials
|
|
): Promise<{ credentials?: DecryptedCredentials; keystore?: Keystore }> {
|
|
if (!credentials) {
|
|
return {};
|
|
}
|
|
|
|
if ("identity" in credentials) {
|
|
return { credentials };
|
|
}
|
|
|
|
const keystore = Keystore.fromString(credentials.keystore);
|
|
|
|
if (!keystore) {
|
|
return {};
|
|
}
|
|
|
|
const decryptedCredentials = await keystore.readCredential(
|
|
credentials.id,
|
|
credentials.password
|
|
);
|
|
|
|
return {
|
|
keystore,
|
|
credentials: decryptedCredentials
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Generates an identity credential from a seed string
|
|
* This is a pure implementation that doesn't rely on Zerokit
|
|
* @param seed A string seed to generate the identity from
|
|
* @returns IdentityCredential
|
|
*/
|
|
private generateSeededIdentityCredential(seed: string): IdentityCredential {
|
|
// Convert the seed to bytes
|
|
const encoder = new TextEncoder();
|
|
const seedBytes = encoder.encode(seed);
|
|
|
|
// Generate deterministic values using HMAC-SHA256
|
|
// We use different context strings for each component to ensure they're different
|
|
const idTrapdoor = hmac(sha256, seedBytes, encoder.encode("IDTrapdoor"));
|
|
const idNullifier = hmac(sha256, seedBytes, encoder.encode("IDNullifier"));
|
|
|
|
// Generate IDSecretHash as a hash of IDTrapdoor and IDNullifier
|
|
const combinedBytes = new Uint8Array([...idTrapdoor, ...idNullifier]);
|
|
const idSecretHash = sha256(combinedBytes);
|
|
|
|
// Generate IDCommitment as a hash of IDSecretHash
|
|
const idCommitment = sha256(idSecretHash);
|
|
|
|
// Convert IDCommitment to BigInt
|
|
const idCommitmentBigInt = buildBigIntFromUint8Array(idCommitment);
|
|
|
|
return new IdentityCredential(
|
|
idTrapdoor,
|
|
idNullifier,
|
|
idSecretHash,
|
|
idCommitment,
|
|
idCommitmentBigInt
|
|
);
|
|
}
|
|
|
|
public async registerMembership(
|
|
options: RegisterMembershipOptions
|
|
): Promise<undefined | DecryptedCredentials> {
|
|
if (!this.contract) {
|
|
throw Error("RLN Contract is not initialized.");
|
|
}
|
|
|
|
let identity = "identity" in options && options.identity;
|
|
|
|
if ("signature" in options) {
|
|
identity = this.generateSeededIdentityCredential(options.signature);
|
|
}
|
|
|
|
if (!identity) {
|
|
throw Error("Missing signature or identity to register membership.");
|
|
}
|
|
|
|
return this.contract.registerWithIdentity(identity);
|
|
}
|
|
|
|
/**
|
|
* Changes credentials in use by relying on provided Keystore earlier in rln.start
|
|
* @param id: string, hash of credentials to select from Keystore
|
|
* @param password: string or bytes to use to decrypt credentials from Keystore
|
|
*/
|
|
public async useCredentials(id: string, password: Password): Promise<void> {
|
|
this._credentials = await this.keystore?.readCredential(id, password);
|
|
}
|
|
}
|