33{.push raises : [].}
44
55import
6- std/ [options, os, osproc, deques, streams, strutils, tempfiles , strformat],
6+ std/ [options, os, osproc, streams, strutils, strformat],
77 results,
88 stew/ byteutils,
99 testutils/ unittests,
1414 web3/ conversions,
1515 web3/ eth_api_types,
1616 json_rpc/ rpcclient,
17- json,
1817 libp2p/ crypto/ crypto,
1918 eth/ keys,
2019 results
@@ -24,25 +23,15 @@ import
2423 waku_rln_relay,
2524 waku_rln_relay/ protocol_types,
2625 waku_rln_relay/ constants,
27- waku_rln_relay/ contract,
2826 waku_rln_relay/ rln,
2927 ],
30- ../ testlib/ common,
31- ./ utils
28+ ../ testlib/ common
3229
3330const CHAIN_ID * = 1234 'u256
34-
35- template skip0xPrefix (hexStr: string ): int =
36- # # Returns the index of the first meaningful char in `hexStr` by skipping
37- # # "0x" prefix
38- if hexStr.len > 1 and hexStr[0 ] == '0' and hexStr[1 ] in {'x' , 'X' }: 2 else : 0
39-
40- func strip0xPrefix (s: string ): string =
41- let prefixLen = skip0xPrefix (s)
42- if prefixLen != 0 :
43- s[prefixLen .. ^ 1 ]
44- else :
45- s
31+ const DEFAULT_ANVIL_STATE_PATH * =
32+ " tests/waku_rln_relay/anvil_state/state-deployed-contracts-mint-and-approved.json"
33+ const TOKEN_ADDRESS * = " 0x5FbDB2315678afecb367f032d93F642f64180aa3"
34+ const WAKU_RLNV2_PROXY_ADDRESS * = " 0x5fc8d32690cc91d4c39d9d3abcbd16989f875707"
4635
4736proc generateCredentials * (): IdentityCredential =
4837 let credRes = membershipKeyGen ()
@@ -488,19 +477,26 @@ proc getAnvilPath*(): string =
488477 return $ anvilPath
489478
490479# Runs Anvil daemon
491- proc runAnvil * (port: int = 8540 , chainId: string = " 1234" ): Process =
480+ proc runAnvil * (
481+ port: int = 8540 ,
482+ chainId: string = " 1234" ,
483+ stateFile: Option [string ] = none (string ),
484+ dumpStateOnExit: bool = false ,
485+ ): Process =
492486 # Passed options are
493487 # --port Port to listen on.
494488 # --gas-limit Sets the block gas limit in WEI.
495489 # --balance The default account balance, specified in ether.
496490 # --chain-id Chain ID of the network.
491+ # --load-state Initialize the chain from a previously saved state snapshot (read-only)
492+ # --dump-state Dump the state on exit to the given file (write-only)
497493 # See anvil documentation https://book.getfoundry.sh/reference/anvil/ for more details
498494 try :
499495 let anvilPath = getAnvilPath ()
500496 info " Anvil path" , anvilPath
501- let runAnvil = startProcess (
502- anvilPath,
503- args = [
497+
498+ var args =
499+ @ [
504500 " --port" ,
505501 $ port,
506502 " --gas-limit" ,
@@ -509,9 +505,45 @@ proc runAnvil*(port: int = 8540, chainId: string = "1234"): Process =
509505 " 1000000000" ,
510506 " --chain-id" ,
511507 $ chainId,
512- ],
513- options = {poUsePath, poStdErrToStdOut},
514- )
508+ ]
509+
510+ # Add state file argument if provided
511+ if stateFile.isSome ():
512+ let statePath = stateFile.get ()
513+ info " State file parameter provided" ,
514+ statePath = statePath,
515+ dumpStateOnExit = dumpStateOnExit,
516+ absolutePath = absolutePath (statePath)
517+
518+ # Ensure the directory exists
519+ let stateDir = parentDir (statePath)
520+ if not dirExists (stateDir):
521+ info " Creating state directory" , dir = stateDir
522+ createDir (stateDir)
523+
524+ # Use --load-state (read-only) when we want to use cached state without modifying it
525+ # Use --dump-state (write-only) when we want to create a new cache from fresh deployment
526+ if dumpStateOnExit:
527+ # Fresh deployment: start clean and dump state on exit
528+ args.add (" --dump-state" )
529+ args.add (statePath)
530+ debug " Anvil configured to dump state on exit" , path = statePath
531+ else :
532+ # Using cache: only load state, don't overwrite it (preserves clean cached state)
533+ if fileExists (statePath):
534+ args.add (" --load-state" )
535+ args.add (statePath)
536+ debug " Anvil configured to load state file (read-only)" , path = statePath
537+ else :
538+ warn " State file does not exist, anvil will start fresh" ,
539+ path = statePath, absolutePath = absolutePath (statePath)
540+ else :
541+ info " No state file provided, anvil will start fresh without state persistence"
542+
543+ info " Starting anvil with arguments" , args = args.join (" " )
544+
545+ let runAnvil =
546+ startProcess (anvilPath, args = args, options = {poUsePath, poStdErrToStdOut})
515547 let anvilPID = runAnvil.processID
516548
517549 # We read stdout from Anvil to see when daemon is ready
@@ -560,52 +592,100 @@ proc stopAnvil*(runAnvil: Process) {.used.} =
560592 info " Error stopping Anvil daemon" , anvilPID = anvilPID, error = e.msg
561593
562594proc setupOnchainGroupManager * (
563- ethClientUrl: string = EthClient , amountEth: UInt256 = 10 .u256
595+ ethClientUrl: string = EthClient ,
596+ amountEth: UInt256 = 10 .u256,
597+ deployContracts: bool = true ,
564598): Future [OnchainGroupManager ] {.async .} =
599+ # # Setup an onchain group manager for testing
600+ # # If deployContracts is false, it will assume that the Anvil testnet already has the required contracts deployed, this significantly speeds up test runs.
601+ # # To run Anvil with a cached state file containing pre-deployed contracts, see runAnvil documentation.
602+ # #
603+ # # To generate/update the cached state file:
604+ # # 1. Call runAnvil with stateFile and dumpStateOnExit=true
605+ # # 2. Run setupOnchainGroupManager with deployContracts=true to deploy contracts
606+ # # 3. The state will be saved to the specified file when anvil exits
607+ # # 4. Commit this file to git
608+ # #
609+ # # To use cached state:
610+ # # 1. Call runAnvil with stateFile and dumpStateOnExit=false
611+ # # 2. Anvil loads state in read-only mode (won't overwrite the cached file)
612+ # # 3. Call setupOnchainGroupManager with deployContracts=false
613+ # # 4. Tests run fast using pre-deployed contracts
565614 let rlnInstanceRes = createRlnInstance ()
566615 check:
567616 rlnInstanceRes.isOk ()
568617
569618 let rlnInstance = rlnInstanceRes.get ()
570619
571- # connect to the eth client
572620 let web3 = await newWeb3 (ethClientUrl)
573621 let accounts = await web3.provider.eth_accounts ()
574622 web3.defaultAccount = accounts[1 ]
575623
576- let (privateKey, acc) = createEthAccount (web3)
624+ var privateKey: keys.PrivateKey
625+ var acc: Address
626+ var testTokenAddress: Address
627+ var contractAddress: Address
577628
578- # we just need to fund the default account
579- # the send procedure returns a tx hash that we don't use, hence discard
580- discard await sendEthTransfer (
581- web3, web3.defaultAccount, acc, ethToWei (1000 .u256), some (0 .u256)
582- )
629+ if not deployContracts:
630+ info " Using contract addresses from constants"
583631
584- let testTokenAddress = (await deployTestToken (privateKey, acc, web3)).valueOr:
585- assert false , " Failed to deploy test token contract: " & $ error
586- return
632+ testTokenAddress = Address (hexToByteArray [20 ](TOKEN_ADDRESS ))
633+ contractAddress = Address (hexToByteArray [20 ](WAKU_RLNV2_PROXY_ADDRESS ))
587634
588- # mint the token from the generated account
589- discard await sendMintCall (
590- web3, web3.defaultAccount, testTokenAddress, acc, ethToWei (1000 .u256), some (0 .u256)
591- )
635+ (privateKey, acc) = createEthAccount (web3)
592636
593- let contractAddress = (await executeForgeContractDeployScripts (privateKey, acc, web3)).valueOr:
594- assert false , " Failed to deploy RLN contract: " & $ error
595- return
637+ # Fund the test account
638+ discard await sendEthTransfer (web3, web3.defaultAccount, acc, ethToWei (1000 .u256))
596639
597- # If the generated account wishes to register a membership, it needs to approve the contract to spend its tokens
598- let tokenApprovalResult = await approveTokenAllowanceAndVerify (
599- web3,
600- acc,
601- privateKey,
602- testTokenAddress,
603- contractAddress,
604- ethToWei (200 .u256),
605- some (0 .u256),
606- )
640+ # Mint tokens to the test account
641+ discard await sendMintCall (
642+ web3, web3.defaultAccount, testTokenAddress, acc, ethToWei (1000 .u256)
643+ )
644+
645+ # Approve the contract to spend tokens
646+ let tokenApprovalResult = await approveTokenAllowanceAndVerify (
647+ web3, acc, privateKey, testTokenAddress, contractAddress, ethToWei (200 .u256)
648+ )
649+ assert tokenApprovalResult.isOk, tokenApprovalResult.error ()
650+ else :
651+ info " Performing Token and RLN contracts deployment"
652+ (privateKey, acc) = createEthAccount (web3)
653+
654+ # fund the default account
655+ discard await sendEthTransfer (
656+ web3, web3.defaultAccount, acc, ethToWei (1000 .u256), some (0 .u256)
657+ )
658+
659+ testTokenAddress = (await deployTestToken (privateKey, acc, web3)).valueOr:
660+ assert false , " Failed to deploy test token contract: " & $ error
661+ return
662+
663+ # mint the token from the generated account
664+ discard await sendMintCall (
665+ web3,
666+ web3.defaultAccount,
667+ testTokenAddress,
668+ acc,
669+ ethToWei (1000 .u256),
670+ some (0 .u256),
671+ )
672+
673+ contractAddress = (await executeForgeContractDeployScripts (privateKey, acc, web3)).valueOr:
674+ assert false , " Failed to deploy RLN contract: " & $ error
675+ return
676+
677+ # If the generated account wishes to register a membership, it needs to approve the contract to spend its tokens
678+ let tokenApprovalResult = await approveTokenAllowanceAndVerify (
679+ web3,
680+ acc,
681+ privateKey,
682+ testTokenAddress,
683+ contractAddress,
684+ ethToWei (200 .u256),
685+ some (0 .u256),
686+ )
607687
608- assert tokenApprovalResult.isOk, tokenApprovalResult.error ()
688+ assert tokenApprovalResult.isOk, tokenApprovalResult.error ()
609689
610690 let manager = OnchainGroupManager (
611691 ethClientUrls: @ [ethClientUrl],
0 commit comments