Skip to content

Commit 3aecfd2

Browse files
committed
feat: add foreign chain tx config checker
1 parent ee6f77d commit 3aecfd2

13 files changed

Lines changed: 1330 additions & 227 deletions

File tree

Cargo.lock

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

Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ members = [
1111
"crates/contract-history",
1212
"crates/devnet",
1313
"crates/e2e-tests",
14+
"crates/foreign-chain-config-tester",
1415
"crates/foreign-chain-inspector",
16+
"crates/foreign-chain-rpc-auth",
1517
"crates/foreign-chain-rpc-interfaces",
1618
"crates/include-measurements",
1719
"crates/launcher-interface",
@@ -53,6 +55,7 @@ chain-gateway = { path = "crates/chain-gateway" }
5355
chain-gateway-test-contract = { path = "crates/chain-gateway-test-contract" }
5456
contract-history = { path = "crates/contract-history" }
5557
foreign-chain-inspector = { path = "crates/foreign-chain-inspector" }
58+
foreign-chain-rpc-auth = { path = "crates/foreign-chain-rpc-auth" }
5659
foreign-chain-rpc-interfaces = { path = "crates/foreign-chain-rpc-interfaces" }
5760
include-measurements = { path = "crates/include-measurements" }
5861
launcher-interface = { path = "crates/launcher-interface" }
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
[package]
2+
name = "foreign-chain-config-tester"
3+
version.workspace = true
4+
edition.workspace = true
5+
license.workspace = true
6+
7+
[[bin]]
8+
name = "foreign-chain-config-tester"
9+
path = "src/main.rs"
10+
11+
[dependencies]
12+
anyhow = { workspace = true }
13+
clap = { workspace = true }
14+
foreign-chain-inspector = { workspace = true }
15+
foreign-chain-rpc-auth = { workspace = true }
16+
foreign-chain-rpc-interfaces = { workspace = true }
17+
hex = { workspace = true }
18+
http = { workspace = true }
19+
mpc-node-config = { workspace = true }
20+
serde_yaml = { workspace = true }
21+
tokio = { workspace = true }
22+
toml = { workspace = true }
23+
24+
[dev-dependencies]
25+
assert_matches = { workspace = true }
26+
httpmock = { workspace = true }
27+
near-mpc-bounded-collections = { workspace = true }
28+
serde_json = { workspace = true }
29+
30+
[lints]
31+
workspace = true
Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
//! Per-provider golden checks. Each function runs a fixed request against a single
2+
//! RPC provider and verifies the extracted value matches the expected golden value.
3+
4+
use std::time::Duration;
5+
6+
use anyhow::{Context, bail, ensure};
7+
use foreign_chain_inspector::{
8+
BlockConfirmations, EthereumFinality, ForeignChainInspector,
9+
aptos::{
10+
AptosExtractedValue, AptosTransactionHash,
11+
inspector::{AptosExtractor, AptosFinality, AptosInspector},
12+
},
13+
bitcoin::{
14+
BitcoinExtractedValue, BitcoinTransactionHash,
15+
inspector::{BitcoinExtractor, BitcoinInspector},
16+
},
17+
evm::inspector::{EvmChain, EvmExtractedValue, EvmExtractor, EvmInspector},
18+
http_client::HttpClient,
19+
starknet::{
20+
StarknetExtractedValue, StarknetTransactionHash,
21+
inspector::{StarknetExtractor, StarknetFinality, StarknetInspector},
22+
},
23+
};
24+
use foreign_chain_rpc_interfaces::aptos::ReqwestAptosClient;
25+
use http::{HeaderName, HeaderValue};
26+
27+
fn verify_block_hash(expected: [u8; 32], got: [u8; 32]) -> anyhow::Result<()> {
28+
if got != expected {
29+
return Err(anyhow::anyhow!(
30+
"block hash mismatch: expected 0x{}, got 0x{} — is this provider on the expected network?",
31+
hex::encode(expected),
32+
hex::encode(got),
33+
));
34+
}
35+
Ok(())
36+
}
37+
38+
/// Verify an EVM-compatible provider returns the golden block hash for the golden tx.
39+
pub async fn check_evm<Chain>(
40+
client: HttpClient,
41+
tx: [u8; 32],
42+
expected_block_hash: [u8; 32],
43+
) -> anyhow::Result<()>
44+
where
45+
Chain: EvmChain + Send + Sync,
46+
{
47+
let inspector = EvmInspector::<HttpClient, Chain>::new(client);
48+
let values = inspector
49+
.extract(
50+
Chain::TransactionHash::from(tx),
51+
EthereumFinality::Finalized,
52+
vec![EvmExtractor::BlockHash],
53+
)
54+
.await?;
55+
match values.into_iter().next().context("RPC returned no value")? {
56+
EvmExtractedValue::BlockHash(hash) => {
57+
let got: [u8; 32] = hash.into();
58+
verify_block_hash(expected_block_hash, got)
59+
}
60+
EvmExtractedValue::Log(_) => bail!("expected a block hash, got a log"),
61+
}
62+
}
63+
64+
/// Verify a Bitcoin provider returns the golden block hash for the golden tx.
65+
pub async fn check_bitcoin(
66+
client: HttpClient,
67+
tx: [u8; 32],
68+
expected_block_hash: [u8; 32],
69+
) -> anyhow::Result<()> {
70+
let inspector = BitcoinInspector::new(client);
71+
let values = inspector
72+
.extract(
73+
BitcoinTransactionHash::from(tx),
74+
BlockConfirmations::from(1),
75+
vec![BitcoinExtractor::BlockHash],
76+
)
77+
.await?;
78+
match values.into_iter().next().context("RPC returned no value")? {
79+
BitcoinExtractedValue::BlockHash(hash) => {
80+
let got: [u8; 32] = hash.into();
81+
verify_block_hash(expected_block_hash, got)
82+
}
83+
}
84+
}
85+
86+
/// Verify a Starknet provider returns the golden block hash for the golden tx.
87+
pub async fn check_starknet(
88+
client: HttpClient,
89+
tx: [u8; 32],
90+
expected_block_hash: [u8; 32],
91+
) -> anyhow::Result<()> {
92+
let inspector = StarknetInspector::new(client);
93+
let values = inspector
94+
.extract(
95+
StarknetTransactionHash::from(tx),
96+
StarknetFinality::AcceptedOnL1,
97+
vec![StarknetExtractor::BlockHash],
98+
)
99+
.await?;
100+
match values.into_iter().next().context("RPC returned no value")? {
101+
StarknetExtractedValue::BlockHash(hash) => {
102+
let got: [u8; 32] = hash.into();
103+
verify_block_hash(expected_block_hash, got)
104+
}
105+
StarknetExtractedValue::Log(_) => bail!("expected a block hash, got a log"),
106+
}
107+
}
108+
109+
/// Verify an Aptos provider returns the golden first event for the golden tx.
110+
pub async fn check_aptos(
111+
url: String,
112+
auth_header: Option<(HeaderName, HeaderValue)>,
113+
timeout: Duration,
114+
tx: [u8; 32],
115+
expected_type_tag: &str,
116+
expected_sequence_number: u64,
117+
) -> anyhow::Result<()> {
118+
let inspector = AptosInspector::new(ReqwestAptosClient::new(url, auth_header, timeout));
119+
let values = inspector
120+
.extract(
121+
AptosTransactionHash::from(tx),
122+
AptosFinality::Committed,
123+
vec![AptosExtractor::Event { event_index: 0 }],
124+
)
125+
.await?;
126+
match values.into_iter().next().context("RPC returned no value")? {
127+
AptosExtractedValue::Event(event) => {
128+
ensure!(
129+
event.type_tag == expected_type_tag,
130+
"event type tag mismatch: expected {expected_type_tag}, got {} — is this provider on the expected network?",
131+
event.type_tag,
132+
);
133+
ensure!(
134+
event.sequence_number == expected_sequence_number,
135+
"event sequence number mismatch: expected {expected_sequence_number}, got {}",
136+
event.sequence_number,
137+
);
138+
Ok(())
139+
}
140+
}
141+
}
142+
143+
#[cfg(test)]
144+
#[expect(non_snake_case)]
145+
mod tests {
146+
use super::*;
147+
use crate::golden;
148+
use httpmock::prelude::*;
149+
150+
fn golden_aptos_body(tx: &str, type_tag: &str, sequence_number: u64) -> serde_json::Value {
151+
serde_json::json!({
152+
"type": "block_metadata_transaction",
153+
"hash": format!("0x{tx}"),
154+
"success": true,
155+
"events": [{
156+
"guid": { "creation_number": "0", "account_address": "0x1" },
157+
"sequence_number": sequence_number.to_string(),
158+
"type": type_tag,
159+
"data": { "epoch": "7510" }
160+
}]
161+
})
162+
}
163+
164+
#[tokio::test]
165+
async fn check_aptos__should_pass_when_provider_returns_golden_event() {
166+
// Given
167+
let server = MockServer::start_async().await;
168+
let tx = golden::APTOS.tx;
169+
let mock = server
170+
.mock_async(|when, then| {
171+
when.method(GET)
172+
.path(format!("/transactions/by_hash/0x{tx}"));
173+
then.status(200).json_body(golden_aptos_body(
174+
tx,
175+
golden::APTOS.event_type_tag,
176+
golden::APTOS.event_sequence_number,
177+
));
178+
})
179+
.await;
180+
181+
// When
182+
let result = check_aptos(
183+
server.base_url(),
184+
None,
185+
Duration::from_secs(5),
186+
golden::hex32(tx).unwrap(),
187+
golden::APTOS.event_type_tag,
188+
golden::APTOS.event_sequence_number,
189+
)
190+
.await;
191+
192+
// Then
193+
result.unwrap();
194+
mock.assert_async().await;
195+
}
196+
197+
#[tokio::test]
198+
async fn check_aptos__should_fail_when_event_type_tag_differs() {
199+
// Given
200+
let server = MockServer::start_async().await;
201+
let tx = golden::APTOS.tx;
202+
server
203+
.mock_async(|when, then| {
204+
when.method(GET)
205+
.path(format!("/transactions/by_hash/0x{tx}"));
206+
then.status(200).json_body(golden_aptos_body(
207+
tx,
208+
"0xdead::wrong::Event",
209+
golden::APTOS.event_sequence_number,
210+
));
211+
})
212+
.await;
213+
214+
// When
215+
let result = check_aptos(
216+
server.base_url(),
217+
None,
218+
Duration::from_secs(5),
219+
golden::hex32(tx).unwrap(),
220+
golden::APTOS.event_type_tag,
221+
golden::APTOS.event_sequence_number,
222+
)
223+
.await;
224+
225+
// Then
226+
let error = result.unwrap_err().to_string();
227+
assert!(error.contains("event type tag mismatch"), "{error}");
228+
}
229+
}

0 commit comments

Comments
 (0)