diff --git a/Cargo.toml b/Cargo.toml index 3d4870ad7..d1f4c1a72 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -56,7 +56,7 @@ ed25519-dalek = { git = "https://github.com/broxus/ed25519-dalek.git", optional tiny-bip39 = { git = "https://github.com/broxus/tiny-bip39.git", default-features = false, optional = true } tiny-hderive = { git = "https://github.com/broxus/tiny-hderive.git", optional = true } -ton_abi = { git = "https://github.com/broxus/ton-labs-abi" } +ton_abi = { git = "https://github.com/broxus/ton-labs-abi"} ton_block = { git = "https://github.com/broxus/ton-labs-block.git" } ton_executor = { git = "https://github.com/broxus/ton-labs-executor.git" } ton_types = { git = "https://github.com/broxus/ton-labs-types.git" } @@ -67,7 +67,7 @@ nekoton-utils = { path = "nekoton-utils" } nekoton-proto = { path = "nekoton-proto", optional = true } [dev-dependencies] -reqwest = { version = "0.11.8", features = ["gzip"] } +reqwest = { version = "0.11.8", features = ["gzip", "json"] } cargo-husky = { version = "1", features = ["default", "run-cargo-fmt", "run-cargo-check"] } tokio = { version = "1", features = ["rt-multi-thread", "macros"] } @@ -85,6 +85,7 @@ web = [ gql_transport = ["dep:erased-serde"] jrpc_transport = ["dep:tiny-jsonrpc"] proto_transport = ["dep:nekoton-proto"] +ton_transport = [] extended_models = [] non_threadsafe = [] wallet_core = ["dep:pbkdf2", "dep:chacha20poly1305", "dep:zeroize", "dep:secstr", "dep:hmac", "dep:ed25519-dalek", diff --git a/nekoton-transport/Cargo.toml b/nekoton-transport/Cargo.toml index 3a57fae79..a74dacfb4 100644 --- a/nekoton-transport/Cargo.toml +++ b/nekoton-transport/Cargo.toml @@ -16,6 +16,7 @@ futures-util = "0.3" log = "0.4" reqwest = { version = "0.11", features = ["json", "gzip", "rustls-tls"], default-features = false } serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" thiserror = "1.0" tokio = { version = "1", features = ["sync", "time"] } @@ -24,7 +25,6 @@ nekoton-utils = { path = "../nekoton-utils" } nekoton = { path = ".." } [dev-dependencies] -base64 = "0.13" tokio = { version = "1", features = ["sync", "time", "macros"] } ton_types = { git = "https://github.com/broxus/ton-labs-types.git" } @@ -34,3 +34,4 @@ default = ["gql_transport"] gql_transport = ["nekoton/gql_transport"] jrpc_transport = ["nekoton/jrpc_transport"] proto_transport = ["nekoton/proto_transport"] +ton_transport = ["nekoton/ton_transport"] diff --git a/nekoton-transport/src/lib.rs b/nekoton-transport/src/lib.rs index 6e3595139..a5db373cf 100644 --- a/nekoton-transport/src/lib.rs +++ b/nekoton-transport/src/lib.rs @@ -58,3 +58,5 @@ pub mod gql; pub mod jrpc; #[cfg(feature = "proto_transport")] pub mod proto; +#[cfg(feature = "ton_transport")] +pub mod ton; diff --git a/nekoton-transport/src/ton.rs b/nekoton-transport/src/ton.rs new file mode 100644 index 000000000..596d95815 --- /dev/null +++ b/nekoton-transport/src/ton.rs @@ -0,0 +1,72 @@ +use reqwest::{IntoUrl, Url}; +use serde::Serialize; + +pub struct TonClient { + endpoint: Url, + client: reqwest::Client, +} + +impl TonClient { + pub fn new_v4(endpoint: U) -> anyhow::Result { + let url = endpoint.into_url()?; + Ok(Self { + endpoint: url, + client: reqwest::Client::new(), + }) + } + + pub fn endpoint(&self) -> &Url { + &self.endpoint + } + + pub async fn send_get(&self, path: U) -> anyhow::Result> { + let path = path.into_url()?; + let result = self + .client + .get(self.endpoint.clone().join(path.as_str())?) + .header("ContentType", "application/json") + .send() + .await?; + + if matches!(result.status(), reqwest::StatusCode::NOT_FOUND) { + return Ok(None); + } + + let result = result.text().await?; + Ok(Some(result)) + } + + pub async fn send_post( + &self, + body: R, + path: U, + ) -> anyhow::Result> { + let path = path.into_url()?; + let result = self + .client + .post(self.endpoint.clone().join(path.as_str())?) + .body(serde_json::to_string(&body)?) + .header("ContentType", "application/json") + .send() + .await?; + + if matches!(result.status(), reqwest::StatusCode::NOT_FOUND) { + return Ok(None); + } + + let result = result.text().await?; + Ok(Some(result)) + } +} + +#[cfg_attr(not(feature = "non_threadsafe"), async_trait::async_trait)] +#[cfg_attr(feature = "non_threadsafe", async_trait::async_trait(?Send))] +impl nekoton::external::TonConnection for TonClient { + async fn send_get(&self, path: &str) -> anyhow::Result> { + self.send_get(path).await + } + + async fn send_post(&self, body: &str, path: &str) -> anyhow::Result> { + self.send_post(body, path).await + } +} diff --git a/nekoton-utils/src/serde_helpers.rs b/nekoton-utils/src/serde_helpers.rs index c6223633e..86501fe34 100644 --- a/nekoton-utils/src/serde_helpers.rs +++ b/nekoton-utils/src/serde_helpers.rs @@ -46,6 +46,42 @@ impl<'de> Deserialize<'de> for StringOrNumber { } } +struct U128StringOrNumber(u128); + +impl Serialize for U128StringOrNumber { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + if self.0 <= 0x1fffffffffffff_u128 || !serializer.is_human_readable() { + serializer.serialize_u128(self.0) + } else { + serializer.serialize_str(&self.0.to_string()) + } + } +} + +impl<'de> Deserialize<'de> for U128StringOrNumber { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + #[derive(Deserialize)] + #[serde(untagged)] + enum Value<'a> { + String(#[serde(borrow)] Cow<'a, str>), + Number(u128), + } + + match Value::deserialize(deserializer)? { + Value::String(str) => u128::from_str(str.as_ref()) + .map(Self) + .map_err(|_| D::Error::custom("Invalid number")), + Value::Number(value) => Ok(Self(value)), + } + } +} + pub mod serde_u64 { use super::*; @@ -64,6 +100,24 @@ pub mod serde_u64 { } } +pub mod serde_u128 { + use super::*; + + pub fn serialize(data: &u128, serializer: S) -> Result + where + S: serde::Serializer, + { + U128StringOrNumber(*data).serialize(serializer) + } + + pub fn deserialize<'de, D>(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + U128StringOrNumber::deserialize(deserializer).map(|U128StringOrNumber(x)| x) + } +} + pub mod serde_optional_u64 { use super::*; @@ -138,6 +192,33 @@ pub mod serde_base64_array { } } +pub mod serde_optional_base64_array { + use super::*; + + pub fn serialize(data: &dyn AsRef<[u8]>, serializer: S) -> Result + where + S: serde::Serializer, + { + serde_bytes_base64::serialize(data, serializer) + } + + pub fn deserialize<'de, D, const N: usize>(deserializer: D) -> Result, D::Error> + where + D: serde::Deserializer<'de>, + { + let data = serde_bytes_base64::deserialize_string_optional(deserializer)?; + match data { + Some(data) => { + let result = data.try_into().map_err(|_| { + D::Error::custom(format!("Invalid array length, expected: {N}")) + })?; + Ok(Some(result)) + } + None => Ok(None), + } + } +} + pub mod serde_hex_array { use super::*; @@ -292,6 +373,25 @@ pub mod serde_uint256 { } } +pub mod serde_base64_uint256 { + use super::*; + + pub fn serialize(data: &UInt256, serializer: S) -> Result + where + S: serde::Serializer, + { + serde_base64_array::serialize(data.as_slice(), serializer) + } + + pub fn deserialize<'de, D>(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let data: [u8; 32] = serde_base64_array::deserialize(deserializer)?; + Ok(UInt256::from_slice(&data[..])) + } +} + pub mod serde_optional_uint256 { use super::*; @@ -397,6 +497,26 @@ pub mod serde_address { } } +pub mod serde_base64_address { + use super::*; + use crate::repack_address; + + pub fn serialize(data: &MsgAddressInt, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(&data.to_string()) + } + + pub fn deserialize<'de, D>(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let data = String::deserialize(deserializer)?; + repack_address(&data).map_err(|_| D::Error::custom("Invalid address")) + } +} + pub mod serde_optional_address { use super::*; @@ -579,6 +699,37 @@ pub mod serde_bytes_base64 { deserializer.deserialize_bytes(BytesVisitor) } } + + pub fn deserialize_string_optional<'de, D>(deserializer: D) -> Result>, D::Error> + where + D: serde::Deserializer<'de>, + { + struct Base64Visitor; + + impl<'de> Visitor<'de> for Base64Visitor { + type Value = Option>; + + fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("base64-encoded byte array") + } + + fn visit_str(self, value: &str) -> Result { + if value.is_empty() { + return Ok(None); + } + base64::decode(value) + .map(|x| Some(x)) + .map_err(|_| E::invalid_type(Unexpected::Str(value), &self)) + } + + // See the `deserializing_flattened_field` test for an example why this is needed. + fn visit_bytes(self, value: &[u8]) -> Result { + Ok(Some(value.to_vec())) + } + } + + deserializer.deserialize_str(Base64Visitor) + } } pub mod serde_bytes_base64_optional { @@ -672,6 +823,26 @@ pub mod serde_cell { } } +pub mod serde_transaction_array { + use super::*; + use ton_block::{Deserializable, Transaction}; + pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> + where + D: serde::Deserializer<'de>, + { + let bytes = serde_bytes_base64::deserialize(deserializer)?; + let cells = + ton_types::deserialize_cells_tree(&mut bytes.as_slice()).map_err(Error::custom)?; + let mut transactions = Vec::new(); + for c in cells { + let t = Transaction::construct_from_cell(c).map_err(Error::custom)?; + transactions.push(t); + } + + Ok(transactions) + } +} + pub mod serde_ton_block { use ton_block::{Deserializable, Serializable}; diff --git a/src/core/contract_subscription/mod.rs b/src/core/contract_subscription/mod.rs index 6a9aa14f2..c3c91541a 100644 --- a/src/core/contract_subscription/mod.rs +++ b/src/core/contract_subscription/mod.rs @@ -44,11 +44,9 @@ impl ContractSubscription { pending_transactions: Vec::new(), transactions_synced: false, }; - result.transactions_synced = !result .refresh_contract_state_impl(None, on_contract_state) .await?; - if !result.transactions_synced { if let Some(on_transactions_found) = on_transactions_found { // Preload transactions if `on_transactions_found` specified diff --git a/src/core/ton_wallet/wallet_v5r1.rs b/src/core/ton_wallet/wallet_v5r1.rs index cc30bbe24..bc4f9259b 100644 --- a/src/core/ton_wallet/wallet_v5r1.rs +++ b/src/core/ton_wallet/wallet_v5r1.rs @@ -382,7 +382,7 @@ mod tests { if let AccountState::AccountActive { state_init } = state.storage.state() { let init_data = InitData::try_from(state_init.data().unwrap())?; - assert_eq!(init_data.is_signature_allowed, true); + assert!(init_data.is_signature_allowed); assert_eq!( init_data.public_key.to_hex_string(), "9107a65271437e1a982bb98404bd9a82c434f31ee30c621b6596702bb59bf0a0" diff --git a/src/external/mod.rs b/src/external/mod.rs index fc5634f6c..01429b3c9 100644 --- a/src/external/mod.rs +++ b/src/external/mod.rs @@ -65,6 +65,14 @@ pub trait ProtoConnection: Send + Sync { async fn post(&self, req: ProtoRequest) -> Result>; } +#[cfg(feature = "ton_transport")] +#[cfg_attr(not(feature = "non_threadsafe"), async_trait::async_trait)] +#[cfg_attr(feature = "non_threadsafe", async_trait::async_trait(?Send))] +pub trait TonConnection: Send + Sync { + async fn send_get(&self, path: &str) -> Result>; + async fn send_post(&self, body: &str, path: &str) -> Result>; +} + #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct LedgerSignatureContext { diff --git a/src/transport/jrpc/mod.rs b/src/transport/jrpc/mod.rs index de20cb5f8..5684ebef3 100644 --- a/src/transport/jrpc/mod.rs +++ b/src/transport/jrpc/mod.rs @@ -398,7 +398,7 @@ mod tests { transport .get_transactions(&address, 21968513000000, 10) .await?; - + transport .get_transaction(&ton_types::UInt256::from_slice( &hex::decode("4a0a06bfbfaba4da8fcc7f5ad617fdee5344d954a1794e35618df2a4b349d15c") diff --git a/src/transport/mod.rs b/src/transport/mod.rs index 8038c8303..0318e8a0f 100644 --- a/src/transport/mod.rs +++ b/src/transport/mod.rs @@ -13,12 +13,15 @@ pub mod gql; pub mod jrpc; #[cfg(feature = "proto_transport")] pub mod proto; +#[cfg(feature = "ton_transport")] +pub mod ton; pub mod models; #[cfg(any( feature = "gql_transport", feature = "jrpc_transport", feature = "proto_transport", + feature = "ton_transport", ))] mod utils; diff --git a/src/transport/ton/mod.rs b/src/transport/ton/mod.rs new file mode 100644 index 000000000..917edd205 --- /dev/null +++ b/src/transport/ton/mod.rs @@ -0,0 +1,532 @@ +mod models; + +use nekoton_abi::{GenTimings, LastTransactionId, TransactionId}; +use nekoton_utils::{pack_std_smc_addr, Clock}; +use std::sync::Arc; +use ton_block::{ + AccountStorage, AccountStuff, Block, CurrencyCollection, GetRepresentationHash, Grams, Message, + MsgAddressInt, Serializable, StorageInfo, VarUInteger7, +}; +use ton_executor::BlockchainConfig; +use ton_types::{Cell, UInt256}; + +use super::{Transport, TransportInfo}; +use crate::external::TonConnection; +use crate::models::{NetworkCapabilities, ReliableBehavior}; +use crate::transport::models::{ + ExistingContract, PollContractState, RawContractState, RawTransaction, +}; +use crate::transport::ton::models::*; +use crate::transport::utils::AccountsCache; + +pub struct TonTransport { + connection: Arc, + accounts_cache: AccountsCache, +} + +impl TonTransport { + pub fn new(connection: Arc) -> Self { + Self { + connection, + accounts_cache: AccountsCache::new(), + } + } + async fn get_latest_block(&self) -> anyhow::Result { + let result = self.connection.send_get("block/latest").await?; + let result = match result { + Some(t) => t, + None => anyhow::bail!("Address or block not found"), + }; + let result = serde_json::from_str(&result)?; + Ok(result) + } + + // pub async fn get_full_block(&self, seqno: u32) -> anyhow::Result { + // let result = self.connection.send_get(&format!("block/{seqno}")).await?; + // let result = serde_json::from_value(result)?; + // Ok(result) + // } + + // pub async fn get_full_block_by_utime(&self, utime: u64) -> anyhow::Result { + // let result = self.connection.send_get(&format!("block/utime/{utime}")).await?; + // let result = serde_json::from_value(result)?; + // Ok(result) + // } + + async fn get_account_state( + &self, + block_seqno: u32, + address: &MsgAddressInt, + ) -> anyhow::Result> { + let base64_address = pack_std_smc_addr(true, address, false)?; + let result = self + .connection + .send_get(&format!("block/{block_seqno}/{base64_address}")) + .await; + let result = match result { + Ok(Some(result)) => Some(serde_json::from_str(&result)?), + Ok(None) => return Ok(None), + Err(err) => return Err(err), + }; + Ok(result) + } + + async fn check_account_changed_ext( + &self, + address: &MsgAddressInt, + since_lt: u64, + ) -> anyhow::Result { + let latest_block = self.get_latest_block().await?; + let result = self + .check_account_changed(latest_block.last.seqno, address, since_lt) + .await?; + let state = self + .get_contract_state_ext(latest_block.last.seqno, latest_block.now, address) + .await?; + + let state = match (result.changed, state) { + (false, RawContractState::Exists(_)) => PollContractState::Unchanged { + timings: GenTimings::Known { + gen_lt: 0, + gen_utime: latest_block.now, + }, + }, + (true, RawContractState::Exists(contract)) => PollContractState::Exists(contract), + (_, RawContractState::NotExists { timings }) => { + PollContractState::NotExists { timings } + } + }; + + Ok(state) + } + + async fn get_contract_state_ext( + &self, + block_seqno: u32, + block_utime: u32, + address: &MsgAddressInt, + ) -> anyhow::Result { + let state_opt = self.get_account_state(block_seqno, address).await?; + + let timings = GenTimings::Known { + gen_lt: 0, + gen_utime: block_utime, + }; //TODO: how to get gen_lt + + let state = match state_opt { + None => return Ok(RawContractState::NotExists { timings }), + Some(state) => state, + }; + + let account_state = match state.account.state { + AccountState::Active { code, data } => ton_block::AccountState::AccountActive { + state_init: ton_block::StateInit { + split_depth: None, + special: None, + code: Some(code), + data: Some(data), + library: Default::default(), + }, + }, + AccountState::Frozen { state_init_hash } => { + ton_block::AccountState::AccountFrozen { state_init_hash } + } + AccountState::Uninit => ton_block::AccountState::AccountUninit, + }; + + let mut balance = CurrencyCollection::new(); + balance.grams = Grams::new(state.account.balance.coins)?; + for (key, value) in state.account.balance.currencies.unwrap_or_default() { + balance.set_other(key, value)?; + } + + let used = state.account.storage_stat.used; + + let stuff = AccountStuff { + addr: address.clone(), + storage_stat: StorageInfo { + used: ton_block::StorageUsed { + cells: VarUInteger7::new(used.cells)?, + bits: VarUInteger7::new(used.bits)?, + public_cells: VarUInteger7::new(used.public_cells)?, + }, + last_paid: state.account.storage_stat.last_paid, + due_payment: match state.account.storage_stat.due_payment { + Some(due_payment) => Some(Grams::new(due_payment)?), + None => None, + }, + }, + storage: AccountStorage { + last_trans_lt: state + .account + .last_transaction + .as_ref() + .map(|x| x.lt) + .unwrap_or_default(), + balance, + state: account_state, + init_code_hash: None, + }, + }; + + let contract_state = RawContractState::Exists(ExistingContract { + account: stuff, + timings, + last_transaction_id: match state.account.last_transaction { + Some(last) => LastTransactionId::Exact(TransactionId { + lt: last.lt, + hash: last.hash, + }), + None => LastTransactionId::Inexact { latest_lt: 0 }, + }, + }); + + Ok(contract_state) + } + + async fn check_account_changed( + &self, + block_seqno: u32, + address: &MsgAddressInt, + since_lt: u64, + ) -> anyhow::Result { + let base64_address = pack_std_smc_addr(true, address, false)?; + let result = self + .connection + .send_get(&format!( + "block/{block_seqno}/{base64_address}/changed/{since_lt}" + )) + .await?; + + let result = match result { + Some(t) => t, + None => anyhow::bail!("Address or block not found"), + }; + + let result = serde_json::from_str(&result)?; + Ok(result) + } + + async fn get_config(&self, block_seqno: u32, params: I) -> anyhow::Result + where + I: IntoIterator, + { + let params_string = params + .into_iter() + .map(|x| x.to_string()) + .collect::>() + .join(","); + let result = self + .connection + .send_get(&format!("block/{block_seqno}/config/{params_string}")) + .await?; + + let result = match result { + Some(t) => t, + None => anyhow::bail!("Block not found"), + }; + + let result = serde_json::from_str(&result)?; + Ok(result) + } + + async fn get_account_transactions( + &self, + address: &MsgAddressInt, + lt: u64, + ) -> anyhow::Result { + let base64_address = pack_std_smc_addr(true, address, false)?; + let result = self + .connection + .send_get(&format!("account/{base64_address}/tx/{lt}/-")) + .await?; + + let result = match result { + Some(t) => t, + None => anyhow::bail!("Address not found"), + }; + let result = serde_json::from_str(&result)?; + Ok(result) + } + + async fn send_message(&self, message_cell: Cell) -> anyhow::Result<()> { + let bytes = ton_types::serialize_toc(&message_cell)?; + let boc = base64::encode(bytes); + let body = serde_json::to_string(&MessageBoc { boc })?; + + self.connection.send_post(&body, "/send").await?; + Ok(()) + } +} + +#[cfg_attr(not(feature = "non_threadsafe"), async_trait::async_trait)] +#[cfg_attr(feature = "non_threadsafe", async_trait::async_trait(?Send))] +impl Transport for TonTransport { + fn info(&self) -> TransportInfo { + TransportInfo { + max_transactions_per_fetch: 20, + reliable_behavior: ReliableBehavior::IntensivePolling, + has_key_blocks: false, + } + } + + async fn send_message(&self, message: &Message) -> anyhow::Result<()> { + let cell = message.serialize()?; + self.send_message(cell).await + } + + async fn get_contract_state( + &self, + address: &MsgAddressInt, + ) -> anyhow::Result { + if let Some(known_state) = self.accounts_cache.get_account_state(address) { + if let Some(last_trans_lt) = known_state.last_known_trans_lt() { + let poll = self.poll_contract_state(address, last_trans_lt).await?; + return Ok(match poll.to_changed() { + Ok(contract) => { + self.accounts_cache.update_account_state(address, &contract); + contract + } + Err(timings) => { + let mut known_state = known_state.as_ref().clone(); + known_state.update_timings(timings); + known_state + } + }); + } + } + + let latest_block = self.get_latest_block().await?; + let state = self + .get_contract_state_ext(latest_block.last.seqno, latest_block.now, address) + .await?; + self.accounts_cache.update_account_state(address, &state); + Ok(state) + } + + async fn get_library_cell(&self, _: &UInt256) -> anyhow::Result> { + Ok(None) + } + + async fn poll_contract_state( + &self, + address: &MsgAddressInt, + last_transaction_lt: u64, + ) -> anyhow::Result { + self.check_account_changed_ext(address, last_transaction_lt) + .await + } + + async fn get_accounts_by_code_hash( + &self, + _: &UInt256, + _: u8, + _: &Option, + ) -> anyhow::Result> { + todo!() + } + + async fn get_transactions( + &self, + address: &MsgAddressInt, + from_lt: u64, + count: u8, + ) -> anyhow::Result> { + const AT_MOST: usize = 20; + + let mut remaining = count; + let mut transactions = Vec::with_capacity(count as usize); + + loop { + let result = self.get_account_transactions(address, from_lt).await?; + let len = result.transactions.len(); + let to_process = if len > remaining as usize { + result + .transactions + .into_iter() + .take(remaining as usize) + .collect::>() + } else { + result.transactions + }; + + for t in &to_process { + transactions.push(RawTransaction { + hash: t.hash()?, + data: t.clone(), + }); + } + remaining = remaining.saturating_sub(len as u8); + + if AT_MOST > len || remaining == 0 { + break; + } + + if let Some(last) = transactions.last() { + if last.data.prev_trans_lt == 0 { + break; + } + } + } + + Ok(transactions) + } + + async fn get_transaction(&self, _: &UInt256) -> anyhow::Result> { + todo!() + } + + async fn get_dst_transaction(&self, _: &UInt256) -> anyhow::Result> { + todo!() + } + + async fn get_latest_key_block(&self) -> anyhow::Result { + todo!() + } + + async fn get_capabilities(&self, _: &dyn Clock) -> anyhow::Result { + todo!() + } + + async fn get_blockchain_config( + &self, + _: &dyn Clock, + _: bool, + ) -> anyhow::Result { + let latest_block = self.get_latest_block().await?; + let config = self + .get_config(latest_block.last.seqno, vec![8, 20, 21, 24, 25, 18, 31]) + .await?; + if let Some(config) = config.config { + let config = ton_block::ConfigParams::with_root(config.cell); + return BlockchainConfig::with_config(config, 0); + } + + anyhow::bail!("Failed to get blockchain config") + } +} + +#[cfg(test)] +pub mod tests { + use nekoton_utils::{unpack_std_smc_addr, SimpleClock}; + use reqwest::Url; + use std::sync::Arc; + + use crate::core::generic_contract::{GenericContract, GenericContractSubscriptionHandler}; + use crate::external::TonConnection; + use crate::models::{ContractState, PendingTransaction, Transaction, TransactionsBatchInfo}; + use crate::transport::ton::TonTransport; + use crate::transport::Transport; + + #[cfg_attr(not(feature = "non_threadsafe"), async_trait::async_trait)] + #[cfg_attr(feature = "non_threadsafe", async_trait::async_trait(?Send))] + impl TonConnection for reqwest::Client { + async fn send_get(&self, path: &str) -> anyhow::Result> { + let base = Url::parse("https://mainnet-v4.tonhubapi.com")?; + let path = base.join(path)?; + + let result = self + .get(path) + .header("ContentType", "application/json") + .send() + .await?; + + if matches!(result.status(), reqwest::StatusCode::NOT_FOUND) { + return Ok(None); + } + + let result = result.text().await?; + Ok(Some(result)) + } + + async fn send_post(&self, _: &str, _: &str) -> anyhow::Result> { + todo!() + } + } + + #[tokio::test] + async fn test_account_state() -> anyhow::Result<()> { + let client = reqwest::Client::new(); + let address = + unpack_std_smc_addr("EQCD39VS5jcptHL8vMjEXrzGaRcCVYto7HUn4bpAOg8xqB2N", true)?; + let transport = TonTransport::new(Arc::new(client)); + let state = transport.get_contract_state(&address).await?; + println!("{:?}", state); + Ok(()) + } + + #[tokio::test] + async fn test_get_transactions() -> anyhow::Result<()> { + let client = reqwest::Client::new(); + let address = + unpack_std_smc_addr("EQCo6VT63H1vKJTiUo6W4M8RrTURCyk5MdbosuL5auEqpz-C", true)?; + let transport = TonTransport::new(Arc::new(client)); + let transactions = transport + .get_transactions(&address, 27668319000001, u8::MAX) + .await?; + + let mut prev_tx_lt = transactions.first().unwrap().data.lt; + for i in &transactions { + assert_eq!(i.data.lt, prev_tx_lt); + prev_tx_lt = i.data.prev_trans_lt; + } + Ok(()) + } + + #[tokio::test] + async fn test_get_config() -> anyhow::Result<()> { + let client = reqwest::Client::new(); + let transport = TonTransport::new(Arc::new(client)); + let config = transport.get_blockchain_config(&SimpleClock, true).await?; + println!("{:?}", config.get_fwd_prices(true)); + println!("{:?}", config.get_fwd_prices(false)); + Ok(()) + } + + #[derive(Copy, Clone, Debug)] + pub struct SimpleHandler; + impl GenericContractSubscriptionHandler for SimpleHandler { + fn on_message_sent( + &self, + _: PendingTransaction, + _: Option, + ) { + println!("on_message_sent"); + } + + fn on_message_expired(&self, _: PendingTransaction) { + println!("on_message_expired"); + } + + fn on_state_changed(&self, _: ContractState) { + println!("on_state_changed"); + } + + fn on_transactions_found( + &self, + _: Vec, + _: TransactionsBatchInfo, + ) { + println!("on_transactions_found"); + } + } + + #[tokio::test] + async fn subscription_test() -> anyhow::Result<()> { + let client = reqwest::Client::new(); + let transport = TonTransport::new(Arc::new(client)); + let address = + unpack_std_smc_addr("UQBXJ9VgpXcBGYGDXYurquCsl3LV0bLmsIWuhv9VmIkxCm8q", true)?; + let sub = GenericContract::subscribe( + Arc::new(SimpleClock), + Arc::new(transport), + address, + Arc::new(SimpleHandler), + true, + ) + .await?; + let state = sub.contract_state(); + println!("{:?}", state); + Ok(()) + } +} diff --git a/src/transport/ton/models.rs b/src/transport/ton/models.rs new file mode 100644 index 000000000..fe9dfeee0 --- /dev/null +++ b/src/transport/ton/models.rs @@ -0,0 +1,189 @@ +use nekoton_utils::{ + serde_base64_address, serde_base64_uint256, serde_cell, serde_optional_base64_array, + serde_transaction_array, serde_u128, serde_u64, +}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use ton_types::{Cell, UInt256}; + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct LatestBlock { + pub last: BlockId, + pub init: Init, + #[serde(with = "serde_optional_base64_array")] + pub state_root_hash: Option<[u8; 32]>, + pub now: u32, +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct BlockId { + #[serde(with = "serde_base64_uint256")] + #[allow(unused)] + pub root_hash: UInt256, + #[serde(with = "serde_base64_uint256")] + pub file_hash: UInt256, + pub seqno: u32, + //#[serde(with = "serde_string_to_u64")] + pub shard: String, + pub workchain: i32, + #[serde(default)] + pub transactions: Vec, +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct Init { + #[serde(with = "serde_base64_uint256")] + #[allow(unused)] + pub root_hash: UInt256, + #[serde(with = "serde_base64_uint256")] + #[allow(unused)] + pub file_hash: UInt256, +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct FullBlock { + pub exist: bool, + pub block: Option, +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct ShardInfo { + pub shards: Vec, +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct Transaction { + #[serde(with = "serde_base64_address")] + #[allow(unused)] + pub account: ton_block::MsgAddressInt, + #[serde(with = "serde_base64_uint256")] + #[allow(unused)] + pub hash: UInt256, + #[serde(with = "serde_u64")] + #[allow(unused)] + pub lt: u64, +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct AccountStateResult { + pub account: AccountInfo, + #[allow(unused)] + pub block: BlockId, +} + +#[derive(Debug, Deserialize)] +#[serde(tag = "type")] +#[serde(rename_all = "camelCase")] +pub enum AccountState { + #[serde(rename = "active")] + Active { + #[serde(with = "serde_cell")] + code: Cell, + #[serde(with = "serde_cell")] + data: Cell, + }, + + #[serde(rename = "frozen")] + Frozen { + #[serde(rename = "state_hash", with = "serde_base64_uint256")] + state_init_hash: UInt256, + }, + + #[serde(rename = "uninit")] + Uninit, +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct AccountInfo { + pub state: AccountState, + pub balance: AccountBalance, + pub storage_stat: StorageStat, + #[serde(rename = "last")] + pub last_transaction: Option, +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct StorageStat { + pub last_paid: u32, + pub used: StorageUsed, + #[serde(default)] + pub due_payment: Option, +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct StorageUsed { + pub cells: u64, + pub bits: u64, + pub public_cells: u64, +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct AccountBalance { + #[serde(with = "serde_u128")] + pub coins: u128, + #[serde(default)] + pub currencies: Option>, +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct LastTransaction { + #[serde(with = "serde_base64_uint256")] + pub hash: UInt256, + #[serde(with = "serde_u64")] + pub lt: u64, +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct AccountChangedResult { + #[allow(unused)] + pub changed: bool, + #[allow(unused)] + pub block: BlockId, +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct ConfigResult { + #[allow(unused)] + pub exist: bool, + pub config: Option, +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct ConfigInfo { + #[serde(with = "serde_cell")] + pub cell: Cell, + #[allow(unused)] + #[serde(with = "serde_base64_address")] + pub address: ton_block::MsgAddressInt, + #[allow(unused)] + pub global_balance: AccountBalance, +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct AccountTransactionsResult { + #[allow(unused)] + pub blocks: Vec, + #[serde(rename = "boc", with = "serde_transaction_array")] + pub transactions: Vec, +} + +#[derive(Serialize, Debug)] +pub struct MessageBoc { + pub boc: String, +}