2025-04-07 16:04:06 +05:30
import { Logger } from "@waku/utils" ;
2025-12-01 17:32:35 -08:00
import { publicActions } from "viem" ;
2025-04-07 16:04:06 +05:30
2025-09-30 16:49:18 -07:00
import { RLN_CONTRACT } from "./contract/constants.js" ;
2025-04-07 16:04:06 +05:30
import { RLNBaseContract } from "./contract/rln_base_contract.js" ;
import { Keystore } from "./keystore/index.js" ;
import type {
DecryptedCredentials ,
EncryptedCredentials
} from "./keystore/index.js" ;
import { KeystoreEntity , Password } from "./keystore/types.js" ;
import { RegisterMembershipOptions , StartRLNOptions } from "./types.js" ;
2025-12-01 17:32:35 -08:00
import { createViemClientFromWindow , RpcClient } from "./utils/index.js" ;
2025-04-07 16:04:06 +05:30
import { Zerokit } from "./zerokit.js" ;
2025-08-27 23:32:41 +10:00
const log = new Logger ( "rln:credentials" ) ;
2025-04-07 16:04:06 +05:30
/ * *
* Manages credentials for RLN
* It is used to register membership and generate identity credentials
* /
export class RLNCredentialsManager {
protected started = false ;
protected starting = false ;
public contract : undefined | RLNBaseContract ;
2025-12-01 17:32:35 -08:00
public rpcClient : undefined | RpcClient ;
2025-04-07 16:04:06 +05:30
protected keystore = Keystore . create ( ) ;
public credentials : undefined | DecryptedCredentials ;
2025-09-30 16:49:18 -07:00
public zerokit : Zerokit ;
2025-04-07 16:04:06 +05:30
2025-09-30 16:49:18 -07:00
public constructor ( zerokit : Zerokit ) {
2025-04-07 16:04:06 +05:30
log . info ( "RLNCredentialsManager initialized" ) ;
this . zerokit = zerokit ;
}
public async start ( options : StartRLNOptions = { } ) : Promise < void > {
if ( this . started || this . starting ) {
log . info ( "RLNCredentialsManager already started or starting" ) ;
return ;
}
log . info ( "Starting RLNCredentialsManager" ) ;
this . starting = true ;
try {
const { credentials , keystore } =
await RLNCredentialsManager . decryptCredentialsIfNeeded (
options . credentials
) ;
if ( credentials ) {
log . info ( "Credentials successfully decrypted" ) ;
}
2025-12-01 17:32:35 -08:00
const { rpcClient , address , rateLimit } =
await this . determineStartOptions ( options , credentials ) ;
2025-04-07 16:04:06 +05:30
log . info ( ` Using contract address: ${ address } ` ) ;
if ( keystore ) {
this . keystore = keystore ;
log . info ( "Using provided keystore" ) ;
}
this . credentials = credentials ;
2025-12-01 17:32:35 -08:00
this . rpcClient = rpcClient ! ;
2025-05-29 16:09:08 +05:30
this . contract = await RLNBaseContract . create ( {
2025-12-01 17:32:35 -08:00
address : address ! as ` 0x ${ string } ` ,
rpcClient : this.rpcClient ,
2025-09-30 16:49:18 -07:00
rateLimit : rateLimit ? ? this . zerokit . rateLimit
2025-04-07 16:04:06 +05:30
} ) ;
log . info ( "RLNCredentialsManager successfully started" ) ;
this . started = true ;
} catch ( error ) {
log . error ( "Failed to start RLNCredentialsManager" , error ) ;
throw error ;
} finally {
this . starting = false ;
}
}
public async registerMembership (
options : RegisterMembershipOptions
) : Promise < undefined | DecryptedCredentials > {
if ( ! this . contract ) {
log . error ( "RLN Contract is not initialized" ) ;
throw Error ( "RLN Contract is not initialized." ) ;
}
log . info ( "Registering membership" ) ;
let identity = "identity" in options && options . identity ;
if ( "signature" in options ) {
2025-09-30 16:49:18 -07:00
log . info ( "Using Zerokit to generate identity" ) ;
identity = this . zerokit . generateSeededIdentityCredential (
options . signature
) ;
2025-04-07 16:04:06 +05:30
}
if ( ! identity ) {
log . error ( "Missing signature or identity to register membership" ) ;
throw Error ( "Missing signature or identity to register membership." ) ;
}
log . info ( "Registering identity with contract" ) ;
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 > {
log . info ( ` Attempting to use credentials with ID: ${ id } ` ) ;
this . credentials = await this . keystore ? . readCredential ( id , password ) ;
if ( this . credentials ) {
log . info ( "Successfully loaded credentials" ) ;
} else {
log . warn ( "Failed to load credentials" ) ;
}
}
protected async determineStartOptions (
options : StartRLNOptions ,
credentials : KeystoreEntity | undefined
2025-12-01 17:32:35 -08:00
) : Promise < StartRLNOptions & { rpcClient : RpcClient } > {
2025-04-07 16:04:06 +05:30
let chainId = credentials ? . membership . chainId ;
const address =
credentials ? . membership . address ||
options . address ||
2025-07-16 15:16:13 +05:30
RLN_CONTRACT . address ;
2025-04-07 16:04:06 +05:30
2025-07-16 15:16:13 +05:30
if ( address === RLN_CONTRACT . address ) {
chainId = RLN_CONTRACT . chainId . toString ( ) ;
2025-04-07 16:04:06 +05:30
log . info ( ` Using Linea contract with chainId: ${ chainId } ` ) ;
}
2025-12-01 17:32:35 -08:00
const rpcClient : RpcClient = options . walletClient
? options . walletClient . extend ( publicActions )
: await createViemClientFromWindow ( ) ;
const currentChainId = rpcClient . chain ? . id ;
2025-04-07 16:04:06 +05:30
log . info ( ` Current chain ID: ${ currentChainId } ` ) ;
2025-12-01 17:32:35 -08:00
if ( chainId && chainId !== currentChainId ? . toString ( ) ) {
2025-04-07 16:04:06 +05:30
log . error (
` Chain ID mismatch: contract= ${ chainId } , current= ${ currentChainId } `
) ;
throw Error (
` Failed to start RLN contract, chain ID of contract is different from current one: contract- ${ chainId } , current network- ${ currentChainId } `
) ;
}
return {
2025-12-01 17:32:35 -08:00
rpcClient ,
2025-04-07 16:04:06 +05:30
address
} ;
}
protected static async decryptCredentialsIfNeeded (
credentials? : EncryptedCredentials | DecryptedCredentials
) : Promise < { credentials? : DecryptedCredentials ; keystore? : Keystore } > {
if ( ! credentials ) {
log . info ( "No credentials provided" ) ;
return { } ;
}
if ( "identity" in credentials ) {
log . info ( "Using already decrypted credentials" ) ;
return { credentials } ;
}
log . info ( "Attempting to decrypt credentials" ) ;
const keystore = Keystore . fromString ( credentials . keystore ) ;
if ( ! keystore ) {
log . warn ( "Failed to create keystore from string" ) ;
return { } ;
}
try {
const decryptedCredentials = await keystore . readCredential (
credentials . id ,
credentials . password
) ;
log . info ( ` Successfully decrypted credentials with ID: ${ credentials . id } ` ) ;
return {
keystore ,
credentials : decryptedCredentials
} ;
} catch ( error ) {
log . error ( "Failed to decrypt credentials" , error ) ;
throw error ;
}
}
protected async verifyCredentialsAgainstContract (
credentials : KeystoreEntity
) : Promise < void > {
2025-12-01 17:32:35 -08:00
if ( ! this . contract || ! this . rpcClient ) {
2025-04-07 16:04:06 +05:30
throw Error (
2025-12-01 17:32:35 -08:00
"Failed to verify chain coordinates: no contract or viem client initialized."
2025-04-07 16:04:06 +05:30
) ;
}
const registryAddress = credentials . membership . address ;
const currentRegistryAddress = this . contract . address ;
if ( registryAddress !== currentRegistryAddress ) {
throw Error (
` Failed to verify chain coordinates: credentials contract address= ${ registryAddress } is not equal to registryContract address= ${ currentRegistryAddress } `
) ;
}
const chainId = credentials . membership . chainId ;
2025-12-01 17:32:35 -08:00
const currentChainId = await this . rpcClient . getChainId ( ) ;
2025-04-10 14:42:57 +05:30
if ( chainId !== currentChainId . toString ( ) ) {
2025-04-07 16:04:06 +05:30
throw Error (
` Failed to verify chain coordinates: credentials chainID= ${ chainId } is not equal to registryContract chainID= ${ currentChainId } `
) ;
}
}
}