Skip to content

Commit cafb403

Browse files
authored
feat(cast): migrate cast vaddr e2e tests from CI shell script (#14646)
* feat(cast): migrate `cast vaddr` e2e tests from CI shell script Replaces the shell-based `cast vaddr` checks removed from the Tempo CI script with nextest tests under `vaddr_e2e::`. Accounts are derived from `handle.dev_wallets()` rather than hardcoded constants. * fix: make clippy happy * fix: apply fig's suggestions * fix: typo
1 parent b21d972 commit cafb403

4 files changed

Lines changed: 242 additions & 0 deletions

File tree

.config/nextest.toml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,13 @@ default-filter = "not test(/flaky_/)"
1010
filter = "test(/ext_integration/)"
1111
slow-timeout = { period = "5m", terminate-after = 4 }
1212

13+
# `cast vaddr` end-to-end tests mine a 4-byte TIP-1022 PoW salt (CPU-bound,
14+
# minutes in debug mode) before exercising register/resolve/forward/watch
15+
# against an in-process Anvil Tempo node.
16+
[[profile.default.overrides]]
17+
filter = "test(/vaddr_e2e::/)"
18+
slow-timeout = { period = "5m", terminate-after = 4 }
19+
1320
# Do not re-run so that `cargo cheats` is ran locally.
1421
[[profile.default.overrides]]
1522
filter = "package(foundry-cheatcodes-spec)"

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/cast/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ alloy-serde.workspace = true
9898
dirs.workspace = true
9999
anvil.workspace = true
100100
foundry-test-utils.workspace = true
101+
tempo-chainspec.workspace = true
101102

102103
[features]
103104
default = ["jemalloc", "asm-keccak", "optimism"]

crates/cast/tests/cli/main.rs

Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)