use crate::sandbox::utils::{ consts::{CURRENT_CONTRACT_DEPLOY_DEPOSIT, GAS_FOR_INIT, GAS_FOR_VOTE_UPDATE, PARTICIPANT_LEN}, contract_build::current_contract, initializing_utils::{start_keygen_instance, vote_add_domains, vote_public_key}, mpc_contract::{assert_running_return_threshold, get_state, submit_participant_info}, shared_key_utils::{DomainKey, make_key_for_domain}, sign_utils::{PendingSignRequest, make_and_submit_requests}, }; use digest::Digest; use dtos::ProtocolContractState; use k256::ecdsa::SigningKey; use mpc_contract::{ crypto_shared::types::PublicKeyExtended, primitives::{ key_state::{AttemptId, EpochId, KeyForDomain, Keyset}, participants::{ParticipantInfo, Participants}, test_utils::{bogus_ed25519_public_key, infer_purpose_from_protocol}, thresholds::{ProposedThresholdParameters, Threshold, ThresholdParameters}, }, tee::tee_state::NodeId, update::{ProposeUpdateArgs, UpdateId}, }; use near_account_id::AccountId; use near_mpc_contract_interface::types::{ AptosAddress, AptosEvent, AptosExtractedValue, AptosExtractor, AptosFinality, AptosRpcRequest, AptosTxId, Curve, DomainConfig, DomainId, DomainPurpose, Protocol, ReconstructionThreshold, SupportedForeignChains, TonAddress, TonCellBody, TonExtractedValue, TonExtractor, TonFinality, TonLog, TonRpcRequest, TonTxId, }; use near_mpc_contract_interface::{ method_names, types::{ self as dtos, Attestation, BitcoinExtractedValue, BitcoinExtractor, BitcoinRpcRequest, BitcoinTxId, BlockConfirmations, EvmExtractedValue, EvmExtractor, EvmFinality, EvmRpcRequest, EvmTxId, ForeignTxSignPayload, ForeignTxSignPayloadV1, MockAttestation, StarknetExtractedValue, StarknetExtractor, StarknetFelt, StarknetFinality, StarknetRpcRequest, StarknetTxId, VerifyForeignTransactionResponse, }, }; use near_mpc_sdk::foreign_chain::{ExtractedValue, ForeignChainRpcRequest, Hash256}; use near_workspaces::{Account, Worker, result::Execution}; use near_workspaces::{Contract, network::Sandbox, result::ExecutionSuccess}; use rand_core::CryptoRngCore; use serde_json::json; use signature::hazmat::PrehashSigner; use std::collections::{BTreeMap, BTreeSet}; use std::time::Duration; use tokio_util::time::FutureExt as _; use super::utils::contract_build; pub async fn create_account_given_id( worker: &Worker, account_id: AccountId, ) -> Result, near_workspaces::error::Error> { let (_, sk) = worker.generate_dev_account_credentials(); worker.create_root_account_subaccount(account_id, sk).await } pub fn gen_participant_info() -> ParticipantInfo { ParticipantInfo { url: "127.0.0.1".into(), tls_public_key: bogus_ed25519_public_key(), } } pub fn candidates(names: Option>) -> Participants { let mut participants: Participants = Participants::new(); let names = names.unwrap_or_else(|| { vec![ "alice.near".parse().unwrap(), "bob.near".parse().unwrap(), "caesar.near".parse().unwrap(), ] }); for account_id in names { let _ = participants.insert(account_id.clone(), gen_participant_info()); } participants } pub async fn gen_account(worker: &Worker) -> (Account, AccountId) { let account = worker.dev_create_account().await.unwrap(); let id = account.id().into(); (account, id) } /// Create `amount` accounts and return them along with the candidate info. pub async fn gen_accounts(worker: &Worker, amount: usize) -> (Vec, Participants) { let mut accounts = Vec::with_capacity(amount); let mut account_ids = Vec::with_capacity(amount); for _ in 0..amount { let (account, account_id) = gen_account(worker).await; accounts.push(account); account_ids.push(account_id); } let candidates = candidates(Some(account_ids)); (accounts, candidates) } pub async fn init() -> (Worker, Contract) { init_with_wasm(current_contract()).await } pub async fn init_with_wasm(wasm: &[u8]) -> (Worker, Contract) { let worker = near_workspaces::sandbox_with_version(test_utils::DEFAULT_SANDBOX_VERSION) .await .unwrap(); let contract = worker.dev_deploy(wasm).await.unwrap(); (worker, contract) } /// Creates threshold parameters with 60% threshold (rounded up). pub fn make_threshold_params(participants: &Participants) -> ThresholdParameters { let threshold = Threshold::new(((participants.len() as f64) * 0.6).ceil() as u64); ThresholdParameters::new(participants.clone(), threshold).unwrap() } /// Initialize the contract with the given parameters. pub async fn init_contract( contract: &Contract, params: ThresholdParameters, init_config: Option, ) -> ExecutionSuccess { let result = contract .call(method_names::INIT) .args_json(json!({ "parameters": params, "init_config": init_config, })) .gas(GAS_FOR_INIT) .transact() .await .unwrap(); assert!(result.is_success(), "init failed: {:?}", result); result.into_result().unwrap() } /// Initialize contract in Running state with domains and keyset. pub async fn init_contract_running( contract: &Contract, domains: Vec, next_domain_id: u64, keyset: Keyset, params: ThresholdParameters, ) -> ExecutionSuccess { let result = contract .call(method_names::INIT_RUNNING) .args_json(json!({ "domains": domains, "next_domain_id": next_domain_id, "keyset": keyset, "parameters": params, })) .gas(GAS_FOR_INIT) .transact() .await .unwrap(); assert!(result.is_success(), "init_running failed: {:?}", result); result.into_result().unwrap() } pub struct SandboxTestSetup { pub worker: Worker, pub contract: Contract, pub mpc_signer_accounts: Vec, pub keys: Vec, } impl SandboxTestSetup { pub fn builder() -> SandboxTestSetupBuilder { SandboxTestSetupBuilder { protocols: Vec::new(), foreign_tx: false, number_of_participants: PARTICIPANT_LEN, init_config: None, with_sandbox_test_methods: false, } } /// Returns the first key with `ForeignTx` purpose. pub fn foreign_tx_key(&self) -> &DomainKey { self.keys .iter() .find(|k| k.domain_config.purpose == DomainPurpose::ForeignTx) .expect("No ForeignTx domain in setup. Did you call .with_foreign_tx_domain() on the builder?") } } pub struct SandboxTestSetupBuilder { protocols: Vec, foreign_tx: bool, number_of_participants: usize, init_config: Option, with_sandbox_test_methods: bool, } impl SandboxTestSetupBuilder { pub fn with_protocols(mut self, protocols: &[Protocol]) -> Self { self.protocols = protocols.to_vec(); self } pub fn with_number_of_participants(mut self, n: usize) -> Self { self.number_of_participants = n; self } pub fn with_init_config(mut self, config: dtos::InitConfig) -> Self { self.init_config = Some(config); self } pub fn with_foreign_tx_domain(mut self) -> Self { self.foreign_tx = true; self } /// Deploys the wasm built with `--features sandbox-test-methods`, exposing the /// introspection view methods in `crate::sandbox_test_methods` (e.g. fan-out queue /// length). pub fn with_sandbox_test_methods(mut self) -> Self { self.with_sandbox_test_methods = true; self } pub async fn build(self) -> SandboxTestSetup { let (worker, contract) = if self.with_sandbox_test_methods { init_with_wasm(contract_build::current_contract_with_sandbox_test_methods()).await } else { init().await }; let (accounts, participants) = gen_accounts(&worker, self.number_of_participants).await; let threshold_parameters = make_threshold_params(&participants); let mut keys = Vec::new(); let mut domain_configs = Vec::new(); let mut key_for_domains = Vec::new(); // Match the per-domain reconstruction threshold to the cluster // threshold by default. DamgardEtAl additionally requires // `2t - 1 <= n`, so cap t at `n.div_ceil(2)`. let n = self.number_of_participants as u64; let cluster_threshold = threshold_parameters.threshold().value(); for protocol in &self.protocols { let curve = Curve::from(*protocol); let (pk, sk) = make_key_for_domain(curve); let purpose = infer_purpose_from_protocol(*protocol); let domain_id = DomainId(domain_configs.len() as u64); let reconstruction_threshold = match *protocol { Protocol::DamgardEtAl => { ReconstructionThreshold::new(cluster_threshold.min(n.div_ceil(2))) } _ => ReconstructionThreshold::new(cluster_threshold), }; let key: PublicKeyExtended = pk.try_into().unwrap(); let config = DomainConfig { id: domain_id, protocol: *protocol, reconstruction_threshold, purpose, }; keys.push(DomainKey { domain_config: config.clone(), domain_secret_key: sk, domain_public_key: key.clone(), }); domain_configs.push(config); key_for_domains.push(KeyForDomain { attempt: AttemptId::new(), domain_id, key, }); } // Optional ForeignTx domain if self.foreign_tx { let (pk, sk) = make_key_for_domain(Curve::Secp256k1); let domain_id = DomainId(domain_configs.len() as u64); let key: PublicKeyExtended = pk.try_into().unwrap(); let config = DomainConfig { id: domain_id, protocol: Protocol::CaitSith, reconstruction_threshold: ReconstructionThreshold::new(cluster_threshold), purpose: DomainPurpose::ForeignTx, }; keys.push(DomainKey { domain_config: config.clone(), domain_secret_key: sk, domain_public_key: key.clone(), }); domain_configs.push(config); key_for_domains.push(KeyForDomain { attempt: AttemptId::new(), domain_id, key, }); } if !domain_configs.is_empty() { let next_domain_id = domain_configs.len() as u64; let keyset = Keyset::new(EpochId::new(5), key_for_domains); init_contract_running( &contract, domain_configs, next_domain_id, keyset, threshold_parameters, ) .await; } else { init_contract(&contract, threshold_parameters, self.init_config).await; } submit_attestations(&contract, &accounts, &participants).await; SandboxTestSetup { worker, contract, mpc_signer_accounts: accounts, keys, } } } /// Upgrades the given contract to the [`current_contract`] binary. /// /// This function: /// 1. Submits a proposal to upgrade the contract. /// 2. Casts votes until the proposal is executed. /// 3. Verifies the contract was upgraded by checking the contract's binary. /// /// Panics if: /// - The proposal transaction fails, /// - The state call is not deserializable, /// - Or the post-upgrade code does not match the expected binary. pub async fn propose_and_vote_contract_binary( accounts: &[Account], contract: &Contract, new_contract_binary: &[u8], ) { let propose_update_execution = accounts[0] .call(contract.id(), method_names::PROPOSE_UPDATE) .args_borsh(ProposeUpdateArgs { code: Some(new_contract_binary.to_vec()), config: None, }) .max_gas() .deposit(CURRENT_CONTRACT_DEPLOY_DEPOSIT) .transact() .await .expect("propose update call succeeds"); assert!( propose_update_execution.is_success(), "propose update call failed" ); let proposal_id: UpdateId = propose_update_execution.json().unwrap(); // Try calling into state and see if it works. let state_request_execution = accounts[0] .call(contract.id(), method_names::STATE) .transact() .await .expect("state request succeeds"); let _state: ProtocolContractState = state_request_execution .json() .expect("state is deserializable."); vote_update_till_completion(contract, accounts, &proposal_id).await; let contract_binary_post_upgrade = contract.view_code().await.unwrap(); assert_eq!( hash(new_contract_binary), hash(&contract_binary_post_upgrade), "Code hash post upgrade is not matching the proposed binary." ); } pub async fn vote_update_till_completion( contract: &Contract, accounts: &[Account], proposal_id: &UpdateId, ) { for voter in accounts { let execution = voter .call(contract.id(), method_names::VOTE_UPDATE) .args_json(serde_json::json!({ "id": proposal_id, })) .gas(GAS_FOR_VOTE_UPDATE) .transact() .await .unwrap(); dbg!(&execution); let update_occurred: bool = execution.json().expect("Vote cast was unsuccessful"); if update_occurred { return; } } panic!("Update didn't occurred") } /// Returns the [`dtos::Ed25519PublicKey`] corresponding to the `Account`'s /// signer. Mirrors what the contract reads via `env::signer_account_pk()` when /// the account submits a transaction. pub fn account_ed25519_public_key(account: &Account) -> dtos::Ed25519PublicKey { let bytes: [u8; 32] = account .secret_key() .public_key() .key_data() .try_into() .expect("sandbox Account key must be Ed25519"); dtos::Ed25519PublicKey::from(bytes) } /// Builds the set of [`NodeId`]s that a sandbox contract will store after /// each participant has submitted its attestation. Both the TLS key (from the /// participant's `tls_public_key`) and the account public key (read from the /// matching `Account`'s signer) must line up with what `submit_participant_info` /// will persist on-chain — it stores `env::signer_account_pk()` alongside the /// TLS key. Keep this in sync with `MpcContract::submit_participant_info` so /// that test-side `NodeId` comparisons against `get_tee_accounts()` stay valid. pub fn build_sandbox_node_ids( participants: &Participants, accounts: &[Account], ) -> BTreeSet { participants .participants() .iter() .map(|(account_id, _, info)| { let account = accounts .iter() .find(|a| a.id() == account_id) .expect("matching Account must exist for each participant"); NodeId { account_id: account_id.clone(), tls_public_key: info.tls_public_key.clone(), account_public_key: account_ed25519_public_key(account), } }) .collect() } pub async fn submit_tee_attestations( contract: &Contract, env_accounts: &mut [Account], node_ids: &BTreeSet, ) -> anyhow::Result<()> { env_accounts.sort_by(|left, right| left.id().cmp(right.id())); for (account, node_id) in env_accounts.iter().zip(node_ids) { assert_eq!(*account.id(), node_id.account_id, "AccountId mismatch"); let attestation = Attestation::Mock(MockAttestation::Valid); // TODO(#1109): add TLS key. let result = submit_participant_info(account, contract, &attestation, &node_id.tls_public_key) .await?; assert!(result.is_success()); } Ok(()) } /// Submit mock attestations for all participants in parallel. pub async fn submit_attestations( contract: &Contract, accounts: &[Account], participants: &Participants, ) { let futures: Vec<_> = participants .participants() .iter() .zip(accounts) .enumerate() .map(|(i, ((_, _, participant), account))| async move { let attestation = Attestation::Mock(MockAttestation::Valid); let tls_key = participant.tls_public_key.clone(); let success = submit_participant_info(account, contract, &attestation, &tls_key) .await .expect("submit_participant_info should not error") .is_success(); assert!( success, "submit_participant_info failed for participant {}", i ); }) .collect(); futures::future::join_all(futures).await; } /// This function assumes that the accounts are sorted by participant id. /// Returns the shared_secret_key in the same order as /// the corresponding domain configs supplied. pub async fn call_contract_key_generation( domains_to_add: &[DomainConfig; N], accounts: &[Account], contract: &Contract, expected_epoch_id: u64, ) -> [DomainKey; N] { let mut domain_keys = vec![]; let existing_domains = { let state: ProtocolContractState = get_state(contract).await; match state { ProtocolContractState::Running(state) => state.domains.domains.len(), _ => panic!("ProtocolContractState must be Running"), } }; vote_add_domains(contract, accounts, domains_to_add) .await .unwrap(); let state: ProtocolContractState = get_state(contract).await; match state { ProtocolContractState::Initializing(state) => { assert_eq!( state.domains.domains.len(), existing_domains + domains_to_add.len() ); } _ => panic!("should be in initializing state"), }; for domain in domains_to_add.iter() { let key_event_id = dtos::KeyEventId { epoch_id: dtos::EpochId(expected_epoch_id), domain_id: dtos::DomainId(*domain.id), attempt_id: dtos::AttemptId(0), }; start_keygen_instance(contract, accounts, key_event_id) .await .unwrap(); let (public_key, shared_secret_key) = make_key_for_domain(Curve::from(domain.protocol)); domain_keys.push(DomainKey { domain_config: domain.clone(), domain_secret_key: shared_secret_key, domain_public_key: public_key.clone().try_into().unwrap(), }); vote_public_key(contract, accounts, key_event_id, public_key) .await .unwrap(); } let state: ProtocolContractState = get_state(contract).await; match state { ProtocolContractState::Running(state) => { assert_eq!(state.keyset.epoch_id.0, expected_epoch_id); assert_eq!( state.domains.domains.len(), domains_to_add.len() + existing_domains ); } state => panic!("should be in running state. Actual state: {state:#?}"), }; domain_keys.try_into().unwrap() } pub struct InjectedContractState { pub pending_sign_requests: Vec, pub domain_keys: Vec, } /// Adds dummy state to a contract (threshold proposal, domains, sign requests) /// so that migration paths are exercised in upgrade tests. /// /// The pending signature requests can be responded to. pub async fn execute_key_generation_and_add_random_state( accounts: &[Account], participants: Participants, contract: &Contract, worker: &Worker, rng: &mut impl CryptoRngCore, ) -> InjectedContractState { const EPOCH_ID: u64 = 0; let threshold = assert_running_return_threshold(contract).await; // 1. Submit a threshold proposal (raise threshold to threshold + 1). let dummy_threshold_parameters = ThresholdParameters::new(participants, Threshold::new(threshold.0 + 1)).unwrap(); let dummy_proposal = json!({ "prospective_epoch_id": 1, "proposal": ProposedThresholdParameters::new( dummy_threshold_parameters, BTreeMap::new(), ), }); accounts[0] .call(contract.id(), method_names::VOTE_NEW_PARAMETERS) .args_json(dummy_proposal) .max_gas() .transact() .await .unwrap() .unwrap(); // 2. Add multiple domains. let domains_to_add = [ DomainConfig { id: 0.into(), protocol: Protocol::Frost, reconstruction_threshold: ReconstructionThreshold::new(6), purpose: DomainPurpose::Sign, }, DomainConfig { id: 1.into(), protocol: Protocol::CaitSith, reconstruction_threshold: ReconstructionThreshold::new(6), purpose: DomainPurpose::Sign, }, DomainConfig { id: 2.into(), protocol: Protocol::Frost, reconstruction_threshold: ReconstructionThreshold::new(6), purpose: DomainPurpose::Sign, }, ]; let domain_keys = call_contract_key_generation(&domains_to_add, accounts, contract, EPOCH_ID).await; // 3. Submit pending sign requests. let (pending_sign_requests, _) = make_and_submit_requests(&domain_keys, contract, worker, rng).await; InjectedContractState { pending_sign_requests, domain_keys: domain_keys.to_vec(), } } fn hash(code: &[u8]) -> [u8; 32] { let mut hasher = sha2::Sha256::new(); hasher.update(code); hasher.finalize().into() } /// registers a foreign chain configuration so the foreign chains are supported pub async fn register_foreign_chain_configuration( chain: near_mpc_contract_interface::types::ForeignChain, contract: &Contract, accounts: &[Account], ) { let node_foreign_chain_support = SupportedForeignChains::from(BTreeSet::from([chain])); for account in accounts { let result = account .call(contract.id(), method_names::REGISTER_FOREIGN_CHAIN_SUPPORT) .args_json(json!({ "foreign_chain_support": node_foreign_chain_support })) .transact() .await .unwrap() .into_result(); assert!( result.is_ok(), "{} should succeed", method_names::REGISTER_FOREIGN_CHAIN_SUPPORT ); } } /// Poll the contract until a pending foreign-tx request appears (or panic after timeout). pub async fn await_pending_foreign_tx_request_observed_on_contract( contract: &Contract, request: &dtos::VerifyForeignTransactionRequest, ) { const TIMEOUT: Duration = Duration::from_secs(10); const POLL_INTERVAL: Duration = Duration::from_millis(100); async { let args = json!({ "request": request }); loop { let result = contract .view(method_names::GET_PENDING_VERIFY_FOREIGN_TX_REQUEST) .args_json(&args) .await; if let Ok(view) = result { // The view returns Option; non-null means the request is pending. let value: serde_json::Value = view.json().unwrap(); if !value.is_null() { return; } } tokio::time::sleep(POLL_INTERVAL).await; } } .timeout(TIMEOUT) .await .expect("Timed out waiting for pending foreign-tx request on-chain"); } /// Sign a foreign-tx payload hash with the root secret key and return the /// payload and contract-level response DTO. pub fn sign_foreign_tx_response( request: &near_mpc_contract_interface::types::ForeignChainRpcRequest, extracted_values: Vec, sk: &threshold_signatures::ecdsa::KeygenOutput, ) -> ( near_mpc_contract_interface::types::ForeignTxSignPayload, near_mpc_contract_interface::types::VerifyForeignTransactionResponse, ) { let payload = ForeignTxSignPayload::V1(ForeignTxSignPayloadV1 { request: request.clone(), values: extracted_values, }); let payload_hash = payload.compute_msg_hash().unwrap(); let signing_key = SigningKey::from_bytes(&sk.private_share.to_scalar().into()).unwrap(); let (signature, recovery_id) = signing_key.sign_prehash(&payload_hash.0).unwrap(); let signature_response = near_mpc_contract_interface::types::SignatureResponse::Secp256k1( near_mpc_contract_interface::types::K256Signature::from_ecdsa_recoverable( &signature, recovery_id, ), ); let response = VerifyForeignTransactionResponse { payload_hash, signature: signature_response, }; (payload, response) } pub async fn generate_participant_and_submit_attestation( worker: &Worker, contract: &Contract, ) -> (Account, AccountId, ParticipantInfo) { let (new_account, account_id) = gen_account(worker).await; let new_participant = gen_participant_info(); // Submit attestation for the new participant, otherwise // the contract will reject the resharing. let result = submit_participant_info( &new_account, contract, &dtos::Attestation::Mock(dtos::MockAttestation::Valid), &new_participant.tls_public_key, ) .await .expect("Attestation submission for new account must succeed."); assert!(result.is_success()); (new_account, account_id, new_participant) } pub fn ethereum_evm_request() -> ForeignChainRpcRequest { ForeignChainRpcRequest::Ethereum(EvmRpcRequest { tx_id: EvmTxId([0xbb; 32]), extractors: vec![EvmExtractor::BlockHash], finality: EvmFinality::Finalized, }) } pub fn abstract_evm_request() -> ForeignChainRpcRequest { ForeignChainRpcRequest::Abstract(EvmRpcRequest { tx_id: EvmTxId([0xbb; 32]), extractors: vec![EvmExtractor::BlockHash], finality: EvmFinality::Finalized, }) } pub fn bitcoin_request() -> ForeignChainRpcRequest { ForeignChainRpcRequest::Bitcoin(BitcoinRpcRequest { tx_id: BitcoinTxId([0xdd; 32]), confirmations: BlockConfirmations(6), extractors: vec![BitcoinExtractor::BlockHash], }) } pub fn starknet_request() -> ForeignChainRpcRequest { ForeignChainRpcRequest::Starknet(StarknetRpcRequest { tx_id: StarknetTxId(StarknetFelt([0xee; 32])), finality: StarknetFinality::AcceptedOnL1, extractors: vec![StarknetExtractor::BlockHash], }) } pub fn evm_block_hash_extracted_values() -> Vec { vec![ExtractedValue::EvmExtractedValue( EvmExtractedValue::BlockHash(Hash256([0xaa; 32])), )] } pub fn bitcoin_extracted_values() -> Vec { vec![ExtractedValue::BitcoinExtractedValue( BitcoinExtractedValue::BlockHash(Hash256([0xaa; 32])), )] } pub fn starknet_extracted_values() -> Vec { vec![ExtractedValue::StarknetExtractedValue( StarknetExtractedValue::BlockHash(StarknetFelt([0xaa; 32])), )] } pub fn bogus_ton_log_extracted_value() -> Vec { vec![ExtractedValue::TonExtractedValue(TonExtractedValue::Log( TonLog { from_address: TonAddress { workchain: 0, hash: Hash256([1; 32]), }, body: TonCellBody::new(vec![].try_into().unwrap(), 0).unwrap(), body_refs: vec![].try_into().unwrap(), }, ))] } pub fn aptos_extracted_values() -> Vec { vec![ExtractedValue::AptosExtractedValue( AptosExtractedValue::Event(AptosEvent { account_address: AptosAddress([1; 32]), sequence_number: 0, type_tag: "0x1::omni_bridge::InitTransfer".to_string(), data: r#"{"amount":"100"}"#.to_string(), }), )] } pub fn bnb_evm_request() -> ForeignChainRpcRequest { ForeignChainRpcRequest::Bnb(EvmRpcRequest { tx_id: EvmTxId([0xbb; 32]), extractors: vec![EvmExtractor::BlockHash], finality: EvmFinality::Finalized, }) } pub fn base_evm_request() -> ForeignChainRpcRequest { ForeignChainRpcRequest::Base(EvmRpcRequest { tx_id: EvmTxId([0xbb; 32]), extractors: vec![EvmExtractor::BlockHash], finality: EvmFinality::Finalized, }) } pub fn arbitrum_evm_request() -> ForeignChainRpcRequest { ForeignChainRpcRequest::Arbitrum(EvmRpcRequest { tx_id: EvmTxId([0xbb; 32]), extractors: vec![EvmExtractor::BlockHash], finality: EvmFinality::Finalized, }) } pub fn hyper_evm_request() -> ForeignChainRpcRequest { ForeignChainRpcRequest::HyperEvm(EvmRpcRequest { tx_id: EvmTxId([0xbb; 32]), extractors: vec![EvmExtractor::BlockHash], finality: EvmFinality::Finalized, }) } pub fn polygon_evm_request() -> ForeignChainRpcRequest { ForeignChainRpcRequest::Polygon(EvmRpcRequest { tx_id: EvmTxId([0xbb; 32]), extractors: vec![EvmExtractor::BlockHash], finality: EvmFinality::Finalized, }) } pub fn ton_request() -> ForeignChainRpcRequest { ForeignChainRpcRequest::Ton(TonRpcRequest { tx_id: TonTxId([0xbb; 32]), extractors: vec![TonExtractor::Log { message_index: 0 }], finality: TonFinality::MasterchainIncluded, account: TonAddress { workchain: 0, hash: Hash256([1; 32]), }, }) } pub fn aptos_request() -> ForeignChainRpcRequest { ForeignChainRpcRequest::Aptos(AptosRpcRequest { tx_id: AptosTxId([0xbb; 32]), finality: AptosFinality::Committed, extractors: vec![AptosExtractor::Event { event_index: 0 }], }) }