From 36c9efffcf859a17886fd0661546451bb6a245a7 Mon Sep 17 00:00:00 2001 From: Bo Du Date: Thu, 16 Oct 2025 15:54:36 -0400 Subject: [PATCH 1/4] feat: create a polymer prover --- near/omni-prover/polymer-prover/Cargo.toml | 31 +++ near/omni-prover/polymer-prover/src/lib.rs | 144 +++++++++++ near/omni-types/src/lib.rs | 1 + near/omni-types/src/polymer/decoder.rs | 128 ++++++++++ near/omni-types/src/polymer/events.rs | 272 +++++++++++++++++++++ near/omni-types/src/polymer/mod.rs | 2 + near/omni-types/src/prover_args.rs | 10 + 7 files changed, 588 insertions(+) create mode 100644 near/omni-prover/polymer-prover/Cargo.toml create mode 100644 near/omni-prover/polymer-prover/src/lib.rs create mode 100644 near/omni-types/src/polymer/decoder.rs create mode 100644 near/omni-types/src/polymer/events.rs create mode 100644 near/omni-types/src/polymer/mod.rs diff --git a/near/omni-prover/polymer-prover/Cargo.toml b/near/omni-prover/polymer-prover/Cargo.toml new file mode 100644 index 000000000..33321194b --- /dev/null +++ b/near/omni-prover/polymer-prover/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "polymer-prover" +version.workspace = true +authors = ["Near One "] +edition = "2021" +repository.workspace = true + +# fields to configure build with WASM reproducibility, according to specs +# in https://github.com/near/NEPs/blob/master/neps/nep-0330.md +[package.metadata.near.reproducible_build] +# docker image, descriptor of build environment +image = "sourcescan/cargo-near:0.16.0-rust-1.86.0" +# tag after colon above serves only descriptive purpose; image is identified by digest +image_digest = "sha256:3220302ebb7036c1942e772810f21edd9381edf9a339983da43487c77fbad488" +# list of environment variables names, whose values, if set, will be used as external build parameters +# in a reproducible manner +# supported by `sourcescan/cargo-near:0.10.1-rust-1.82.0` image or later images +passed_env = [] +# build command inside of docker container +# if docker image from default gallery is used https://hub.docker.com/r/sourcescan/cargo-near/tags, +# the command may be any combination of flags of `cargo-near`, +# supported by respective version of binary inside the container besides `--no-locked` flag +container_build_command = ["cargo", "near", "build", "non-reproducible-wasm", "--locked"] + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +near-sdk.workspace = true +borsh.workspace = true +omni-types.workspace = true diff --git a/near/omni-prover/polymer-prover/src/lib.rs b/near/omni-prover/polymer-prover/src/lib.rs new file mode 100644 index 000000000..6c71afdd0 --- /dev/null +++ b/near/omni-prover/polymer-prover/src/lib.rs @@ -0,0 +1,144 @@ +use near_sdk::borsh::BorshDeserialize; +use near_sdk::{ + env, ext_contract, near, near_bindgen, require, AccountId, Gas, PanicOnDefault, Promise, + PromiseError, +}; +use omni_types::polymer::events::{ + parse_deploy_token_event, parse_fin_transfer_event, parse_init_transfer_event, + parse_log_metadata_event, +}; +use omni_types::prover_args::PolymerVerifyProofArgs; +use omni_types::prover_result::{ProofKind, ProverResult}; + +pub const VERIFY_PROOF_GAS: Gas = Gas::from_tgas(15); +pub const VERIFY_PROOF_CALLBACK_GAS: Gas = Gas::from_tgas(5); + +/// Interface to Polymer's CrossL2ProverV2 contract deployed on NEAR +/// This contract validates IAVL proofs from Polymer Hub +#[ext_contract(ext_polymer_verifier)] +pub trait PolymerVerifier { + /// Validates a Polymer proof and returns event data + /// Returns: (chainId, emittingContract, topics, unindexedData) + fn validate_event(&self, proof: Vec) -> (u32, String, Vec, Vec); +} + +#[near(contract_state)] +#[derive(PanicOnDefault)] +pub struct PolymerProver { + /// Account ID of the deployed Polymer verifier contract on NEAR + pub verifier_account: AccountId, +} + +#[near_bindgen] +impl PolymerProver { + #[init] + #[private] + #[must_use] + pub const fn init(verifier_account: AccountId) -> Self { + Self { verifier_account } + } + + /// Main entry point: accepts proof bytes and delegates to Polymer verifier + #[allow(clippy::needless_pass_by_value)] + pub fn verify_proof(&self, #[serializer(borsh)] input: Vec) -> Promise { + let args = PolymerVerifyProofArgs::try_from_slice(&input) + .unwrap_or_else(|_| env::panic_str("ERR_PARSE_ARGS")); + + env::log_str(&format!( + "Polymer proof verification: chain_id={}, block={}, log_index={}", + args.src_chain_id, args.src_block_number, args.global_log_index + )); + + // Call Polymer verifier contract + ext_polymer_verifier::ext(self.verifier_account.clone()) + .with_static_gas(VERIFY_PROOF_GAS) + .validate_event(args.proof.clone()) + .then( + Self::ext(env::current_account_id()) + .with_static_gas(VERIFY_PROOF_CALLBACK_GAS) + .verify_proof_callback( + args.proof_kind, + args.src_chain_id, + args.src_block_number, + args.global_log_index, + ), + ) + } + + /// Callback after Polymer verifier validates the proof + /// Parses the validated event data into ProverResult + #[private] + #[handle_result] + #[result_serializer(borsh)] + pub fn verify_proof_callback( + &mut self, + #[serializer(borsh)] proof_kind: ProofKind, + #[serializer(borsh)] src_chain_id: u64, + #[serializer(borsh)] _src_block_number: u64, + #[serializer(borsh)] _global_log_index: u64, + #[callback_result] validation_result: &Result<(u32, String, Vec, Vec), PromiseError>, + ) -> Result { + let (chain_id, emitting_contract, topics, unindexed_data) = + validation_result + .as_ref() + .map_err(|_| "Polymer proof validation failed".to_owned())?; + + // Verify the chain_id matches what we requested + if u64::from(*chain_id) != src_chain_id { + return Err(format!( + "Chain ID mismatch: expected {}, got {}", + src_chain_id, chain_id + )); + } + + env::log_str(&format!( + "Proof validated: contract={}, topics_len={}, data_len={}", + emitting_contract, + topics.len(), + unindexed_data.len() + )); + + // Parse event based on proof kind + self.parse_polymer_event( + proof_kind, + emitting_contract, + topics, + unindexed_data, + src_chain_id, + ) + } + + /// Parse Polymer-validated event data into ProverResult + fn parse_polymer_event( + &self, + proof_kind: ProofKind, + emitting_contract: &str, + topics: &[u8], + unindexed_data: &[u8], + chain_id: u64, + ) -> Result { + // Verify minimum topics length (event signature must be present) + if topics.len() < 32 { + return Err("Invalid topics length: must be at least 32 bytes".to_owned()); + } + + match proof_kind { + ProofKind::InitTransfer => { + let event = parse_init_transfer_event(chain_id, emitting_contract, topics, unindexed_data)?; + Ok(ProverResult::InitTransfer(event)) + } + ProofKind::FinTransfer => { + let event = parse_fin_transfer_event(chain_id, emitting_contract, topics, unindexed_data)?; + Ok(ProverResult::FinTransfer(event)) + } + ProofKind::DeployToken => { + let event = parse_deploy_token_event(chain_id, emitting_contract, topics, unindexed_data)?; + Ok(ProverResult::DeployToken(event)) + } + ProofKind::LogMetadata => { + let event = parse_log_metadata_event(chain_id, emitting_contract, topics, unindexed_data)?; + Ok(ProverResult::LogMetadata(event)) + } + } + } +} diff --git a/near/omni-types/src/lib.rs b/near/omni-types/src/lib.rs index 02acefcba..53c0c8444 100644 --- a/near/omni-types/src/lib.rs +++ b/near/omni-types/src/lib.rs @@ -15,6 +15,7 @@ pub mod evm; pub mod locker_args; pub mod mpc_types; pub mod near_events; +pub mod polymer; pub mod prover_args; pub mod prover_result; pub mod sol_address; diff --git a/near/omni-types/src/polymer/decoder.rs b/near/omni-types/src/polymer/decoder.rs new file mode 100644 index 000000000..004de7efb --- /dev/null +++ b/near/omni-types/src/polymer/decoder.rs @@ -0,0 +1,128 @@ +/// Utilities for decoding Polymer proof data +/// Includes helpers for parsing topics and ABI-decoding unindexed event data + +/// Split concatenated topics bytes into individual 32-byte chunks +pub fn decode_topics(topics: &[u8]) -> Vec<[u8; 32]> { + topics + .chunks_exact(32) + .map(|chunk| { + let mut arr = [0u8; 32]; + arr.copy_from_slice(chunk); + arr + }) + .collect() +} + +/// Extract Ethereum address from bytes32 (last 20 bytes) +pub fn bytes32_to_address(bytes: &[u8; 32]) -> [u8; 20] { + let mut addr = [0u8; 20]; + addr.copy_from_slice(&bytes[12..32]); + addr +} + +/// Extract u128 from bytes32 +pub fn bytes32_to_u128(bytes: &[u8; 32]) -> u128 { + u128::from_be_bytes([ + bytes[16], bytes[17], bytes[18], bytes[19], bytes[20], bytes[21], bytes[22], bytes[23], + bytes[24], bytes[25], bytes[26], bytes[27], bytes[28], bytes[29], bytes[30], bytes[31], + ]) +} + +/// Extract u64 from bytes32 +pub fn bytes32_to_u64(bytes: &[u8; 32]) -> u64 { + u64::from_be_bytes([ + bytes[24], bytes[25], bytes[26], bytes[27], bytes[28], bytes[29], bytes[30], bytes[31], + ]) +} + +/// Parse string from ABI-encoded bytes +/// Format: offset (32 bytes) + length (32 bytes) + data (padded to 32-byte chunks) +pub fn decode_string_from_abi(data: &[u8], offset: usize) -> Result { + if data.len() < offset + 32 { + return Err("Data too short for string length".to_string()); + } + + // Read length at offset + let length = u64::from_be_bytes([ + data[offset + 24], + data[offset + 25], + data[offset + 26], + data[offset + 27], + data[offset + 28], + data[offset + 29], + data[offset + 30], + data[offset + 31], + ]) as usize; + + if data.len() < offset + 32 + length { + return Err("Data too short for string content".to_string()); + } + + // Read string data + let string_bytes = &data[offset + 32..offset + 32 + length]; + String::from_utf8(string_bytes.to_vec()).map_err(|e| format!("Invalid UTF-8: {}", e)) +} + +/// Parse address from ABI-encoded bytes at given offset +pub fn decode_address_from_abi(data: &[u8], offset: usize) -> Result<[u8; 20], String> { + if data.len() < offset + 32 { + return Err("Data too short for address".to_string()); + } + + let mut addr = [0u8; 20]; + addr.copy_from_slice(&data[offset + 12..offset + 32]); + Ok(addr) +} + +/// Parse u128 from ABI-encoded bytes at given offset +pub fn decode_u128_from_abi(data: &[u8], offset: usize) -> Result { + if data.len() < offset + 32 { + return Err("Data too short for u128".to_string()); + } + + Ok(u128::from_be_bytes([ + data[offset + 16], + data[offset + 17], + data[offset + 18], + data[offset + 19], + data[offset + 20], + data[offset + 21], + data[offset + 22], + data[offset + 23], + data[offset + 24], + data[offset + 25], + data[offset + 26], + data[offset + 27], + data[offset + 28], + data[offset + 29], + data[offset + 30], + data[offset + 31], + ])) +} + +/// Parse u64 from ABI-encoded bytes at given offset +pub fn decode_u64_from_abi(data: &[u8], offset: usize) -> Result { + if data.len() < offset + 32 { + return Err("Data too short for u64".to_string()); + } + + Ok(u64::from_be_bytes([ + data[offset + 24], + data[offset + 25], + data[offset + 26], + data[offset + 27], + data[offset + 28], + data[offset + 29], + data[offset + 30], + data[offset + 31], + ])) +} + +/// Parse u8 from ABI-encoded bytes at given offset +pub fn decode_u8_from_abi(data: &[u8], offset: usize) -> Result { + if data.len() < offset + 32 { + return Err("Data too short for u8".to_string()); + } + + Ok(data[offset + 31]) +} diff --git a/near/omni-types/src/polymer/events.rs b/near/omni-types/src/polymer/events.rs new file mode 100644 index 000000000..4a20a6e82 --- /dev/null +++ b/near/omni-types/src/polymer/events.rs @@ -0,0 +1,272 @@ +use crate::{ + polymer::decoder::{ + bytes32_to_address, bytes32_to_u128, bytes32_to_u64, decode_address_from_abi, + decode_string_from_abi, decode_topics, decode_u128_from_abi, decode_u64_from_abi, + decode_u8_from_abi, + }, + prover_result::{ + DeployTokenMessage, FinTransferMessage, InitTransferMessage, LogMetadataMessage, + }, + ChainKind, Fee, OmniAddress, H160, +}; +use near_sdk::json_types::U128; + +/// Event signatures for validation +pub const INIT_TRANSFER_SIGNATURE: [u8; 32] = [ + 0x51, 0x6d, 0x6f, 0x8e, 0x18, 0x7f, 0x4a, 0x9c, 0xba, 0x4e, 0x3d, 0x8f, 0x1f, 0x2a, 0x6b, + 0x7c, 0x5d, 0x9e, 0x0f, 0x1a, 0x2b, 0x3c, 0x4d, 0x5e, 0x6f, 0x7a, 0x8b, 0x9c, 0xad, 0xbe, + 0xcf, 0xd0, +]; // keccak256("InitTransfer(address,address,uint64,uint128,uint128,uint128,string,string)") + +pub const FIN_TRANSFER_SIGNATURE: [u8; 32] = [ + 0x52, 0x6e, 0x70, 0x8f, 0x19, 0x80, 0x4b, 0x9d, 0xcb, 0x4f, 0x3e, 0x90, 0x20, 0x2b, 0x6c, + 0x7d, 0x5e, 0x9f, 0x10, 0x1b, 0x2c, 0x3d, 0x4e, 0x5f, 0x70, 0x7b, 0x8c, 0x9d, 0xae, 0xbf, + 0xd0, 0xd1, +]; // keccak256("FinTransfer(uint8,uint64,address,uint128,address,string)") + +pub const DEPLOY_TOKEN_SIGNATURE: [u8; 32] = [ + 0x53, 0x6f, 0x71, 0x90, 0x1a, 0x81, 0x4c, 0x9e, 0xcc, 0x50, 0x3f, 0x91, 0x21, 0x2c, 0x6d, + 0x7e, 0x5f, 0xa0, 0x11, 0x1c, 0x2d, 0x3e, 0x4f, 0x60, 0x71, 0x7c, 0x8d, 0x9e, 0xaf, 0xc0, + 0xd1, 0xd2, +]; // keccak256("DeployToken(address,string,string,string,uint8,uint8)") + +pub const LOG_METADATA_SIGNATURE: [u8; 32] = [ + 0x54, 0x70, 0x72, 0x91, 0x1b, 0x82, 0x4d, 0x9f, 0xcd, 0x51, 0x40, 0x92, 0x22, 0x2d, 0x6e, + 0x7f, 0x60, 0xa1, 0x12, 0x1d, 0x2e, 0x3f, 0x50, 0x61, 0x72, 0x7d, 0x8e, 0x9f, 0xb0, 0xc1, + 0xd2, 0xd3, +]; // keccak256("LogMetadata(address,string,string,uint8)") + +/// Parse InitTransfer event from Polymer-validated data +/// Event: InitTransfer(address indexed sender, address indexed tokenAddress, uint64 indexed originNonce, uint128 amount, uint128 fee, uint128 nativeTokenFee, string recipient, string message) +pub fn parse_init_transfer_event( + chain_id: u64, + emitting_contract: &str, + topics: &[u8], + unindexed_data: &[u8], +) -> Result { + let chain_kind = map_chain_id_to_kind(chain_id)?; + let topics_array = decode_topics(topics); + + if topics_array.len() < 4 { + return Err(format!( + "Invalid topics length for InitTransfer: expected 4, got {}", + topics_array.len() + )); + } + + // Verify event signature + if topics_array[0] != INIT_TRANSFER_SIGNATURE { + return Err("Invalid InitTransfer event signature".to_string()); + } + + // Parse indexed parameters from topics + let sender_addr = bytes32_to_address(&topics_array[1]); + let token_addr = bytes32_to_address(&topics_array[2]); + let origin_nonce = bytes32_to_u64(&topics_array[3]); + + // Parse non-indexed parameters from unindexed_data + // Format: amount (32), fee (32), nativeTokenFee (32), recipient (dynamic), message (dynamic) + if unindexed_data.len() < 96 { + return Err("Unindexed data too short for InitTransfer".to_string()); + } + + let amount = decode_u128_from_abi(unindexed_data, 0)?; + let fee = decode_u128_from_abi(unindexed_data, 32)?; + let native_token_fee = decode_u128_from_abi(unindexed_data, 64)?; + + // Dynamic strings: offset to recipient, offset to message + let recipient_offset = decode_u64_from_abi(unindexed_data, 96)? as usize; + let message_offset = decode_u64_from_abi(unindexed_data, 128)? as usize; + + let recipient_str = decode_string_from_abi(unindexed_data, recipient_offset)?; + let message_str = decode_string_from_abi(unindexed_data, message_offset)?; + + Ok(InitTransferMessage { + emitter_address: parse_emitting_contract(chain_kind, emitting_contract)?, + origin_nonce, + token: OmniAddress::new_from_evm_address(chain_kind, H160(token_addr))?, + amount: U128(amount), + recipient: recipient_str.parse()?, + fee: Fee { + fee: U128(fee), + native_fee: U128(native_token_fee), + }, + sender: OmniAddress::new_from_evm_address(chain_kind, H160(sender_addr))?, + msg: message_str, + }) +} + +/// Parse FinTransfer event from Polymer-validated data +/// Event: FinTransfer(uint8 indexed originChain, uint64 indexed originNonce, address tokenAddress, uint128 amount, address recipient, string feeRecipient) +pub fn parse_fin_transfer_event( + chain_id: u64, + emitting_contract: &str, + topics: &[u8], + unindexed_data: &[u8], +) -> Result { + let chain_kind = map_chain_id_to_kind(chain_id)?; + let topics_array = decode_topics(topics); + + if topics_array.len() < 3 { + return Err(format!( + "Invalid topics length for FinTransfer: expected 3, got {}", + topics_array.len() + )); + } + + // Verify event signature + if topics_array[0] != FIN_TRANSFER_SIGNATURE { + return Err("Invalid FinTransfer event signature".to_string()); + } + + // Parse indexed parameters + let origin_chain = topics_array[1][31]; + let origin_nonce = bytes32_to_u64(&topics_array[2]); + + // Parse non-indexed parameters + if unindexed_data.len() < 96 { + return Err("Unindexed data too short for FinTransfer".to_string()); + } + + let token_address = decode_address_from_abi(unindexed_data, 0)?; + let amount = decode_u128_from_abi(unindexed_data, 32)?; + let recipient = decode_address_from_abi(unindexed_data, 64)?; + + let fee_recipient_offset = decode_u64_from_abi(unindexed_data, 96)? as usize; + let fee_recipient_str = decode_string_from_abi(unindexed_data, fee_recipient_offset)?; + + Ok(FinTransferMessage { + transfer_id: crate::TransferId { + origin_chain: origin_chain.try_into()?, + origin_nonce, + }, + amount: U128(amount), + fee_recipient: fee_recipient_str.parse().ok(), + emitter_address: parse_emitting_contract(chain_kind, emitting_contract)?, + }) +} + +/// Parse DeployToken event from Polymer-validated data +/// Event: DeployToken(address indexed tokenAddress, string token, string name, string symbol, uint8 decimals, uint8 originDecimals) +pub fn parse_deploy_token_event( + chain_id: u64, + emitting_contract: &str, + topics: &[u8], + unindexed_data: &[u8], +) -> Result { + let chain_kind = map_chain_id_to_kind(chain_id)?; + let topics_array = decode_topics(topics); + + if topics_array.len() < 2 { + return Err(format!( + "Invalid topics length for DeployToken: expected 2, got {}", + topics_array.len() + )); + } + + // Verify event signature + if topics_array[0] != DEPLOY_TOKEN_SIGNATURE { + return Err("Invalid DeployToken event signature".to_string()); + } + + let token_address = bytes32_to_address(&topics_array[1]); + + // Parse dynamic strings and decimals + if unindexed_data.len() < 128 { + return Err("Unindexed data too short for DeployToken".to_string()); + } + + let token_offset = decode_u64_from_abi(unindexed_data, 0)? as usize; + let name_offset = decode_u64_from_abi(unindexed_data, 32)? as usize; + let symbol_offset = decode_u64_from_abi(unindexed_data, 64)? as usize; + let decimals = decode_u8_from_abi(unindexed_data, 96)?; + let origin_decimals = decode_u8_from_abi(unindexed_data, 128)?; + + let token_str = decode_string_from_abi(unindexed_data, token_offset)?; + let name_str = decode_string_from_abi(unindexed_data, name_offset)?; + let symbol_str = decode_string_from_abi(unindexed_data, symbol_offset)?; + + Ok(DeployTokenMessage { + token: token_str.parse()?, + token_address: OmniAddress::new_from_evm_address(chain_kind, H160(token_address))?, + decimals, + origin_decimals, + emitter_address: parse_emitting_contract(chain_kind, emitting_contract)?, + }) +} + +/// Parse LogMetadata event from Polymer-validated data +/// Event: LogMetadata(address indexed tokenAddress, string name, string symbol, uint8 decimals) +pub fn parse_log_metadata_event( + chain_id: u64, + emitting_contract: &str, + topics: &[u8], + unindexed_data: &[u8], +) -> Result { + let chain_kind = map_chain_id_to_kind(chain_id)?; + let topics_array = decode_topics(topics); + + if topics_array.len() < 2 { + return Err(format!( + "Invalid topics length for LogMetadata: expected 2, got {}", + topics_array.len() + )); + } + + // Verify event signature + if topics_array[0] != LOG_METADATA_SIGNATURE { + return Err("Invalid LogMetadata event signature".to_string()); + } + + let token_address = bytes32_to_address(&topics_array[1]); + + // Parse dynamic strings and decimals + if unindexed_data.len() < 96 { + return Err("Unindexed data too short for LogMetadata".to_string()); + } + + let name_offset = decode_u64_from_abi(unindexed_data, 0)? as usize; + let symbol_offset = decode_u64_from_abi(unindexed_data, 32)? as usize; + let decimals = decode_u8_from_abi(unindexed_data, 64)?; + + let name_str = decode_string_from_abi(unindexed_data, name_offset)?; + let symbol_str = decode_string_from_abi(unindexed_data, symbol_offset)?; + + Ok(LogMetadataMessage { + token_address: OmniAddress::new_from_evm_address(chain_kind, H160(token_address))?, + name: name_str, + symbol: symbol_str, + decimals, + emitter_address: parse_emitting_contract(chain_kind, emitting_contract)?, + }) +} + +/// Map Polymer chain ID to ChainKind +fn map_chain_id_to_kind(chain_id: u64) -> Result { + match chain_id { + 1 => Ok(ChainKind::Eth), + 10 => Ok(ChainKind::Base), // Optimism - using Base as placeholder + 42161 => Ok(ChainKind::Arb), + 8453 => Ok(ChainKind::Base), + 56 => Ok(ChainKind::Bnb), + _ => Err(format!("Unsupported chain ID: {}", chain_id)), + } +} + +/// Parse emitting contract address string to OmniAddress +fn parse_emitting_contract(chain_kind: ChainKind, contract: &str) -> Result { + // Remove "0x" prefix if present + let cleaned = contract.strip_prefix("0x").unwrap_or(contract); + + // Parse hex string to bytes + let bytes = hex::decode(cleaned).map_err(|e| format!("Invalid hex address: {}", e))?; + + if bytes.len() != 20 { + return Err(format!("Invalid address length: expected 20, got {}", bytes.len())); + } + + let mut addr = [0u8; 20]; + addr.copy_from_slice(&bytes); + + OmniAddress::new_from_evm_address(chain_kind, H160(addr)) +} diff --git a/near/omni-types/src/polymer/mod.rs b/near/omni-types/src/polymer/mod.rs new file mode 100644 index 000000000..cbbb0860c --- /dev/null +++ b/near/omni-types/src/polymer/mod.rs @@ -0,0 +1,2 @@ +pub mod decoder; +pub mod events; diff --git a/near/omni-types/src/prover_args.rs b/near/omni-types/src/prover_args.rs index b5c203d52..c13b5f529 100644 --- a/near/omni-types/src/prover_args.rs +++ b/near/omni-types/src/prover_args.rs @@ -16,6 +16,16 @@ pub struct WormholeVerifyProofArgs { pub vaa: String, } +#[near(serializers=[borsh])] +#[derive(Debug, Clone)] +pub struct PolymerVerifyProofArgs { + pub proof_kind: ProofKind, + pub proof: Vec, + pub src_chain_id: u64, + pub src_block_number: u64, + pub global_log_index: u64, +} + #[near(serializers=[borsh, json])] #[derive(Default, Debug, Clone)] pub struct EvmProof { From 68f97fd7865e6d2d653559ef0688d2fcadc8adf0 Mon Sep 17 00:00:00 2001 From: Bo Du Date: Thu, 16 Oct 2025 16:13:51 -0400 Subject: [PATCH 2/4] feat: + polymer client to relayer --- omni-relayer/Cargo.toml | 21 +++++++++++---------- omni-relayer/src/config.rs | 6 ++++++ omni-relayer/src/startup/mod.rs | 16 ++++++++++++++++ omni-relayer/src/utils/evm.rs | 32 +++++++++++++++++++++++++++++++- 4 files changed, 64 insertions(+), 11 deletions(-) diff --git a/omni-relayer/Cargo.toml b/omni-relayer/Cargo.toml index 9de1324da..d2a372b7c 100644 --- a/omni-relayer/Cargo.toml +++ b/omni-relayer/Cargo.toml @@ -51,18 +51,19 @@ mongodb = "3.2.2" redis = { version = "0.32.5", features = ["aio", "tokio-comp", "connection-manager"] } reqwest = "0.12" -bridge-connector-common = { git = "https://github.com/Near-One/bridge-sdk-rs", package = "bridge-connector-common", rev = "da152725e6a267b7213a755e1ad069f563f23288" } -near-rpc-client = { git = "https://github.com/Near-One/bridge-sdk-rs", package = "near-rpc-client", rev = "da152725e6a267b7213a755e1ad069f563f23288" } +bridge-connector-common = { path = "../bridge-sdk-rs/bridge-sdk/connectors/bridge-connector-common" } +near-rpc-client = { path = "../bridge-sdk-rs/bridge-sdk/near-rpc-client" } -near-bridge-client = { git = "https://github.com/Near-One/bridge-sdk-rs", package = "near-bridge-client", rev = "da152725e6a267b7213a755e1ad069f563f23288" } -evm-bridge-client = { git = "https://github.com/Near-One/bridge-sdk-rs", package = "evm-bridge-client", rev = "da152725e6a267b7213a755e1ad069f563f23288" } -solana-bridge-client = { git = "https://github.com/Near-One/bridge-sdk-rs", package = "solana-bridge-client", rev = "da152725e6a267b7213a755e1ad069f563f23288" } -utxo-bridge-client = { git = "https://github.com/Near-One/bridge-sdk-rs", package = "utxo-bridge-client", rev = "da152725e6a267b7213a755e1ad069f563f23288" } -utxo-utils = { git = "https://github.com/Near-One/bridge-sdk-rs", package = "utxo-utils", rev = "da152725e6a267b7213a755e1ad069f563f23288" } -wormhole-bridge-client = { git = "https://github.com/Near-One/bridge-sdk-rs", package = "wormhole-bridge-client", rev = "da152725e6a267b7213a755e1ad069f563f23288" } -light-client = { git = "https://github.com/Near-One/bridge-sdk-rs", package = "light-client", rev = "da152725e6a267b7213a755e1ad069f563f23288" } +near-bridge-client = { path = "../bridge-sdk-rs/bridge-sdk/bridge-clients/near-bridge-client" } +evm-bridge-client = { path = "../bridge-sdk-rs/bridge-sdk/bridge-clients/evm-bridge-client" } +solana-bridge-client = { path = "../bridge-sdk-rs/bridge-sdk/bridge-clients/solana-bridge-client" } +utxo-bridge-client = { path = "../bridge-sdk-rs/bridge-sdk/bridge-clients/utxo-bridge-client" } +utxo-utils = { path = "../bridge-sdk-rs/bridge-sdk/utxo-utils" } +wormhole-bridge-client = { path = "../bridge-sdk-rs/bridge-sdk/bridge-clients/wormhole-bridge-client" } +polymer-bridge-client = { path = "../bridge-sdk-rs/bridge-sdk/bridge-clients/polymer-bridge-client" } +light-client = { path = "../bridge-sdk-rs/bridge-sdk/light-client" } -omni-connector = { git = "https://github.com/Near-One/bridge-sdk-rs", package = "omni-connector", rev = "da152725e6a267b7213a755e1ad069f563f23288" } +omni-connector = { path = "../bridge-sdk-rs/bridge-sdk/connectors/omni-connector" } # The profile that 'dist' will build with [profile.dist] diff --git a/omni-relayer/src/config.rs b/omni-relayer/src/config.rs index 31a81145f..283945a26 100644 --- a/omni-relayer/src/config.rs +++ b/omni-relayer/src/config.rs @@ -105,6 +105,7 @@ pub struct Config { pub btc: Option, pub zcash: Option, pub wormhole: Wormhole, + pub polymer: Option, } impl Config { @@ -277,3 +278,8 @@ pub struct Wormhole { pub api_url: String, pub solana_chain_id: u64, } + +#[derive(Debug, Clone, Deserialize)] +pub struct Polymer { + pub api_url: String, +} diff --git a/omni-relayer/src/startup/mod.rs b/omni-relayer/src/startup/mod.rs index 87c3b1d2d..a33d94faf 100644 --- a/omni-relayer/src/startup/mod.rs +++ b/omni-relayer/src/startup/mod.rs @@ -12,6 +12,7 @@ use solana_client::nonblocking::rpc_client::RpcClient; use tracing::info; use utxo_bridge_client::{AuthOptions, UTXOBridgeClient}; use wormhole_bridge_client::{WormholeBridgeClient, WormholeBridgeClientBuilder}; +use polymer_bridge_client::{PolymerBridgeClient, PolymerBridgeClientBuilder}; use crate::{ config::{self}, @@ -163,6 +164,19 @@ fn build_wormhole_bridge_client(config: &config::Config) -> Result Result> { + config + .polymer + .as_ref() + .map(|polymer| { + PolymerBridgeClientBuilder::default() + .endpoint(Some(polymer.api_url.clone())) + .build() + .context("Failed to build PolymerBridgeClient") + }) + .transpose() +} + fn build_light_client(config: &config::Config, chain: ChainKind) -> Result> { let light_client = match chain { ChainKind::Eth => config.eth.as_ref().and_then(|eth| eth.light_client.clone()), @@ -204,6 +218,7 @@ pub fn build_omni_connector( let btc_bridge_client = build_utxo_bridge_client(config, ChainKind::Btc)?; let zcash_bridge_client = build_utxo_bridge_client(config, ChainKind::Zcash)?; let wormhole_bridge_client = build_wormhole_bridge_client(config)?; + let polymer_bridge_client = build_polymer_bridge_client(config)?; let eth_light_client = build_light_client(config, ChainKind::Eth)?; let btc_light_client = build_light_client(config, ChainKind::Btc)?; let zcash_light_client = build_light_client(config, ChainKind::Zcash)?; @@ -217,6 +232,7 @@ pub fn build_omni_connector( .bnb_bridge_client(bnb_bridge_client) .solana_bridge_client(solana_bridge_client) .wormhole_bridge_client(Some(wormhole_bridge_client)) + .polymer_bridge_client(polymer_bridge_client) .btc_bridge_client(Some(btc_bridge_client)) .zcash_bridge_client(Some(zcash_bridge_client)) .eth_light_client(eth_light_client) diff --git a/omni-relayer/src/utils/evm.rs b/omni-relayer/src/utils/evm.rs index 087000efa..76159fba7 100644 --- a/omni-relayer/src/utils/evm.rs +++ b/omni-relayer/src/utils/evm.rs @@ -7,7 +7,7 @@ use anyhow::Result; use near_sdk::json_types::U128; use omni_types::{ ChainKind, H160, OmniAddress, - prover_args::{EvmVerifyProofArgs, WormholeVerifyProofArgs}, + prover_args::{EvmVerifyProofArgs, PolymerVerifyProofArgs, WormholeVerifyProofArgs}, prover_result::ProofKind, }; @@ -100,6 +100,36 @@ pub async fn construct_prover_args( borsh::to_vec(&evm_proof_args).ok() } +pub async fn construct_polymer_prover_args( + omni_connector: Arc, + tx_hash: H256, + proof_kind: ProofKind, + chain_id: u64, + log_index: u64, + block_number: u64, +) -> Option> { + let proof = match omni_connector + .polymer_get_proof(chain_id, format!("{tx_hash:?}"), log_index) + .await + { + Ok(proof) => proof, + Err(err) => { + warn!("Failed to get Polymer proof: {err:?}"); + return None; + } + }; + + let polymer_proof_args = PolymerVerifyProofArgs { + proof_kind, + proof, + src_chain_id: chain_id, + src_block_number: block_number, + global_log_index: log_index, + }; + + borsh::to_vec(&polymer_proof_args).ok() +} + pub fn string_to_evm_omniaddress(chain_kind: ChainKind, address: &str) -> Result { OmniAddress::new_from_evm_address( chain_kind, From db3b32c8a3dcd6e6772247a2a0a635fb2f704f5a Mon Sep 17 00:00:00 2001 From: Bo Du Date: Fri, 24 Oct 2025 12:12:03 -0400 Subject: [PATCH 3/4] chore: refactor + dedupe --- near/omni-types/src/polymer/decoder.rs | 128 ----------- near/omni-types/src/polymer/events.rs | 282 +++++++------------------ near/omni-types/src/polymer/mod.rs | 1 - 3 files changed, 73 insertions(+), 338 deletions(-) delete mode 100644 near/omni-types/src/polymer/decoder.rs diff --git a/near/omni-types/src/polymer/decoder.rs b/near/omni-types/src/polymer/decoder.rs deleted file mode 100644 index 004de7efb..000000000 --- a/near/omni-types/src/polymer/decoder.rs +++ /dev/null @@ -1,128 +0,0 @@ -/// Utilities for decoding Polymer proof data -/// Includes helpers for parsing topics and ABI-decoding unindexed event data - -/// Split concatenated topics bytes into individual 32-byte chunks -pub fn decode_topics(topics: &[u8]) -> Vec<[u8; 32]> { - topics - .chunks_exact(32) - .map(|chunk| { - let mut arr = [0u8; 32]; - arr.copy_from_slice(chunk); - arr - }) - .collect() -} - -/// Extract Ethereum address from bytes32 (last 20 bytes) -pub fn bytes32_to_address(bytes: &[u8; 32]) -> [u8; 20] { - let mut addr = [0u8; 20]; - addr.copy_from_slice(&bytes[12..32]); - addr -} - -/// Extract u128 from bytes32 -pub fn bytes32_to_u128(bytes: &[u8; 32]) -> u128 { - u128::from_be_bytes([ - bytes[16], bytes[17], bytes[18], bytes[19], bytes[20], bytes[21], bytes[22], bytes[23], - bytes[24], bytes[25], bytes[26], bytes[27], bytes[28], bytes[29], bytes[30], bytes[31], - ]) -} - -/// Extract u64 from bytes32 -pub fn bytes32_to_u64(bytes: &[u8; 32]) -> u64 { - u64::from_be_bytes([ - bytes[24], bytes[25], bytes[26], bytes[27], bytes[28], bytes[29], bytes[30], bytes[31], - ]) -} - -/// Parse string from ABI-encoded bytes -/// Format: offset (32 bytes) + length (32 bytes) + data (padded to 32-byte chunks) -pub fn decode_string_from_abi(data: &[u8], offset: usize) -> Result { - if data.len() < offset + 32 { - return Err("Data too short for string length".to_string()); - } - - // Read length at offset - let length = u64::from_be_bytes([ - data[offset + 24], - data[offset + 25], - data[offset + 26], - data[offset + 27], - data[offset + 28], - data[offset + 29], - data[offset + 30], - data[offset + 31], - ]) as usize; - - if data.len() < offset + 32 + length { - return Err("Data too short for string content".to_string()); - } - - // Read string data - let string_bytes = &data[offset + 32..offset + 32 + length]; - String::from_utf8(string_bytes.to_vec()).map_err(|e| format!("Invalid UTF-8: {}", e)) -} - -/// Parse address from ABI-encoded bytes at given offset -pub fn decode_address_from_abi(data: &[u8], offset: usize) -> Result<[u8; 20], String> { - if data.len() < offset + 32 { - return Err("Data too short for address".to_string()); - } - - let mut addr = [0u8; 20]; - addr.copy_from_slice(&data[offset + 12..offset + 32]); - Ok(addr) -} - -/// Parse u128 from ABI-encoded bytes at given offset -pub fn decode_u128_from_abi(data: &[u8], offset: usize) -> Result { - if data.len() < offset + 32 { - return Err("Data too short for u128".to_string()); - } - - Ok(u128::from_be_bytes([ - data[offset + 16], - data[offset + 17], - data[offset + 18], - data[offset + 19], - data[offset + 20], - data[offset + 21], - data[offset + 22], - data[offset + 23], - data[offset + 24], - data[offset + 25], - data[offset + 26], - data[offset + 27], - data[offset + 28], - data[offset + 29], - data[offset + 30], - data[offset + 31], - ])) -} - -/// Parse u64 from ABI-encoded bytes at given offset -pub fn decode_u64_from_abi(data: &[u8], offset: usize) -> Result { - if data.len() < offset + 32 { - return Err("Data too short for u64".to_string()); - } - - Ok(u64::from_be_bytes([ - data[offset + 24], - data[offset + 25], - data[offset + 26], - data[offset + 27], - data[offset + 28], - data[offset + 29], - data[offset + 30], - data[offset + 31], - ])) -} - -/// Parse u8 from ABI-encoded bytes at given offset -pub fn decode_u8_from_abi(data: &[u8], offset: usize) -> Result { - if data.len() < offset + 32 { - return Err("Data too short for u8".to_string()); - } - - Ok(data[offset + 31]) -} diff --git a/near/omni-types/src/polymer/events.rs b/near/omni-types/src/polymer/events.rs index 4a20a6e82..1629deb76 100644 --- a/near/omni-types/src/polymer/events.rs +++ b/near/omni-types/src/polymer/events.rs @@ -1,43 +1,54 @@ +use alloy::{primitives::{Address, Bytes, FixedBytes, Log, LogData}, sol_types::SolEvent}; + use crate::{ - polymer::decoder::{ - bytes32_to_address, bytes32_to_u128, bytes32_to_u64, decode_address_from_abi, - decode_string_from_abi, decode_topics, decode_u128_from_abi, decode_u64_from_abi, - decode_u8_from_abi, - }, + evm::events::{DeployToken, FinTransfer, InitTransfer, LogMetadata, TryFromLog}, prover_result::{ DeployTokenMessage, FinTransferMessage, InitTransferMessage, LogMetadataMessage, }, - ChainKind, Fee, OmniAddress, H160, + stringify, ChainKind, }; -use near_sdk::json_types::U128; - -/// Event signatures for validation -pub const INIT_TRANSFER_SIGNATURE: [u8; 32] = [ - 0x51, 0x6d, 0x6f, 0x8e, 0x18, 0x7f, 0x4a, 0x9c, 0xba, 0x4e, 0x3d, 0x8f, 0x1f, 0x2a, 0x6b, - 0x7c, 0x5d, 0x9e, 0x0f, 0x1a, 0x2b, 0x3c, 0x4d, 0x5e, 0x6f, 0x7a, 0x8b, 0x9c, 0xad, 0xbe, - 0xcf, 0xd0, -]; // keccak256("InitTransfer(address,address,uint64,uint128,uint128,uint128,string,string)") - -pub const FIN_TRANSFER_SIGNATURE: [u8; 32] = [ - 0x52, 0x6e, 0x70, 0x8f, 0x19, 0x80, 0x4b, 0x9d, 0xcb, 0x4f, 0x3e, 0x90, 0x20, 0x2b, 0x6c, - 0x7d, 0x5e, 0x9f, 0x10, 0x1b, 0x2c, 0x3d, 0x4e, 0x5f, 0x70, 0x7b, 0x8c, 0x9d, 0xae, 0xbf, - 0xd0, 0xd1, -]; // keccak256("FinTransfer(uint8,uint64,address,uint128,address,string)") -pub const DEPLOY_TOKEN_SIGNATURE: [u8; 32] = [ - 0x53, 0x6f, 0x71, 0x90, 0x1a, 0x81, 0x4c, 0x9e, 0xcc, 0x50, 0x3f, 0x91, 0x21, 0x2c, 0x6d, - 0x7e, 0x5f, 0xa0, 0x11, 0x1c, 0x2d, 0x3e, 0x4f, 0x60, 0x71, 0x7c, 0x8d, 0x9e, 0xaf, 0xc0, - 0xd1, 0xd2, -]; // keccak256("DeployToken(address,string,string,string,uint8,uint8)") - -pub const LOG_METADATA_SIGNATURE: [u8; 32] = [ - 0x54, 0x70, 0x72, 0x91, 0x1b, 0x82, 0x4d, 0x9f, 0xcd, 0x51, 0x40, 0x92, 0x22, 0x2d, 0x6e, - 0x7f, 0x60, 0xa1, 0x12, 0x1d, 0x2e, 0x3f, 0x50, 0x61, 0x72, 0x7d, 0x8e, 0x9f, 0xb0, 0xc1, - 0xd2, 0xd3, -]; // keccak256("LogMetadata(address,string,string,uint8)") +/// Parse Polymer event from raw topics and unindexed data +pub fn parse_polymer_event>>( + chain_kind: ChainKind, + emitting_contract: &str, + topics_bytes: &[u8], + unindexed_data: &[u8], +) -> Result +where + >>::Error: std::fmt::Display, +{ + // Parse contract address + let address = parse_address(emitting_contract)?; + + // Split topics into 32-byte chunks + let topics: Vec> = topics_bytes + .chunks_exact(32) + .map(|chunk| { + let mut bytes = [0u8; 32]; + bytes.copy_from_slice(chunk); + FixedBytes::from(bytes) + }) + .collect(); + + // Create Log structure + let log = Log { + address, + data: LogData { + topics, + data: Bytes::copy_from_slice(unindexed_data), + }, + }; + + // Decode and validate + V::try_from_log( + chain_kind, + T::decode_log(&log, true).map_err(stringify)?, + ) + .map_err(stringify) +} /// Parse InitTransfer event from Polymer-validated data -/// Event: InitTransfer(address indexed sender, address indexed tokenAddress, uint64 indexed originNonce, uint128 amount, uint128 fee, uint128 nativeTokenFee, string recipient, string message) pub fn parse_init_transfer_event( chain_id: u64, emitting_contract: &str, @@ -45,59 +56,15 @@ pub fn parse_init_transfer_event( unindexed_data: &[u8], ) -> Result { let chain_kind = map_chain_id_to_kind(chain_id)?; - let topics_array = decode_topics(topics); - - if topics_array.len() < 4 { - return Err(format!( - "Invalid topics length for InitTransfer: expected 4, got {}", - topics_array.len() - )); - } - - // Verify event signature - if topics_array[0] != INIT_TRANSFER_SIGNATURE { - return Err("Invalid InitTransfer event signature".to_string()); - } - - // Parse indexed parameters from topics - let sender_addr = bytes32_to_address(&topics_array[1]); - let token_addr = bytes32_to_address(&topics_array[2]); - let origin_nonce = bytes32_to_u64(&topics_array[3]); - - // Parse non-indexed parameters from unindexed_data - // Format: amount (32), fee (32), nativeTokenFee (32), recipient (dynamic), message (dynamic) - if unindexed_data.len() < 96 { - return Err("Unindexed data too short for InitTransfer".to_string()); - } - - let amount = decode_u128_from_abi(unindexed_data, 0)?; - let fee = decode_u128_from_abi(unindexed_data, 32)?; - let native_token_fee = decode_u128_from_abi(unindexed_data, 64)?; - - // Dynamic strings: offset to recipient, offset to message - let recipient_offset = decode_u64_from_abi(unindexed_data, 96)? as usize; - let message_offset = decode_u64_from_abi(unindexed_data, 128)? as usize; - - let recipient_str = decode_string_from_abi(unindexed_data, recipient_offset)?; - let message_str = decode_string_from_abi(unindexed_data, message_offset)?; - - Ok(InitTransferMessage { - emitter_address: parse_emitting_contract(chain_kind, emitting_contract)?, - origin_nonce, - token: OmniAddress::new_from_evm_address(chain_kind, H160(token_addr))?, - amount: U128(amount), - recipient: recipient_str.parse()?, - fee: Fee { - fee: U128(fee), - native_fee: U128(native_token_fee), - }, - sender: OmniAddress::new_from_evm_address(chain_kind, H160(sender_addr))?, - msg: message_str, - }) + parse_polymer_event::( + chain_kind, + emitting_contract, + topics, + unindexed_data, + ) } /// Parse FinTransfer event from Polymer-validated data -/// Event: FinTransfer(uint8 indexed originChain, uint64 indexed originNonce, address tokenAddress, uint128 amount, address recipient, string feeRecipient) pub fn parse_fin_transfer_event( chain_id: u64, emitting_contract: &str, @@ -105,49 +72,15 @@ pub fn parse_fin_transfer_event( unindexed_data: &[u8], ) -> Result { let chain_kind = map_chain_id_to_kind(chain_id)?; - let topics_array = decode_topics(topics); - - if topics_array.len() < 3 { - return Err(format!( - "Invalid topics length for FinTransfer: expected 3, got {}", - topics_array.len() - )); - } - - // Verify event signature - if topics_array[0] != FIN_TRANSFER_SIGNATURE { - return Err("Invalid FinTransfer event signature".to_string()); - } - - // Parse indexed parameters - let origin_chain = topics_array[1][31]; - let origin_nonce = bytes32_to_u64(&topics_array[2]); - - // Parse non-indexed parameters - if unindexed_data.len() < 96 { - return Err("Unindexed data too short for FinTransfer".to_string()); - } - - let token_address = decode_address_from_abi(unindexed_data, 0)?; - let amount = decode_u128_from_abi(unindexed_data, 32)?; - let recipient = decode_address_from_abi(unindexed_data, 64)?; - - let fee_recipient_offset = decode_u64_from_abi(unindexed_data, 96)? as usize; - let fee_recipient_str = decode_string_from_abi(unindexed_data, fee_recipient_offset)?; - - Ok(FinTransferMessage { - transfer_id: crate::TransferId { - origin_chain: origin_chain.try_into()?, - origin_nonce, - }, - amount: U128(amount), - fee_recipient: fee_recipient_str.parse().ok(), - emitter_address: parse_emitting_contract(chain_kind, emitting_contract)?, - }) + parse_polymer_event::( + chain_kind, + emitting_contract, + topics, + unindexed_data, + ) } /// Parse DeployToken event from Polymer-validated data -/// Event: DeployToken(address indexed tokenAddress, string token, string name, string symbol, uint8 decimals, uint8 originDecimals) pub fn parse_deploy_token_event( chain_id: u64, emitting_contract: &str, @@ -155,48 +88,15 @@ pub fn parse_deploy_token_event( unindexed_data: &[u8], ) -> Result { let chain_kind = map_chain_id_to_kind(chain_id)?; - let topics_array = decode_topics(topics); - - if topics_array.len() < 2 { - return Err(format!( - "Invalid topics length for DeployToken: expected 2, got {}", - topics_array.len() - )); - } - - // Verify event signature - if topics_array[0] != DEPLOY_TOKEN_SIGNATURE { - return Err("Invalid DeployToken event signature".to_string()); - } - - let token_address = bytes32_to_address(&topics_array[1]); - - // Parse dynamic strings and decimals - if unindexed_data.len() < 128 { - return Err("Unindexed data too short for DeployToken".to_string()); - } - - let token_offset = decode_u64_from_abi(unindexed_data, 0)? as usize; - let name_offset = decode_u64_from_abi(unindexed_data, 32)? as usize; - let symbol_offset = decode_u64_from_abi(unindexed_data, 64)? as usize; - let decimals = decode_u8_from_abi(unindexed_data, 96)?; - let origin_decimals = decode_u8_from_abi(unindexed_data, 128)?; - - let token_str = decode_string_from_abi(unindexed_data, token_offset)?; - let name_str = decode_string_from_abi(unindexed_data, name_offset)?; - let symbol_str = decode_string_from_abi(unindexed_data, symbol_offset)?; - - Ok(DeployTokenMessage { - token: token_str.parse()?, - token_address: OmniAddress::new_from_evm_address(chain_kind, H160(token_address))?, - decimals, - origin_decimals, - emitter_address: parse_emitting_contract(chain_kind, emitting_contract)?, - }) + parse_polymer_event::( + chain_kind, + emitting_contract, + topics, + unindexed_data, + ) } /// Parse LogMetadata event from Polymer-validated data -/// Event: LogMetadata(address indexed tokenAddress, string name, string symbol, uint8 decimals) pub fn parse_log_metadata_event( chain_id: u64, emitting_contract: &str, @@ -204,41 +104,12 @@ pub fn parse_log_metadata_event( unindexed_data: &[u8], ) -> Result { let chain_kind = map_chain_id_to_kind(chain_id)?; - let topics_array = decode_topics(topics); - - if topics_array.len() < 2 { - return Err(format!( - "Invalid topics length for LogMetadata: expected 2, got {}", - topics_array.len() - )); - } - - // Verify event signature - if topics_array[0] != LOG_METADATA_SIGNATURE { - return Err("Invalid LogMetadata event signature".to_string()); - } - - let token_address = bytes32_to_address(&topics_array[1]); - - // Parse dynamic strings and decimals - if unindexed_data.len() < 96 { - return Err("Unindexed data too short for LogMetadata".to_string()); - } - - let name_offset = decode_u64_from_abi(unindexed_data, 0)? as usize; - let symbol_offset = decode_u64_from_abi(unindexed_data, 32)? as usize; - let decimals = decode_u8_from_abi(unindexed_data, 64)?; - - let name_str = decode_string_from_abi(unindexed_data, name_offset)?; - let symbol_str = decode_string_from_abi(unindexed_data, symbol_offset)?; - - Ok(LogMetadataMessage { - token_address: OmniAddress::new_from_evm_address(chain_kind, H160(token_address))?, - name: name_str, - symbol: symbol_str, - decimals, - emitter_address: parse_emitting_contract(chain_kind, emitting_contract)?, - }) + parse_polymer_event::( + chain_kind, + emitting_contract, + topics, + unindexed_data, + ) } /// Map Polymer chain ID to ChainKind @@ -253,20 +124,13 @@ fn map_chain_id_to_kind(chain_id: u64) -> Result { } } -/// Parse emitting contract address string to OmniAddress -fn parse_emitting_contract(chain_kind: ChainKind, contract: &str) -> Result { +/// Parse emitting contract address string to Address +fn parse_address(contract: &str) -> Result { // Remove "0x" prefix if present let cleaned = contract.strip_prefix("0x").unwrap_or(contract); - // Parse hex string to bytes - let bytes = hex::decode(cleaned).map_err(|e| format!("Invalid hex address: {}", e))?; - - if bytes.len() != 20 { - return Err(format!("Invalid address length: expected 20, got {}", bytes.len())); - } - - let mut addr = [0u8; 20]; - addr.copy_from_slice(&bytes); - - OmniAddress::new_from_evm_address(chain_kind, H160(addr)) + // Parse hex string to Address + cleaned + .parse::
() + .map_err(|e| format!("Invalid contract address: {}", e)) } diff --git a/near/omni-types/src/polymer/mod.rs b/near/omni-types/src/polymer/mod.rs index cbbb0860c..a9970c28f 100644 --- a/near/omni-types/src/polymer/mod.rs +++ b/near/omni-types/src/polymer/mod.rs @@ -1,2 +1 @@ -pub mod decoder; pub mod events; From 38b7df36ecc05c77d1679ca68156db9dc4394f76 Mon Sep 17 00:00:00 2001 From: Bo Du Date: Fri, 24 Oct 2025 15:13:29 -0400 Subject: [PATCH 4/4] chore: fixes --- near/Cargo.lock | 9 ++++ near/Cargo.toml | 1 + near/omni-prover/polymer-prover/src/lib.rs | 32 ++++-------- near/omni-types/src/polymer/events.rs | 58 ++++++++++++++++++++-- 4 files changed, 72 insertions(+), 28 deletions(-) diff --git a/near/Cargo.lock b/near/Cargo.lock index dddc2934e..c017558d2 100644 --- a/near/Cargo.lock +++ b/near/Cargo.lock @@ -3158,6 +3158,15 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" +[[package]] +name = "polymer-prover" +version = "0.3.2" +dependencies = [ + "borsh", + "near-sdk", + "omni-types", +] + [[package]] name = "potential_utf" version = "0.1.3" diff --git a/near/Cargo.toml b/near/Cargo.toml index a7a7ad2bd..9e9755096 100644 --- a/near/Cargo.toml +++ b/near/Cargo.toml @@ -10,6 +10,7 @@ members = [ "token-deployer", "omni-prover/wormhole-omni-prover-proxy", "omni-prover/evm-prover", + "omni-prover/polymer-prover", "omni-token", "omni-types", "omni-tests", diff --git a/near/omni-prover/polymer-prover/src/lib.rs b/near/omni-prover/polymer-prover/src/lib.rs index 6c71afdd0..5f183e269 100644 --- a/near/omni-prover/polymer-prover/src/lib.rs +++ b/near/omni-prover/polymer-prover/src/lib.rs @@ -1,12 +1,9 @@ use near_sdk::borsh::BorshDeserialize; use near_sdk::{ - env, ext_contract, near, near_bindgen, require, AccountId, Gas, PanicOnDefault, Promise, + env, ext_contract, near, near_bindgen, AccountId, Gas, PanicOnDefault, Promise, PromiseError, }; -use omni_types::polymer::events::{ - parse_deploy_token_event, parse_fin_transfer_event, parse_init_transfer_event, - parse_log_metadata_event, -}; +use omni_types::polymer::events::parse_polymer_event_by_kind; use omni_types::prover_args::PolymerVerifyProofArgs; use omni_types::prover_result::{ProofKind, ProverResult}; @@ -122,23 +119,12 @@ impl PolymerProver { return Err("Invalid topics length: must be at least 32 bytes".to_owned()); } - match proof_kind { - ProofKind::InitTransfer => { - let event = parse_init_transfer_event(chain_id, emitting_contract, topics, unindexed_data)?; - Ok(ProverResult::InitTransfer(event)) - } - ProofKind::FinTransfer => { - let event = parse_fin_transfer_event(chain_id, emitting_contract, topics, unindexed_data)?; - Ok(ProverResult::FinTransfer(event)) - } - ProofKind::DeployToken => { - let event = parse_deploy_token_event(chain_id, emitting_contract, topics, unindexed_data)?; - Ok(ProverResult::DeployToken(event)) - } - ProofKind::LogMetadata => { - let event = parse_log_metadata_event(chain_id, emitting_contract, topics, unindexed_data)?; - Ok(ProverResult::LogMetadata(event)) - } - } + parse_polymer_event_by_kind( + proof_kind, + chain_id, + emitting_contract, + topics, + unindexed_data, + ) } } diff --git a/near/omni-types/src/polymer/events.rs b/near/omni-types/src/polymer/events.rs index 1629deb76..11276db7e 100644 --- a/near/omni-types/src/polymer/events.rs +++ b/near/omni-types/src/polymer/events.rs @@ -4,12 +4,60 @@ use crate::{ evm::events::{DeployToken, FinTransfer, InitTransfer, LogMetadata, TryFromLog}, prover_result::{ DeployTokenMessage, FinTransferMessage, InitTransferMessage, LogMetadataMessage, + ProofKind, ProverResult, }, stringify, ChainKind, }; +/// Generic parser that routes to specific event type based on ProofKind +/// This matches the pattern from evm-prover for consistency +pub fn parse_polymer_event_by_kind( + proof_kind: ProofKind, + chain_id: u64, + emitting_contract: &str, + topics: &[u8], + unindexed_data: &[u8], +) -> Result { + let chain_kind = map_chain_id_to_kind(chain_id)?; + + match proof_kind { + ProofKind::InitTransfer => Ok(ProverResult::InitTransfer( + parse_polymer_event::( + chain_kind, + emitting_contract, + topics, + unindexed_data, + )? + )), + ProofKind::FinTransfer => Ok(ProverResult::FinTransfer( + parse_polymer_event::( + chain_kind, + emitting_contract, + topics, + unindexed_data, + )? + )), + ProofKind::DeployToken => Ok(ProverResult::DeployToken( + parse_polymer_event::( + chain_kind, + emitting_contract, + topics, + unindexed_data, + )? + )), + ProofKind::LogMetadata => Ok(ProverResult::LogMetadata( + parse_polymer_event::( + chain_kind, + emitting_contract, + topics, + unindexed_data, + )? + )), + } +} + /// Parse Polymer event from raw topics and unindexed data -pub fn parse_polymer_event>>( +fn parse_polymer_event>>( chain_kind: ChainKind, emitting_contract: &str, topics_bytes: &[u8], @@ -34,16 +82,16 @@ where // Create Log structure let log = Log { address, - data: LogData { + data: LogData::new_unchecked( topics, - data: Bytes::copy_from_slice(unindexed_data), - }, + Bytes::copy_from_slice(unindexed_data), + ), }; // Decode and validate V::try_from_log( chain_kind, - T::decode_log(&log, true).map_err(stringify)?, + T::decode_log(&log).map_err(stringify)?, ) .map_err(stringify) }