feat: Scraper populates transaction fields for Solana (#4801)

### Description

Scraper populates transaction fields for Solana

### Related issues

- Contributes into
https://github.com/hyperlane-xyz/hyperlane-monorepo/issues/4272

### Backward compatibility

Yes (configuration is updated)

### Testing

Manual test of Scraper
Run E2E tests for Ethereum and Solana

---------

Co-authored-by: Danil Nemirovsky <4614623+ameten@users.noreply.github.com>
pull/4770/head
Danil Nemirovsky 2 weeks ago committed by GitHub
parent fbce40f891
commit c064881727
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 31
      rust/main/chains/hyperlane-cosmos/src/providers/cosmos/provider.rs
  2. 4
      rust/main/chains/hyperlane-cosmos/src/providers/grpc/tests.rs
  3. 13
      rust/main/chains/hyperlane-cosmos/src/trait_builder.rs
  4. 11
      rust/main/chains/hyperlane-sealevel/src/error.rs
  5. 125
      rust/main/chains/hyperlane-sealevel/src/provider.rs
  6. 3
      rust/main/chains/hyperlane-sealevel/src/rpc/client.rs
  7. 4
      rust/main/chains/hyperlane-sealevel/src/trait_builder.rs
  8. 70
      rust/main/hyperlane-base/src/settings/parser/connection_parser.rs
  9. 2
      rust/main/hyperlane-core/src/types/mod.rs
  10. 8
      rust/main/hyperlane-core/src/types/native_token.rs
  11. 13
      rust/main/hyperlane-core/src/utils.rs
  12. 4
      rust/main/utils/run-locally/src/cosmos/types.rs

@ -18,9 +18,10 @@ use tracing::{error, warn};
use crypto::decompress_public_key;
use hyperlane_core::{
bytes_to_h512, h512_to_bytes, AccountAddressType, BlockInfo, ChainCommunicationError,
ChainInfo, ChainResult, ContractLocator, HyperlaneChain, HyperlaneDomain, HyperlaneProvider,
HyperlaneProviderError, TxnInfo, TxnReceiptInfo, H256, H512, U256,
bytes_to_h512, h512_to_bytes, utils::to_atto, AccountAddressType, BlockInfo,
ChainCommunicationError, ChainInfo, ChainResult, ContractLocator, HyperlaneChain,
HyperlaneDomain, HyperlaneProvider, HyperlaneProviderError, TxnInfo, TxnReceiptInfo, H256,
H512, U256,
};
use crate::grpc::{WasmGrpcProvider, WasmProvider};
@ -33,9 +34,6 @@ use crate::{
mod parse;
/// Exponent value for atto units (10^-18).
const ATTO_EXPONENT: u32 = 18;
/// Injective public key type URL for protobuf Any
const INJECTIVE_PUBLIC_KEY_TYPE_URL: &str = "/injective.crypto.v1beta1.ethsecp256k1.PubKey";
@ -320,26 +318,25 @@ impl CosmosProvider {
/// `OSMO` and it will keep fees expressed in `inj` as is.
///
/// If fees are expressed in an unsupported denomination, they will be ignored.
fn convert_fee(&self, coin: &Coin) -> U256 {
fn convert_fee(&self, coin: &Coin) -> ChainResult<U256> {
let native_token = self.connection_conf.get_native_token();
if coin.denom.as_ref() != native_token.denom {
return U256::zero();
return Ok(U256::zero());
}
let exponent = ATTO_EXPONENT - native_token.decimals;
let coefficient = U256::from(10u128.pow(exponent));
let amount_in_native_denom = U256::from(coin.amount);
amount_in_native_denom * coefficient
to_atto(amount_in_native_denom, native_token.decimals).ok_or(
ChainCommunicationError::CustomError("Overflow in calculating fees".to_owned()),
)
}
fn calculate_gas_price(&self, hash: &H256, tx: &Tx) -> U256 {
fn calculate_gas_price(&self, hash: &H256, tx: &Tx) -> ChainResult<U256> {
// TODO support multiple denominations for amount
let supported = self.report_unsupported_denominations(tx, hash);
if supported.is_err() {
return U256::max_value();
return Ok(U256::max_value());
}
let gas_limit = U256::from(tx.auth_info.fee.gas_limit);
@ -349,13 +346,13 @@ impl CosmosProvider {
.amount
.iter()
.map(|c| self.convert_fee(c))
.fold(U256::zero(), |acc, v| acc + v);
.fold_ok(U256::zero(), |acc, v| acc + v)?;
if fee < gas_limit {
warn!(tx_hash = ?hash, ?fee, ?gas_limit, "calculated fee is less than gas limit. it will result in zero gas price");
}
fee / gas_limit
Ok(fee / gas_limit)
}
}
@ -425,7 +422,7 @@ impl HyperlaneProvider for CosmosProvider {
let contract = Self::contract(&tx, &hash)?;
let (sender, nonce) = self.sender_and_nonce(&tx)?;
let gas_price = self.calculate_gas_price(&hash, &tx);
let gas_price = self.calculate_gas_price(&hash, &tx)?;
let tx_info = TxnInfo {
hash: hash.into(),

@ -3,10 +3,10 @@ use std::str::FromStr;
use url::Url;
use hyperlane_core::config::OperationBatchConfig;
use hyperlane_core::{ContractLocator, HyperlaneDomain, KnownHyperlaneDomain};
use hyperlane_core::{ContractLocator, HyperlaneDomain, KnownHyperlaneDomain, NativeToken};
use crate::grpc::{WasmGrpcProvider, WasmProvider};
use crate::{ConnectionConf, CosmosAddress, CosmosAmount, NativeToken, RawCosmosAmount};
use crate::{ConnectionConf, CosmosAddress, CosmosAmount, RawCosmosAmount};
#[ignore]
#[tokio::test]

@ -3,7 +3,9 @@ use std::str::FromStr;
use derive_new::new;
use url::Url;
use hyperlane_core::{config::OperationBatchConfig, ChainCommunicationError, FixedPointNumber};
use hyperlane_core::{
config::OperationBatchConfig, ChainCommunicationError, FixedPointNumber, NativeToken,
};
/// Cosmos connection configuration
#[derive(Debug, Clone)]
@ -60,15 +62,6 @@ impl TryFrom<RawCosmosAmount> for CosmosAmount {
}
}
/// Chain native token denomination and number of decimal places
#[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize)]
pub struct NativeToken {
/// The number of decimal places in token which can be expressed by denomination
pub decimals: u32,
/// Denomination of the token
pub denom: String,
}
/// An error type when parsing a connection configuration.
#[derive(thiserror::Error, Debug)]
pub enum ConnectionConfError {

@ -1,7 +1,7 @@
use hyperlane_core::{ChainCommunicationError, H512};
use solana_client::client_error::ClientError;
use solana_sdk::pubkey::ParsePubkeyError;
use solana_transaction_status::EncodedTransaction;
use solana_transaction_status::{EncodedTransaction, UiMessage};
/// Errors from the crates specific to the hyperlane-sealevel
/// implementation.
@ -27,12 +27,21 @@ pub enum HyperlaneSealevelError {
/// Unsupported transaction encoding
#[error("{0:?}")]
UnsupportedTransactionEncoding(EncodedTransaction),
/// Unsupported message encoding
#[error("{0:?}")]
UnsupportedMessageEncoding(UiMessage),
/// Unsigned transaction
#[error("{0}")]
UnsignedTransaction(H512),
/// Incorrect transaction
#[error("received incorrect transaction, expected hash: {0:?}, received hash: {1:?}")]
IncorrectTransaction(Box<H512>, Box<H512>),
/// Empty metadata
#[error("received empty metadata in transaction")]
EmptyMetadata,
/// Empty compute units consumed
#[error("received empty compute units consumed in transaction")]
EmptyComputeUnitsConsumed,
}
impl From<HyperlaneSealevelError> for ChainCommunicationError {

@ -2,11 +2,16 @@ use std::sync::Arc;
use async_trait::async_trait;
use solana_sdk::signature::Signature;
use solana_transaction_status::EncodedTransaction;
use solana_transaction_status::{
option_serializer::OptionSerializer, EncodedTransaction, EncodedTransactionWithStatusMeta,
UiMessage, UiTransaction, UiTransactionStatusMeta,
};
use tracing::warn;
use hyperlane_core::{
BlockInfo, ChainCommunicationError, ChainInfo, ChainResult, HyperlaneChain, HyperlaneDomain,
HyperlaneProvider, HyperlaneProviderError, TxnInfo, TxnReceiptInfo, H256, H512, U256,
utils::to_atto, BlockInfo, ChainCommunicationError, ChainInfo, ChainResult, HyperlaneChain,
HyperlaneDomain, HyperlaneProvider, HyperlaneProviderError, NativeToken, TxnInfo,
TxnReceiptInfo, H256, H512, U256,
};
use crate::error::HyperlaneSealevelError;
@ -18,6 +23,7 @@ use crate::{ConnectionConf, SealevelRpcClient};
pub struct SealevelProvider {
domain: HyperlaneDomain,
rpc_client: Arc<SealevelRpcClient>,
native_token: NativeToken,
}
impl SealevelProvider {
@ -25,14 +31,83 @@ impl SealevelProvider {
pub fn new(domain: HyperlaneDomain, conf: &ConnectionConf) -> Self {
// Set the `processed` commitment at rpc level
let rpc_client = Arc::new(SealevelRpcClient::new(conf.url.to_string()));
let native_token = conf.native_token.clone();
SealevelProvider { domain, rpc_client }
SealevelProvider {
domain,
rpc_client,
native_token,
}
}
/// Get an rpc client
pub fn rpc(&self) -> &SealevelRpcClient {
&self.rpc_client
}
fn validate_transaction(hash: &H512, txn: &UiTransaction) -> ChainResult<()> {
let received_signature = txn
.signatures
.first()
.ok_or(HyperlaneSealevelError::UnsignedTransaction(*hash))?;
let received_hash = decode_h512(received_signature)?;
if &received_hash != hash {
Err(Into::<ChainCommunicationError>::into(
HyperlaneSealevelError::IncorrectTransaction(
Box::new(*hash),
Box::new(received_hash),
),
))?;
}
Ok(())
}
fn sender(hash: &H512, txn: &UiTransaction) -> ChainResult<H256> {
let message = match &txn.message {
UiMessage::Parsed(m) => m,
m => Err(Into::<ChainCommunicationError>::into(
HyperlaneSealevelError::UnsupportedMessageEncoding(m.clone()),
))?,
};
let signer = message
.account_keys
.first()
.ok_or(HyperlaneSealevelError::UnsignedTransaction(*hash))?;
let pubkey = decode_pubkey(&signer.pubkey)?;
let sender = H256::from_slice(&pubkey.to_bytes());
Ok(sender)
}
fn gas(meta: &UiTransactionStatusMeta) -> ChainResult<U256> {
let OptionSerializer::Some(gas) = meta.compute_units_consumed else {
Err(HyperlaneSealevelError::EmptyComputeUnitsConsumed)?
};
Ok(U256::from(gas))
}
/// Extracts and converts fees into atto (10^-18) units.
///
/// We convert fees into atto units since otherwise a compute unit price (gas price)
/// becomes smaller than 1 lamport (or 1 unit of native token) and the price is rounded
/// to zero. We normalise the gas price for all the chain to be expressed in atto units.
fn fee(&self, meta: &UiTransactionStatusMeta) -> ChainResult<U256> {
let amount_in_native_denom = U256::from(meta.fee);
to_atto(amount_in_native_denom, self.native_token.decimals).ok_or(
ChainCommunicationError::CustomError("Overflow in calculating fees".to_owned()),
)
}
fn meta(txn: &EncodedTransactionWithStatusMeta) -> ChainResult<&UiTransactionStatusMeta> {
let meta = txn
.meta
.as_ref()
.ok_or(HyperlaneSealevelError::EmptyMetadata)?;
Ok(meta)
}
}
impl HyperlaneChain for SealevelProvider {
@ -44,6 +119,7 @@ impl HyperlaneChain for SealevelProvider {
Box::new(SealevelProvider {
domain: self.domain.clone(),
rpc_client: self.rpc_client.clone(),
native_token: self.native_token.clone(),
})
}
}
@ -76,44 +152,43 @@ impl HyperlaneProvider for SealevelProvider {
/// for all chains, not only Ethereum-like chains.
async fn get_txn_by_hash(&self, hash: &H512) -> ChainResult<TxnInfo> {
let signature = Signature::new(hash.as_bytes());
let transaction = self.rpc_client.get_transaction(&signature).await?;
let ui_transaction = match transaction.transaction.transaction {
let txn_confirmed = self.rpc_client.get_transaction(&signature).await?;
let txn_with_meta = &txn_confirmed.transaction;
let txn = match &txn_with_meta.transaction {
EncodedTransaction::Json(t) => t,
t => Err(Into::<ChainCommunicationError>::into(
HyperlaneSealevelError::UnsupportedTransactionEncoding(t),
HyperlaneSealevelError::UnsupportedTransactionEncoding(t.clone()),
))?,
};
let received_signature = ui_transaction
.signatures
.first()
.ok_or(HyperlaneSealevelError::UnsignedTransaction(*hash))?;
let received_hash = decode_h512(received_signature)?;
Self::validate_transaction(hash, txn)?;
let sender = Self::sender(hash, txn)?;
let meta = Self::meta(txn_with_meta)?;
let gas_used = Self::gas(meta)?;
let fee = self.fee(meta)?;
if &received_hash != hash {
Err(Into::<ChainCommunicationError>::into(
HyperlaneSealevelError::IncorrectTransaction(
Box::new(*hash),
Box::new(received_hash),
),
))?;
if fee < gas_used {
warn!(tx_hash = ?hash, ?fee, ?gas_used, "calculated fee is less than gas used. it will result in zero gas price");
}
let gas_price = Some(fee / gas_used);
let receipt = TxnReceiptInfo {
gas_used: Default::default(),
cumulative_gas_used: Default::default(),
effective_gas_price: None,
gas_used,
cumulative_gas_used: gas_used,
effective_gas_price: gas_price,
};
Ok(TxnInfo {
hash: *hash,
gas_limit: Default::default(),
gas_limit: gas_used,
max_priority_fee_per_gas: None,
max_fee_per_gas: None,
gas_price: None,
gas_price,
nonce: 0,
sender: Default::default(),
sender,
recipient: None,
receipt: Some(receipt),
raw_input_data: None,

@ -17,7 +17,7 @@ use solana_sdk::{
};
use solana_transaction_status::{
EncodedConfirmedTransactionWithStatusMeta, TransactionStatus, UiConfirmedBlock,
UiReturnDataEncoding, UiTransactionReturnData,
UiReturnDataEncoding, UiTransactionEncoding, UiTransactionReturnData,
};
use hyperlane_core::{ChainCommunicationError, ChainResult, U256};
@ -188,6 +188,7 @@ impl SealevelRpcClient {
signature: &Signature,
) -> ChainResult<EncodedConfirmedTransactionWithStatusMeta> {
let config = RpcTransactionConfig {
encoding: Some(UiTransactionEncoding::JsonParsed),
commitment: Some(CommitmentConfig::finalized()),
..Default::default()
};

@ -1,4 +1,4 @@
use hyperlane_core::{config::OperationBatchConfig, ChainCommunicationError};
use hyperlane_core::{config::OperationBatchConfig, ChainCommunicationError, NativeToken};
use url::Url;
/// Sealevel connection configuration
@ -8,6 +8,8 @@ pub struct ConnectionConf {
pub url: Url,
/// Operation batching configuration
pub operation_batch: OperationBatchConfig,
/// Native token and its denomination
pub native_token: NativeToken,
}
/// An error type when parsing a connection configuration.

@ -2,9 +2,9 @@ use eyre::eyre;
use url::Url;
use h_eth::TransactionOverrides;
use hyperlane_core::config::{ConfigErrResultExt, OperationBatchConfig};
use hyperlane_core::{config::ConfigParsingError, HyperlaneDomainProtocol};
use hyperlane_cosmos::NativeToken;
use hyperlane_core::{config::ConfigParsingError, HyperlaneDomainProtocol, NativeToken};
use crate::settings::envs::*;
use crate::settings::ChainConnectionConf;
@ -137,24 +137,7 @@ pub fn build_cosmos_connection_conf(
.parse_u64()
.end();
let native_token_decimals = chain
.chain(err)
.get_key("nativeToken")
.get_key("decimals")
.parse_u32()
.unwrap_or(18);
let native_token_denom = chain
.chain(err)
.get_key("nativeToken")
.get_key("denom")
.parse_string()
.unwrap_or("");
let native_token = NativeToken {
decimals: native_token_decimals,
denom: native_token_denom.to_owned(),
};
let native_token = parse_native_token(chain, err, 18);
if !local_err.is_ok() {
err.merge(local_err);
@ -174,6 +157,45 @@ pub fn build_cosmos_connection_conf(
}
}
fn build_sealevel_connection_conf(
url: &Url,
chain: &ValueParser,
err: &mut ConfigParsingError,
operation_batch: OperationBatchConfig,
) -> h_sealevel::ConnectionConf {
let native_token = parse_native_token(chain, err, 9);
h_sealevel::ConnectionConf {
url: url.clone(),
operation_batch,
native_token,
}
}
fn parse_native_token(
chain: &ValueParser,
err: &mut ConfigParsingError,
default_decimals: u32,
) -> NativeToken {
let native_token_decimals = chain
.chain(err)
.get_opt_key("nativeToken")
.get_opt_key("decimals")
.parse_u32()
.unwrap_or(default_decimals);
let native_token_denom = chain
.chain(err)
.get_opt_key("nativeToken")
.get_opt_key("denom")
.parse_string()
.unwrap_or("");
NativeToken {
decimals: native_token_decimals,
denom: native_token_denom.to_owned(),
}
}
pub fn build_connection_conf(
domain_protocol: HyperlaneDomainProtocol,
rpcs: &[Url],
@ -195,10 +217,12 @@ pub fn build_connection_conf(
.next()
.map(|url| ChainConnectionConf::Fuel(h_fuel::ConnectionConf { url: url.clone() })),
HyperlaneDomainProtocol::Sealevel => rpcs.iter().next().map(|url| {
ChainConnectionConf::Sealevel(h_sealevel::ConnectionConf {
url: url.clone(),
ChainConnectionConf::Sealevel(build_sealevel_connection_conf(
url,
chain,
err,
operation_batch,
})
))
}),
HyperlaneDomainProtocol::Cosmos => {
build_cosmos_connection_conf(rpcs, chain, err, operation_batch)

@ -17,6 +17,7 @@ pub use indexing::*;
pub use log_metadata::*;
pub use merkle_tree::*;
pub use message::*;
pub use native_token::NativeToken;
pub use reorg::*;
pub use transaction::*;
@ -33,6 +34,7 @@ mod indexing;
mod log_metadata;
mod merkle_tree;
mod message;
mod native_token;
mod reorg;
mod serialize;
mod transaction;

@ -0,0 +1,8 @@
/// Chain native token denomination and number of decimal places
#[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize)]
pub struct NativeToken {
/// The number of decimal places in token which can be expressed by denomination
pub decimals: u32,
/// Denomination of the token
pub denom: String,
}

@ -5,7 +5,7 @@ use std::str::FromStr;
#[cfg(feature = "float")]
use std::time::Duration;
use crate::{KnownHyperlaneDomain, H160, H256};
use crate::{KnownHyperlaneDomain, H160, H256, U256};
/// Converts a hex or base58 string to an H256.
pub fn hex_or_base58_to_h256(string: &str) -> Result<H256> {
@ -62,6 +62,17 @@ pub fn bytes_to_hex(bytes: &[u8]) -> String {
format!("0x{}", hex::encode(bytes))
}
/// Exponent value for atto units (10^-18).
const ATTO_EXPONENT: u32 = 18;
/// Converts `value` expressed with `decimals` into `atto` (`10^-18`) decimals.
pub fn to_atto(value: U256, decimals: u32) -> Option<U256> {
assert!(decimals <= ATTO_EXPONENT);
let exponent = ATTO_EXPONENT - decimals;
let coefficient = U256::from(10u128.pow(exponent));
value.checked_mul(coefficient)
}
/// Format a domain id as a name if it is known or just the number if not.
pub fn fmt_domain(domain: u32) -> String {
#[cfg(feature = "strum")]

@ -1,8 +1,10 @@
use std::{collections::BTreeMap, path::PathBuf};
use hyperlane_cosmos::{NativeToken, RawCosmosAmount};
use hyperlane_cosmwasm_interface::types::bech32_decode;
use hyperlane_core::NativeToken;
use hyperlane_cosmos::RawCosmosAmount;
use super::{cli::OsmosisCLI, CosmosNetwork};
#[derive(serde::Serialize, serde::Deserialize)]

Loading…
Cancel
Save