diff --git a/tests/factory/test_external_config.nim b/tests/factory/test_external_config.nim index b79553034..52e82a425 100644 --- a/tests/factory/test_external_config.nim +++ b/tests/factory/test_external_config.nim @@ -262,56 +262,111 @@ suite "Waku external config - Shards": suite "Waku external config - http url parsing": test "Basic HTTP URLs without authentication": - check string(parseCmdArg(EthRpcUrl, "https://example.com/path")) == "https://example.com/path" - check string(parseCmdArg(EthRpcUrl, "https://example.com/")) == "https://example.com/" - check string(parseCmdArg(EthRpcUrl, "http://localhost:8545")) == "http://localhost:8545" - check string(parseCmdArg(EthRpcUrl, "https://mainnet.infura.io")) == "https://mainnet.infura.io" + check string(parseCmdArg(EthRpcUrl, "https://example.com/path")) == + "https://example.com/path" + check string(parseCmdArg(EthRpcUrl, "https://example.com/")) == + "https://example.com/" + check string(parseCmdArg(EthRpcUrl, "http://localhost:8545")) == + "http://localhost:8545" + check string(parseCmdArg(EthRpcUrl, "https://mainnet.infura.io")) == + "https://mainnet.infura.io" test "Basic authentication with simple credentials": - check string(parseCmdArg(EthRpcUrl, "https://user:pass@example.com/path")) == "https://user:pass@example.com/path" - check string(parseCmdArg(EthRpcUrl, "https://john.doe:secret123@example.com/api/v1")) == "https://john.doe:secret123@example.com/api/v1" - check string(parseCmdArg(EthRpcUrl, "https://user_name:pass_word@example.com/")) == "https://user_name:pass_word@example.com/" - check string(parseCmdArg(EthRpcUrl, "https://user-name:pass-word@example.com/")) == "https://user-name:pass-word@example.com/" - check string(parseCmdArg(EthRpcUrl, "https://user123:pass456@example.com/")) == "https://user123:pass456@example.com/" + check string(parseCmdArg(EthRpcUrl, "https://user:pass@example.com/path")) == + "https://user:pass@example.com/path" + check string( + parseCmdArg(EthRpcUrl, "https://john.doe:secret123@example.com/api/v1") + ) == "https://john.doe:secret123@example.com/api/v1" + check string(parseCmdArg(EthRpcUrl, "https://user_name:pass_word@example.com/")) == + "https://user_name:pass_word@example.com/" + check string(parseCmdArg(EthRpcUrl, "https://user-name:pass-word@example.com/")) == + "https://user-name:pass-word@example.com/" + check string(parseCmdArg(EthRpcUrl, "https://user123:pass456@example.com/")) == + "https://user123:pass456@example.com/" test "Special characters (percent-encoded) in credentials": - check string(parseCmdArg(EthRpcUrl, "https://user%40email:pass%21%23%24@example.com/")) == "https://user%40email:pass%21%23%24@example.com/" - check string(parseCmdArg(EthRpcUrl, "https://user%2Bplus:pass%26and@example.com/")) == "https://user%2Bplus:pass%26and@example.com/" - check string(parseCmdArg(EthRpcUrl, "https://user%3Acolon:pass%3Bsemi@example.com/")) == "https://user%3Acolon:pass%3Bsemi@example.com/" - check string(parseCmdArg(EthRpcUrl, "https://user%2Fslash:pass%3Fquest@example.com/")) == "https://user%2Fslash:pass%3Fquest@example.com/" - check string(parseCmdArg(EthRpcUrl, "https://user%5Bbracket:pass%5Dbracket@example.com/")) == "https://user%5Bbracket:pass%5Dbracket@example.com/" - check string(parseCmdArg(EthRpcUrl, "https://user%20space:pass%20space@example.com/")) == "https://user%20space:pass%20space@example.com/" - check string(parseCmdArg(EthRpcUrl, "https://user%3Cless:pass%3Egreater@example.com/")) == "https://user%3Cless:pass%3Egreater@example.com/" - check string(parseCmdArg(EthRpcUrl, "https://user%7Bbrace:pass%7Dbrace@example.com/")) == "https://user%7Bbrace:pass%7Dbrace@example.com/" - check string(parseCmdArg(EthRpcUrl, "https://user%5Cback:pass%7Cpipe@example.com/")) == "https://user%5Cback:pass%7Cpipe@example.com/" + check string( + parseCmdArg(EthRpcUrl, "https://user%40email:pass%21%23%24@example.com/") + ) == "https://user%40email:pass%21%23%24@example.com/" + check string(parseCmdArg(EthRpcUrl, "https://user%2Bplus:pass%26and@example.com/")) == + "https://user%2Bplus:pass%26and@example.com/" + check string( + parseCmdArg(EthRpcUrl, "https://user%3Acolon:pass%3Bsemi@example.com/") + ) == "https://user%3Acolon:pass%3Bsemi@example.com/" + check string( + parseCmdArg(EthRpcUrl, "https://user%2Fslash:pass%3Fquest@example.com/") + ) == "https://user%2Fslash:pass%3Fquest@example.com/" + check string( + parseCmdArg(EthRpcUrl, "https://user%5Bbracket:pass%5Dbracket@example.com/") + ) == "https://user%5Bbracket:pass%5Dbracket@example.com/" + check string( + parseCmdArg(EthRpcUrl, "https://user%20space:pass%20space@example.com/") + ) == "https://user%20space:pass%20space@example.com/" + check string( + parseCmdArg(EthRpcUrl, "https://user%3Cless:pass%3Egreater@example.com/") + ) == "https://user%3Cless:pass%3Egreater@example.com/" + check string( + parseCmdArg(EthRpcUrl, "https://user%7Bbrace:pass%7Dbrace@example.com/") + ) == "https://user%7Bbrace:pass%7Dbrace@example.com/" + check string(parseCmdArg(EthRpcUrl, "https://user%5Cback:pass%7Cpipe@example.com/")) == + "https://user%5Cback:pass%7Cpipe@example.com/" test "Complex passwords with special characters": - check string(parseCmdArg(EthRpcUrl, "https://admin:P%40ssw0rd%21%23%24%25%5E%26*()@example.com/")) == "https://admin:P%40ssw0rd%21%23%24%25%5E%26*()@example.com/" - check string(parseCmdArg(EthRpcUrl, "https://user:abc123%21%40%23DEF456@example.com/")) == "https://user:abc123%21%40%23DEF456@example.com/" - check string(parseCmdArg(EthRpcUrl, "https://user:P%40%24%24w0rd%21%23%24%25%5E%26%2A%28%29_%2B-%3D%5B%5D%7B%7D%7C%3B%27%3A%22%2C.%2F%3C%3E%3F%60~%5C@example.com")) == "https://user:P%40%24%24w0rd%21%23%24%25%5E%26%2A%28%29_%2B-%3D%5B%5D%7B%7D%7C%3B%27%3A%22%2C.%2F%3C%3E%3F%60~%5C@example.com" + check string( + parseCmdArg( + EthRpcUrl, "https://admin:P%40ssw0rd%21%23%24%25%5E%26*()@example.com/" + ) + ) == "https://admin:P%40ssw0rd%21%23%24%25%5E%26*()@example.com/" + check string( + parseCmdArg(EthRpcUrl, "https://user:abc123%21%40%23DEF456@example.com/") + ) == "https://user:abc123%21%40%23DEF456@example.com/" + check string( + parseCmdArg( + EthRpcUrl, + "https://user:P%40%24%24w0rd%21%23%24%25%5E%26%2A%28%29_%2B-%3D%5B%5D%7B%7D%7C%3B%27%3A%22%2C.%2F%3C%3E%3F%60~%5C@example.com", + ) + ) == + "https://user:P%40%24%24w0rd%21%23%24%25%5E%26%2A%28%29_%2B-%3D%5B%5D%7B%7D%7C%3B%27%3A%22%2C.%2F%3C%3E%3F%60~%5C@example.com" test "Different hostname types": - check string(parseCmdArg(EthRpcUrl, "https://user:pass@subdomain.example.com/path")) == "https://user:pass@subdomain.example.com/path" - check string(parseCmdArg(EthRpcUrl, "https://user:pass@192.168.1.1/admin")) == "https://user:pass@192.168.1.1/admin" - check string(parseCmdArg(EthRpcUrl, "https://user:pass@[2001:db8::1]/path")) == "https://user:pass@[2001:db8::1]/path" - check string(parseCmdArg(EthRpcUrl, "https://user:pass@example.co.uk/path")) == "https://user:pass@example.co.uk/path" + check string(parseCmdArg(EthRpcUrl, "https://user:pass@subdomain.example.com/path")) == + "https://user:pass@subdomain.example.com/path" + check string(parseCmdArg(EthRpcUrl, "https://user:pass@192.168.1.1/admin")) == + "https://user:pass@192.168.1.1/admin" + check string(parseCmdArg(EthRpcUrl, "https://user:pass@[2001:db8::1]/path")) == + "https://user:pass@[2001:db8::1]/path" + check string(parseCmdArg(EthRpcUrl, "https://user:pass@example.co.uk/path")) == + "https://user:pass@example.co.uk/path" test "URLs with port numbers": - check string(parseCmdArg(EthRpcUrl, "https://user:pass@example.com:8080/path")) == "https://user:pass@example.com:8080/path" - check string(parseCmdArg(EthRpcUrl, "https://user:pass@example.com:443/")) == "https://user:pass@example.com:443/" - check string(parseCmdArg(EthRpcUrl, "http://user:pass@example.com:80/path")) == "http://user:pass@example.com:80/path" + check string(parseCmdArg(EthRpcUrl, "https://user:pass@example.com:8080/path")) == + "https://user:pass@example.com:8080/path" + check string(parseCmdArg(EthRpcUrl, "https://user:pass@example.com:443/")) == + "https://user:pass@example.com:443/" + check string(parseCmdArg(EthRpcUrl, "http://user:pass@example.com:80/path")) == + "http://user:pass@example.com:80/path" test "URLs with query parameters and fragments": - check string(parseCmdArg(EthRpcUrl, "https://user:pass@example.com/path?query=1#section")) == "https://user:pass@example.com/path?query=1#section" - check string(parseCmdArg(EthRpcUrl, "https://user:pass@example.com/?foo=bar&baz=qux")) == "https://user:pass@example.com/?foo=bar&baz=qux" - check string(parseCmdArg(EthRpcUrl, "https://api.example.com/rpc?key=value")) == "https://api.example.com/rpc?key=value" - check string(parseCmdArg(EthRpcUrl, "https://api.example.com/rpc#section")) == "https://api.example.com/rpc#section" + check string( + parseCmdArg(EthRpcUrl, "https://user:pass@example.com/path?query=1#section") + ) == "https://user:pass@example.com/path?query=1#section" + check string( + parseCmdArg(EthRpcUrl, "https://user:pass@example.com/?foo=bar&baz=qux") + ) == "https://user:pass@example.com/?foo=bar&baz=qux" + check string(parseCmdArg(EthRpcUrl, "https://api.example.com/rpc?key=value")) == + "https://api.example.com/rpc?key=value" + check string(parseCmdArg(EthRpcUrl, "https://api.example.com/rpc#section")) == + "https://api.example.com/rpc#section" test "Edge cases with credentials": - check string(parseCmdArg(EthRpcUrl, "https://a:b@example.com/")) == "https://a:b@example.com/" - check string(parseCmdArg(EthRpcUrl, "https://user:@example.com/")) == "https://user:@example.com/" - check string(parseCmdArg(EthRpcUrl, "https://:pass@example.com/")) == "https://:pass@example.com/" - check string(parseCmdArg(EthRpcUrl, "http://user:pass@example.com/")) == "http://user:pass@example.com/" + check string(parseCmdArg(EthRpcUrl, "https://a:b@example.com/")) == + "https://a:b@example.com/" + check string(parseCmdArg(EthRpcUrl, "https://user:@example.com/")) == + "https://user:@example.com/" + check string(parseCmdArg(EthRpcUrl, "https://:pass@example.com/")) == + "https://:pass@example.com/" + check string(parseCmdArg(EthRpcUrl, "http://user:pass@example.com/")) == + "http://user:pass@example.com/" test "Websocket URLs are rejected": expect(ValueError): diff --git a/waku/waku_rln_relay/group_manager/on_chain/group_manager.nim b/waku/waku_rln_relay/group_manager/on_chain/group_manager.nim index 4f2fb5228..311739237 100644 --- a/waku/waku_rln_relay/group_manager/on_chain/group_manager.nim +++ b/waku/waku_rln_relay/group_manager/on_chain/group_manager.nim @@ -20,40 +20,14 @@ import ../../rln/rln_interface, ../../conversion_utils, ../group_manager_base, - ./retry_wrapper + ./retry_wrapper, + ./rpc_wrapper export group_manager_base logScope: topics = "waku rln_relay onchain_group_manager" -# using the when predicate does not work within the contract macro, hence need to dupe -contract(WakuRlnContract): - # this serves as an entrypoint into the rln membership set - proc register( - idCommitment: UInt256, userMessageLimit: UInt32, idCommitmentsToErase: seq[UInt256] - ) - - # Initializes the implementation contract (only used in unit tests) - proc initialize(maxMessageLimit: UInt256) - # this event is emitted when a new member is registered - proc MembershipRegistered( - idCommitment: UInt256, membershipRateLimit: UInt256, index: UInt32 - ) {.event.} - - # this function denotes existence of a given user - proc isInMembershipSet(idCommitment: Uint256): bool {.view.} - # this constant describes the next index of a new member - proc nextFreeIndex(): UInt256 {.view.} - # this constant describes the block number this contract was deployed on - proc deployedBlockNumber(): UInt256 {.view.} - # this constant describes max message limit of rln contract - proc maxMembershipRateLimit(): UInt256 {.view.} - # this function returns the merkleProof for a given index - # proc getMerkleProof(index: EthereumUInt40): seq[array[32, byte]] {.view.} - # this function returns the Merkle root - proc root(): Uint256 {.view.} - type WakuRlnContractWithSender = Sender[WakuRlnContract] OnchainGroupManager* = ref object of GroupManager @@ -70,6 +44,106 @@ type latestProcessedBlock*: BlockNumber merkleProofCache*: seq[byte] +# The below code is not working with the latest web3 version due to chainId being null (specifically on linea-sepolia) +# TODO: find better solution than this custom sendEthCallWithoutParams call + +proc fetchMerkleProofElements*( + g: OnchainGroupManager +): Future[Result[seq[byte], string]] {.async.} = + try: + let membershipIndex = g.membershipIndex.get() + let index40 = stuint(membershipIndex, 40) + + let methodSig = "getMerkleProof(uint40)" + var paddedParam = newSeq[byte](32) + let indexBytes = index40.toBytesBE() + for i in 0 ..< min(indexBytes.len, paddedParam.len): + paddedParam[paddedParam.len - indexBytes.len + i] = indexBytes[i] + + let response = await sendEthCallWithParams( + ethRpc = g.ethRpc.get(), + functionSignature = methodSig, + params = paddedParam, + fromAddress = g.ethRpc.get().defaultAccount, + toAddress = fromHex(Address, g.ethContractAddress), + chainId = g.chainId, + ) + + return response + except CatchableError: + error "Failed to fetch Merkle proof elements", error = getCurrentExceptionMsg() + return err("Failed to fetch merkle proof elements: " & getCurrentExceptionMsg()) + +proc fetchMerkleRoot*( + g: OnchainGroupManager +): Future[Result[UInt256, string]] {.async.} = + try: + let merkleRoot = await sendEthCallWithoutParams( + ethRpc = g.ethRpc.get(), + functionSignature = "root()", + fromAddress = g.ethRpc.get().defaultAccount, + toAddress = fromHex(Address, g.ethContractAddress), + chainId = g.chainId, + ) + return merkleRoot + except CatchableError: + error "Failed to fetch Merkle root", error = getCurrentExceptionMsg() + return err("Failed to fetch merkle root: " & getCurrentExceptionMsg()) + +proc fetchNextFreeIndex*( + g: OnchainGroupManager +): Future[Result[UInt256, string]] {.async.} = + try: + let nextFreeIndex = await sendEthCallWithoutParams( + ethRpc = g.ethRpc.get(), + functionSignature = "nextFreeIndex()", + fromAddress = g.ethRpc.get().defaultAccount, + toAddress = fromHex(Address, g.ethContractAddress), + chainId = g.chainId, + ) + return nextFreeIndex + except CatchableError: + error "Failed to fetch next free index", error = getCurrentExceptionMsg() + return err("Failed to fetch next free index: " & getCurrentExceptionMsg()) + +proc fetchMembershipStatus*( + g: OnchainGroupManager, idCommitment: IDCommitment +): Future[Result[bool, string]] {.async.} = + try: + let params = idCommitment.reversed() + let resultBytes = await sendEthCallWithParams( + ethRpc = g.ethRpc.get(), + functionSignature = "isInMembershipSet(uint256)", + params = params, + fromAddress = g.ethRpc.get().defaultAccount, + toAddress = fromHex(Address, g.ethContractAddress), + chainId = g.chainId, + ) + if resultBytes.isErr(): + return err("Failed to check membership: " & resultBytes.error) + let responseBytes = resultBytes.get() + + return ok(responseBytes.len == 32 and responseBytes[^1] == 1'u8) + except CatchableError: + error "Failed to fetch membership set membership", error = getCurrentExceptionMsg() + return err("Failed to fetch membership set membership: " & getCurrentExceptionMsg()) + +proc fetchMaxMembershipRateLimit*( + g: OnchainGroupManager +): Future[Result[UInt256, string]] {.async.} = + try: + let maxMembershipRateLimit = await sendEthCallWithoutParams( + ethRpc = g.ethRpc.get(), + functionSignature = "maxMembershipRateLimit()", + fromAddress = g.ethRpc.get().defaultAccount, + toAddress = fromHex(Address, g.ethContractAddress), + chainId = g.chainId, + ) + return maxMembershipRateLimit + except CatchableError: + error "Failed to fetch max membership rate limit", error = getCurrentExceptionMsg() + return err("Failed to fetch max membership rate limit: " & getCurrentExceptionMsg()) + proc setMetadata*( g: OnchainGroupManager, lastProcessedBlock = none(BlockNumber) ): GroupManagerResult[void] = @@ -89,113 +163,6 @@ proc setMetadata*( return err("failed to persist rln metadata: " & getCurrentExceptionMsg()) return ok() -proc sendEthCallWithChainId( - ethRpc: Web3, - functionSignature: string, - fromAddress: Address, - toAddress: Address, - chainId: UInt256, -): Future[Result[UInt256, string]] {.async.} = - ## Workaround for web3 chainId=null issue on some networks (e.g., linea-sepolia) - ## Makes contract calls with explicit chainId for view functions with no parameters - let functionHash = - keccak256.digest(functionSignature.toOpenArrayByte(0, functionSignature.len - 1)) - let functionSelector = functionHash.data[0 .. 3] - let dataSignature = "0x" & functionSelector.mapIt(it.toHex(2)).join("") - - var tx: TransactionArgs - tx.`from` = Opt.some(fromAddress) - tx.to = Opt.some(toAddress) - tx.value = Opt.some(0.u256) - tx.data = Opt.some(byteutils.hexToSeqByte(dataSignature)) - tx.chainId = Opt.some(chainId) - - let resultBytes = await ethRpc.provider.eth_call(tx, "latest") - if resultBytes.len == 0: - return err("No result returned for function call: " & functionSignature) - return ok(UInt256.fromBytesBE(resultBytes)) - -proc sendEthCallWithParams( - ethRpc: Web3, - functionSignature: string, - params: seq[byte], - fromAddress: Address, - toAddress: Address, - chainId: UInt256, -): Future[Result[seq[byte], string]] {.async.} = - ## Workaround for web3 chainId=null issue with parameterized contract calls - let functionHash = - keccak256.digest(functionSignature.toOpenArrayByte(0, functionSignature.len - 1)) - let functionSelector = functionHash.data[0 .. 3] - let callData = functionSelector & params - - var tx: TransactionArgs - tx.`from` = Opt.some(fromAddress) - tx.to = Opt.some(toAddress) - tx.value = Opt.some(0.u256) - tx.data = Opt.some(callData) - tx.chainId = Opt.some(chainId) - - let resultBytes = await ethRpc.provider.eth_call(tx, "latest") - return ok(resultBytes) - -proc fetchMerkleProofElements*( - g: OnchainGroupManager -): Future[Result[seq[byte], string]] {.async.} = - try: - # let merkleRootInvocation = g.wakuRlnContract.get().root() - # let merkleRoot = await merkleRootInvocation.call() - # The above code is not working with the latest web3 version due to chainId being null (specifically on linea-sepolia) - # TODO: find better solution than this custom sendEthCallWithChainId call - let membershipIndex = g.membershipIndex.get() - let index40 = stuint(membershipIndex, 40) - - let methodSig = "getMerkleProof(uint40)" - let methodIdDigest = keccak.keccak256.digest(methodSig) - let methodId = methodIdDigest.data[0 .. 3] - - var paddedParam = newSeq[byte](32) - let indexBytes = index40.toBytesBE() - for i in 0 ..< min(indexBytes.len, paddedParam.len): - paddedParam[paddedParam.len - indexBytes.len + i] = indexBytes[i] - - var callData = newSeq[byte]() - for b in methodId: - callData.add(b) - callData.add(paddedParam) - - var tx: TransactionArgs - tx.to = Opt.some(fromHex(Address, g.ethContractAddress)) - tx.data = Opt.some(callData) - tx.chainId = Opt.some(g.chainId) # Explicitly set the chain ID - - let responseBytes = await g.ethRpc.get().provider.eth_call(tx, "latest") - - return ok(responseBytes) - except CatchableError: - error "Failed to fetch Merkle proof elements", error = getCurrentExceptionMsg() - return err("Failed to fetch merkle proof elements: " & getCurrentExceptionMsg()) - -proc fetchMerkleRoot*( - g: OnchainGroupManager -): Future[Result[UInt256, string]] {.async.} = - try: - let merkleRoot = ( - await sendEthCallWithChainId( - ethRpc = g.ethRpc.get(), - functionSignature = "root()", - fromAddress = g.ethRpc.get().defaultAccount, - toAddress = fromHex(Address, g.ethContractAddress), - chainId = g.chainId, - ) - ).valueOr: - error "Failed to fetch Merkle root", error = $error - return err("Failed to fetch merkle root: " & $error) - return ok(merkleRoot) - except CatchableError: - error "Failed to fetch Merkle root", error = getCurrentExceptionMsg() - return err("Failed to fetch merkle root: " & getCurrentExceptionMsg()) - template initializedGuard(g: OnchainGroupManager): untyped = if not g.initialized: raise newException(CatchableError, "OnchainGroupManager is not initialized") @@ -250,19 +217,7 @@ proc trackRootChanges*(g: OnchainGroupManager) {.async: (raises: [CatchableError error "Failed to fetch Merkle proof", error = proofResult.error g.merkleProofCache = proofResult.get() - # also need to update registered membership - # g.rlnRelayMaxMessageLimit = - # cast[uint64](await wakuRlnContract.nextFreeIndex().call()) - # The above code is not working with the latest web3 version due to chainId being null (specifically on linea-sepolia) - # TODO: find better solution than this custom sendEthCallWithChainId call - let nextFreeIndex = await sendEthCallWithChainId( - ethRpc = ethRpc, - functionSignature = "nextFreeIndex()", - fromAddress = ethRpc.defaultAccount, - toAddress = fromHex(Address, g.ethContractAddress), - chainId = g.chainId, - ) - + let nextFreeIndex = await g.fetchNextFreeIndex() if nextFreeIndex.isErr(): error "Failed to fetch next free index", error = nextFreeIndex.error raise newException( @@ -638,28 +593,10 @@ method init*(g: OnchainGroupManager): Future[GroupManagerResult[void]] {.async.} debug "Keystore idCommitment in bytes", idCommitmentBytes = idCommitmentBytes debug "Keystore idCommitment in UInt256 ", idCommitmentUInt256 = idCommitmentUInt256 debug "Keystore idCommitment in hex ", idCommitmentHex = idCommitmentHex - let idCommitment = idCommitmentUInt256 - try: - let commitmentBytes = keystoreCred.identityCredential.idCommitment - let params = commitmentBytes.reversed() - let resultBytes = await sendEthCallWithParams( - ethRpc = g.ethRpc.get(), - functionSignature = "isInMembershipSet(uint256)", - params = params, - fromAddress = ethRpc.defaultAccount, - toAddress = contractAddress, - chainId = g.chainId, - ) - if resultBytes.isErr(): - return err("Failed to check membership: " & resultBytes.error) - let responseBytes = resultBytes.get() - let membershipExists = responseBytes.len == 32 and responseBytes[^1] == 1'u8 - - debug "membershipExists", membershipExists = membershipExists - if membershipExists == false: - return err("the commitment does not have a membership") - except CatchableError: - return err("failed to check if the commitment has a membership") + let idCommitment = keystoreCred.identityCredential.idCommitment + let membershipExists = (await g.fetchMembershipStatus(idCommitment)).valueOr: + return err("the commitment does not have a membership: " & error) + debug "membershipExists", membershipExists = membershipExists g.idCredentials = some(keystoreCred.identityCredential) @@ -673,16 +610,9 @@ method init*(g: OnchainGroupManager): Future[GroupManagerResult[void]] {.async.} if metadata.contractAddress != g.ethContractAddress.toLower(): return err("persisted data: contract address mismatch") - let maxMembershipRateLimit = ( - await sendEthCallWithChainId( - ethRpc = ethRpc, - functionSignature = "maxMembershipRateLimit()", - fromAddress = ethRpc.defaultAccount, - toAddress = contractAddress, - chainId = g.chainId, - ) - ).valueOr: - return err("Failed to fetch max membership rate limit: " & $error) + let maxMembershipRateLimitRes = await g.fetchMaxMembershipRateLimit() + let maxMembershipRateLimit = maxMembershipRateLimitRes.valueOr: + return err("failed to fetch max membership rate limit: " & error) g.rlnRelayMaxMessageLimit = cast[uint64](maxMembershipRateLimit) diff --git a/waku/waku_rln_relay/group_manager/on_chain/rpc_wrapper.nim b/waku/waku_rln_relay/group_manager/on_chain/rpc_wrapper.nim new file mode 100644 index 000000000..867e9e7f0 --- /dev/null +++ b/waku/waku_rln_relay/group_manager/on_chain/rpc_wrapper.nim @@ -0,0 +1,101 @@ +import + os, + web3, + web3/eth_api_types, + web3/primitives, + eth/keys as keys, + chronicles, + nimcrypto/keccak as keccak, + stint, + json, + std/[strutils, tables, algorithm], + stew/[byteutils, arrayops], + sequtils + +import + ../../../waku_keystore, + ../../rln, + ../../rln/rln_interface, + ../../conversion_utils, + ../../protocol_types, + ../group_manager_base + +logScope: + topics = "waku rln_relay onchain rpc_wrapper" + +# using the when predicate does not work within the contract macro, hence need to dupe +contract(WakuRlnContract): + # this serves as an entrypoint into the rln membership set + proc register( + idCommitment: UInt256, userMessageLimit: UInt32, idCommitmentsToErase: seq[UInt256] + ) + + # this event is emitted when a new member is registered + proc MembershipRegistered( + idCommitment: UInt256, membershipRateLimit: UInt256, index: UInt32 + ) {.event.} + + # Initializes the implementation contract (only used in unit tests) + proc initialize(maxMessageLimit: UInt256) + # this function denotes existence of a given user + proc isInMembershipSet(idCommitment: Uint256): bool {.view.} + # this constant describes the next index of a new member + proc nextFreeIndex(): UInt256 {.view.} + # this constant describes the block number this contract was deployed on + proc deployedBlockNumber(): UInt256 {.view.} + # this constant describes max message limit of rln contract + proc maxMembershipRateLimit(): UInt256 {.view.} + # this function returns the merkleProof for a given index + proc getMerkleProof(index: UInt256): seq[byte] {.view.} + # this function returns the Merkle root + proc root(): Uint256 {.view.} + +proc sendEthCallWithoutParams*( + ethRpc: Web3, + functionSignature: string, + fromAddress: Address, + toAddress: Address, + chainId: UInt256, +): Future[Result[UInt256, string]] {.async.} = + ## Workaround for web3 chainId=null issue on some networks (e.g., linea-sepolia) + ## Makes contract calls with explicit chainId for view functions with no parameters + let functionHash = + keccak256.digest(functionSignature.toOpenArrayByte(0, functionSignature.len - 1)) + let functionSelector = functionHash.data[0 .. 3] + let dataSignature = "0x" & functionSelector.mapIt(it.toHex(2)).join("") + + var tx: TransactionArgs + tx.`from` = Opt.some(fromAddress) + tx.to = Opt.some(toAddress) + tx.value = Opt.some(0.u256) + tx.data = Opt.some(byteutils.hexToSeqByte(dataSignature)) + tx.chainId = Opt.some(chainId) + + let resultBytes = await ethRpc.provider.eth_call(tx, "latest") + if resultBytes.len == 0: + return err("No result returned for function call: " & functionSignature) + return ok(UInt256.fromBytesBE(resultBytes)) + +proc sendEthCallWithParams*( + ethRpc: Web3, + functionSignature: string, + params: seq[byte], + fromAddress: Address, + toAddress: Address, + chainId: UInt256, +): Future[Result[seq[byte], string]] {.async.} = + ## Workaround for web3 chainId=null issue with parameterized contract calls + let functionHash = + keccak256.digest(functionSignature.toOpenArrayByte(0, functionSignature.len - 1)) + let functionSelector = functionHash.data[0 .. 3] + let callData = functionSelector & params + + var tx: TransactionArgs + tx.`from` = Opt.some(fromAddress) + tx.to = Opt.some(toAddress) + tx.value = Opt.some(0.u256) + tx.data = Opt.some(callData) + tx.chainId = Opt.some(chainId) + + let resultBytes = await ethRpc.provider.eth_call(tx, "latest") + return ok(resultBytes)