@@ -5220,3 +5220,236 @@ Virtual addresses:
52205220
52215221"# ] ] ) ;
52225222} ) ;
5223+
5224+ // End-to-end `cast vaddr` tests against a local Anvil Tempo node.
5225+ //
5226+ // These tests exercise the full TIP-1022 lifecycle. Mining a 4-byte PoW salt is CPU-bound;
5227+ // we use all available CPUs so each test does not stall a nextest worker long enough to trip
5228+ // its per-test timeout.
5229+ mod vaddr_e2e {
5230+ use super :: * ;
5231+ use std:: {
5232+ io:: { BufRead , BufReader } ,
5233+ process:: Stdio ,
5234+ time:: { Duration , Instant } ,
5235+ } ;
5236+ use tempo_chainspec:: hardfork:: TempoHardfork ;
5237+ use tempo_contracts:: precompiles:: DEFAULT_FEE_TOKEN ;
5238+
5239+ /// `cast vaddr` exercises TIP-1022, which is enabled after `TempoHardfork::T3`.
5240+ fn tempo_t3_config ( ) -> anvil:: NodeConfig {
5241+ anvil:: NodeConfig :: test_tempo ( ) . with_hardfork ( Some ( TempoHardfork :: T3 . into ( ) ) )
5242+ }
5243+
5244+ /// Number of mining threads — use all available CPUs to keep wall time
5245+ /// down on hosts where nextest limits per-test parallelism.
5246+ fn mining_threads ( ) -> String {
5247+ std:: thread:: available_parallelism ( ) . map_or ( 8 , |n| n. get ( ) ) . to_string ( )
5248+ }
5249+
5250+ /// Run `cast vaddr create` (mine + register) and parse the user-tag-zero virtual
5251+ /// address from the plain-text output.
5252+ fn create_and_register_vaddr (
5253+ cmd : & mut foundry_test_utils:: TestCommand ,
5254+ rpc : & str ,
5255+ owner : & PrivateKeySigner ,
5256+ ) -> String {
5257+ let owner_pk = format ! ( "0x{}" , hex:: encode( owner. credential( ) . to_bytes( ) ) ) ;
5258+ let owner_addr = format ! ( "{:#x}" , owner. address( ) ) ;
5259+ let out = cmd
5260+ . cast_fuse ( )
5261+ . args ( [
5262+ "vaddr" ,
5263+ "create" ,
5264+ "--owner" ,
5265+ & owner_addr,
5266+ "--private-key" ,
5267+ & owner_pk,
5268+ "-j" ,
5269+ & mining_threads ( ) ,
5270+ "--rpc-url" ,
5271+ rpc,
5272+ ] )
5273+ . assert_success ( )
5274+ . get_output ( )
5275+ . stdout_lossy ( ) ;
5276+
5277+ out. lines ( )
5278+ . find_map ( |line| {
5279+ let rest = line. trim_start ( ) . strip_prefix ( "tag=0x000000000000" ) ?;
5280+ rest. split_whitespace ( ) . next ( ) . map ( str:: to_string)
5281+ } )
5282+ . unwrap_or_else ( || panic ! ( "could not parse vaddr from create output:\n {out}" ) )
5283+ }
5284+
5285+ // `cast vaddr create` mines a PoW salt, registers a virtual master on-chain,
5286+ // and `cast vaddr resolve` returns the registered owner.
5287+ casttest ! ( vaddr_create_register_and_resolve, async |_prj, cmd| {
5288+ let ( _api, handle) = anvil:: spawn( tempo_t3_config( ) ) . await ;
5289+ let rpc = handle. http_endpoint( ) ;
5290+ let owner = handle. dev_wallets( ) . next( ) . unwrap( ) ;
5291+
5292+ let vaddr = create_and_register_vaddr( & mut cmd, & rpc, & owner) ;
5293+
5294+ let resolve_out = cmd
5295+ . cast_fuse( )
5296+ . args( [ "--json" , "vaddr" , "resolve" , & vaddr, "--rpc-url" , & rpc] )
5297+ . assert_success( )
5298+ . get_output( )
5299+ . stdout_lossy( ) ;
5300+
5301+ let v: serde_json:: Value =
5302+ serde_json:: from_str( resolve_out. trim( ) ) . expect( "resolve --json output is valid JSON" ) ;
5303+ assert_eq!(
5304+ v[ "address" ] . as_str( ) . unwrap( ) . to_lowercase( ) ,
5305+ vaddr. to_lowercase( ) ,
5306+ "resolve.address mismatch: {resolve_out}"
5307+ ) ;
5308+ assert_eq!(
5309+ v[ "master_address" ] . as_str( ) . unwrap( ) . to_lowercase( ) ,
5310+ format!( "{:#x}" , owner. address( ) ) ,
5311+ "resolve.master_address should match the registered owner: {resolve_out}"
5312+ ) ;
5313+ } ) ;
5314+
5315+ // Transferring a TIP-20 fee token to a registered virtual address must
5316+ // auto-forward the deposit to the master wallet at the protocol level.
5317+ casttest ! ( vaddr_auto_forward_to_master, async |_prj, cmd| {
5318+ let ( _api, handle) = anvil:: spawn( tempo_t3_config( ) ) . await ;
5319+ let rpc = handle. http_endpoint( ) ;
5320+ let owner = handle. dev_wallets( ) . next( ) . unwrap( ) ;
5321+ let sender = handle. dev_wallets( ) . nth( 1 ) . unwrap( ) ;
5322+ let sender_pk = format!( "0x{}" , hex:: encode( sender. credential( ) . to_bytes( ) ) ) ;
5323+ let owner_addr = format!( "{:#x}" , owner. address( ) ) ;
5324+
5325+ let vaddr = create_and_register_vaddr( & mut cmd, & rpc, & owner) ;
5326+
5327+ let balance = |cmd: & mut foundry_test_utils:: TestCommand | -> u128 {
5328+ let out = cmd
5329+ . cast_fuse( )
5330+ . args( [
5331+ "call" ,
5332+ & DEFAULT_FEE_TOKEN . to_string( ) ,
5333+ "balanceOf(address)(uint256)" ,
5334+ & owner_addr,
5335+ "--rpc-url" ,
5336+ & rpc,
5337+ ] )
5338+ . assert_success( )
5339+ . get_output( )
5340+ . stdout_lossy( ) ;
5341+ // `cast call` with a typed signature prints e.g. `1000000000000 [1e12]`.
5342+ out. split_whitespace( ) . next( ) . unwrap( ) . parse( ) . unwrap( )
5343+ } ;
5344+
5345+ let before = balance( & mut cmd) ;
5346+
5347+ let amount: u128 = 1_000_000 ;
5348+ cmd. cast_fuse( )
5349+ . args( [
5350+ "send" ,
5351+ & DEFAULT_FEE_TOKEN . to_string( ) ,
5352+ "transfer(address,uint256)" ,
5353+ & vaddr,
5354+ & amount. to_string( ) ,
5355+ "--rpc-url" ,
5356+ & rpc,
5357+ "--private-key" ,
5358+ & sender_pk,
5359+ ] )
5360+ . assert_success( ) ;
5361+
5362+ let after = balance( & mut cmd) ;
5363+ assert_eq!(
5364+ after - before,
5365+ amount,
5366+ "transfer to virtual address should auto-forward to master (before={before}, after={after})"
5367+ ) ;
5368+ } ) ;
5369+
5370+ // `cast vaddr watch --from-block` must replay historical TIP-20 Transfer
5371+ // logs targeted at the virtual address. The command then polls forever, so
5372+ // we spawn it as a child process and kill it once we observe the expected
5373+ // historical line (or the deadline elapses).
5374+ casttest ! ( vaddr_watch_historical, async |_prj, cmd| {
5375+ let ( _api, handle) = anvil:: spawn( tempo_t3_config( ) ) . await ;
5376+ let rpc = handle. http_endpoint( ) ;
5377+ let owner = handle. dev_wallets( ) . next( ) . unwrap( ) ;
5378+ let sender = handle. dev_wallets( ) . nth( 1 ) . unwrap( ) ;
5379+ let sender_pk = format!( "0x{}" , hex:: encode( sender. credential( ) . to_bytes( ) ) ) ;
5380+ let sender_addr = format!( "{:#x}" , sender. address( ) ) ;
5381+
5382+ let vaddr = create_and_register_vaddr( & mut cmd, & rpc, & owner) ;
5383+
5384+ // Capture the block before the transfer so `--from-block` replays it.
5385+ let block_before_out = cmd
5386+ . cast_fuse( )
5387+ . args( [ "block-number" , "--rpc-url" , & rpc] )
5388+ . assert_success( )
5389+ . get_output( )
5390+ . stdout_lossy( ) ;
5391+ let block_before: u64 = block_before_out. trim( ) . parse( ) . unwrap( ) ;
5392+
5393+ let amount: u128 = 1_000_000 ;
5394+ cmd. cast_fuse( )
5395+ . args( [
5396+ "send" ,
5397+ & DEFAULT_FEE_TOKEN . to_string( ) ,
5398+ "transfer(address,uint256)" ,
5399+ & vaddr,
5400+ & amount. to_string( ) ,
5401+ "--rpc-url" ,
5402+ & rpc,
5403+ "--private-key" ,
5404+ & sender_pk,
5405+ ] )
5406+ . assert_success( ) ;
5407+
5408+ // Spawn `cast vaddr watch` as a child process (it loops indefinitely).
5409+ cmd. cast_fuse( ) . args( [
5410+ "vaddr" ,
5411+ "watch" ,
5412+ & vaddr,
5413+ "--token" ,
5414+ & DEFAULT_FEE_TOKEN . to_string( ) ,
5415+ "--from-block" ,
5416+ & block_before. to_string( ) ,
5417+ "--rpc-url" ,
5418+ & rpc,
5419+ ] ) ;
5420+ let mut child = cmd. cmd( ) . stdout( Stdio :: piped( ) ) . stderr( Stdio :: piped( ) ) . spawn( ) . unwrap( ) ;
5421+
5422+ let mut stdout = BufReader :: new( child. stdout. take( ) . unwrap( ) ) ;
5423+ let expected = format!(
5424+ "token={} from={} amount={}" ,
5425+ DEFAULT_FEE_TOKEN . to_string( ) . to_lowercase( ) ,
5426+ sender_addr,
5427+ amount
5428+ ) ;
5429+
5430+ let deadline = Instant :: now( ) + Duration :: from_secs( 15 ) ;
5431+ let mut captured = String :: new( ) ;
5432+ let mut found = false ;
5433+ while Instant :: now( ) < deadline {
5434+ let mut line = String :: new( ) ;
5435+ match stdout. read_line( & mut line) {
5436+ Ok ( 0 ) => break ,
5437+ Ok ( _) => {
5438+ captured. push_str( & line) ;
5439+ if line. to_lowercase( ) . contains( & expected) {
5440+ found = true ;
5441+ break ;
5442+ }
5443+ }
5444+ Err ( _) => break ,
5445+ }
5446+ }
5447+ let _ = child. kill( ) ;
5448+ let _ = child. wait( ) ;
5449+
5450+ assert!(
5451+ found,
5452+ "cast vaddr watch did not emit historical transfer; expected substring `{expected}` in:\n {captured}"
5453+ ) ;
5454+ } ) ;
5455+ }
0 commit comments