Skip to content
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
6 changes: 5 additions & 1 deletion apps/api/src/chain/client.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use std::error::Error;

use primitives::{Asset, AssetBalance, Chain, ChainAddress, Transaction};
use primitives::{Asset, AssetBalance, Chain, ChainAddress, Transaction, TransactionUpdate};
use settings_chain::{ChainProviders, TransactionsRequest};

pub struct ChainClient {
Expand Down Expand Up @@ -51,6 +51,10 @@ impl ChainClient {
self.providers.get_transaction_by_hash(chain, hash).await
}

pub async fn get_transaction_status(&self, chain: Chain, hash: String) -> Result<TransactionUpdate, Box<dyn Error + Send + Sync>> {
self.providers.get_transaction_status(chain, hash).await
}

pub async fn get_block_transactions(&self, chain: Chain, block_number: i64, transaction_type: Option<&str>) -> Result<Vec<Transaction>, Box<dyn Error + Send + Sync>> {
let transactions = self.providers.get_block_transactions(chain, block_number as u64).await?;
Ok(self.filter_transactions(transactions, transaction_type))
Expand Down
7 changes: 6 additions & 1 deletion apps/api/src/chain/transaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,16 @@ use rocket::{State, get, tokio::sync::Mutex};

use crate::params::ChainParam;
use crate::responders::{ApiError, ApiResponse};
use primitives::Transaction;
use primitives::{Transaction, TransactionUpdate};

use super::ChainClient;

#[get("/chain/transactions/<chain>/<hash>")]
pub async fn get_transaction(chain: ChainParam, hash: &str, client: &State<Mutex<ChainClient>>) -> Result<ApiResponse<Option<Transaction>>, ApiError> {
Ok(client.lock().await.get_transaction_by_hash(chain.0, hash.to_string()).await?.into())
}

#[get("/chain/transactions/<chain>/<hash>/status")]
pub async fn get_transaction_status(chain: ChainParam, hash: &str, client: &State<Mutex<ChainClient>>) -> Result<ApiResponse<TransactionUpdate>, ApiError> {
Ok(client.lock().await.get_transaction_status(chain.0, hash.to_string()).await?.into())
}
1 change: 1 addition & 0 deletions apps/api/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ fn mount_routes(rocket: Rocket<Build>, admin_enabled: bool) -> Rocket<Build> {
chain::nft::get_nft_asset,
chain::nft::get_nft_collection,
chain::transaction::get_transaction,
chain::transaction::get_transaction_status,
referral::get_rewards_leaderboard,
swap::post_near_intents_quote,
swap::okx::post_okx_quote,
Expand Down
17 changes: 13 additions & 4 deletions crates/gem_ton/src/address.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ type Workchain = i32;
type HashPart = [u8; 32];
type RawBytes = [u8; 33];

const USER_FRIENDLY_FLAG: u8 = 0x11;
const TAG_BOUNCEABLE_MAINNET: u8 = 0x11;
const TAG_NON_BOUNCEABLE_MAINNET: u8 = TAG_BOUNCEABLE_MAINNET | 0x40;
const RAW_ADDRESS_LEN: usize = 33;
const USER_FRIENDLY_ADDRESS_LEN: usize = 36;

Expand Down Expand Up @@ -72,10 +73,10 @@ impl Address {
Ok(())
}

fn encode_user_friendly(&self) -> String {
fn encode_user_friendly(&self, flag: u8) -> String {
let mut buffer = [0u8; USER_FRIENDLY_ADDRESS_LEN];

buffer[0] = USER_FRIENDLY_FLAG;
buffer[0] = flag;
buffer[1..RAW_ADDRESS_LEN + 1].copy_from_slice(&self.bytes);

let crc = crc16(&buffer[..RAW_ADDRESS_LEN + 1]);
Expand All @@ -84,6 +85,14 @@ impl Address {

encode_base64_url(&buffer)
}

pub fn encode_bounceable(&self) -> String {
self.encode_user_friendly(TAG_BOUNCEABLE_MAINNET)
}

pub fn encode_non_bounceable(&self) -> String {
self.encode_user_friendly(TAG_NON_BOUNCEABLE_MAINNET)
}
}

impl FromStr for Address {
Expand Down Expand Up @@ -119,7 +128,7 @@ impl AddressTrait for Address {
}

fn encode(&self) -> String {
self.encode_user_friendly()
self.encode_bounceable()
}
}

Expand Down
150 changes: 108 additions & 42 deletions crates/gem_ton/src/models/transaction.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
use std::collections::HashMap;

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

pub trait HasMemo {
fn comment(&self) -> &Option<String>;
fn decoded_body(&self) -> &Option<DecodedBody>;
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DecodedBody {
#[serde(rename = "type")]
Expand All @@ -20,6 +18,111 @@ 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 TraceByTransactionQuery {
pub tx_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, Serialize)]
pub struct TraceByAddressQuery {
pub account: String,
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
}
}

pub const TRACE_ACTION_JETTON_SWAP: &str = "jetton_swap";

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TraceAction {
pub success: Option<bool>,
#[serde(rename = "type")]
pub action_type: Option<String>,
pub details: Option<serde_json::Value>,
}

#[derive(Debug, Clone, Deserialize)]
pub struct JettonSwapDetails {
pub dex: Option<String>,
pub sender: String,
pub asset_in: Option<String>,
pub asset_out: Option<String>,
pub dex_incoming_transfer: SwapTransfer,
pub dex_outgoing_transfer: SwapTransfer,
}

#[derive(Debug, Clone, Deserialize)]
pub struct SwapTransfer {
pub amount: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TransactionMessage {
pub hash: String,
Expand All @@ -37,53 +140,16 @@ pub struct OutMessage {
pub destination: Option<String>,
pub value: Option<String>,
pub op_code: Option<String>,
pub decoded_op_name: Option<String>,
pub body: Option<String>,
pub comment: Option<String>,
pub decoded_body: Option<DecodedBody>,
}

impl HasMemo for OutMessage {
fn comment(&self) -> &Option<String> {
&self.comment
}

fn decoded_body(&self) -> &Option<DecodedBody> {
&self.decoded_body
}
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InMessage {
pub hash: String,
pub msg_type: Option<String>,
pub value: Option<String>,
pub source: Option<String>,
pub destination: Option<String>,
pub body: Option<String>,
pub comment: Option<String>,
pub decoded_body: Option<DecodedBody>,
}

impl HasMemo for InMessage {
fn comment(&self) -> &Option<String> {
&self.comment
}

fn decoded_body(&self) -> &Option<DecodedBody> {
&self.decoded_body
}
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TransactionInMessage {
pub hash: String,
pub source: Option<String>,
pub destination: String,
pub value: Option<String>,
pub opcode: Option<String>,
pub bounce: Option<bool>,
pub bounced: Option<bool>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
Expand Down
49 changes: 49 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,50 @@ 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 TEST_TRANSACTION_HEX_HASH: &str = "8328eaffb209e4aa52bd9967c22c5a4b7463236c64d7ee69ba9d24fbe4bfc976";
#[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 FAILED_SWAP_ROOT_TRANSACTION_HEX_HASH: &str = "2f9120a5ff48decb8897a09d75d726312e3881e58b14a8078b711b043cfbab2f";
#[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)]
pub const SUCCESS_SWAP_ROOT_TRANSACTION_HEX_HASH: &str = "e993d4c13053978b6265157561c454ef731274d836e3139ed64fdf58b6635bf7";

#[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()],
}
}

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

#[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_hash(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
Loading
Loading