@@ -31,18 +31,22 @@ import
3131 ./ utils
3232
3333const 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
34+ const DEFAULT_ANVIL_STATE_PATH * =
35+ " tests/waku_rln_relay/anvil_state/state-deployed-contracts-mint-and-approved.json"
36+ const TOKEN_ADDRESS * = " 0x5FbDB2315678afecb367f032d93F642f64180aa3"
37+ const WAKU_RLNV2_PROXY_ADDRESS * = " 0x5fc8d32690cc91d4c39d9d3abcbd16989f875707"
38+
39+ # template skip0xPrefix(hexStr: string): int =
40+ # ## Returns the index of the first meaningful char in `hexStr` by skipping
41+ # ## "0x" prefix
42+ # if hexStr.len > 1 and hexStr[0] == '0' and hexStr[1] in {'x', 'X'}: 2 else: 0
43+
44+ # func strip0xPrefix(s: string): string =
45+ # let prefixLen = skip0xPrefix(s)
46+ # if prefixLen != 0:
47+ # s[prefixLen .. ^1]
48+ # else:
49+ # s
4650
4751proc generateCredentials * (): IdentityCredential =
4852 let credRes = membershipKeyGen ()
@@ -488,19 +492,26 @@ proc getAnvilPath*(): string =
488492 return $ anvilPath
489493
490494# Runs Anvil daemon
491- proc runAnvil * (port: int = 8540 , chainId: string = " 1234" ): Process =
495+ proc runAnvil * (
496+ port: int = 8540 ,
497+ chainId: string = " 1234" ,
498+ stateFile: Option [string ] = none (string ),
499+ dumpStateOnExit: bool = false ,
500+ ): Process =
492501 # Passed options are
493502 # --port Port to listen on.
494503 # --gas-limit Sets the block gas limit in WEI.
495504 # --balance The default account balance, specified in ether.
496505 # --chain-id Chain ID of the network.
506+ # --load-state Initialize the chain from a previously saved state snapshot (read-only)
507+ # --dump-state Dump the state on exit to the given file (write-only)
497508 # See anvil documentation https://book.getfoundry.sh/reference/anvil/ for more details
498509 try :
499510 let anvilPath = getAnvilPath ()
500511 info " Anvil path" , anvilPath
501- let runAnvil = startProcess (
502- anvilPath,
503- args = [
512+
513+ var args =
514+ @ [
504515 " --port" ,
505516 $ port,
506517 " --gas-limit" ,
@@ -509,9 +520,45 @@ proc runAnvil*(port: int = 8540, chainId: string = "1234"): Process =
509520 " 1000000000" ,
510521 " --chain-id" ,
511522 $ chainId,
512- ],
513- options = {poUsePath, poStdErrToStdOut},
514- )
523+ ]
524+
525+ # Add state file argument if provided
526+ if stateFile.isSome ():
527+ let statePath = stateFile.get ()
528+ info " State file parameter provided" ,
529+ statePath = statePath,
530+ dumpStateOnExit = dumpStateOnExit,
531+ absolutePath = absolutePath (statePath)
532+
533+ # Ensure the directory exists
534+ let stateDir = parentDir (statePath)
535+ if not dirExists (stateDir):
536+ info " Creating state directory" , dir = stateDir
537+ createDir (stateDir)
538+
539+ # Use --load-state (read-only) when we want to use cached state without modifying it
540+ # Use --dump-state (write-only) when we want to create a new cache from fresh deployment
541+ if dumpStateOnExit:
542+ # Fresh deployment: start clean and dump state on exit
543+ args.add (" --dump-state" )
544+ args.add (statePath)
545+ debug " Anvil configured to dump state on exit" , path = statePath
546+ else :
547+ # Using cache: only load state, don't overwrite it (preserves clean cached state)
548+ if fileExists (statePath):
549+ args.add (" --load-state" )
550+ args.add (statePath)
551+ debug " Anvil configured to load state file (read-only)" , path = statePath
552+ else :
553+ warn " State file does not exist, anvil will start fresh" ,
554+ path = statePath, absolutePath = absolutePath (statePath)
555+ else :
556+ info " No state file provided, anvil will start fresh without state persistence"
557+
558+ info " Starting anvil with arguments" , args = args.join (" " )
559+
560+ let runAnvil =
561+ startProcess (anvilPath, args = args, options = {poUsePath, poStdErrToStdOut})
515562 let anvilPID = runAnvil.processID
516563
517564 # We read stdout from Anvil to see when daemon is ready
@@ -560,52 +607,100 @@ proc stopAnvil*(runAnvil: Process) {.used.} =
560607 info " Error stopping Anvil daemon" , anvilPID = anvilPID, error = e.msg
561608
562609proc setupOnchainGroupManager * (
563- ethClientUrl: string = EthClient , amountEth: UInt256 = 10 .u256
610+ ethClientUrl: string = EthClient ,
611+ amountEth: UInt256 = 10 .u256,
612+ deployContracts: bool = true ,
564613): Future [OnchainGroupManager ] {.async .} =
614+ # # Setup an onchain group manager for testing
615+ # # If deployContracts is false, it will assume that the Anvil testnet already has the required contracts deployed, this significantly speeds up test runs.
616+ # # To run Anvil with a cached state file containing pre-deployed contracts, see runAnvil documentation.
617+ # #
618+ # # To generate/update the cached state file:
619+ # # 1. Call runAnvil with stateFile and dumpStateOnExit=true
620+ # # 2. Run setupOnchainGroupManager with deployContracts=true to deploy contracts
621+ # # 3. The state will be saved to the specified file when anvil exits
622+ # # 4. Commit this file to git
623+ # #
624+ # # To use cached state:
625+ # # 1. Call runAnvil with stateFile and dumpStateOnExit=false
626+ # # 2. Anvil loads state in read-only mode (won't overwrite the cached file)
627+ # # 3. Call setupOnchainGroupManager with deployContracts=false
628+ # # 4. Tests run fast using pre-deployed contracts
565629 let rlnInstanceRes = createRlnInstance ()
566630 check:
567631 rlnInstanceRes.isOk ()
568632
569633 let rlnInstance = rlnInstanceRes.get ()
570634
571- # connect to the eth client
572635 let web3 = await newWeb3 (ethClientUrl)
573636 let accounts = await web3.provider.eth_accounts ()
574637 web3.defaultAccount = accounts[1 ]
575638
576- let (privateKey, acc) = createEthAccount (web3)
639+ var privateKey: keys.PrivateKey
640+ var acc: Address
641+ var testTokenAddress: Address
642+ var contractAddress: Address
577643
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- )
644+ if not deployContracts:
645+ info " Using contract addresses from constants"
583646
584- let testTokenAddress = (await deployTestToken (privateKey, acc, web3)).valueOr:
585- assert false , " Failed to deploy test token contract: " & $ error
586- return
647+ testTokenAddress = Address (hexToByteArray [20 ](TOKEN_ADDRESS ))
648+ contractAddress = Address (hexToByteArray [20 ](WAKU_RLNV2_PROXY_ADDRESS ))
587649
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- )
650+ (privateKey, acc) = createEthAccount (web3)
592651
593- let contractAddress = (await executeForgeContractDeployScripts (privateKey, acc, web3)).valueOr:
594- assert false , " Failed to deploy RLN contract: " & $ error
595- return
652+ # Fund the test account
653+ discard await sendEthTransfer (web3, web3.defaultAccount, acc, ethToWei (1000 .u256))
596654
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- )
655+ # Mint tokens to the test account
656+ discard await sendMintCall (
657+ web3, web3.defaultAccount, testTokenAddress, acc, ethToWei (1000 .u256)
658+ )
659+
660+ # Approve the contract to spend tokens
661+ let tokenApprovalResult = await approveTokenAllowanceAndVerify (
662+ web3, acc, privateKey, testTokenAddress, contractAddress, ethToWei (200 .u256)
663+ )
664+ assert tokenApprovalResult.isOk, tokenApprovalResult.error ()
665+ else :
666+ info " Performing Token and RLN contracts deployment"
667+ (privateKey, acc) = createEthAccount (web3)
668+
669+ # fund the default account
670+ discard await sendEthTransfer (
671+ web3, web3.defaultAccount, acc, ethToWei (1000 .u256), some (0 .u256)
672+ )
673+
674+ testTokenAddress = (await deployTestToken (privateKey, acc, web3)).valueOr:
675+ assert false , " Failed to deploy test token contract: " & $ error
676+ return
677+
678+ # mint the token from the generated account
679+ discard await sendMintCall (
680+ web3,
681+ web3.defaultAccount,
682+ testTokenAddress,
683+ acc,
684+ ethToWei (1000 .u256),
685+ some (0 .u256),
686+ )
687+
688+ contractAddress = (await executeForgeContractDeployScripts (privateKey, acc, web3)).valueOr:
689+ assert false , " Failed to deploy RLN contract: " & $ error
690+ return
691+
692+ # If the generated account wishes to register a membership, it needs to approve the contract to spend its tokens
693+ let tokenApprovalResult = await approveTokenAllowanceAndVerify (
694+ web3,
695+ acc,
696+ privateKey,
697+ testTokenAddress,
698+ contractAddress,
699+ ethToWei (200 .u256),
700+ some (0 .u256),
701+ )
607702
608- assert tokenApprovalResult.isOk, tokenApprovalResult.error ()
703+ assert tokenApprovalResult.isOk, tokenApprovalResult.error ()
609704
610705 let manager = OnchainGroupManager (
611706 ethClientUrls: @ [ethClientUrl],
0 commit comments