Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Scraper populates transaction fields for Solana #4801

Merged
merged 9 commits into from
Nov 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand All @@ -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";

Expand Down Expand Up @@ -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);
Expand All @@ -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)
}
}

Expand Down Expand Up @@ -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(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
13 changes: 3 additions & 10 deletions rust/main/chains/hyperlane-cosmos/src/trait_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -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 {
Expand Down
11 changes: 10 additions & 1 deletion rust/main/chains/hyperlane-sealevel/src/error.rs
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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 {
Expand Down
125 changes: 100 additions & 25 deletions rust/main/chains/hyperlane-sealevel/src/provider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -18,21 +23,91 @@ use crate::{ConnectionConf, SealevelRpcClient};
pub struct SealevelProvider {
domain: HyperlaneDomain,
rpc_client: Arc<SealevelRpcClient>,
native_token: NativeToken,
}

impl SealevelProvider {
/// Create a new Sealevel provider.
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()),
ameten marked this conversation as resolved.
Show resolved Hide resolved
))?,
};

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> {
ameten marked this conversation as resolved.
Show resolved Hide resolved
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 {
Expand All @@ -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(),
})
}
}
Expand Down Expand Up @@ -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);
ameten marked this conversation as resolved.
Show resolved Hide resolved

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,
Expand Down
3 changes: 2 additions & 1 deletion rust/main/chains/hyperlane-sealevel/src/rpc/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -188,6 +188,7 @@ impl SealevelRpcClient {
signature: &Signature,
) -> ChainResult<EncodedConfirmedTransactionWithStatusMeta> {
let config = RpcTransactionConfig {
encoding: Some(UiTransactionEncoding::JsonParsed),
commitment: Some(CommitmentConfig::finalized()),
..Default::default()
};
Expand Down
4 changes: 3 additions & 1 deletion rust/main/chains/hyperlane-sealevel/src/trait_builder.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use hyperlane_core::{config::OperationBatchConfig, ChainCommunicationError};
use hyperlane_core::{config::OperationBatchConfig, ChainCommunicationError, NativeToken};
use url::Url;

/// Sealevel connection configuration
Expand All @@ -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.
Expand Down
Loading