Skip to content
Open
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
73 changes: 73 additions & 0 deletions crates/gem_ton/src/models/transaction.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
use std::collections::HashMap;

use num_bigint::BigUint;
use primitives::TransactionState;
use serde::{Deserialize, Serialize};
use serde_serializers::deserialize_biguint_from_str;

Expand All @@ -20,6 +23,76 @@ pub struct MessageTransactions {
pub transactions: Vec<TransactionMessage>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TraceResponse {
pub traces: Vec<Trace>,
}

impl TraceResponse {
pub fn root_transaction(&self) -> Option<&TransactionMessage> {
self.traces.first()?.root_transaction()
}

pub fn action_state(&self) -> Option<TransactionState> {
self.traces.first().map(Trace::action_state)
}

pub fn has_actions(&self) -> bool {
self.traces.first().is_some_and(Trace::has_actions)
}
}

#[derive(Debug, Serialize)]
pub struct TraceByMessageQuery {
pub msg_hash: String,
pub include_actions: bool,
}

#[derive(Debug, Serialize)]
pub struct TraceByBlockQuery {
pub mc_seqno: u64,
pub include_actions: bool,
pub limit: usize,
pub offset: usize,
pub sort: &'static str,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Trace {
pub is_incomplete: bool,
pub actions: Vec<TraceAction>,
pub transactions_order: Vec<String>,
pub transactions: HashMap<String, TransactionMessage>,
}

impl Trace {
pub fn root_transaction(&self) -> Option<&TransactionMessage> {
let transaction_id = self.transactions_order.first()?;
self.transactions.get(transaction_id)
}

pub fn has_actions(&self) -> bool {
!self.actions.is_empty()
}

pub fn action_state(&self) -> TransactionState {
if self.is_incomplete {
return TransactionState::Pending;
}
for action in &self.actions {
if action.success == Some(false) {
return TransactionState::Reverted;
}
}
TransactionState::Confirmed
}
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TraceAction {
pub success: Option<bool>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TransactionMessage {
pub hash: String,
Expand Down
39 changes: 39 additions & 0 deletions crates/gem_ton/src/provider/testkit.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
#[cfg(test)]
use std::collections::HashMap;

#[cfg(test)]
use crate::models::{Trace, TraceAction, TraceResponse, TransactionMessage};
#[cfg(all(test, feature = "chain_integration_tests"))]
use crate::rpc::client::TonClient;
#[cfg(all(test, feature = "chain_integration_tests"))]
Expand All @@ -9,6 +14,40 @@ use settings::testkit::get_test_settings;
pub const TEST_ADDRESS: &str = "UQAzoUpalAaXnVm5MoiYWRZguLFzY0KxFjLv3MkRq5BXz3VV";
#[cfg(test)]
pub const TEST_TRANSACTION_ID: &str = "gyjq/7IJ5KpSvZlnwixaS3RjI2xk1+5pup0k++S/yXY=";
#[cfg(test)]
pub const FAILED_SWAP_MESSAGE_HASH: &str = "cf2fc2efd8d6f6b018f949b8f07e7e4b898a34a8bd422fcffb76bdc6e947b7e7";
#[cfg(test)]
pub const FAILED_SWAP_ROOT_TRANSACTION_HASH: &str = "L5Egpf9I3suIl6CdddcmMS44geWLFKgHi3EbBDz7qy8=";
#[cfg(test)]
pub const SUCCESS_SWAP_MESSAGE_HASH: &str = "e993d4c13053978b6265157561c454ef731274d836e3139ed64fdf58b6635bf7";
#[cfg(test)]
pub const SUCCESS_SWAP_ROOT_TRANSACTION_HASH: &str = "6ZPUwTBTl4tiZRV1YcRU73MSdNg24xOe1k/fWLZjW/c=";

#[cfg(test)]
impl TraceResponse {
pub fn mock(transaction: TransactionMessage, is_incomplete: bool, actions: Vec<TraceAction>) -> Self {
Self {
traces: vec![Trace {
is_incomplete,
actions,
transactions_order: vec![transaction.hash.clone()],
transactions: HashMap::from([(transaction.hash.clone(), transaction)]),
}],
}
}

pub fn mock_block_traces() -> Self {
serde_json::from_str(include_str!("../../testdata/block_traces.json")).unwrap()
}

pub fn mock_block_trace(index: usize) -> Self {
let traces = Self::mock_block_traces();

TraceResponse {
traces: vec![traces.traces[index].clone()],
}
}
}

#[cfg(all(test, feature = "chain_integration_tests"))]
pub fn create_ton_test_client() -> TonClient<ReqwestClient> {
Expand Down
16 changes: 14 additions & 2 deletions crates/gem_ton/src/provider/transaction_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ use crate::{provider::transaction_state_mapper::map_transaction_status, rpc::cli
#[async_trait]
impl<C: Client> ChainTransactionState for TonClient<C> {
async fn get_transaction_status(&self, request: TransactionStateRequest) -> Result<TransactionUpdate, Box<dyn Error + Sync + Send>> {
let transactions = self.get_transaction(request.id.clone()).await?;
map_transaction_status(request, transactions)
let traces = self.get_traces_by_message(request.id.clone()).await?;
map_transaction_status(request, traces)
}
}

Expand All @@ -21,6 +21,18 @@ mod chain_integration_tests {
use chain_traits::ChainTransactionState;
use primitives::{TransactionState, TransactionStateRequest};

#[tokio::test]
async fn test_get_traces_by_message() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let client = create_ton_test_client();
let traces = client.get_traces_by_message(FAILED_SWAP_MESSAGE_HASH.to_string()).await?;
let transaction = traces.root_transaction().ok_or("missing root transaction")?;

assert!(traces.has_actions());
assert_eq!(transaction.hash.as_str(), FAILED_SWAP_ROOT_TRANSACTION_HASH);

Ok(())
}

#[tokio::test]
async fn test_ton_transaction_status_confirmed() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let client = create_ton_test_client();
Expand Down
55 changes: 49 additions & 6 deletions crates/gem_ton/src/provider/transaction_state_mapper.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,16 @@ use std::error::Error;

use primitives::{TransactionChange, TransactionStateRequest, TransactionUpdate};

use crate::models::MessageTransactions;
use crate::models::TraceResponse;
use crate::provider::transactions_mapper::map_transaction_state;

pub fn map_transaction_status(_request: TransactionStateRequest, transactions: MessageTransactions) -> Result<TransactionUpdate, Box<dyn Error + Sync + Send>> {
let transaction = transactions.transactions.first().ok_or("Transaction not found")?;
let state = map_transaction_state(transaction);
pub fn map_transaction_status(_request: TransactionStateRequest, traces: TraceResponse) -> Result<TransactionUpdate, Box<dyn Error + Sync + Send>> {
let transaction = traces.root_transaction().ok_or("Transaction not found")?;
let state = if traces.has_actions() {
traces.action_state().ok_or("Trace not found")?
} else {
map_transaction_state(transaction)
};

let fee = transaction.total_fees.clone();

Expand All @@ -17,14 +21,17 @@ pub fn map_transaction_status(_request: TransactionStateRequest, transactions: M
#[cfg(test)]
mod tests {
use super::*;
use crate::models::{MessageTransactions, TraceAction};
use crate::provider::testkit::{FAILED_SWAP_MESSAGE_HASH, SUCCESS_SWAP_MESSAGE_HASH};
use primitives::TransactionState;

#[test]
fn test_map_transaction_status_confirmed() {
let request = TransactionStateRequest::new_id("hash".to_string());
let transactions: MessageTransactions = serde_json::from_str(include_str!("../../testdata/transaction_transfer_state_success.json")).unwrap();
let traces = TraceResponse::mock(transactions.transactions.first().unwrap().clone(), false, vec![]);

let update = map_transaction_status(request, transactions).unwrap();
let update = map_transaction_status(request, traces).unwrap();
assert_eq!(update.state, TransactionState::Confirmed);
assert!(!update.changes.is_empty());
}
Expand All @@ -33,9 +40,45 @@ mod tests {
fn test_ton_transaction_jetton_transfer_reverted() {
let request = TransactionStateRequest::new_id("hash".to_string());
let transactions: MessageTransactions = serde_json::from_str(include_str!("../../testdata/transaction_transfer_jetton_error_2.json")).unwrap();
let traces = TraceResponse::mock(transactions.transactions.first().unwrap().clone(), false, vec![]);

let update = map_transaction_status(request, transactions).unwrap();
let update = map_transaction_status(request, traces).unwrap();
assert_eq!(update.state, TransactionState::Reverted);
assert!(!update.changes.is_empty());
}

#[test]
fn test_map_transaction_status_success_trace_action() {
let request = TransactionStateRequest::new_id(SUCCESS_SWAP_MESSAGE_HASH.to_string());
let traces = TraceResponse::mock_block_trace(0);

let update = map_transaction_status(request, traces).unwrap();
assert_eq!(update.state, TransactionState::Confirmed);
assert!(!update.changes.is_empty());
}

#[test]
fn test_map_transaction_status_failed_trace_action() {
let request = TransactionStateRequest::new_id(FAILED_SWAP_MESSAGE_HASH.to_string());
let traces = TraceResponse::mock_block_trace(1);
let transaction = traces.root_transaction().unwrap().clone();

let root_update = map_transaction_status(request.clone(), TraceResponse::mock(transaction, false, vec![])).unwrap();
assert_eq!(root_update.state, TransactionState::Confirmed);

let update = map_transaction_status(request, traces).unwrap();
assert_eq!(update.state, TransactionState::Reverted);
assert!(!update.changes.is_empty());
}

#[test]
fn test_map_transaction_status_incomplete_trace() {
let request = TransactionStateRequest::new_id("hash".to_string());
let transactions: MessageTransactions = serde_json::from_str(include_str!("../../testdata/transaction_transfer_state_success.json")).unwrap();
let traces = TraceResponse::mock(transactions.transactions.first().unwrap().clone(), true, vec![TraceAction { success: Some(true) }]);

let update = map_transaction_status(request, traces).unwrap();
assert_eq!(update.state, TransactionState::Pending);
assert!(!update.changes.is_empty());
}
}
9 changes: 6 additions & 3 deletions crates/gem_ton/src/provider/transactions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,16 @@ use std::error::Error;
use gem_client::Client;
use primitives::Transaction;

use crate::{provider::transactions_mapper::map_transactions, rpc::client::TonClient};
use crate::{
provider::transactions_mapper::{map_trace_transactions, map_transactions},
rpc::client::TonClient,
};

#[async_trait]
impl<C: Client> ChainTransactions for TonClient<C> {
async fn get_transactions_by_block(&self, block: u64) -> Result<Vec<Transaction>, Box<dyn Error + Sync + Send>> {
let transactions = self.get_transactions_by_masterchain_block(block.to_string()).await?;
Ok(map_transactions(transactions.transactions))
let traces = self.get_traces_by_masterchain_block(block).await?;
Ok(map_trace_transactions(traces.traces))
}

async fn get_transaction_by_hash(&self, hash: String) -> Result<Option<Transaction>, Box<dyn Error + Sync + Send>> {
Expand Down
46 changes: 42 additions & 4 deletions crates/gem_ton/src/provider/transactions_mapper.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use crate::address::Address;
use crate::constants::FAILED_OPERATION_OPCODES;
use crate::models::{BroadcastTransaction, HasMemo, TransactionMessage};
use crate::models::{BroadcastTransaction, HasMemo, Trace, TransactionMessage};
use chrono::DateTime;
use gem_encoding::decode_base64;
use primitives::{Address as AddressTrait, Transaction, TransactionState, TransactionType, chain::Chain};
Expand Down Expand Up @@ -58,9 +58,33 @@ pub fn map_transactions(transactions: Vec<TransactionMessage>) -> Vec<Transactio
transactions.into_iter().filter_map(map_transaction_message).collect()
}

pub fn map_trace_transactions(traces: Vec<Trace>) -> Vec<Transaction> {
traces.into_iter().flat_map(map_trace_transaction).collect()
}

fn map_trace_transaction(trace: Trace) -> Vec<Transaction> {
let root_hash = trace.transactions_order.first().cloned();
let trace_state = if trace.is_incomplete || trace.has_actions() { Some(trace.action_state()) } else { None };
let mut transactions = trace.transactions;

trace
.transactions_order
.into_iter()
.filter_map(|hash| {
let transaction = transactions.remove(&hash)?;
let state = if root_hash.as_ref() == Some(&hash) { trace_state } else { None };
map_transaction_message_with_state(transaction, state)
})
.collect()
}

fn map_transaction_message(transaction: TransactionMessage) -> Option<Transaction> {
map_transaction_message_with_state(transaction, None)
}

fn map_transaction_message_with_state(transaction: TransactionMessage, state: Option<TransactionState>) -> Option<Transaction> {
let asset_id = Chain::Ton.as_asset_id();
let state = map_transaction_state(&transaction);
let state = state.unwrap_or_else(|| map_transaction_state(&transaction));
let created_at = DateTime::from_timestamp(transaction.now, 0)?;
let hash = transaction.hash.clone();

Expand Down Expand Up @@ -159,8 +183,8 @@ fn extract_memo<T: HasMemo>(message: &T) -> Option<String> {
#[cfg(test)]
mod tests {
use super::*;
use crate::models::MessageTransactions;
use crate::provider::testkit::TEST_TRANSACTION_ID;
use crate::models::{MessageTransactions, TraceResponse};
use crate::provider::testkit::{FAILED_SWAP_ROOT_TRANSACTION_HASH, SUCCESS_SWAP_ROOT_TRANSACTION_HASH, TEST_TRANSACTION_ID};

#[test]
fn test_transaction_transfer_state_success() {
Expand Down Expand Up @@ -285,4 +309,18 @@ mod tests {
assert!(!transaction.to.is_empty());
}
}

#[test]
fn test_map_trace_transactions_by_block() {
let traces = TraceResponse::mock_block_traces();

assert_eq!(traces.traces.len(), 2);

let transactions = map_trace_transactions(traces.traces);
let hashes = transactions.iter().map(|transaction| transaction.hash.as_str()).collect::<Vec<_>>();

assert_eq!(hashes, vec![SUCCESS_SWAP_ROOT_TRANSACTION_HASH, FAILED_SWAP_ROOT_TRANSACTION_HASH]);
assert_eq!(transactions[0].state, TransactionState::Confirmed);
assert_eq!(transactions[1].state, TransactionState::Reverted);
}
}
Loading
Loading