mirror of
https://github.com/logos-messaging/logos-messaging-nim.git
synced 2026-06-27 11:59:26 +00:00
Add tests for merkle root and proof updates (#3950)
* make generateProof async and add ensureFreshMerkleProofPath * Use Wakumessage.new() * Add trigger for client side only merkleproofcache updates * full decoupling of updateRoots and merkleproofcache update * Add tests for on demand merkle path updates * debugging WIP * fix for getTransactionReceipt endless loop * clean up group manager logs * Remove unused code from rebase mistake * Improve Anvil proc for RLN testing * fixing from rebase * Fix message ref in tests and clean up logs in utils_onchain * Clean up more logs * Change root update log to trace * Increase approved token amount for RLN registration testing * Reduce comments in utils_onchain * simplify benchmarks test result output * Add test names * remove duplicated tests for test_rln_groupmanager * Trim group_manager tests * Remove long test for local window of roots and improve amvil test performance * Optimise tests for group_manager
This commit is contained in:
parent
066838aa03
commit
02ad7783bf
@ -19,10 +19,8 @@ proc benchmark(
|
||||
|
||||
var start_time = getTime()
|
||||
for i in 0 .. registerCount - 1:
|
||||
try:
|
||||
await manager.register(idCredentials[i], UserMessageLimit(messageLimit + 1))
|
||||
except Exception, CatchableError:
|
||||
assert false, "exception raised: " & getCurrentExceptionMsg()
|
||||
(await manager.register(idCredentials[i], UserMessageLimit(messageLimit + 1))).isOkOr:
|
||||
assert false, "register failed: " & error
|
||||
|
||||
info "registration finished",
|
||||
iter = i, elapsed_ms = (getTime() - start_time).inMilliseconds
|
||||
@ -47,14 +45,21 @@ proc benchmark(
|
||||
proofGenTimes.add(getTime() - generate_time)
|
||||
|
||||
let verify_time = getTime()
|
||||
let ok = manager.verifyProof(data, proof).valueOr:
|
||||
discard manager.verifyProof(data, proof).valueOr:
|
||||
raiseAssert $error
|
||||
proofVerTimes.add(getTime() - verify_time)
|
||||
info "iteration finished",
|
||||
iter = i, elapsed_ms = (getTime() - start_time).inMilliseconds
|
||||
|
||||
echo "Proof generation times: ", sum(proofGenTimes) div len(proofGenTimes)
|
||||
echo "Proof verification times: ", sum(proofVerTimes) div len(proofVerTimes)
|
||||
proc fmtMs(d: times.Duration): string =
|
||||
formatFloat(d.inNanoseconds.float / 1_000_000.0, ffDecimal, 3) & " ms"
|
||||
|
||||
let avgGen = sum(proofGenTimes) div len(proofGenTimes)
|
||||
let avgVer = sum(proofVerTimes) div len(proofVerTimes)
|
||||
echo "Proof generation (avg/min/max): ",
|
||||
fmtMs(avgGen), " / ", fmtMs(min(proofGenTimes)), " / ", fmtMs(max(proofGenTimes))
|
||||
echo "Proof verification (avg/min/max): ",
|
||||
fmtMs(avgVer), " / ", fmtMs(min(proofVerTimes)), " / ", fmtMs(max(proofVerTimes))
|
||||
|
||||
proc main() =
|
||||
# Start a local Ethereum JSON-RPC (Anvil) so that the group-manager setup can connect.
|
||||
|
||||
@ -215,7 +215,7 @@ proc updateRecentRoots*(g: OnchainGroupManager): Future[bool] {.async.} =
|
||||
# Append new roots to the tail; trim happens below if we exceed the window.
|
||||
for root in toAdd:
|
||||
g.validRoots.addLast(root)
|
||||
debug "appended recent roots to list of valid roots", count = toAdd.len, roots = toAdd
|
||||
trace "appended recent roots to list of valid roots", count = toAdd.len, roots = toAdd
|
||||
|
||||
while g.validRoots.len > AcceptableRootWindowSize:
|
||||
discard g.validRoots.popFirst()
|
||||
@ -339,15 +339,13 @@ method register*(
|
||||
return high(int)
|
||||
else:
|
||||
let calculatedGasPrice = int(fetchedGasPrice) * 2
|
||||
debug "Gas price calculated",
|
||||
trace "Gas price calculated",
|
||||
fetchedGasPrice = fetchedGasPrice, gasPrice = calculatedGasPrice
|
||||
return calculatedGasPrice,
|
||||
)
|
||||
).valueOr:
|
||||
return err("Failed to get gas price: " & error)
|
||||
|
||||
let idCommitmentHex = identityCredential.idCommitment.inHex()
|
||||
debug "identityCredential idCommitmentHex", idCommitment = idCommitmentHex
|
||||
let idCommitment = identityCredential.idCommitment.toUInt256()
|
||||
let idCommitmentsToErase: seq[UInt256] = @[]
|
||||
info "registering the member",
|
||||
@ -366,20 +364,22 @@ method register*(
|
||||
).valueOr:
|
||||
return err("Failed to register member: " & error)
|
||||
|
||||
# wait for the transaction to be mined
|
||||
# wait for the transaction to be mined and get the receipt
|
||||
let tsReceipt = (
|
||||
await retryWrapper(
|
||||
RetryStrategy.new(),
|
||||
"Failed to get the transaction receipt",
|
||||
proc(): Future[ReceiptObject] {.async.} =
|
||||
return await ethRpc.getMinedTransactionReceipt(txHash),
|
||||
let r = await ethRpc.provider.eth_getTransactionReceipt(txHash)
|
||||
if r.isNil():
|
||||
raise newException(CatchableError, "transaction not yet mined")
|
||||
return r,
|
||||
)
|
||||
).valueOr:
|
||||
return err("Failed to get transaction receipt: " & error)
|
||||
debug "registration transaction mined", txHash = txHash
|
||||
g.registrationTxHash = some(txHash)
|
||||
# the receipt topic holds the hash of signature of the raised events
|
||||
debug "ts receipt", receipt = tsReceipt[]
|
||||
trace "registration receipt", receipt = tsReceipt[]
|
||||
|
||||
if tsReceipt.status.isNone():
|
||||
return err("Transaction failed: status is None")
|
||||
@ -409,7 +409,6 @@ method register*(
|
||||
## Extract membership index from transaction log data (big endian)
|
||||
membershipIndex = UInt256.fromBytesBE(arguments[64 .. 95])
|
||||
|
||||
trace "parsed membershipIndex", membershipIndex
|
||||
g.userMessageLimit = some(userMessageLimit)
|
||||
g.membershipIndex = some(membershipIndex.toMembershipIndex())
|
||||
g.idCredentials = some(identityCredential)
|
||||
@ -645,17 +644,10 @@ method init*(g: OnchainGroupManager): Future[GroupManagerResult[void]] {.async.}
|
||||
|
||||
g.membershipIndex = some(keystoreCred.treeIndex)
|
||||
g.userMessageLimit = some(keystoreCred.userMessageLimit)
|
||||
# now we check on the contract if the commitment actually has a membership
|
||||
let idCommitmentBytes = keystoreCred.identityCredential.idCommitment
|
||||
let idCommitmentUInt256 = keystoreCred.identityCredential.idCommitment.toUInt256()
|
||||
let idCommitmentHex = idCommitmentBytes.inHex()
|
||||
info "Keystore idCommitment in bytes", idCommitmentBytes = idCommitmentBytes
|
||||
info "Keystore idCommitment in UInt256 ", idCommitmentUInt256 = idCommitmentUInt256
|
||||
info "Keystore idCommitment in hex ", idCommitmentHex = idCommitmentHex
|
||||
|
||||
let idCommitment = keystoreCred.identityCredential.idCommitment
|
||||
let membershipExists = (await g.fetchMembershipStatus(idCommitment)).valueOr:
|
||||
return err("the commitment does not have a membership: " & error)
|
||||
info "membershipExists", membershipExists = membershipExists
|
||||
|
||||
g.idCredentials = some(keystoreCred.identityCredential)
|
||||
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
{.push raises: [].}
|
||||
|
||||
import
|
||||
std/[options, sequtils, deques, random, locks, osproc, algorithm],
|
||||
std/[options, sequtils, deques, random, locks, osproc, algorithm, exitprocs],
|
||||
results,
|
||||
stew/byteutils,
|
||||
testutils/unittests,
|
||||
@ -29,16 +29,37 @@ import
|
||||
../testlib/wakucore,
|
||||
./utils_onchain
|
||||
|
||||
# Anvil is started once for the whole suite. The first test runs the full
|
||||
# `setupOnchainGroupManager` flow (fund a fresh account + mint tokens + approve
|
||||
# allowance) and then takes a baseline snapshot capturing that post-setup chain
|
||||
# state. Subsequent tests revert to the baseline (restoring the funded account)
|
||||
# and build a bare manager pointing at the same key. evm_revert consumes the snapshot
|
||||
# ID, so we re-snapshot after every revert. Cleanup is registered via
|
||||
# addExitProc so anvil is terminated when the test binary exits.
|
||||
var sharedAnvilProc: Process
|
||||
var anvilStarted: bool = false
|
||||
var baselineSnapshotId: string
|
||||
var fundedPrivateKey: string
|
||||
|
||||
suite "Onchain group manager":
|
||||
var anvilProc {.threadVar.}: Process
|
||||
var manager {.threadVar.}: OnchainGroupManager
|
||||
|
||||
setup:
|
||||
anvilProc = runAnvil(stateFile = some(DEFAULT_ANVIL_STATE_PATH))
|
||||
manager = waitFor setupOnchainGroupManager(deployContracts = false)
|
||||
|
||||
teardown:
|
||||
stopAnvil(anvilProc)
|
||||
if not anvilStarted:
|
||||
sharedAnvilProc = runAnvil(stateFile = some(DEFAULT_ANVIL_STATE_PATH))
|
||||
anvilStarted = true
|
||||
addExitProc(
|
||||
proc() =
|
||||
if not sharedAnvilProc.isNil:
|
||||
stopAnvil(sharedAnvilProc)
|
||||
)
|
||||
manager = waitFor setupOnchainGroupManager(deployContracts = false)
|
||||
fundedPrivateKey = manager.ethPrivateKey.get()
|
||||
baselineSnapshotId = waitFor takeEvmSnapshot()
|
||||
else:
|
||||
discard waitFor revertEvmSnapshot(baselineSnapshotId)
|
||||
baselineSnapshotId = waitFor takeEvmSnapshot()
|
||||
manager = buildOnchainGroupManager(fundedPrivateKey)
|
||||
|
||||
test "should initialize successfully":
|
||||
(waitFor manager.init()).isOkOr:
|
||||
@ -83,7 +104,7 @@ suite "Onchain group manager":
|
||||
raiseAssert "updateMemberCount failed (initial): " & error
|
||||
check waku_rln_number_registered_memberships.value() == 0.0
|
||||
|
||||
const credentialCount = 3
|
||||
const credentialCount = 1
|
||||
let credentials = generateCredentials(credentialCount)
|
||||
for i in 0 ..< credentials.len:
|
||||
(waitFor manager.register(credentials[i], UserMessageLimit(20))).isOkOr:
|
||||
@ -95,7 +116,7 @@ suite "Onchain group manager":
|
||||
|
||||
test "updateRoots: appends new on-chain root to validRoots after registration":
|
||||
# basic check for the soon to be deprecated root contract function, is replaced by getRecentRoots()
|
||||
const credentialCount = 6
|
||||
const credentialCount = 2
|
||||
let credentials = generateCredentials(credentialCount)
|
||||
(waitFor manager.init()).isOkOr:
|
||||
raiseAssert $error
|
||||
@ -148,41 +169,6 @@ suite "Onchain group manager":
|
||||
manager.validRoots.len == credentialCount
|
||||
manager.validRoots.items().toSeq().allIt(it != default(MerkleNode))
|
||||
|
||||
test "updateRecentRoots: oldest roots are evicted once the window is exceeded":
|
||||
const
|
||||
initialCount = AcceptableRootWindowSize - RlnContractRootCacheSize
|
||||
additionalCount = RlnContractRootCacheSize + 1
|
||||
# one more than the cache size to ensure eviction occurs
|
||||
let credentials = generateCredentials(initialCount + additionalCount)
|
||||
(waitFor manager.init()).isOkOr:
|
||||
raiseAssert $error
|
||||
|
||||
# Register the first credentials and snapshot the 3 oldest roots.
|
||||
for i in 0 ..< initialCount:
|
||||
(waitFor manager.register(credentials[i], UserMessageLimit(20))).isOkOr:
|
||||
assert false, "Failed to register credential " & $i & ": " & error
|
||||
discard waitFor manager.updateRecentRoots()
|
||||
|
||||
check manager.validRoots.len >= 3
|
||||
let firstThreeBefore =
|
||||
@[manager.validRoots[0], manager.validRoots[1], manager.validRoots[2]]
|
||||
|
||||
# Register the remaining credentials, pushing the deque past AcceptableRootWindowSize.
|
||||
for i in initialCount ..< credentials.len:
|
||||
(waitFor manager.register(credentials[i], UserMessageLimit(20))).isOkOr:
|
||||
assert false, "Failed to register credential " & $i & ": " & error
|
||||
discard waitFor manager.updateRecentRoots()
|
||||
|
||||
let rootsAfter = manager.validRoots.items().toSeq()
|
||||
|
||||
# AcceptableRootWindowSize + 1 registrations evicts exactly the single oldest root,
|
||||
# so only the first of the original three is gone; the other two remain.
|
||||
check:
|
||||
manager.validRoots.len == AcceptableRootWindowSize
|
||||
firstThreeBefore[0] notin rootsAfter
|
||||
firstThreeBefore[1] in rootsAfter
|
||||
firstThreeBefore[2] in rootsAfter
|
||||
|
||||
test "register: should guard against uninitialized state":
|
||||
let dummyCommitment = default(IDCommitment)
|
||||
|
||||
@ -406,6 +392,49 @@ suite "Onchain group manager":
|
||||
check:
|
||||
manager.lastRootsRefreshMoment == firstRefreshTs
|
||||
|
||||
test "validateRoot: concurrent misses coalesce onto a single refresh future":
|
||||
(waitFor manager.init()).isOkOr:
|
||||
raiseAssert $error
|
||||
|
||||
let credentials = generateCredentials()
|
||||
(waitFor manager.register(credentials, UserMessageLimit(20))).isOkOr:
|
||||
assert false, "register failed: " & error
|
||||
|
||||
# Clean gates: the throttle window must not short-circuit the first call,
|
||||
# and the in-flight slot must start empty so we can observe whether
|
||||
# subsequent callers install a second future.
|
||||
manager.lastRootsRefreshMoment = default(Moment)
|
||||
manager.rootsRefreshInFlightFut = nil
|
||||
|
||||
var badRoot: MerkleNode
|
||||
badRoot[0] = 0x77
|
||||
|
||||
# The first validateRoot call runs synchronously down to the suspended
|
||||
# RPC inside doRefresh, installing rootsRefreshInFlightFut along the way.
|
||||
# Calls 2 and 3 enter while that future is still pending, so they must
|
||||
# observe the same reference and await it rather than start a second
|
||||
# doRefresh.
|
||||
let f1 = manager.validateRoot(badRoot)
|
||||
let inFlight = manager.rootsRefreshInFlightFut
|
||||
let f2 = manager.validateRoot(badRoot)
|
||||
let afterSecond = manager.rootsRefreshInFlightFut
|
||||
let f3 = manager.validateRoot(badRoot)
|
||||
let afterThird = manager.rootsRefreshInFlightFut
|
||||
|
||||
check:
|
||||
inFlight != nil
|
||||
not inFlight.finished()
|
||||
# No new doRefresh future was created by the coalescing callers.
|
||||
afterSecond == inFlight
|
||||
afterThird == inFlight
|
||||
|
||||
waitFor allFutures(f1, f2, f3)
|
||||
|
||||
check:
|
||||
# The same in-flight future served all three callers and was never
|
||||
# replaced by a competing refresh.
|
||||
manager.rootsRefreshInFlightFut == inFlight
|
||||
|
||||
test "generateProof: fast-paths without refresh inside throttle window":
|
||||
(waitFor manager.init()).isOkOr:
|
||||
raiseAssert $error
|
||||
@ -498,7 +527,7 @@ suite "Onchain group manager":
|
||||
(waitFor manager.init()).isOkOr:
|
||||
raiseAssert $error
|
||||
|
||||
const credentialCount = 4
|
||||
const credentialCount = 2
|
||||
let credentials = generateCredentials(credentialCount)
|
||||
for i in 0 ..< credentials.len:
|
||||
(waitFor manager.register(credentials[i], UserMessageLimit(20))).isOkOr:
|
||||
@ -517,6 +546,49 @@ suite "Onchain group manager":
|
||||
manager.merkleProofCache.len > 0
|
||||
waku_rln_number_registered_memberships.value() == float64(credentialCount)
|
||||
|
||||
test "ensureFreshMerkleProofPath: concurrent calls coalesce onto a single refresh future":
|
||||
(waitFor manager.init()).isOkOr:
|
||||
raiseAssert $error
|
||||
|
||||
let credentials = generateCredentials()
|
||||
(waitFor manager.register(credentials, UserMessageLimit(20))).isOkOr:
|
||||
assert false, "register failed: " & error
|
||||
|
||||
# Empty cache + epoch-zero check timestamp guarantees the first caller
|
||||
# will fall through to fetchMerkleProofElements; the followers should
|
||||
# observe the resulting in-flight future and await it rather than start
|
||||
# their own refresh.
|
||||
manager.merkleProofCache = @[]
|
||||
manager.lastMerklePathCheckMoment = default(Moment)
|
||||
manager.proofPathRefreshInFlightFut = nil
|
||||
|
||||
let f1 = manager.ensureFreshMerkleProofPath()
|
||||
let inFlight = manager.proofPathRefreshInFlightFut
|
||||
let f2 = manager.ensureFreshMerkleProofPath()
|
||||
let afterSecond = manager.proofPathRefreshInFlightFut
|
||||
let f3 = manager.ensureFreshMerkleProofPath()
|
||||
let afterThird = manager.proofPathRefreshInFlightFut
|
||||
|
||||
check:
|
||||
inFlight != nil
|
||||
not inFlight.finished()
|
||||
afterSecond == inFlight
|
||||
afterThird == inFlight
|
||||
|
||||
waitFor allFutures(f1, f2, f3)
|
||||
let r1 = f1.read()
|
||||
let r2 = f2.read()
|
||||
let r3 = f3.read()
|
||||
|
||||
check:
|
||||
r1.isOk()
|
||||
r2.isOk()
|
||||
r3.isOk()
|
||||
# Same future served all callers; field was not replaced by a second
|
||||
# refresh while the first was still in flight.
|
||||
manager.proofPathRefreshInFlightFut == inFlight
|
||||
manager.merkleProofCache.len > 0
|
||||
|
||||
test "verifyProof: should verify valid proof":
|
||||
let credentials = generateCredentials()
|
||||
(waitFor manager.init()).isOkOr:
|
||||
@ -608,43 +680,6 @@ suite "Onchain group manager":
|
||||
check:
|
||||
verified == false
|
||||
|
||||
test "root queue should be updated correctly":
|
||||
const credentialCount = 9
|
||||
let credentials = generateCredentials(credentialCount)
|
||||
(waitFor manager.init()).isOkOr:
|
||||
raiseAssert $error
|
||||
|
||||
type TestBackfillFuts = array[0 .. credentialCount - 1, Future[void]]
|
||||
var futures: TestBackfillFuts
|
||||
for i in 0 ..< futures.len:
|
||||
futures[i] = newFuture[void]()
|
||||
|
||||
proc generateCallback(
|
||||
futs: TestBackfillFuts, credentials: seq[IdentityCredential]
|
||||
): OnRegisterCallback =
|
||||
var futureIndex = 0
|
||||
proc callback(registrations: seq[Membership]): Future[void] {.async.} =
|
||||
if registrations.len == 1 and
|
||||
registrations[0].rateCommitment ==
|
||||
getRateCommitment(credentials[futureIndex], UserMessageLimit(20)).get() and
|
||||
registrations[0].index == MembershipIndex(futureIndex):
|
||||
futs[futureIndex].complete()
|
||||
futureIndex += 1
|
||||
|
||||
return callback
|
||||
|
||||
manager.onRegister(generateCallback(futures, credentials))
|
||||
|
||||
for i in 0 ..< credentials.len:
|
||||
(waitFor manager.register(credentials[i], UserMessageLimit(20))).isOkOr:
|
||||
assert false, "Failed to register credential " & $i & ": " & error
|
||||
discard waitFor manager.updateRecentRoots()
|
||||
|
||||
waitFor allFutures(futures)
|
||||
|
||||
check:
|
||||
manager.validRoots.len == credentialCount
|
||||
|
||||
test "isReady should return false if ethRpc is none":
|
||||
(waitFor manager.init()).isOkOr:
|
||||
raiseAssert $error
|
||||
|
||||
@ -4,7 +4,7 @@ import libp2p/crypto/rng
|
||||
{.push raises: [].}
|
||||
|
||||
import
|
||||
std/[options, os, osproc, streams, strutils, strformat],
|
||||
std/[net, options, os, osproc, streams, strutils, strformat],
|
||||
results,
|
||||
stew/byteutils,
|
||||
testutils/unittests,
|
||||
@ -30,12 +30,10 @@ import
|
||||
|
||||
const CHAIN_ID* = 1234'u256
|
||||
|
||||
# Path to the file which Anvil loads at startup to initialize the chain with pre-deployed contracts, an account funded with tokens and approved for spending
|
||||
# Cached Anvil state with pre-deployed contracts and a pre-funded/approved account.
|
||||
const DEFAULT_ANVIL_STATE_PATH* =
|
||||
"tests/waku_rln_relay/anvil_state/state-deployed-contracts-mint-and-approved.json.gz"
|
||||
# The contract address of the TestStableToken used for the RLN Membership registration fee
|
||||
const TOKEN_ADDRESS* = "0x5FbDB2315678afecb367f032d93F642f64180aa3"
|
||||
# The contract address used ti interact with the WakuRLNV2 contract via the proxy
|
||||
const WAKU_RLNV2_PROXY_ADDRESS* = "0x5fc8d32690cc91d4c39d9d3abcbd16989f875707"
|
||||
|
||||
proc generateCredentials*(): IdentityCredential =
|
||||
@ -84,6 +82,13 @@ contract(ERC20Token):
|
||||
proc allowance(owner: Address, spender: Address): UInt256 {.view.}
|
||||
proc balanceOf(account: Address): UInt256 {.view.}
|
||||
|
||||
# Custom Anvil/EVM JSON-RPC method bindings.
|
||||
# evm_revert consumes its snapshot ID; callers must re-snapshot after each revert
|
||||
# if they want to keep a baseline.
|
||||
createRpcSigsFromNim(RpcClient):
|
||||
proc evm_snapshot(): JsonString
|
||||
proc evm_revert(snapshotId: JsonString): JsonString
|
||||
|
||||
proc getTokenBalance(
|
||||
web3: Web3, tokenAddress: Address, account: Address
|
||||
): Future[UInt256] {.async.} =
|
||||
@ -109,12 +114,9 @@ proc sendMintCall(
|
||||
assert balanceBeforeMint == balanceBeforeExpectedTokens,
|
||||
fmt"Balance is {balanceBeforeMint} before minting but expected {balanceBeforeExpectedTokens}"
|
||||
|
||||
# Create mint transaction
|
||||
# Method ID for mint(address,uint256) is 0x40c10f19 which is part of the openzeppelin ERC20 standard
|
||||
# The method ID for a deployed test token can be viewed here https://sepolia.lineascan.build/address/0x185A0015aC462a0aECb81beCc0497b649a64B9ea#writeContract
|
||||
# OpenZeppelin ERC20 mint(address,uint256) selector.
|
||||
let mintSelector = "0x40c10f19"
|
||||
let addressHex = recipientAddress.toHex()
|
||||
# Pad the address and amount to 32 bytes each
|
||||
let paddedAddress = addressHex.align(64, '0')
|
||||
|
||||
let amountHex = amountTokens.toHex()
|
||||
@ -127,11 +129,10 @@ proc sendMintCall(
|
||||
let mintCallData = mintSelector & paddedAddress & paddedAmount
|
||||
let gasPrice = int(await web3.provider.eth_gasPrice())
|
||||
|
||||
# Create the transaction
|
||||
var tx: TransactionArgs
|
||||
tx.`from` = Opt.some(accountFrom)
|
||||
tx.to = Opt.some(tokenAddress)
|
||||
tx.value = Opt.some(0.u256) # No ETH is sent for token operations
|
||||
tx.value = Opt.some(0.u256)
|
||||
tx.gasPrice = Opt.some(Quantity(gasPrice))
|
||||
tx.data = Opt.some(byteutils.hexToSeqByte(mintCallData))
|
||||
|
||||
@ -141,8 +142,7 @@ proc sendMintCall(
|
||||
let balanceOfSelector = "0x70a08231"
|
||||
let balanceCallData = balanceOfSelector & paddedAddress
|
||||
|
||||
# Wait a bit for transaction to be mined
|
||||
await sleepAsync(500.milliseconds)
|
||||
await sleepAsync(200.milliseconds)
|
||||
|
||||
if doBalanceAssert:
|
||||
let balanceAfterMint = await getTokenBalance(web3, tokenAddress, recipientAddress)
|
||||
@ -151,7 +151,6 @@ proc sendMintCall(
|
||||
assert balanceAfterMint == balanceAfterExpectedTokens,
|
||||
fmt"Balance is {balanceAfterMint} after transfer but expected {balanceAfterExpectedTokens}"
|
||||
|
||||
# Check how many tokens a spender (the RLN contract) is allowed to spend on behalf of the owner (account which wishes to register a membership)
|
||||
proc checkTokenAllowance(
|
||||
web3: Web3, tokenAddress: Address, owner: Address, spender: Address
|
||||
): Future[UInt256] {.async.} =
|
||||
@ -164,33 +163,28 @@ proc setupContractDeployment(
|
||||
forgePath: string, submodulePath: string
|
||||
): Result[void, string] =
|
||||
trace "Contract deployer paths", forgePath = forgePath, submodulePath = submodulePath
|
||||
# Build the Foundry project
|
||||
try:
|
||||
let (forgeCleanOutput, forgeCleanExitCode) =
|
||||
execCmdEx(fmt"""cd {submodulePath} && {forgePath} clean""")
|
||||
trace "Executed forge clean command", output = forgeCleanOutput
|
||||
if forgeCleanExitCode != 0:
|
||||
return err("forge clean command failed")
|
||||
|
||||
let (forgeInstallOutput, forgeInstallExitCode) =
|
||||
execCmdEx(fmt"""cd {submodulePath} && {forgePath} install""")
|
||||
trace "Executed forge install command", output = forgeInstallOutput
|
||||
if forgeInstallExitCode != 0:
|
||||
return err("forge install command failed")
|
||||
|
||||
let (pnpmInstallOutput, pnpmInstallExitCode) =
|
||||
execCmdEx(fmt"""cd {submodulePath} && pnpm install""")
|
||||
trace "Executed pnpm install command", output = pnpmInstallOutput
|
||||
if pnpmInstallExitCode != 0:
|
||||
return err("pnpm install command failed" & pnpmInstallOutput)
|
||||
|
||||
let (forgeBuildOutput, forgeBuildExitCode) =
|
||||
execCmdEx(fmt"""cd {submodulePath} && {forgePath} build""")
|
||||
trace "Executed forge build command", output = forgeBuildOutput
|
||||
if forgeBuildExitCode != 0:
|
||||
return err("forge build command failed")
|
||||
|
||||
# Set the environment variable API keys to anything for local testnet deployment
|
||||
# Forge requires these env vars to be set; values are unused on local testnet.
|
||||
putEnv("API_KEY_CARDONA", "123")
|
||||
putEnv("API_KEY_LINEASCAN", "123")
|
||||
putEnv("API_KEY_ETHERSCAN", "123")
|
||||
@ -201,13 +195,11 @@ proc setupContractDeployment(
|
||||
proc deployTestToken*(
|
||||
pk: keys.PrivateKey, acc: Address, web3: Web3
|
||||
): Future[Result[Address, string]] {.async.} =
|
||||
## Executes a Foundry forge script that deploys the a token contract (ERC-20) used for testing. This is a prerequisite to enable the contract deployment and this token contract address needs to be minted and approved for the accounts that need to register memberships with the contract
|
||||
## submodulePath: path to the submodule containing contract deploy scripts
|
||||
## Deploys the ERC-20 test token used to pay the RLN membership registration fee.
|
||||
|
||||
# All RLN related tests should be run from the root directory of the project
|
||||
# Path is relative; RLN tests must be run from the project root.
|
||||
let submodulePath = absolutePath("./vendor/waku-rlnv2-contract")
|
||||
|
||||
# Verify submodule path exists
|
||||
if not dirExists(submodulePath):
|
||||
error "Submodule path does not exist", submodulePath = submodulePath
|
||||
return err("Submodule path does not exist: " & submodulePath)
|
||||
@ -218,26 +210,22 @@ proc deployTestToken*(
|
||||
error "Failed to setup contract deployment", error = $error
|
||||
return err("Failed to setup contract deployment: " & $error)
|
||||
|
||||
# Deploy TestToken contract
|
||||
let forgeCmdTestToken =
|
||||
fmt"""cd {submodulePath} && {forgePath} script test/TestToken.sol --broadcast -vvv --rpc-url http://localhost:8540 --tc TestTokenFactory --private-key {pk} && rm -rf broadcast/*/*/run-1*.json && rm -rf cache/*/*/run-1*.json"""
|
||||
let (outputDeployTestToken, exitCodeDeployTestToken) = execForge(forgeCmdTestToken)
|
||||
trace "Executed forge command to deploy TestToken contract",
|
||||
output = outputDeployTestToken
|
||||
if exitCodeDeployTestToken != 0:
|
||||
error "Forge command to deploy TestToken contract failed",
|
||||
error = outputDeployTestToken
|
||||
return
|
||||
err("Forge command to deploy TestToken contract failed: " & outputDeployTestToken)
|
||||
|
||||
# Parse the command output to find contract address
|
||||
let testTokenAddress = getContractAddressFromDeployScriptOutput(outputDeployTestToken).valueOr:
|
||||
error "Failed to get TestToken contract address from deploy script output",
|
||||
error = $error
|
||||
return err(
|
||||
"Failed to get TestToken contract address from deploy script output: " & $error
|
||||
)
|
||||
info "Address of the TestToken contract", testTokenAddress
|
||||
debug "Address of the TestToken contract", testTokenAddress
|
||||
|
||||
let testTokenAddressBytes = hexToByteArray[20](testTokenAddress)
|
||||
let testTokenAddressAddress = Address(testTokenAddressBytes)
|
||||
@ -245,7 +233,6 @@ proc deployTestToken*(
|
||||
|
||||
return ok(testTokenAddressAddress)
|
||||
|
||||
# Sends an ERC20 token approval call to allow a spender to spend a certain amount of tokens on behalf of the owner
|
||||
proc approveTokenAllowanceAndVerify*(
|
||||
web3: Web3,
|
||||
accountFrom: Address,
|
||||
@ -264,14 +251,14 @@ proc approveTokenAllowanceAndVerify*(
|
||||
return
|
||||
err(fmt"Allowance is {allowanceBefore} before approval but expected {expected}")
|
||||
|
||||
# Temporarily set the private key
|
||||
# Swap in the holder's key so the approve tx is signed as the token owner;
|
||||
# restored in `finally`.
|
||||
let oldPrivateKey = web3.privateKey
|
||||
web3.privateKey = Opt.some(privateKey)
|
||||
web3.lastKnownNonce = Opt.none(Quantity)
|
||||
|
||||
try:
|
||||
# ERC20 approve function signature: approve(address spender, uint256 amount)
|
||||
# Method ID for approve(address,uint256) is 0x095ea7b3
|
||||
# ERC20 approve(address,uint256) selector.
|
||||
const APPROVE_SELECTOR = "0x095ea7b3"
|
||||
let addressHex = spender.toHex().align(64, '0')
|
||||
let amountHex = amountWei.toHex().align(64, '0')
|
||||
@ -297,7 +284,6 @@ proc approveTokenAllowanceAndVerify*(
|
||||
if receipt.status.get() != 1.Quantity:
|
||||
return err("Approval transaction failed status quantity not 1")
|
||||
|
||||
# Single verification check after mining (no extra sleep needed)
|
||||
let allowanceAfter =
|
||||
await checkTokenAllowance(web3, tokenAddress, accountFrom, spender)
|
||||
let expectedAfter =
|
||||
@ -315,80 +301,64 @@ proc approveTokenAllowanceAndVerify*(
|
||||
except CatchableError as e:
|
||||
return err(fmt"Failed to send approve transaction: {e.msg}")
|
||||
finally:
|
||||
# Restore the old private key
|
||||
web3.privateKey = oldPrivateKey
|
||||
|
||||
proc executeForgeContractDeployScripts*(
|
||||
privateKey: keys.PrivateKey, acc: Address, web3: Web3
|
||||
): Future[Result[Address, string]] {.async, gcsafe.} =
|
||||
## Executes a set of foundry forge scripts required to deploy the RLN contract and returns the deployed proxy contract address
|
||||
## submodulePath: path to the submodule containing contract deploy scripts
|
||||
## Deploys the RLN contracts via forge scripts; returns the proxy address.
|
||||
|
||||
# All RLN related tests should be run from the root directory of the project
|
||||
# Path is relative; RLN tests must be run from the project root.
|
||||
let submodulePath = "./vendor/waku-rlnv2-contract"
|
||||
|
||||
# Verify submodule path exists
|
||||
if not dirExists(submodulePath):
|
||||
error "Submodule path does not exist", submodulePath = submodulePath
|
||||
return err("Submodule path does not exist: " & submodulePath)
|
||||
|
||||
let forgePath = getForgePath()
|
||||
info "Forge path", forgePath
|
||||
|
||||
# Verify forge executable exists
|
||||
if not fileExists(forgePath):
|
||||
error "Forge executable not found", forgePath = forgePath
|
||||
return err("Forge executable not found: " & forgePath)
|
||||
|
||||
trace "contract deployer account details", account = acc, privateKey = privateKey
|
||||
let setupContractEnv = setupContractDeployment(forgePath, submodulePath)
|
||||
if setupContractEnv.isErr():
|
||||
error "Failed to setup contract deployment"
|
||||
return err("Failed to setup contract deployment")
|
||||
|
||||
# Deploy LinearPriceCalculator contract
|
||||
let forgeCmdPriceCalculator =
|
||||
fmt"""cd {submodulePath} && {forgePath} script script/Deploy.s.sol --broadcast -vvvv --rpc-url http://localhost:8540 --tc DeployPriceCalculator --private-key {privateKey} && rm -rf broadcast/*/*/run-1*.json && rm -rf cache/*/*/run-1*.json"""
|
||||
let (outputDeployPriceCalculator, exitCodeDeployPriceCalculator) =
|
||||
execForge(forgeCmdPriceCalculator)
|
||||
trace "Executed forge command to deploy LinearPriceCalculator contract",
|
||||
output = outputDeployPriceCalculator
|
||||
if exitCodeDeployPriceCalculator != 0:
|
||||
return error("Forge command to deploy LinearPriceCalculator contract failed")
|
||||
|
||||
# Parse the output to find contract address
|
||||
let priceCalculatorAddressRes =
|
||||
getContractAddressFromDeployScriptOutput(outputDeployPriceCalculator)
|
||||
if priceCalculatorAddressRes.isErr():
|
||||
error "Failed to get LinearPriceCalculator contract address from deploy script output"
|
||||
let priceCalculatorAddress = priceCalculatorAddressRes.get()
|
||||
info "Address of the LinearPriceCalculator contract", priceCalculatorAddress
|
||||
putEnv("PRICE_CALCULATOR_ADDRESS", priceCalculatorAddress)
|
||||
|
||||
let forgeCmdWakuRln =
|
||||
fmt"""cd {submodulePath} && {forgePath} script script/Deploy.s.sol --broadcast -vvvv --rpc-url http://localhost:8540 --tc DeployWakuRlnV2 --private-key {privateKey} && rm -rf broadcast/*/*/run-1*.json && rm -rf cache/*/*/run-1*.json"""
|
||||
let (outputDeployWakuRln, exitCodeDeployWakuRln) = execForge(forgeCmdWakuRln)
|
||||
trace "Executed forge command to deploy WakuRlnV2 contract",
|
||||
output = outputDeployWakuRln
|
||||
if exitCodeDeployWakuRln != 0:
|
||||
error "Forge command to deploy WakuRlnV2 contract failed",
|
||||
output = outputDeployWakuRln
|
||||
return err("Forge command to deploy WakuRlnV2 contract failed")
|
||||
|
||||
# Parse the output to find contract address
|
||||
let wakuRlnV2AddressRes =
|
||||
getContractAddressFromDeployScriptOutput(outputDeployWakuRln)
|
||||
if wakuRlnV2AddressRes.isErr():
|
||||
error "Failed to get WakuRlnV2 contract address from deploy script output"
|
||||
##TODO: raise exception here?
|
||||
return err("Failed to get WakuRlnV2 contract address from deploy script output")
|
||||
let wakuRlnV2Address = wakuRlnV2AddressRes.get()
|
||||
info "Address of the WakuRlnV2 contract", wakuRlnV2Address
|
||||
putEnv("WAKURLNV2_ADDRESS", wakuRlnV2Address)
|
||||
|
||||
# Deploy Proxy contract
|
||||
let forgeCmdProxy =
|
||||
fmt"""cd {submodulePath} && {forgePath} script script/Deploy.s.sol --broadcast -vvvv --rpc-url http://localhost:8540 --tc DeployProxy --private-key {privateKey} && rm -rf broadcast/*/*/run-1*.json && rm -rf cache/*/*/run-1*.json"""
|
||||
let (outputDeployProxy, exitCodeDeployProxy) = execForge(forgeCmdProxy)
|
||||
trace "Executed forge command to deploy proxy contract", output = outputDeployProxy
|
||||
if exitCodeDeployProxy != 0:
|
||||
error "Forge command to deploy Proxy failed", error = outputDeployProxy
|
||||
return err("Forge command to deploy Proxy failed")
|
||||
@ -397,7 +367,7 @@ proc executeForgeContractDeployScripts*(
|
||||
let proxyAddressBytes = hexToByteArray[20](proxyAddress.get())
|
||||
let proxyAddressAddress = Address(proxyAddressBytes)
|
||||
|
||||
info "Address of the Proxy contract", proxyAddressAddress
|
||||
debug "Address of the Proxy contract", proxyAddressAddress
|
||||
|
||||
await web3.close()
|
||||
return ok(proxyAddressAddress)
|
||||
@ -428,7 +398,6 @@ proc sendEthTransfer*(
|
||||
# TODO: handle the error if sending fails
|
||||
let txHash = await web3.send(tx)
|
||||
|
||||
# Wait a bit for transaction to be mined
|
||||
await sleepAsync(200.milliseconds)
|
||||
|
||||
if doBalanceAssert:
|
||||
@ -456,7 +425,6 @@ proc createEthAccount*(
|
||||
tx.to = Opt.some(acc)
|
||||
tx.gasPrice = Opt.some(Quantity(gasPrice))
|
||||
|
||||
# Send ethAmount to acc
|
||||
discard await web3.send(tx)
|
||||
let balance = await web3.provider.eth_getBalance(acc, "latest")
|
||||
assert balance == ethToWei(ethAmount),
|
||||
@ -516,25 +484,17 @@ proc compressGzipFile*(sourcePath: string, targetPath: string): Result[void, str
|
||||
|
||||
ok()
|
||||
|
||||
# Runs Anvil daemon
|
||||
proc runAnvil*(
|
||||
port: int = 8540,
|
||||
chainId: string = "1234",
|
||||
stateFile: Option[string] = none(string),
|
||||
dumpStateOnExit: bool = false,
|
||||
): Process =
|
||||
# Passed options are
|
||||
# --port Port to listen on.
|
||||
# --gas-limit Sets the block gas limit in WEI.
|
||||
# --balance The default account balance, specified in ether.
|
||||
# --chain-id Chain ID of the network.
|
||||
# --load-state Initialize the chain from a previously saved state snapshot (read-only)
|
||||
# --dump-state Dump the state on exit to the given file (write-only)
|
||||
# Values used are representative of Linea Sepolia testnet
|
||||
# See anvil documentation https://book.getfoundry.sh/reference/anvil/ for more details
|
||||
# Gas/fee values mirror Linea Sepolia testnet.
|
||||
# See https://book.getfoundry.sh/reference/anvil/ for option details.
|
||||
try:
|
||||
let anvilPath = getAnvilPath()
|
||||
info "Anvil path", anvilPath
|
||||
debug "Anvil path", anvilPath
|
||||
|
||||
var args = @[
|
||||
"--port",
|
||||
@ -550,21 +510,18 @@ proc runAnvil*(
|
||||
"--chain-id",
|
||||
$chainId,
|
||||
"--disable-min-priority-fee",
|
||||
"--silent",
|
||||
]
|
||||
|
||||
# Add state file argument if provided
|
||||
if stateFile.isSome():
|
||||
var statePath = stateFile.get()
|
||||
info "State file parameter provided",
|
||||
debug "State file parameter provided",
|
||||
statePath = statePath,
|
||||
dumpStateOnExit = dumpStateOnExit,
|
||||
absolutePath = absolutePath(statePath)
|
||||
|
||||
# Check if the file is gzip compressed and handle decompression
|
||||
if statePath.endsWith(".gz"):
|
||||
let decompressedPath = statePath[0 .. ^4] # Remove .gz extension
|
||||
debug "Gzip compressed state file detected",
|
||||
compressedPath = statePath, decompressedPath = decompressedPath
|
||||
let decompressedPath = statePath[0 .. ^4]
|
||||
|
||||
if not fileExists(decompressedPath):
|
||||
decompressGzipFile(statePath, decompressedPath).isOkOr:
|
||||
@ -574,16 +531,15 @@ proc runAnvil*(
|
||||
statePath = decompressedPath
|
||||
|
||||
if dumpStateOnExit:
|
||||
# Ensure the directory exists
|
||||
let stateDir = parentDir(statePath)
|
||||
if not dirExists(stateDir):
|
||||
createDir(stateDir)
|
||||
# Fresh deployment: start clean and dump state on exit
|
||||
# Fresh deployment: start clean and dump state on exit.
|
||||
args.add("--dump-state")
|
||||
args.add(statePath)
|
||||
debug "Anvil configured to dump state on exit", path = statePath
|
||||
else:
|
||||
# Using cache: only load state, don't overwrite it (preserves clean cached state)
|
||||
# Load-only so we don't clobber the committed cached state file.
|
||||
if fileExists(statePath):
|
||||
args.add("--load-state")
|
||||
args.add(statePath)
|
||||
@ -592,65 +548,101 @@ proc runAnvil*(
|
||||
warn "State file does not exist, anvil will start fresh",
|
||||
path = statePath, absolutePath = absolutePath(statePath)
|
||||
else:
|
||||
info "No state file provided, anvil will start fresh without state persistence"
|
||||
debug "No state file provided, anvil will start fresh without state persistence"
|
||||
|
||||
info "Starting anvil with arguments", args = args.join(" ")
|
||||
debug "Starting anvil with arguments", args = args.join(" ")
|
||||
|
||||
let runAnvil =
|
||||
startProcess(anvilPath, args = args, options = {poUsePath, poStdErrToStdOut})
|
||||
let anvilPID = runAnvil.processID
|
||||
|
||||
# We read stdout from Anvil to see when daemon is ready
|
||||
var anvilStartLog: string
|
||||
var cmdline: string
|
||||
while true:
|
||||
# Poll the JSON-RPC port to detect Anvil process readiness.
|
||||
const startupTimeoutMs = 10_000
|
||||
const pollIntervalMs = 100
|
||||
var elapsed = 0
|
||||
var ready = false
|
||||
while elapsed < startupTimeoutMs:
|
||||
if not runAnvil.running:
|
||||
error "Anvil daemon exited before becoming ready", pid = anvilPID
|
||||
return
|
||||
try:
|
||||
if runAnvil.outputstream.readLine(cmdline):
|
||||
anvilStartLog.add(cmdline)
|
||||
if cmdline.contains("Listening on 127.0.0.1:" & $port):
|
||||
break
|
||||
else:
|
||||
error "Anvil daemon exited (closed output)",
|
||||
pid = anvilPID, startLog = anvilStartLog
|
||||
return
|
||||
except Exception, CatchableError:
|
||||
warn "Anvil daemon stdout reading error; assuming it started OK",
|
||||
pid = anvilPID, startLog = anvilStartLog, err = getCurrentExceptionMsg()
|
||||
break
|
||||
info "Anvil daemon is running and ready", pid = anvilPID, startLog = anvilStartLog
|
||||
let sock = newSocket()
|
||||
try:
|
||||
sock.connect("127.0.0.1", Port(port), timeout = 500)
|
||||
ready = true
|
||||
finally:
|
||||
close(sock)
|
||||
if ready:
|
||||
break
|
||||
except CatchableError:
|
||||
discard
|
||||
sleep(pollIntervalMs)
|
||||
elapsed += pollIntervalMs
|
||||
|
||||
if not ready:
|
||||
error "Anvil daemon did not become ready within timeout",
|
||||
pid = anvilPID, timeoutMs = startupTimeoutMs
|
||||
return
|
||||
|
||||
debug "Anvil daemon is running and ready", pid = anvilPID
|
||||
return runAnvil
|
||||
except: # TODO: Fix "BareExcept" warning
|
||||
error "Anvil daemon run failed", err = getCurrentExceptionMsg()
|
||||
|
||||
# Stops Anvil daemon
|
||||
proc takeEvmSnapshot*(ethClientUrl: string = EthClient): Future[string] {.async.} =
|
||||
## Captures Anvil chain state and returns the snapshot ID as a JSON-encoded
|
||||
## hex string (e.g. "\"0x1\""). The ID is consumed by revertEvmSnapshot, so
|
||||
## re-snapshot after revert if you need to roll back to the same baseline again.
|
||||
let web3 = await newWeb3(ethClientUrl)
|
||||
try:
|
||||
let raw = await web3.provider.evm_snapshot()
|
||||
return string(raw)
|
||||
finally:
|
||||
try:
|
||||
await web3.close()
|
||||
except CatchableError:
|
||||
discard
|
||||
|
||||
proc revertEvmSnapshot*(
|
||||
snapshotId: string, ethClientUrl: string = EthClient
|
||||
): Future[bool] {.async.} =
|
||||
## Rolls the chain back to the given snapshot. The snapshot ID is consumed by
|
||||
## this call; take a new snapshot if you intend to revert to this state again.
|
||||
let web3 = await newWeb3(ethClientUrl)
|
||||
try:
|
||||
let raw = await web3.provider.evm_revert(JsonString(snapshotId))
|
||||
return string(raw) == "true"
|
||||
finally:
|
||||
try:
|
||||
await web3.close()
|
||||
except CatchableError:
|
||||
discard
|
||||
|
||||
proc stopAnvil*(runAnvil: Process) {.used.} =
|
||||
if runAnvil.isNil:
|
||||
info "stopAnvil called with nil Process"
|
||||
error "stopAnvil called with nil Process"
|
||||
return
|
||||
|
||||
let anvilPID = runAnvil.processID
|
||||
info "Stopping Anvil daemon", anvilPID = anvilPID
|
||||
debug "Stopping Anvil daemon", anvilPID = anvilPID
|
||||
|
||||
try:
|
||||
# Send termination signals
|
||||
when not defined(windows):
|
||||
discard execCmdEx(fmt"kill -TERM {anvilPID}")
|
||||
# Wait for graceful shutdown to allow state dumping
|
||||
# Give Anvil time to dump state on graceful shutdown before escalating to KILL.
|
||||
sleep(200)
|
||||
# Only force kill if process is still running
|
||||
let checkResult = execCmdEx(fmt"kill -0 {anvilPID} 2>/dev/null")
|
||||
if checkResult.exitCode == 0:
|
||||
info "Anvil process still running after TERM signal, sending KILL",
|
||||
warn "Anvil process still running after TERM signal, sending KILL",
|
||||
anvilPID = anvilPID
|
||||
discard execCmdEx(fmt"kill -9 {anvilPID}")
|
||||
else:
|
||||
discard execCmdEx(fmt"taskkill /F /PID {anvilPID}")
|
||||
|
||||
# Close Process object to release resources
|
||||
close(runAnvil)
|
||||
info "Anvil daemon stopped", anvilPID = anvilPID
|
||||
debug "Anvil daemon stopped", anvilPID = anvilPID
|
||||
except Exception as e:
|
||||
info "Error stopping Anvil daemon", anvilPID = anvilPID, error = e.msg
|
||||
error "Error stopping Anvil daemon", anvilPID = anvilPID, error = e.msg
|
||||
|
||||
proc setupOnchainGroupManager*(
|
||||
ethClientUrl: string = EthClient,
|
||||
@ -678,7 +670,7 @@ proc setupOnchainGroupManager*(
|
||||
|
||||
let rlnInstance = rlnInstanceRes.get()
|
||||
|
||||
let web3 = await newWeb3(ethClientUrl)
|
||||
var web3 = await newWeb3(ethClientUrl)
|
||||
let accounts = await web3.provider.eth_accounts()
|
||||
web3.defaultAccount = accounts[1]
|
||||
|
||||
@ -688,31 +680,27 @@ proc setupOnchainGroupManager*(
|
||||
var contractAddress: Address
|
||||
|
||||
if not deployContracts:
|
||||
info "Using contract addresses from constants"
|
||||
debug "Using contract addresses from constants"
|
||||
|
||||
testTokenAddress = Address(hexToByteArray[20](TOKEN_ADDRESS))
|
||||
contractAddress = Address(hexToByteArray[20](WAKU_RLNV2_PROXY_ADDRESS))
|
||||
|
||||
(privateKey, acc) = createEthAccount(web3)
|
||||
|
||||
# Fund the test account
|
||||
discard await sendEthTransfer(web3, web3.defaultAccount, acc, ethToWei(1000.u256))
|
||||
|
||||
# Mint tokens to the test account
|
||||
await sendMintCall(
|
||||
web3, web3.defaultAccount, testTokenAddress, acc, ethToWei(1000.u256)
|
||||
)
|
||||
|
||||
# Approve the contract to spend tokens
|
||||
let tokenApprovalResult = await approveTokenAllowanceAndVerify(
|
||||
web3, acc, privateKey, testTokenAddress, contractAddress, ethToWei(200.u256)
|
||||
web3, acc, privateKey, testTokenAddress, contractAddress, ethToWei(2000.u256)
|
||||
)
|
||||
assert tokenApprovalResult.isOk(), tokenApprovalResult.error
|
||||
else:
|
||||
info "Performing Token and RLN contracts deployment"
|
||||
debug "Performing Token and RLN contracts deployment"
|
||||
(privateKey, acc) = createEthAccount(web3)
|
||||
|
||||
# fund the default account
|
||||
discard await sendEthTransfer(
|
||||
web3, web3.defaultAccount, acc, ethToWei(1000.u256), some(0.u256)
|
||||
)
|
||||
@ -721,7 +709,6 @@ proc setupOnchainGroupManager*(
|
||||
assert false, "Failed to deploy test token contract: " & $error
|
||||
return
|
||||
|
||||
# mint the token from the generated account
|
||||
await sendMintCall(
|
||||
web3,
|
||||
web3.defaultAccount,
|
||||
@ -735,14 +722,24 @@ proc setupOnchainGroupManager*(
|
||||
assert false, "Failed to deploy RLN contract: " & $error
|
||||
return
|
||||
|
||||
# If the generated account wishes to register a membership, it needs to approve the contract to spend its tokens
|
||||
# `executeForgeContractDeployScripts` shells out to `forge` via blocking
|
||||
# `execCmdEx` calls (many seconds). While those run the chronos event loop
|
||||
# is frozen and the existing web3 HTTP connection to Anvil rots; the next
|
||||
# eth_call fails with "Not connected". Reconnect before continuing.
|
||||
try:
|
||||
await web3.close()
|
||||
except CatchableError:
|
||||
discard
|
||||
web3 = await newWeb3(ethClientUrl)
|
||||
web3.defaultAccount = accounts[1]
|
||||
|
||||
let tokenApprovalResult = await approveTokenAllowanceAndVerify(
|
||||
web3,
|
||||
acc,
|
||||
privateKey,
|
||||
testTokenAddress,
|
||||
contractAddress,
|
||||
ethToWei(200.u256),
|
||||
ethToWei(2000.u256),
|
||||
some(0.u256),
|
||||
)
|
||||
|
||||
@ -761,4 +758,26 @@ proc setupOnchainGroupManager*(
|
||||
|
||||
return manager
|
||||
|
||||
proc buildOnchainGroupManager*(
|
||||
privateKey: string, ethClientUrl: string = EthClient
|
||||
): OnchainGroupManager =
|
||||
## Constructs an OnchainGroupManager pointing at the cached RLN proxy contract
|
||||
## using the supplied private key. No on-chain work happens here — the caller
|
||||
## is expected to have an Anvil snapshot where this key already owns a funded,
|
||||
## token-approved account (e.g. via a prior `setupOnchainGroupManager` followed
|
||||
## by `takeEvmSnapshot`). Each call returns a fresh RLN instance.
|
||||
let rlnInstanceRes = createRlnInstance()
|
||||
check:
|
||||
rlnInstanceRes.isOk()
|
||||
return OnchainGroupManager(
|
||||
ethClientUrls: @[ethClientUrl],
|
||||
ethContractAddress: WAKU_RLNV2_PROXY_ADDRESS,
|
||||
chainId: CHAIN_ID,
|
||||
ethPrivateKey: some(privateKey),
|
||||
rlnInstance: rlnInstanceRes.get(),
|
||||
onFatalErrorAction: proc(errStr: string) =
|
||||
raiseAssert errStr
|
||||
,
|
||||
)
|
||||
|
||||
{.pop.}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user