From c22c85838db55a41be6951c91ba3ac96f88b189e Mon Sep 17 00:00:00 2001 From: Alexey Pashinov Date: Mon, 18 Nov 2024 17:43:26 +0100 Subject: [PATCH 01/38] Add jetton contract --- Cargo.toml | 3 +- nekoton-contracts/Cargo.toml | 4 + nekoton-contracts/src/jetton/mod.rs | 199 +++++++++ .../src/jetton/root_token_contract.rs | 98 +++++ .../src/jetton/token_wallet_contract.rs | 58 +++ nekoton-contracts/src/lib.rs | 1 + nekoton-jetton/Cargo.toml | 27 ++ nekoton-jetton/src/dict.rs | 52 +++ nekoton-jetton/src/lib.rs | 5 + nekoton-jetton/src/meta.rs | 92 +++++ src/core/jetton_wallet/mod.rs | 388 ++++++++++++++++++ src/core/mod.rs | 1 + src/core/parsing.rs | 31 +- src/models.rs | 7 + 14 files changed, 964 insertions(+), 2 deletions(-) create mode 100644 nekoton-contracts/src/jetton/mod.rs create mode 100644 nekoton-contracts/src/jetton/root_token_contract.rs create mode 100644 nekoton-contracts/src/jetton/token_wallet_contract.rs create mode 100644 nekoton-jetton/Cargo.toml create mode 100644 nekoton-jetton/src/dict.rs create mode 100644 nekoton-jetton/src/lib.rs create mode 100644 nekoton-jetton/src/meta.rs create mode 100644 src/core/jetton_wallet/mod.rs diff --git a/Cargo.toml b/Cargo.toml index fe15af623..a246ea3fb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,7 @@ members = [ "nekoton-abi", "nekoton-contracts", "nekoton-derive", + "nekoton-jetton", "nekoton-proto", "nekoton-transport", "nekoton-utils", @@ -40,7 +41,7 @@ once_cell = "1.12.0" parking_lot = "0.12.0" pbkdf2 = { version = "0.12.2", optional = true } quick_cache = "0.4.1" -rand = { version = "0.8", features = ["getrandom"] , optional = true } +rand = { version = "0.8", features = ["getrandom"], optional = true } secstr = { version = "0.5.0", features = ["serde"], optional = true } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" diff --git a/nekoton-contracts/Cargo.toml b/nekoton-contracts/Cargo.toml index 0192d8d41..35de2e683 100644 --- a/nekoton-contracts/Cargo.toml +++ b/nekoton-contracts/Cargo.toml @@ -20,7 +20,11 @@ ton_types = { git = "https://github.com/broxus/ton-labs-types.git" } ton_abi = { git = "https://github.com/broxus/ton-labs-abi" } nekoton-abi = { path = "../nekoton-abi", features = ["derive"] } +nekoton-jetton = { path = "../nekoton-jetton" } nekoton-utils = { path = "../nekoton-utils" } +[dev-dependencies] +base64 = "0.13" + [features] web = ["ton_abi/web"] diff --git a/nekoton-contracts/src/jetton/mod.rs b/nekoton-contracts/src/jetton/mod.rs new file mode 100644 index 000000000..3a3441f42 --- /dev/null +++ b/nekoton-contracts/src/jetton/mod.rs @@ -0,0 +1,199 @@ +use nekoton_abi::num_bigint::BigUint; +use nekoton_abi::{ExecutionContext, StackItem}; +use thiserror::Error; +use ton_block::Serializable; +use ton_types::SliceData; + +pub use root_token_contract::JettonRootData; +pub use token_wallet_contract::JettonWalletData; + +mod root_token_contract; +mod token_wallet_contract; + +#[derive(Copy, Clone)] +pub struct RootTokenContract<'a>(pub ExecutionContext<'a>); + +pub const GET_JETTON_DATA: &str = "get_jetton_data"; +pub const GET_WALLET_DATA: &str = "get_wallet_data"; +pub const GET_WALLET_ADDRESS: &str = "get_wallet_address"; + +impl RootTokenContract<'_> { + pub fn name(&self) -> anyhow::Result { + let result = self.0.run_getter(GET_JETTON_DATA, &[])?; + + let data = root_token_contract::get_jetton_data(result)?; + data.content.name.ok_or(JettonDataError::NotExist.into()) + } + + pub fn symbol(&self) -> anyhow::Result { + let result = self.0.run_getter(GET_JETTON_DATA, &[])?; + + let data = root_token_contract::get_jetton_data(result)?; + data.content.symbol.ok_or(JettonDataError::NotExist.into()) + } + + pub fn decimals(&self) -> anyhow::Result { + let result = self.0.run_getter(GET_JETTON_DATA, &[])?; + + let data = root_token_contract::get_jetton_data(result)?; + data.content + .decimals + .ok_or(JettonDataError::NotExist.into()) + } + + pub fn total_supply(&self) -> anyhow::Result { + let result = self.0.run_getter(GET_JETTON_DATA, &[])?; + + let data = root_token_contract::get_jetton_data(result)?; + Ok(data.total_supply) + } + + pub fn wallet_code(&self) -> anyhow::Result { + let result = self.0.run_getter(GET_JETTON_DATA, &[])?; + + let data = root_token_contract::get_jetton_data(result)?; + Ok(data.wallet_code) + } + + pub fn get_wallet_address( + &self, + owner: &ton_block::MsgAddressInt, + ) -> anyhow::Result { + let arg = StackItem::Slice(SliceData::load_cell(owner.serialize()?)?); + let result = self.0.run_getter(GET_WALLET_ADDRESS, &[arg])?; + + let address = root_token_contract::get_wallet_address(result)?; + Ok(address) + } + + pub fn get_details(&self) -> anyhow::Result { + let result = self.0.run_getter(GET_JETTON_DATA, &[])?; + + let data = root_token_contract::get_jetton_data(result)?; + Ok(data) + } +} + +#[derive(Copy, Clone)] +pub struct TokenWalletContract<'a>(pub ExecutionContext<'a>); + +impl<'a> TokenWalletContract<'a> { + pub fn root(&self) -> anyhow::Result { + let result = self.0.run_getter(GET_WALLET_DATA, &[])?; + + let data = token_wallet_contract::get_wallet_data(result)?; + Ok(data.root_address) + } + + pub fn balance(&self) -> anyhow::Result { + let result = self.0.run_getter(GET_WALLET_DATA, &[])?; + + let data = token_wallet_contract::get_wallet_data(result)?; + Ok(data.balance) + } + + pub fn get_details(&self) -> anyhow::Result { + let result = self.0.run_getter(GET_WALLET_DATA, &[])?; + + let data = token_wallet_contract::get_wallet_data(result)?; + Ok(data) + } +} + +#[derive(Error, Debug)] +pub enum JettonDataError { + #[error("Not exist")] + NotExist, +} + +#[cfg(test)] +mod tests { + use std::str::FromStr; + + use nekoton_abi::num_bigint::BigUint; + use nekoton_abi::num_traits::FromPrimitive; + use nekoton_abi::ExecutionContext; + use nekoton_utils::SimpleClock; + use ton_block::MsgAddressInt; + + use crate::jetton; + + #[test] + fn root_token_contract() -> anyhow::Result<()> { + let cell = ton_types::deserialize_tree_of_cells(&mut base64::decode("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").unwrap().as_slice()).unwrap(); + let state = nekoton_utils::deserialize_account_stuff(cell)?; + + let contract = jetton::RootTokenContract(ExecutionContext { + clock: &SimpleClock, + account_stuff: &state, + }); + + let name = contract.name()?; + assert_eq!(name, "TonUP"); + + let symbol = contract.symbol()?; + assert_eq!(symbol, "UP"); + + let decimals = contract.decimals()?; + assert_eq!(decimals, 9); + + let total_supply = contract.total_supply()?; + assert_eq!(total_supply, BigUint::from_u128(56837335582855498).unwrap()); + + let wallet_code = contract.wallet_code()?; + let expected_code = ton_types::deserialize_tree_of_cells(&mut base64::decode("te6cckECEQEAAyMAART/APSkE/S88sgLAQIBYgIDAgLMBAUAG6D2BdqJofQB9IH0gahhAgHUBgcCASAICQDDCDHAJJfBOAB0NMDAXGwlRNfA/AM4PpA+kAx+gAxcdch+gAx+gAwc6m0AALTH4IQD4p+pVIgupUxNFnwCeCCEBeNRRlSILqWMUREA/AK4DWCEFlfB7y6k1nwC+BfBIQP8vCAAET6RDBwuvLhTYAIBIAoLAIPUAQa5D2omh9AH0gfSBqGAJpj8EIC8aijKkQXUEIPe7L7wndCVj5cWLpn5j9ABgJ0CgR5CgCfQEsZ4sA54tmZPaqQB8VA9M/+gD6QCHwAe1E0PoA+kD6QNQwUTahUirHBfLiwSjC//LiwlQ0QnBUIBNUFAPIUAT6AljPFgHPFszJIsjLARL0APQAywDJIPkAcHTIywLKB8v/ydAE+kD0BDH6ACDXScIA8uLEd4AYyMsFUAjPFnD6AhfLaxPMgMAgEgDQ4AnoIQF41FGcjLHxnLP1AH+gIizxZQBs8WJfoCUAPPFslQBcwjkXKRceJQCKgToIIJycOAoBS88uLFBMmAQPsAECPIUAT6AljPFgHPFszJ7VQC9ztRND6APpA+kDUMAjTP/oAUVGgBfpA+kBTW8cFVHNtcFQgE1QUA8hQBPoCWM8WAc8WzMkiyMsBEvQA9ADLAMn5AHB0yMsCygfL/8nQUA3HBRyx8uLDCvoAUaihggiYloBmtgihggiYloCgGKEnlxBJEDg3XwTjDSXXCwGAPEADXO1E0PoA+kD6QNQwB9M/+gD6QDBRUaFSSccF8uLBJ8L/8uLCBYIJMS0AoBa88uLDghB73ZfeyMsfFcs/UAP6AiLPFgHPFslxgBjIywUkzxZw+gLLaszJgED7AEATyFAE+gJYzxYBzxbMye1UgAHBSeaAYoYIQc2LQnMjLH1Iwyz9Y+gJQB88WUAfPFslxgBDIywUkzxZQBvoCFctqFMzJcfsAECQQIwB8wwAjwgCwjiGCENUydttwgBDIywVQCM8WUAT6AhbLahLLHxLLP8ly+wCTNWwh4gPIUAT6AljPFgHPFszJ7VSV6u3X")?.as_slice())?; + assert_eq!(wallet_code, expected_code); + + Ok(()) + } + + #[test] + fn wallet_token_contract() -> anyhow::Result<()> { + let cell = ton_types::deserialize_tree_of_cells(&mut base64::decode("te6ccgECEwEAA6kAAm6ADhM4pof2jbUfk8bYdFfLChAB6cUG0z98g+xOrlf4LCzkTQz7BmMVjoAAAVA6z3tMKgC1Y0MmAgEBj0dzWUAIAc+mHGAhdljL3SZo8QcvtpqlV+kMrO8+wlbsMF9hxLGVACvaf0JMrv6BOvPpAgee08uQM/uRpUd//fhA7Nm8twzvYAIBFP8A9KQT9LzyyAsDAgFiBQQAG6D2BdqJofQB9IH0gahhAgLMEAYCASAIBwCD1AEGuQ9qJofQB9IH0gahgCaY/BCAvGooypEF1BCD3uy+8J3QlY+XFi6Z+Y/QAYCdAoEeQoAn0BLGeLAOeLZmT2qkAgEgDgkCASALCgDXO1E0PoA+kD6QNQwB9M/+gD6QDBRUaFSSccF8uLBJ8L/8uLCBYIJMS0AoBa88uLDghB73ZfeyMsfFcs/UAP6AiLPFgHPFslxgBjIywUkzxZw+gLLaszJgED7AEATyFAE+gJYzxYBzxbMye1UgAvc7UTQ+gD6QPpA1DAI0z/6AFFRoAX6QPpAU1vHBVRzbXBUIBNUFAPIUAT6AljPFgHPFszJIsjLARL0APQAywDJ+QBwdMjLAsoHy//J0FANxwUcsfLiwwr6AFGooYIImJaAZrYIoYIImJaAoBihJ5cQSRA4N18E4w0l1wsBgDQwAfMMAI8IAsI4hghDVMnbbcIAQyMsFUAjPFlAE+gIWy2oSyx8Syz/JcvsAkzVsIeIDyFAE+gJYzxYBzxbMye1UAHBSeaAYoYIQc2LQnMjLH1Iwyz9Y+gJQB88WUAfPFslxgBDIywUkzxZQBvoCFctqFMzJcfsAECQQIwHxUD0z/6APpAIfAB7UTQ+gD6QPpA1DBRNqFSKscF8uLBKML/8uLCVDRCcFQgE1QUA8hQBPoCWM8WAc8WzMkiyMsBEvQA9ADLAMkg+QBwdMjLAsoHy//J0AT6QPQEMfoAINdJwgDy4sR3gBjIywVQCM8WcPoCF8trE8yA8AnoIQF41FGcjLHxnLP1AH+gIizxZQBs8WJfoCUAPPFslQBcwjkXKRceJQCKgToIIJycOAoBS88uLFBMmAQPsAECPIUAT6AljPFgHPFszJ7VQCAdQSEQARPpEMHC68uFNgAMMIMcAkl8E4AHQ0wMBcbCVE18D8Azg+kD6QDH6ADFx1yH6ADH6ADBzqbQAAtMfghAPin6lUiC6lTE0WfAJ4IIQF41FGVIgupYxREQD8ArgNYIQWV8HvLqTWfAL4F8EhA/y8IA==").unwrap().as_slice()).unwrap(); + let state = nekoton_utils::deserialize_account_stuff(cell)?; + + let contract = jetton::TokenWalletContract(ExecutionContext { + clock: &SimpleClock, + account_stuff: &state, + }); + + let balance = contract.balance()?; + assert_eq!(balance, BigUint::from_u32(2000000000).unwrap()); + + let root = contract.root()?; + assert_eq!( + root, + MsgAddressInt::from_str( + "0:af69fd0932bbfa04ebcfa4081e7b4f2e40cfee46951dfff7e103b366f2dc33bd" + )? + ); + + Ok(()) + } + + #[test] + fn wallet_address() -> anyhow::Result<()> { + let cell = ton_types::deserialize_tree_of_cells(&mut base64::decode("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").unwrap().as_slice()).unwrap(); + let state = nekoton_utils::deserialize_account_stuff(cell)?; + + let contract = jetton::RootTokenContract(ExecutionContext { + clock: &SimpleClock, + account_stuff: &state, + }); + + let owner = nekoton_utils::unpack_std_smc_addr( + "EQC-D0YPvNUq92FeG7_ZGFQY-L-lZ0wayn8arc4AKElbSo6v", + true, + )?; + + let expected = nekoton_utils::unpack_std_smc_addr( + "EQBWqBJJQriSjGTOBXPPSZjZoTnESO3RqPLrO6enXSq--yes", + true, + )?; + + let address = contract.get_wallet_address(&owner)?; + assert_eq!(address, expected); + + Ok(()) + } +} diff --git a/nekoton-contracts/src/jetton/root_token_contract.rs b/nekoton-contracts/src/jetton/root_token_contract.rs new file mode 100644 index 000000000..77f3c8971 --- /dev/null +++ b/nekoton-contracts/src/jetton/root_token_contract.rs @@ -0,0 +1,98 @@ +use anyhow::Result; +use nekoton_abi::num_bigint::BigUint; +use nekoton_abi::VmGetterOutput; +use nekoton_jetton::{JettonMetaData, MetaDataContent}; +use thiserror::Error; +use ton_block::{Deserializable, MsgAddressInt}; +use ton_types::Cell; + +#[derive(PartialEq, Eq, Debug, Clone)] +pub struct JettonRootData { + pub total_supply: BigUint, + pub mintable: bool, + pub admin_address: MsgAddressInt, + pub content: JettonMetaData, + pub wallet_code: Cell, +} + +pub fn get_jetton_data(res: VmGetterOutput) -> Result { + if !res.is_ok { + return Err(RootContractError::ExecutionFailed { + exit_code: res.exit_code, + } + .into()); + } + + const JETTON_DATA_STACK_ELEMENTS: usize = 5; + + let stack = res.stack; + if stack.len() != JETTON_DATA_STACK_ELEMENTS { + return Err(RootContractError::InvalidMethodResultStackSize { + actual: stack.len(), + expected: JETTON_DATA_STACK_ELEMENTS, + } + .into()); + } + + let total_supply = stack[0].as_integer()?.into(0..=u128::MAX)?; + + let mintable = stack[1].as_bool()?; + + let mut address_data = stack[2].as_slice()?.clone(); + let admin_address = MsgAddressInt::construct_from(&mut address_data)?; + + let content = stack[3].as_cell()?; + let content = MetaDataContent::parse(content)?; + + let wallet_code = stack[4].as_cell()?.clone(); + + let dict = match content { + MetaDataContent::Internal { dict } => dict, + MetaDataContent::Unsupported => { + return Err(RootContractError::UnsupportedContentType.into()) + } + }; + + Ok(JettonRootData { + total_supply: BigUint::from(total_supply), + mintable, + admin_address, + wallet_code, + content: (&dict).into(), + }) +} + +pub fn get_wallet_address(res: VmGetterOutput) -> Result { + if !res.is_ok { + return Err(RootContractError::ExecutionFailed { + exit_code: res.exit_code, + } + .into()); + } + + const WALLET_ADDRESS_STACK_ELEMENTS: usize = 1; + + let stack = res.stack; + if stack.len() != WALLET_ADDRESS_STACK_ELEMENTS { + return Err(RootContractError::InvalidMethodResultStackSize { + actual: stack.len(), + expected: WALLET_ADDRESS_STACK_ELEMENTS, + } + .into()); + } + + let mut address_data = stack[0].as_slice()?.clone(); + let address = MsgAddressInt::construct_from(&mut address_data)?; + + Ok(address) +} + +#[derive(Error, Debug)] +pub enum RootContractError { + #[error("ExecutionFailed (exit_code: {exit_code})")] + ExecutionFailed { exit_code: i32 }, + #[error("Invalid method result stack size (actual: {actual}, expected {expected})")] + InvalidMethodResultStackSize { actual: usize, expected: usize }, + #[error("Unsupported metadata content type")] + UnsupportedContentType, +} diff --git a/nekoton-contracts/src/jetton/token_wallet_contract.rs b/nekoton-contracts/src/jetton/token_wallet_contract.rs new file mode 100644 index 000000000..cc7918867 --- /dev/null +++ b/nekoton-contracts/src/jetton/token_wallet_contract.rs @@ -0,0 +1,58 @@ +use nekoton_abi::num_bigint::BigUint; +use nekoton_abi::VmGetterOutput; +use thiserror::Error; +use ton_block::{Deserializable, MsgAddressInt}; +use ton_types::Cell; + +#[derive(Debug, Clone)] +pub struct JettonWalletData { + pub balance: BigUint, + pub owner_address: MsgAddressInt, + pub root_address: MsgAddressInt, + pub wallet_code: Cell, +} + +pub fn get_wallet_data(res: VmGetterOutput) -> anyhow::Result { + if !res.is_ok { + return Err(WalletContractError::ExecutionFailed { + exit_code: res.exit_code, + } + .into()); + } + + const WALLET_DATA_STACK_ELEMENTS: usize = 4; + + let stack = res.stack; + if stack.len() == WALLET_DATA_STACK_ELEMENTS { + let balance = stack[0].as_integer()?.into(0..=u128::MAX)?; + + let mut address_data = stack[1].as_slice()?.clone(); + let owner_address = MsgAddressInt::construct_from(&mut address_data)?; + + let mut data = stack[2].as_slice()?.clone(); + let root_address = MsgAddressInt::construct_from(&mut data)?; + + let wallet_code = stack[3].as_cell()?.clone(); + + Ok(JettonWalletData { + balance: BigUint::from(balance), + owner_address, + root_address, + wallet_code, + }) + } else { + Err(WalletContractError::InvalidMethodResultStackSize { + actual: stack.len(), + expected: WALLET_DATA_STACK_ELEMENTS, + } + .into()) + } +} + +#[derive(Error, Debug)] +pub enum WalletContractError { + #[error("ExecutionFailed (exit_code: {exit_code})")] + ExecutionFailed { exit_code: i32 }, + #[error("Invalid method result stack size (actual: {actual}, expected {expected})")] + InvalidMethodResultStackSize { actual: usize, expected: usize }, +} diff --git a/nekoton-contracts/src/lib.rs b/nekoton-contracts/src/lib.rs index 1210886f2..18c681468 100644 --- a/nekoton-contracts/src/lib.rs +++ b/nekoton-contracts/src/lib.rs @@ -55,6 +55,7 @@ use anyhow::Result; use nekoton_abi::{ExecutionContext, ExecutionOutput}; pub mod dens; +pub mod jetton; pub mod old_tip3; pub mod tip1155; pub mod tip3; diff --git a/nekoton-jetton/Cargo.toml b/nekoton-jetton/Cargo.toml new file mode 100644 index 000000000..3a7060df0 --- /dev/null +++ b/nekoton-jetton/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "nekoton-jetton" +version = "0.13.0" +authors = [ + "Alexey Pashinov ", + "Vladimir Petrzhikovskiy ", + "Ivan Kalinin " +] +rust-version = "1.62.0" +edition = "2021" + +[dependencies] +anyhow = "1.0" +lazy_static = "1.5.0" +serde = { version = "1.0.183", features = ["derive"] } +sha2 = "0.10.8" + +ton_block = { git = "https://github.com/broxus/ton-labs-block.git" } +ton_types = { git = "https://github.com/broxus/ton-labs-types.git" } +ton_abi = { git = "https://github.com/broxus/ton-labs-abi" } + +nekoton-utils = { path = "../nekoton-utils" } +num-bigint = "0.4.6" +num-traits = "0.2.19" + +[features] +web = ["ton_abi/web"] diff --git a/nekoton-jetton/src/dict.rs b/nekoton-jetton/src/dict.rs new file mode 100644 index 000000000..71a250820 --- /dev/null +++ b/nekoton-jetton/src/dict.rs @@ -0,0 +1,52 @@ +use std::collections::HashMap; + +use ton_types::{HashmapType, SliceData, UInt256}; + +pub type SnakeFormatDict = HashMap>; + +pub fn load_dict_snake_format(slice: &mut SliceData) -> anyhow::Result { + let content_dict = slice.get_dictionary()?.reference_opt(0); + let content_map = ton_types::HashmapE::with_hashmap(32 * 8, content_dict); + + let mut dict = HashMap::with_capacity(content_map.len().unwrap()); + for item in content_map.iter() { + let (k, v) = item.unwrap(); + + // Load value + if let Some(cell) = v.reference_opt(0) { + let buffer = parse_snake_data(cell).unwrap(); + dict.insert(UInt256::from_slice(k.data()), buffer); + } + } + + Ok(dict) +} + +fn parse_snake_data(cell: ton_types::Cell) -> anyhow::Result> { + let mut buffer = vec![]; + + let mut cell = cell; + let mut first_cell = true; + loop { + let mut slice_data = SliceData::load_cell_ref(&cell)?; + if first_cell { + let first_byte = slice_data.get_next_byte()?; + + if first_byte != 0 { + anyhow::bail!("Invalid snake format") + } + } + + buffer.extend(slice_data.remaining_data().data()); + match slice_data.remaining_references() { + 0 => return Ok(buffer), + 1 => { + cell = cell.reference(0)?; + first_cell = false; + } + n => { + anyhow::bail!("Invalid snake format string: found cell with {n} references") + } + } + } +} diff --git a/nekoton-jetton/src/lib.rs b/nekoton-jetton/src/lib.rs new file mode 100644 index 000000000..5e948c47b --- /dev/null +++ b/nekoton-jetton/src/lib.rs @@ -0,0 +1,5 @@ +pub use self::dict::*; +pub use self::meta::*; + +mod dict; +mod meta; diff --git a/nekoton-jetton/src/meta.rs b/nekoton-jetton/src/meta.rs new file mode 100644 index 000000000..961273373 --- /dev/null +++ b/nekoton-jetton/src/meta.rs @@ -0,0 +1,92 @@ +use lazy_static::lazy_static; +use sha2::{Digest, Sha256}; +use ton_types::{SliceData, UInt256}; + +use crate::{load_dict_snake_format, SnakeFormatDict}; + +pub struct MetaDataField { + pub key: UInt256, +} + +impl MetaDataField { + fn new(name: &str) -> MetaDataField { + let key = Self::key_from_str(name); + MetaDataField { key } + } + + fn key_from_str(k: &str) -> UInt256 { + let mut hasher: Sha256 = Sha256::new(); + hasher.update(k); + let slice = &hasher.finalize()[..]; + UInt256::from_slice(slice) + } + + pub fn use_string_or(&self, src: Option, dict: &SnakeFormatDict) -> Option { + src.or(dict + .get(&self.key) + .cloned() + .and_then(|vec| String::from_utf8(vec).ok())) + } +} + +lazy_static! { + pub static ref META_NAME: MetaDataField = MetaDataField::new("name"); + pub static ref META_DESCRIPTION: MetaDataField = MetaDataField::new("description"); + pub static ref META_IMAGE: MetaDataField = MetaDataField::new("image"); + pub static ref META_SYMBOL: MetaDataField = MetaDataField::new("symbol"); + pub static ref META_IMAGE_DATA: MetaDataField = MetaDataField::new("image_data"); + pub static ref META_DECIMALS: MetaDataField = MetaDataField::new("decimals"); + pub static ref META_URI: MetaDataField = MetaDataField::new("uri"); + pub static ref META_CONTENT_URL: MetaDataField = MetaDataField::new("content_url"); + pub static ref META_ATTRIBUTES: MetaDataField = MetaDataField::new("attributes"); + pub static ref META_SOCIAL_LINKS: MetaDataField = MetaDataField::new("social_links"); + pub static ref META_MARKETPLACE: MetaDataField = MetaDataField::new("marketplace"); +} + +#[derive(PartialEq, Eq, Debug, Clone)] +pub enum MetaDataContent { + Internal { dict: SnakeFormatDict }, + Unsupported, +} + +impl MetaDataContent { + pub fn parse(cell: &ton_types::Cell) -> anyhow::Result { + let mut content = SliceData::load_cell_ref(cell)?; + + let content_representation = content.get_next_byte()?; + match content_representation { + 0 => { + let dict = load_dict_snake_format(&mut content)?; + Ok(MetaDataContent::Internal { dict }) + } + _ => Ok(MetaDataContent::Unsupported), + } + } +} + +#[derive(PartialEq, Eq, Debug, Clone)] +pub struct JettonMetaData { + pub name: Option, + pub uri: Option, + pub symbol: Option, + pub description: Option, + pub image: Option, + pub image_data: Option>, + pub decimals: Option, +} + +impl From<&SnakeFormatDict> for JettonMetaData { + fn from(dict: &SnakeFormatDict) -> Self { + JettonMetaData { + name: META_NAME.use_string_or(None, dict), + uri: META_URI.use_string_or(None, dict), + symbol: META_SYMBOL.use_string_or(None, dict), + description: META_DESCRIPTION.use_string_or(None, dict), + image: META_IMAGE.use_string_or(None, dict), + image_data: dict.get(&META_IMAGE_DATA.key).cloned(), + decimals: META_DECIMALS + .use_string_or(None, dict) + .map(|v| v.parse::().unwrap()), + } + } +} diff --git a/src/core/jetton_wallet/mod.rs b/src/core/jetton_wallet/mod.rs new file mode 100644 index 000000000..276b9c793 --- /dev/null +++ b/src/core/jetton_wallet/mod.rs @@ -0,0 +1,388 @@ +use std::convert::TryFrom; +use std::sync::Arc; + +use anyhow::Result; +use nekoton_abi::num_traits::ToPrimitive; +use nekoton_abi::*; +use nekoton_contracts::jetton::{JettonRootData, JettonWalletData}; +use nekoton_utils::*; +use num_bigint::{BigInt, BigUint, ToBigInt}; +use ton_block::{MsgAddressInt, Serializable}; +use ton_types::{BuilderData, IBitstring, SliceData}; + +use crate::core::models::*; +use crate::core::parsing::*; +use crate::transport::models::{RawContractState, RawTransaction}; +use crate::transport::Transport; + +use super::{ContractSubscription, InternalMessage}; + +pub const JETTON_TRANSFER_OPCODE: u32 = 0x0f8a7ea5; + +pub struct JettonWallet { + clock: Arc, + contract_subscription: ContractSubscription, + handler: Arc, + owner: MsgAddressInt, + balance: BigUint, +} + +impl JettonWallet { + pub async fn subscribe( + clock: Arc, + transport: Arc, + owner: MsgAddressInt, + root_token_contract: MsgAddressInt, + handler: Arc, + ) -> Result { + let state = match transport.get_contract_state(&root_token_contract).await? { + RawContractState::Exists(state) => state, + RawContractState::NotExists { .. } => { + return Err(JettonWalletError::InvalidRootTokenContract.into()) + } + }; + let state = nekoton_contracts::jetton::RootTokenContract(state.as_context(clock.as_ref())); + + let address = state.get_wallet_address(&owner)?; + + let mut balance = Default::default(); + let contract_subscription = ContractSubscription::subscribe( + clock.clone(), + transport, + address, + &mut make_contract_state_handler(clock.clone(), &mut balance), + Some(&mut make_transactions_handler(handler.as_ref())), + ) + .await?; + + handler.on_balance_changed(balance.clone()); + + Ok(Self { + clock, + contract_subscription, + handler, + owner, + balance, + }) + } + + pub fn contract_subscription(&self) -> &ContractSubscription { + &self.contract_subscription + } + + pub fn owner(&self) -> &MsgAddressInt { + &self.owner + } + + pub fn address(&self) -> &MsgAddressInt { + self.contract_subscription.address() + } + + pub fn balance(&self) -> &BigUint { + &self.balance + } + + pub fn contract_state(&self) -> &ContractState { + self.contract_subscription.contract_state() + } + + pub async fn estimate_min_attached_amount( + &self, + _destination: TransferRecipient, + _tokens: BigUint, + _notify_receiver: bool, + _payload: ton_types::Cell, + ) -> Result { + // TODO: estimate? + const ATTACHED_AMOUNT: u64 = 100_000_000; + Ok(ATTACHED_AMOUNT) + } + + pub fn prepare_transfer( + &self, + query_id: u64, + amount: BigUint, + destination: MsgAddressInt, + remaining_gas_to: MsgAddressInt, + custom_payload: Option, + callback_to: BigUint, + callback_payload: Option, + attached_amount: u64, + ) -> Result { + let mut builder = BuilderData::new(); + + // Opcode + builder.append_u32(JETTON_TRANSFER_OPCODE)?; + + // Query id + builder.append_u64(query_id)?; + + // Amount + let grams = + ton_block::Grams::new(amount.to_u128().ok_or(JettonWalletError::TryFromGrams)?)?; + grams.write_to(&mut builder)?; + + // Recipient + destination.write_to(&mut builder)?; + + // Response destination + remaining_gas_to.write_to(&mut builder)?; + + // Optional(TvmCell) + match custom_payload { + Some(payload) => { + builder.append_bit_one()?; + builder.checked_append_reference(payload)?; + } + None => { + builder.append_bit_zero()?; + } + } + + builder.checked_append_reference(Default::default())?; + + // Callback value + let grams = ton_block::Grams::new( + callback_to + .to_u128() + .ok_or(JettonWalletError::TryFromGrams)?, + )?; + grams.write_to(&mut builder)?; + + // Optional(TvmCell) + match callback_payload { + Some(payload) => { + builder.append_bit_one()?; + builder.checked_append_reference(payload)?; + } + None => { + builder.append_bit_zero()?; + } + } + + let body = builder.into_cell().map(SliceData::load_cell)??; + + Ok(InternalMessage { + source: Some(self.owner.clone()), + destination: self.address().clone(), + amount: attached_amount, + bounce: true, + body, + }) + } + + pub async fn refresh(&mut self) -> Result<()> { + let mut balance = self.balance.clone(); + + let handler = self.handler.as_ref(); + self.contract_subscription + .refresh( + &mut make_contract_state_handler(self.clock.clone(), &mut balance), + &mut make_transactions_handler(handler), + &mut |_, _| {}, + &mut |_| {}, + ) + .await?; + + if balance != self.balance { + self.balance = balance; + handler.on_balance_changed(self.balance.clone()); + } + + Ok(()) + } + + pub async fn handle_block(&mut self, block: &ton_block::Block) -> Result<()> { + let mut balance: BigInt = self.balance.clone().into(); + + let handler = self.handler.as_ref(); + self.contract_subscription.handle_block( + block, + &mut |transactions, batch_info| { + let transactions = transactions + .into_iter() + .filter_map(|transaction| { + let description = match transaction.data.description.read_struct().ok()? { + ton_block::TransactionDescr::Ordinary(description) => description, + _ => return None, + }; + + let data = parse_jetton_transaction(&transaction.data, &description); + + if let Some(data) = &data { + match data { + JettonWalletTransaction::Transfer(tokens) => { + balance -= tokens.clone().to_bigint().trust_me(); + } + } + } + + let transaction = + Transaction::try_from((transaction.hash, transaction.data)).ok()?; + + Some(TransactionWithData { transaction, data }) + }) + .collect(); + + handler.on_transactions_found(transactions, batch_info) + }, + &mut |_, _| {}, + &mut |_| {}, + )?; + + let balance = balance.to_biguint().unwrap_or_default(); + if balance != self.balance { + self.balance = balance; + handler.on_balance_changed(self.balance.clone()); + } + + Ok(()) + } + + pub async fn preload_transactions(&mut self, from_lt: u64) -> Result<()> { + let handler = self.handler.as_ref(); + self.contract_subscription + .preload_transactions(from_lt, &mut make_transactions_handler(handler)) + .await + } +} + +pub trait JettonWalletSubscriptionHandler: Send + Sync { + fn on_balance_changed(&self, balance: BigUint); + + /// Called every time new transactions are detected. + /// - When new block found + /// - When manually requesting the latest transactions (can be called several times) + /// - When preloading transactions + fn on_transactions_found( + &self, + transactions: Vec>, + batch_info: TransactionsBatchInfo, + ); +} + +pub async fn get_token_wallet_details( + clock: &dyn Clock, + transport: &dyn Transport, + token_wallet: &MsgAddressInt, +) -> Result<(JettonWalletData, JettonRootData)> { + let token_wallet_state = match transport.get_contract_state(token_wallet).await? { + RawContractState::Exists(state) => state, + RawContractState::NotExists { .. } => { + return Err(JettonWalletError::InvalidTokenWalletContract.into()) + } + }; + let token_wallet_state = + nekoton_contracts::jetton::TokenWalletContract(token_wallet_state.as_context(clock)); + + let token_wallet_details = token_wallet_state.get_details()?; + + let root_contract_state = match transport + .get_contract_state(&token_wallet_details.root_address) + .await? + { + RawContractState::Exists(state) => state, + RawContractState::NotExists { .. } => { + return Err(JettonWalletError::InvalidRootTokenContract.into()) + } + }; + let root_contract_details = + nekoton_contracts::jetton::RootTokenContract(root_contract_state.as_context(clock)) + .get_details()?; + + Ok((token_wallet_details, root_contract_details)) +} + +pub async fn get_token_root_details( + clock: &dyn Clock, + transport: &dyn Transport, + root_token_contract: &MsgAddressInt, +) -> Result { + let state = match transport.get_contract_state(root_token_contract).await? { + RawContractState::Exists(state) => state, + RawContractState::NotExists { .. } => { + return Err(JettonWalletError::InvalidRootTokenContract.into()) + } + }; + nekoton_contracts::jetton::RootTokenContract(state.as_context(clock)).get_details() +} + +pub async fn get_token_root_details_from_token_wallet( + clock: &dyn Clock, + transport: &dyn Transport, + token_wallet_address: &MsgAddressInt, +) -> Result<(MsgAddressInt, JettonRootData)> { + let state = match transport.get_contract_state(token_wallet_address).await? { + RawContractState::Exists(state) => state, + RawContractState::NotExists { .. } => { + return Err(JettonWalletError::WalletNotDeployed.into()) + } + }; + + let root_token_contract = + nekoton_contracts::jetton::TokenWalletContract(state.as_context(clock)).root()?; + + let state = match transport.get_contract_state(&root_token_contract).await? { + RawContractState::Exists(state) => state, + RawContractState::NotExists { .. } => { + return Err(JettonWalletError::InvalidRootTokenContract.into()) + } + }; + let state = nekoton_contracts::jetton::RootTokenContract(state.as_context(clock)); + let details = state.get_details()?; + + Ok((root_token_contract, details)) +} + +fn make_contract_state_handler( + clock: Arc, + balance: &'_ mut BigUint, +) -> impl FnMut(&RawContractState) + '_ { + move |contract_state| { + if let RawContractState::Exists(state) = contract_state { + if let Ok(new_balance) = + nekoton_contracts::jetton::TokenWalletContract(state.as_context(clock.as_ref())) + .balance() + { + *balance = new_balance; + } + } + } +} + +fn make_transactions_handler( + handler: &'_ dyn JettonWalletSubscriptionHandler, +) -> impl FnMut(Vec, TransactionsBatchInfo) + '_ { + move |transactions, batch_info| { + let transactions = transactions + .into_iter() + .filter_map( + |transaction| match transaction.data.description.read_struct().ok()? { + ton_block::TransactionDescr::Ordinary(description) => { + let data = parse_jetton_transaction(&transaction.data, &description); + + let transaction = + Transaction::try_from((transaction.hash, transaction.data)).ok()?; + + Some(TransactionWithData { transaction, data }) + } + _ => None, + }, + ) + .collect(); + + handler.on_transactions_found(transactions, batch_info) + } +} + +#[derive(thiserror::Error, Debug)] +enum JettonWalletError { + #[error("Invalid root token contract")] + InvalidRootTokenContract, + #[error("Invalid token wallet contract")] + InvalidTokenWalletContract, + #[error("Wallet not deployed")] + WalletNotDeployed, + #[error("Failed to convert grams")] + TryFromGrams, +} diff --git a/src/core/mod.rs b/src/core/mod.rs index 531fc761b..cdbe5094c 100644 --- a/src/core/mod.rs +++ b/src/core/mod.rs @@ -12,6 +12,7 @@ pub mod dens; pub mod generic_contract; pub mod keystore; pub use super::models; +pub mod jetton_wallet; pub mod nft_wallet; pub mod owners_cache; pub mod parsing; diff --git a/src/core/parsing.rs b/src/core/parsing.rs index 3af574531..c5cf10eae 100644 --- a/src/core/parsing.rs +++ b/src/core/parsing.rs @@ -3,13 +3,14 @@ use std::convert::TryFrom; use anyhow::Result; use num_bigint::BigUint; use once_cell::race::OnceBox; -use ton_block::MsgAddressInt; +use ton_block::{Deserializable, MsgAddressInt}; use ton_types::UInt256; use nekoton_abi::*; use nekoton_contracts::tip4_1::nft_contract; use nekoton_contracts::{old_tip3, tip3_1}; +use crate::core::jetton_wallet::JETTON_TRANSFER_OPCODE; use crate::core::models::*; use crate::core::ton_wallet::{MultisigType, WalletType}; @@ -587,6 +588,34 @@ pub fn parse_token_transaction( } } +pub fn parse_jetton_transaction( + tx: &ton_block::Transaction, + description: &ton_block::TransactionDescrOrdinary, +) -> Option { + if description.aborted { + return None; + } + + let in_msg = tx.in_msg.as_ref()?.read_struct().ok()?; + + let mut body = in_msg.body()?; + let opcode = body.get_next_u32().ok()?; + + if opcode == JETTON_TRANSFER_OPCODE { + // Skip query id + body.move_by(64).ok()?; + + let mut amount = ton_block::Grams::default(); + amount.read_from(&mut body).ok()?; + + return Some(JettonWalletTransaction::Transfer(BigUint::from( + amount.as_u128(), + ))); + } + + None +} + struct NftFunctions { transfer: &'static ton_abi::Function, change_owner: &'static ton_abi::Function, diff --git a/src/models.rs b/src/models.rs index 8f1a9205d..42a8fe11d 100644 --- a/src/models.rs +++ b/src/models.rs @@ -274,6 +274,13 @@ pub enum TokenWalletTransaction { SwapBackBounced(BigUint), } +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "snake_case", tag = "type", content = "data")] +pub enum JettonWalletTransaction { + #[serde(with = "serde_string")] + Transfer(BigUint), +} + #[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "snake_case", tag = "type", content = "data")] pub enum NftTransaction { From 652de484814dfeab7cbf8e9de1f4c4fae95020f9 Mon Sep 17 00:00:00 2001 From: Alexey Pashinov Date: Wed, 20 Nov 2024 15:54:42 +0100 Subject: [PATCH 02/38] TON walletV5 --- nekoton-contracts/src/wallets/code/mod.rs | 1 + .../src/wallets/code/wallet_v5r1_code.boc | Bin 0 -> 657 bytes src/core/jetton_wallet/mod.rs | 9 +- src/core/parsing.rs | 3 + src/core/ton_wallet/mod.rs | 27 ++ src/core/ton_wallet/wallet_v5r1.rs | 380 ++++++++++++++++++ 6 files changed, 415 insertions(+), 5 deletions(-) create mode 100644 nekoton-contracts/src/wallets/code/wallet_v5r1_code.boc create mode 100644 src/core/ton_wallet/wallet_v5r1.rs diff --git a/nekoton-contracts/src/wallets/code/mod.rs b/nekoton-contracts/src/wallets/code/mod.rs index 344c05b1a..c9b8df296 100644 --- a/nekoton-contracts/src/wallets/code/mod.rs +++ b/nekoton-contracts/src/wallets/code/mod.rs @@ -20,6 +20,7 @@ declare_tvc! { multisig2_1 => "./Multisig2_1.tvc" (MULTISIG2_1_CODE), surf_wallet => "./Surf.tvc" (SURF_WALLET_CODE), wallet_v3 => "./wallet_v3_code.boc" (WALLET_V3_CODE), + wallet_v5r1 => "./wallet_v5r1_code.boc" (WALLET_V5R1_CODE), highload_wallet_v2 => "./highload_wallet_v2_code.boc" (HIGHLOAD_WALLET_V2_CODE), ever_wallet => "./ever_wallet_code.boc" (EVER_WALLET_CODE), } diff --git a/nekoton-contracts/src/wallets/code/wallet_v5r1_code.boc b/nekoton-contracts/src/wallets/code/wallet_v5r1_code.boc new file mode 100644 index 0000000000000000000000000000000000000000..56c9a34f87d3b9fbc895cc06d4e864c1e0f20f76 GIT binary patch literal 657 zcmXw0Ur19?7(eH1b89Piox>WaJ6AJ&P`HB%0v{&%P~pQg17VO*M1PPqOrq|D`~gTkeoy^$qxP`Gh5(z`U9 z=(6a4?-t~_7-b^XdYM=VN`;nueW69_TvEb%u0@OEW+oc8A-hp}{b@jvwqijhMh7;U z^Z)?FNo-z`^k_zK1Ga5Hn38=2N9+#dd-kb+`bv;y$Fd`PD)K$A+J;mx(@_;TI$h{s zvTnESXk}ljZ+TTmWiKy4c5FFDk?&2H``VFVhqH9v{iFdCUW%*DDNOR0BMV%=H{ixU zNJB=Tqz&kUsAqYr+(R`D*^K@)lRW^|+Ji(uI{F^ORnBZ+QN zZY3f+Aw?&|Rg{z)F{`Kj)hD|_7}de`amvo9r0A`|rtQAV|%+GGmuze9; z;S&OvJC{i#tRj#-%NiVBszfI-1eKmlyYS+>8=94Q%F!e)1o8hJ0cqvmjt&_%>tmC1 z&kn!`0x^P Result { // TODO: estimate? - const ATTACHED_AMOUNT: u64 = 100_000_000; + const ATTACHED_AMOUNT: u64 = 100_000_000; // 0.1 TON Ok(ATTACHED_AMOUNT) } pub fn prepare_transfer( &self, - query_id: u64, amount: BigUint, destination: MsgAddressInt, remaining_gas_to: MsgAddressInt, custom_payload: Option, - callback_to: BigUint, + callback_value: BigUint, callback_payload: Option, attached_amount: u64, ) -> Result { @@ -115,7 +114,7 @@ impl JettonWallet { builder.append_u32(JETTON_TRANSFER_OPCODE)?; // Query id - builder.append_u64(query_id)?; + builder.append_u64(self.clock.now_ms_u64())?; // Amount let grams = @@ -143,7 +142,7 @@ impl JettonWallet { // Callback value let grams = ton_block::Grams::new( - callback_to + callback_value .to_u128() .ok_or(JettonWalletError::TryFromGrams)?, )?; diff --git a/src/core/parsing.rs b/src/core/parsing.rs index c5cf10eae..31f67d1ac 100644 --- a/src/core/parsing.rs +++ b/src/core/parsing.rs @@ -122,6 +122,9 @@ pub fn parse_transaction_additional_info( WalletInteractionMethod::Multisig(Box::new(method)), ) } + WalletType::WalletV5R1 => { + todo!() + } }; return Some(TransactionAdditionalInfo::WalletInteraction( diff --git a/src/core/ton_wallet/mod.rs b/src/core/ton_wallet/mod.rs index 7cae9926d..60430b5aa 100644 --- a/src/core/ton_wallet/mod.rs +++ b/src/core/ton_wallet/mod.rs @@ -30,6 +30,7 @@ pub mod ever_wallet; pub mod highload_wallet_v2; pub mod multisig; pub mod wallet_v3; +pub mod wallet_v5r1; pub const DEFAULT_WORKCHAIN: i8 = 0; @@ -228,6 +229,12 @@ impl TonWallet { self.workchain(), expiration, ), + WalletType::WalletV5R1 => wallet_v5r1::prepare_deploy( + self.clock.as_ref(), + &self.public_key, + self.workchain(), + expiration, + ), WalletType::EverWallet => ever_wallet::prepare_deploy( self.clock.as_ref(), &self.public_key, @@ -318,6 +325,14 @@ impl TonWallet { vec![gift], expiration, ), + WalletType::WalletV5R1 => wallet_v5r1::prepare_transfer( + self.clock.as_ref(), + public_key, + current_state, + 0, + vec![gift], + expiration, + ), WalletType::EverWallet => ever_wallet::prepare_transfer( self.clock.as_ref(), public_key, @@ -635,6 +650,10 @@ pub fn extract_wallet_init_data(contract: &ExistingContract) -> Result<(PublicKe } else if wallet_v3::is_wallet_v3(&code_hash) { let public_key = PublicKey::from_bytes(wallet_v3::InitData::try_from(data)?.public_key())?; Ok((public_key, WalletType::WalletV3)) + } else if wallet_v5r1::is_wallet_v5r1(&code_hash) { + let public_key = + PublicKey::from_bytes(wallet_v5r1::InitData::try_from(data)?.public_key())?; + Ok((public_key, WalletType::WalletV5R1)) } else if ever_wallet::is_ever_wallet(&code_hash) { let public_key = extract_public_key(&contract.account)?; Ok((public_key, WalletType::EverWallet)) @@ -868,6 +887,7 @@ pub enum TransferAction { pub enum WalletType { Multisig(MultisigType), WalletV3, + WalletV5R1, HighloadWalletV2, EverWallet, } @@ -877,6 +897,7 @@ impl WalletType { match self { Self::Multisig(multisig_type) => multisig::ton_wallet_details(*multisig_type), Self::WalletV3 => wallet_v3::DETAILS, + Self::WalletV5R1 => wallet_v5r1::DETAILS, Self::HighloadWalletV2 => highload_wallet_v2::DETAILS, Self::EverWallet => ever_wallet::DETAILS, } @@ -895,6 +916,7 @@ impl WalletType { match self { Self::Multisig(multisig_type) => multisig_type.code_hash(), Self::WalletV3 => wallet_v3::CODE_HASH, + Self::WalletV5R1 => wallet_v5r1::CODE_HASH, Self::HighloadWalletV2 => highload_wallet_v2::CODE_HASH, Self::EverWallet => ever_wallet::CODE_HASH, } @@ -905,6 +927,7 @@ impl WalletType { match self { Self::Multisig(multisig_type) => multisig_type.code(), Self::WalletV3 => wallets::code::wallet_v3(), + Self::WalletV5R1 => wallets::code::wallet_v5r1(), Self::HighloadWalletV2 => wallets::code::highload_wallet_v2(), Self::EverWallet => wallets::code::ever_wallet(), } @@ -917,6 +940,7 @@ impl FromStr for WalletType { fn from_str(s: &str) -> Result { Ok(match s { "WalletV3" => Self::WalletV3, + "WalletV5R1" => Self::WalletV5R1, "HighloadWalletV2" => Self::HighloadWalletV2, "EverWallet" => Self::EverWallet, s => Self::Multisig(MultisigType::from_str(s)?), @@ -938,6 +962,7 @@ impl TryInto for WalletType { WalletType::Multisig(MultisigType::SurfWallet) => 6, WalletType::Multisig(MultisigType::Multisig2) => 7, WalletType::Multisig(MultisigType::Multisig2_1) => 8, + WalletType::WalletV5R1 => 9, _ => anyhow::bail!("Unimplemented wallet type"), }; @@ -950,6 +975,7 @@ impl std::fmt::Display for WalletType { match self { Self::Multisig(multisig_type) => multisig_type.fmt(f), Self::WalletV3 => f.write_str("WalletV3"), + Self::WalletV5R1 => f.write_str("WalletV5R1"), Self::HighloadWalletV2 => f.write_str("HighloadWalletV2"), Self::EverWallet => f.write_str("EverWallet"), } @@ -966,6 +992,7 @@ pub fn compute_address( multisig::compute_contract_address(public_key, multisig_type, workchain_id) } WalletType::WalletV3 => wallet_v3::compute_contract_address(public_key, workchain_id), + WalletType::WalletV5R1 => wallet_v5r1::compute_contract_address(public_key, workchain_id), WalletType::EverWallet => ever_wallet::compute_contract_address(public_key, workchain_id), WalletType::HighloadWalletV2 => { highload_wallet_v2::compute_contract_address(public_key, workchain_id) diff --git a/src/core/ton_wallet/wallet_v5r1.rs b/src/core/ton_wallet/wallet_v5r1.rs new file mode 100644 index 000000000..8680a0f3b --- /dev/null +++ b/src/core/ton_wallet/wallet_v5r1.rs @@ -0,0 +1,380 @@ +use std::convert::TryFrom; + +use anyhow::Result; +use ed25519_dalek::PublicKey; +use ton_block::{MsgAddrStd, MsgAddressInt, Serializable}; +use ton_types::{BuilderData, Cell, IBitstring, SliceData, UInt256}; + +use nekoton_utils::*; + +use super::{Gift, TonWalletDetails, TransferAction}; +use crate::core::models::{Expiration, ExpireAt}; +use crate::crypto::{SignedMessage, UnsignedMessage}; + +const SIGNED_EXTERNAL_PREFIX: u32 = 0x7369676E; +const ACTION_SEND_MSG_PREFIX: u32 = 0x0ec3c86d; + +pub fn prepare_deploy( + clock: &dyn Clock, + public_key: &PublicKey, + workchain: i8, + expiration: Expiration, +) -> Result> { + let init_data = InitData::from_key(public_key); + let dst = compute_contract_address(public_key, workchain); + let mut message = + ton_block::Message::with_ext_in_header(ton_block::ExternalInboundMessageHeader { + dst, + ..Default::default() + }); + + message.set_state_init(init_data.make_state_init()?); + + let expire_at = ExpireAt::new(clock, expiration); + let (hash, payload) = init_data.make_transfer_payload(None, expire_at.timestamp)?; + + Ok(Box::new(UnsignedWalletV5 { + init_data, + gifts: Vec::new(), + payload, + message, + expire_at, + hash, + })) +} + +pub fn prepare_transfer( + clock: &dyn Clock, + public_key: &PublicKey, + current_state: &ton_block::AccountStuff, + seqno_offset: u32, + gifts: Vec, + expiration: Expiration, +) -> Result { + if gifts.len() > MAX_MESSAGES { + return Err(WalletV5Error::TooManyGifts.into()); + } + + let (mut init_data, with_state_init) = match ¤t_state.storage.state { + ton_block::AccountState::AccountActive { state_init, .. } => match &state_init.data { + Some(data) => (InitData::try_from(data)?, false), + None => return Err(WalletV5Error::InvalidInitData.into()), + }, + ton_block::AccountState::AccountFrozen { .. } => { + return Err(WalletV5Error::AccountIsFrozen.into()) + } + ton_block::AccountState::AccountUninit => (InitData::from_key(public_key), true), + }; + + init_data.seqno += seqno_offset; + + let mut message = + ton_block::Message::with_ext_in_header(ton_block::ExternalInboundMessageHeader { + dst: current_state.addr.clone(), + ..Default::default() + }); + + if with_state_init { + message.set_state_init(init_data.make_state_init()?); + } + + let expire_at = ExpireAt::new(clock, expiration); + let (hash, payload) = init_data.make_transfer_payload(gifts.clone(), expire_at.timestamp)?; + + Ok(TransferAction::Sign(Box::new(UnsignedWalletV5 { + init_data, + gifts, + payload, + hash, + expire_at, + message, + }))) +} + +#[derive(Clone)] +struct UnsignedWalletV5 { + init_data: InitData, + gifts: Vec, + payload: BuilderData, + hash: UInt256, + expire_at: ExpireAt, + message: ton_block::Message, +} + +impl UnsignedMessage for UnsignedWalletV5 { + fn refresh_timeout(&mut self, clock: &dyn Clock) { + if !self.expire_at.refresh(clock) { + return; + } + + let (hash, payload) = self + .init_data + .make_transfer_payload(self.gifts.clone(), self.expire_at()) + .trust_me(); + self.hash = hash; + self.payload = payload; + } + + fn expire_at(&self) -> u32 { + self.expire_at.timestamp + } + + fn hash(&self) -> &[u8] { + self.hash.as_slice() + } + + fn sign(&self, signature: &[u8; ed25519_dalek::SIGNATURE_LENGTH]) -> Result { + let mut payload = self.payload.clone(); + payload.append_raw(signature, signature.len() * 8)?; + + let mut message = self.message.clone(); + message.set_body(SliceData::load_builder(payload)?); + + Ok(SignedMessage { + message, + expire_at: self.expire_at(), + }) + } + + fn sign_with_pruned_payload( + &self, + signature: &[u8; ed25519_dalek::SIGNATURE_LENGTH], + prune_after_depth: u16, + ) -> Result { + let mut payload = self.payload.clone(); + payload.append_raw(signature, signature.len() * 8)?; + let body = payload.into_cell()?; + + let mut message = self.message.clone(); + message.set_body(prune_deep_cells(&body, prune_after_depth)?); + + Ok(SignedMessage { + message, + expire_at: self.expire_at(), + }) + } +} + +pub static CODE_HASH: &[u8; 32] = &[ + 0x20, 0x83, 0x4b, 0x7b, 0x72, 0xb1, 0x12, 0x14, 0x7e, 0x1b, 0x2f, 0xb4, 0x57, 0xb8, 0x4e, 0x74, + 0xd1, 0xa3, 0x0f, 0x04, 0xf7, 0x37, 0xd4, 0xf6, 0x2a, 0x66, 0x8e, 0x95, 0x52, 0xd2, 0xb7, 0x2f, +]; + +pub fn is_wallet_v5r1(code_hash: &UInt256) -> bool { + code_hash.as_slice() == CODE_HASH +} + +pub fn compute_contract_address(public_key: &PublicKey, workchain_id: i8) -> MsgAddressInt { + InitData::from_key(public_key) + .with_is_signature_allowed(true) + .with_wallet_id(WALLET_ID) + .compute_addr(workchain_id) + .trust_me() +} + +pub static DETAILS: TonWalletDetails = TonWalletDetails { + requires_separate_deploy: false, + min_amount: 1, // 0.000000001 TON + max_messages: MAX_MESSAGES, + supports_payload: true, + supports_state_init: true, + supports_multiple_owners: false, + supports_code_update: false, + expiration_time: 0, + required_confirmations: None, +}; + +const MAX_MESSAGES: usize = 250; + +/// `WalletV5` init data +#[derive(Clone)] +pub struct InitData { + pub is_signature_allowed: bool, + pub seqno: u32, + pub wallet_id: u32, + pub public_key: UInt256, + pub extensions: Option, +} + +impl InitData { + pub fn public_key(&self) -> &[u8; 32] { + self.public_key.as_slice() + } + + pub fn from_key(key: &PublicKey) -> Self { + Self { + is_signature_allowed: false, + seqno: 0, + wallet_id: 0, + public_key: key.as_bytes().into(), + extensions: Default::default(), + } + } + + pub fn with_is_signature_allowed(mut self, is_allowed: bool) -> Self { + self.is_signature_allowed = is_allowed; + self + } + + pub fn with_wallet_id(mut self, id: u32) -> Self { + self.wallet_id = id; + self + } + + pub fn compute_addr(&self, workchain_id: i8) -> Result { + let init_state = self.make_state_init()?.serialize()?; + let hash = init_state.repr_hash(); + Ok(MsgAddressInt::AddrStd(MsgAddrStd { + anycast: None, + workchain_id, + address: hash.into(), + })) + } + + pub fn make_state_init(&self) -> Result { + Ok(ton_block::StateInit { + code: Some(nekoton_contracts::wallets::code::wallet_v5r1()), + data: Some(self.serialize()?), + ..Default::default() + }) + } + + pub fn serialize(&self) -> Result { + let mut data = BuilderData::new(); + data.append_bit_bool(self.is_signature_allowed)? + .append_u32(self.seqno)? + .append_u32(self.wallet_id)? + .append_raw(self.public_key.as_slice(), 256)?; + + if let Some(extensions) = &self.extensions { + data.append_bit_one()? + .checked_append_reference(extensions.clone())?; + } else { + data.append_bit_zero()?; + } + + data.into_cell() + } + + pub fn make_transfer_payload( + &self, + gifts: impl IntoIterator, + expire_at: u32, + ) -> Result<(UInt256, BuilderData)> { + // Check if signatures are allowed + if !self.is_signature_allowed { + return if self.extensions.is_none() { + Err(WalletV5Error::WalletLocked.into()) + } else { + Err(WalletV5Error::SignaturesDisabled.into()) + }; + } + + let mut payload = BuilderData::new(); + + // insert prefix + payload + .append_u32(SIGNED_EXTERNAL_PREFIX)? + .append_u32(self.wallet_id)? + .append_u32(expire_at)? + .append_u32(self.seqno)?; + + // create internal message + for gift in gifts { + let mut internal_message = + ton_block::Message::with_int_header(ton_block::InternalMessageHeader { + ihr_disabled: true, + bounce: gift.bounce, + dst: gift.destination, + value: gift.amount.into(), + ..Default::default() + }); + + if let Some(body) = gift.body { + internal_message.set_body(body); + } + + if let Some(state_init) = gift.state_init { + internal_message.set_state_init(state_init); + } + + let action = ton_block::OutAction::SendMsg { + mode: gift.flags, + out_msg: internal_message, + }; + + payload + .append_u32(ACTION_SEND_MSG_PREFIX)? + .checked_append_reference(action.serialize()?)?; + } + + let hash = payload.clone().into_cell()?.repr_hash(); + + Ok((hash, payload)) + } +} + +impl TryFrom<&Cell> for InitData { + type Error = anyhow::Error; + + fn try_from(data: &Cell) -> Result { + let mut cs = SliceData::load_cell_ref(data)?; + Ok(Self { + is_signature_allowed: cs.get_next_bit()?, + seqno: cs.get_next_u32()?, + wallet_id: cs.get_next_u32()?, + public_key: UInt256::from_be_bytes(&cs.get_next_bytes(32)?), + extensions: cs.get_next_dictionary()?, + }) + } +} + +const WALLET_ID: u32 = 0x7FFFFF11; + +#[derive(thiserror::Error, Debug)] +enum WalletV5Error { + #[error("Invalid init data")] + InvalidInitData, + #[error("Account is frozen")] + AccountIsFrozen, + #[error("Too many outgoing messages")] + TooManyGifts, + #[error("Signatures are disabled")] + SignaturesDisabled, + #[error("Wallet locked")] + WalletLocked, +} + +#[cfg(test)] +mod tests { + use ed25519_dalek::PublicKey; + use ton_block::AccountState; + + use crate::core::ton_wallet::wallet_v5r1::{compute_contract_address, InitData, WALLET_ID}; + + #[test] + fn state_init() -> anyhow::Result<()> { + let cell = ton_types::deserialize_tree_of_cells(&mut base64::decode("te6ccgECFgEAAucAAm6ADZRqTnEksRaYvpXRMbgzB92SzFv/19WbfQQgdDo7lYwEWQnKBnPzD1AAAXPmjwdAEj9i9OgmAgEAUYAAAAG///+IyIPTKTihvw1MFdzCAl7NQWIaeY9xhjENsss4FdrN+FAgART/APSkE/S88sgLAwIBIAYEAQLyBQEeINcLH4IQc2lnbrry4Ip/EQIBSBAHAgEgCQgAGb5fD2omhAgKDrkPoCwCASANCgIBSAwLABGyYvtRNDXCgCAAF7Ml+1E0HHXIdcLH4AIBbg8OABmvHfaiaEAQ65DrhY/AABmtznaiaEAg65Drhf/AAtzQINdJwSCRW49jINcLHyCCEGV4dG69IYIQc2ludL2wkl8D4IIQZXh0brqOtIAg1yEB0HTXIfpAMPpE+Cj6RDBYvZFb4O1E0IEBQdch9AWDB/QOb6ExkTDhgEDXIXB/2zzgMSDXSYECgLmRMOBw4hIRAeaO8O2i7fshgwjXIgKDCNcjIIAg1yHTH9Mf0x/tRNDSANMfINMf0//XCgAK+QFAzPkQmiiUXwrbMeHywIffArNQB7Dy0IRRJbry4IVQNrry4Ib4I7vy0IgikvgA3gGkf8jKAMsfAc8Wye1UIJL4D95w2zzYEgP27aLt+wL0BCFukmwhjkwCIdc5MHCUIccAs44tAdcoIHYeQ2wg10nACPLgkyDXSsAC8uCTINcdBscSwgBSMLDy0InXTNc5MAGk6GwShAe78uCT10rAAPLgk+1V4tIAAcAAkVvg69csCBQgkXCWAdcsCBwS4lIQseMPINdKFRQTABCTW9sx4ddM0AByMNcsCCSOLSHy4JLSAO1E0NIAURO68tCPVFAwkTGcAYEBQNch1woA8uCO4sjKAFjPFsntVJPywI3iAJYB+kAB+kT4KPpEMFi68uCR7UTQgQFB1xj0BQSdf8jKAEAEgwf0U/Lgi44UA4MH9Fvy4Iwi1woAIW4Bs7Dy0JDiyFADzxYS9ADJ7VQ=").unwrap().as_slice()).unwrap(); + let state = nekoton_utils::deserialize_account_stuff(cell)?; + + 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_eq!( + init_data.public_key.to_hex_string(), + "9107a65271437e1a982bb98404bd9a82c434f31ee30c621b6596702bb59bf0a0" + ); + assert_eq!(init_data.wallet_id, WALLET_ID); + assert_eq!(init_data.extensions, None); + + let public_key = PublicKey::from_bytes(init_data.public_key.as_slice())?; + let address = compute_contract_address(&public_key, 0); + assert_eq!( + address.to_string(), + "0:6ca35273892588b4c5f4ae898dc1983eec9662dffebeacdbe82103a1d1dcac60" + ); + } + + Ok(()) + } +} From a69067eb919d8334bd53fbf562926c71755a9e1d Mon Sep 17 00:00:00 2001 From: Alexey Pashinov Date: Thu, 21 Nov 2024 21:25:23 +0100 Subject: [PATCH 03/38] Parse WalletV5 transaction --- src/core/parsing.rs | 50 ++++++++++++++++++++++++++++++++++++++++++--- src/models.rs | 10 +++++++++ 2 files changed, 57 insertions(+), 3 deletions(-) diff --git a/src/core/parsing.rs b/src/core/parsing.rs index 31f67d1ac..e4abfd95b 100644 --- a/src/core/parsing.rs +++ b/src/core/parsing.rs @@ -65,6 +65,30 @@ pub fn parse_payload(payload: ton_types::SliceData) -> Option { None } +pub fn parse_payload_wallet_v5r1(payload: ton_types::SliceData) -> Option { + let mut payload = payload; + + let opcode = payload.get_next_u32().ok()?; + if opcode != JETTON_TRANSFER_OPCODE { + return None; + } + + let _query_id = payload.get_next_u64().ok()?; + + let mut amount = ton_block::Grams::default(); + amount.read_from(&mut payload).ok()?; + + let mut dst_addr = MsgAddressInt::default(); + dst_addr.read_from(&mut payload).ok()?; + + Some(KnownPayload::JettonOutgoingTransfer( + JettonOutgoingTransfer { + to: dst_addr, + tokens: BigUint::from(amount.as_u128()), + }, + )) +} + pub fn parse_transaction_additional_info( tx: &ton_block::Transaction, wallet_type: WalletType, @@ -97,6 +121,29 @@ pub fn parse_transaction_additional_info( WalletInteractionMethod::WalletV3Transfer, ) } + WalletType::WalletV5R1 => { + let mut out_msg = None; + tx.out_msgs + .iterate(|item| { + out_msg = Some(item.0); + Ok(false) + }) + .ok()?; + + let out_msg = out_msg?; + let recipient = match out_msg.header() { + ton_block::CommonMsgInfo::IntMsgInfo(header) => &header.dst, + _ => return None, + }; + + let known_payload = out_msg.body().and_then(parse_payload_wallet_v5r1); + + ( + Some(recipient.clone()), + known_payload, + WalletInteractionMethod::WalletV5R1Transfer, + ) + } WalletType::Multisig(multisig_type) => { let method = parse_multisig_transaction_impl(multisig_type, in_msg, tx)?; let (recipient, known_payload) = match &method { @@ -122,9 +169,6 @@ pub fn parse_transaction_additional_info( WalletInteractionMethod::Multisig(Box::new(method)), ) } - WalletType::WalletV5R1 => { - todo!() - } }; return Some(TransactionAdditionalInfo::WalletInteraction( diff --git a/src/models.rs b/src/models.rs index 42a8fe11d..c4bab0a92 100644 --- a/src/models.rs +++ b/src/models.rs @@ -53,12 +53,14 @@ pub enum KnownPayload { Comment(String), TokenOutgoingTransfer(TokenOutgoingTransfer), TokenSwapBack(TokenSwapBack), + JettonOutgoingTransfer(JettonOutgoingTransfer), } #[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "snake_case", tag = "type", content = "data")] pub enum WalletInteractionMethod { WalletV3Transfer, + WalletV5R1Transfer, Multisig(Box), } @@ -309,6 +311,14 @@ pub struct TokenOutgoingTransfer { pub payload: ton_types::Cell, } +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct JettonOutgoingTransfer { + #[serde(with = "serde_string")] + pub to: MsgAddressInt, + #[serde(with = "serde_string")] + pub tokens: BigUint, +} + #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct IncomingNftTransfer { From 4f580fe679ebb6b82a27e0876757c813cc6ea9f5 Mon Sep 17 00:00:00 2001 From: Alexey Pashinov Date: Fri, 22 Nov 2024 12:06:07 +0100 Subject: [PATCH 04/38] Fix building WalletV5 transaction --- nekoton-contracts/src/jetton/mod.rs | 2 +- src/core/jetton_wallet/mod.rs | 18 +++++++++--------- src/core/ton_wallet/wallet_v5r1.rs | 25 ++++++++++++++++++------- 3 files changed, 28 insertions(+), 17 deletions(-) diff --git a/nekoton-contracts/src/jetton/mod.rs b/nekoton-contracts/src/jetton/mod.rs index 3a3441f42..f06367a9c 100644 --- a/nekoton-contracts/src/jetton/mod.rs +++ b/nekoton-contracts/src/jetton/mod.rs @@ -158,7 +158,7 @@ mod tests { }); let balance = contract.balance()?; - assert_eq!(balance, BigUint::from_u32(2000000000).unwrap()); + assert_eq!(balance, BigUint::from_u128(2000000000).unwrap()); let root = contract.root()?; assert_eq!( diff --git a/src/core/jetton_wallet/mod.rs b/src/core/jetton_wallet/mod.rs index 924a98b95..f3325257a 100644 --- a/src/core/jetton_wallet/mod.rs +++ b/src/core/jetton_wallet/mod.rs @@ -148,15 +148,15 @@ impl JettonWallet { )?; grams.write_to(&mut builder)?; - // Optional(TvmCell) - match callback_payload { - Some(payload) => { - builder.append_bit_one()?; - builder.checked_append_reference(payload)?; - } - None => { - builder.append_bit_zero()?; - } + let callback_payload = callback_payload.unwrap_or_default(); + if callback_payload.bit_length() < builder.bits_free() { + builder.append_bit_zero()?; + + let callback_builder = BuilderData::from_cell(&callback_payload); + builder.append_builder(&callback_builder)?; + } else { + builder.append_bit_one()?; + builder.checked_append_reference(callback_payload)?; } let body = builder.into_cell().map(SliceData::load_cell)??; diff --git a/src/core/ton_wallet/wallet_v5r1.rs b/src/core/ton_wallet/wallet_v5r1.rs index 8680a0f3b..d970b09b5 100644 --- a/src/core/ton_wallet/wallet_v5r1.rs +++ b/src/core/ton_wallet/wallet_v5r1.rs @@ -12,7 +12,6 @@ use crate::core::models::{Expiration, ExpireAt}; use crate::crypto::{SignedMessage, UnsignedMessage}; const SIGNED_EXTERNAL_PREFIX: u32 = 0x7369676E; -const ACTION_SEND_MSG_PREFIX: u32 = 0x0ec3c86d; pub fn prepare_deploy( clock: &dyn Clock, @@ -20,7 +19,9 @@ pub fn prepare_deploy( workchain: i8, expiration: Expiration, ) -> Result> { - let init_data = InitData::from_key(public_key); + let init_data = InitData::from_key(public_key) + .with_wallet_id(WALLET_ID) + .with_is_signature_allowed(true); let dst = compute_contract_address(public_key, workchain); let mut message = ton_block::Message::with_ext_in_header(ton_block::ExternalInboundMessageHeader { @@ -63,7 +64,12 @@ pub fn prepare_transfer( ton_block::AccountState::AccountFrozen { .. } => { return Err(WalletV5Error::AccountIsFrozen.into()) } - ton_block::AccountState::AccountUninit => (InitData::from_key(public_key), true), + ton_block::AccountState::AccountUninit => ( + InitData::from_key(public_key) + .with_wallet_id(WALLET_ID) + .with_is_signature_allowed(true), + true, + ), }; init_data.seqno += seqno_offset; @@ -279,7 +285,8 @@ impl InitData { .append_u32(expire_at)? .append_u32(self.seqno)?; - // create internal message + let mut actions = ton_block::OutActions::new(); + for gift in gifts { let mut internal_message = ton_block::Message::with_int_header(ton_block::InternalMessageHeader { @@ -303,11 +310,15 @@ impl InitData { out_msg: internal_message, }; - payload - .append_u32(ACTION_SEND_MSG_PREFIX)? - .checked_append_reference(action.serialize()?)?; + actions.push_back(action); } + payload.append_bit_one()?; + payload.checked_append_reference(actions.serialize()?)?; + + // has_other_actions + payload.append_bit_zero()?; + let hash = payload.clone().into_cell()?.repr_hash(); Ok((hash, payload)) From e973e88ec0f0ea30870935ff39d85a9d0ac21871 Mon Sep 17 00:00:00 2001 From: Alexey Pashinov Date: Fri, 22 Nov 2024 23:23:27 +0100 Subject: [PATCH 05/38] Fix jetton token wallet --- src/core/jetton_wallet/mod.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/core/jetton_wallet/mod.rs b/src/core/jetton_wallet/mod.rs index f3325257a..a586fff9e 100644 --- a/src/core/jetton_wallet/mod.rs +++ b/src/core/jetton_wallet/mod.rs @@ -138,8 +138,6 @@ impl JettonWallet { } } - builder.checked_append_reference(Default::default())?; - // Callback value let grams = ton_block::Grams::new( callback_value From 9dcf842651fffceaad5329144b3e5d862a529d92 Mon Sep 17 00:00:00 2001 From: Alexey Pashinov Date: Sat, 23 Nov 2024 19:13:06 +0100 Subject: [PATCH 06/38] Parse incoming Jetton transactions --- src/core/jetton_wallet/mod.rs | 4 +++ src/core/parsing.rs | 52 +++++++++++++++++++++++------------ src/models.rs | 2 ++ 3 files changed, 40 insertions(+), 18 deletions(-) diff --git a/src/core/jetton_wallet/mod.rs b/src/core/jetton_wallet/mod.rs index a586fff9e..08bae4e4c 100644 --- a/src/core/jetton_wallet/mod.rs +++ b/src/core/jetton_wallet/mod.rs @@ -18,6 +18,7 @@ use crate::transport::Transport; use super::{ContractSubscription, InternalMessage}; pub const JETTON_TRANSFER_OPCODE: u32 = 0x0f8a7ea5; +pub const JETTON_NOTIFY_OPCODE: u32 = 0x7362d09c; pub struct JettonWallet { clock: Arc, @@ -211,6 +212,9 @@ impl JettonWallet { JettonWalletTransaction::Transfer(tokens) => { balance -= tokens.clone().to_bigint().trust_me(); } + JettonWalletTransaction::Notify(tokens) => { + balance += tokens.clone().to_bigint().trust_me(); + } } } diff --git a/src/core/parsing.rs b/src/core/parsing.rs index e4abfd95b..fb9d0f83a 100644 --- a/src/core/parsing.rs +++ b/src/core/parsing.rs @@ -10,7 +10,7 @@ use nekoton_abi::*; use nekoton_contracts::tip4_1::nft_contract; use nekoton_contracts::{old_tip3, tip3_1}; -use crate::core::jetton_wallet::JETTON_TRANSFER_OPCODE; +use crate::core::jetton_wallet::{JETTON_NOTIFY_OPCODE, JETTON_TRANSFER_OPCODE}; use crate::core::models::*; use crate::core::ton_wallet::{MultisigType, WalletType}; @@ -637,30 +637,33 @@ pub fn parse_token_transaction( pub fn parse_jetton_transaction( tx: &ton_block::Transaction, - description: &ton_block::TransactionDescrOrdinary, + _description: &ton_block::TransactionDescrOrdinary, ) -> Option { - if description.aborted { - return None; - } + // if description.aborted { + // return None; + // } let in_msg = tx.in_msg.as_ref()?.read_struct().ok()?; - let mut body = in_msg.body()?; let opcode = body.get_next_u32().ok()?; - if opcode == JETTON_TRANSFER_OPCODE { - // Skip query id - body.move_by(64).ok()?; + if opcode != JETTON_TRANSFER_OPCODE && opcode != JETTON_NOTIFY_OPCODE { + return None; + } - let mut amount = ton_block::Grams::default(); - amount.read_from(&mut body).ok()?; + // Skip query id + body.move_by(64).ok()?; - return Some(JettonWalletTransaction::Transfer(BigUint::from( - amount.as_u128(), - ))); - } + let mut grams = ton_block::Grams::default(); + grams.read_from(&mut body).ok()?; - None + let amount = BigUint::from(grams.as_u128()); + + match opcode { + JETTON_TRANSFER_OPCODE => Some(JettonWalletTransaction::Transfer(amount)), + JETTON_NOTIFY_OPCODE => Some(JettonWalletTransaction::Notify(amount)), + _ => None, + } } struct NftFunctions { @@ -955,11 +958,12 @@ impl TryFrom for IncomingChangeOwner { mod tests { use std::str::FromStr; - use ton_block::{Deserializable, Transaction, TransactionDescrOrdinary}; - use super::*; use crate::core::ton_wallet::MultisigType; + use nekoton_abi::num_traits::ToPrimitive; + use ton_block::{Deserializable, Transaction, TransactionDescrOrdinary}; + fn parse_transaction(data: &str) -> (Transaction, TransactionDescrOrdinary) { let tx = Transaction::construct_from_base64(data).unwrap(); let description = match tx.description.read_struct().unwrap() { @@ -1091,4 +1095,16 @@ mod tests { TokenWalletTransaction::TransferBounced(_) )); } + + #[test] + fn test_jetton_tokens_incoming_transfer() { + let (tx, description) = parse_transaction("te6ccgECBgEAAUEAA7F5WehEBzKkG99WEqFRkzi98nCWmpXQJBUBmsu+62sSSaAAAujDMA9YPbDkegBLk39wTvj+oYkQn/oiUxWp8d52BTFTfDVpe66gAALouYkW/BZ0Hn3gAAAoSAMCAQARDFCJAT9rXEEgAIJyP0XywcegmfnHNRlyzBk1fCDnCH7vhvlGb9ZcIR/c1sVcySdx1adc9HfHMJmGQfZqbrhnFk2erivzrCxhpNhIwQEBoAQBsUgBCW497xW4vg6Jw7WmJUrLC2JRQDOPb6GH1xJsclEw3tkAJWehEBzKkG99WEqFRkzi98nCWmpXQJBUBmsu+62sSSaQE/a1xAYMRbIAAF0YZgHrBM6Dz7zABQBmc2LQnAAAAZNZcVp9UXSHboAIANlGpOcSSxFpi+ldExuDMH3ZLMW//X1Zt9BCB0OjuVjA"); + + let wallet_transaction = parse_jetton_transaction(&tx, &description); + assert!(wallet_transaction.is_some()); + + if let Some(JettonWalletTransaction::Notify(amount)) = wallet_transaction { + assert_eq!(amount.to_u128().unwrap(), 100000000000); + } + } } diff --git a/src/models.rs b/src/models.rs index c4bab0a92..cb8d31604 100644 --- a/src/models.rs +++ b/src/models.rs @@ -281,6 +281,8 @@ pub enum TokenWalletTransaction { pub enum JettonWalletTransaction { #[serde(with = "serde_string")] Transfer(BigUint), + #[serde(with = "serde_string")] + Notify(BigUint), } #[derive(Clone, Debug, Serialize, Deserialize)] From 85371cacaf7a52c72a1faa1c048beb436b338072 Mon Sep 17 00:00:00 2001 From: Alexey Pashinov Date: Tue, 26 Nov 2024 20:47:03 +0100 Subject: [PATCH 07/38] Update library cell in contract states --- nekoton-contracts/src/jetton/mod.rs | 113 ++++++++++++++---- .../wallets/code/jetton_wallet_governed.boc | Bin 0 -> 1023 bytes nekoton-contracts/src/wallets/code/mod.rs | 21 +++- src/core/contract_subscription/mod.rs | 6 +- src/core/generic_contract/mod.rs | 2 +- src/core/jetton_wallet/mod.rs | 42 ++++--- src/core/nft_wallet/mod.rs | 2 +- src/core/parsing.rs | 24 +++- src/core/token_wallet/mod.rs | 2 +- src/core/ton_wallet/mod.rs | 2 +- src/models.rs | 14 ++- 11 files changed, 172 insertions(+), 56 deletions(-) create mode 100644 nekoton-contracts/src/wallets/code/jetton_wallet_governed.boc diff --git a/nekoton-contracts/src/jetton/mod.rs b/nekoton-contracts/src/jetton/mod.rs index f06367a9c..34772ec4c 100644 --- a/nekoton-contracts/src/jetton/mod.rs +++ b/nekoton-contracts/src/jetton/mod.rs @@ -1,12 +1,13 @@ use nekoton_abi::num_bigint::BigUint; use nekoton_abi::{ExecutionContext, StackItem}; -use thiserror::Error; -use ton_block::Serializable; -use ton_types::SliceData; +use ton_block::{AccountState, Deserializable, Serializable}; +use ton_types::{CellType, SliceData, UInt256}; pub use root_token_contract::JettonRootData; pub use token_wallet_contract::JettonWalletData; +use crate::wallets; + mod root_token_contract; mod token_wallet_contract; @@ -18,27 +19,25 @@ pub const GET_WALLET_DATA: &str = "get_wallet_data"; pub const GET_WALLET_ADDRESS: &str = "get_wallet_address"; impl RootTokenContract<'_> { - pub fn name(&self) -> anyhow::Result { + pub fn name(&self) -> anyhow::Result> { let result = self.0.run_getter(GET_JETTON_DATA, &[])?; let data = root_token_contract::get_jetton_data(result)?; - data.content.name.ok_or(JettonDataError::NotExist.into()) + Ok(data.content.name) } - pub fn symbol(&self) -> anyhow::Result { + pub fn symbol(&self) -> anyhow::Result> { let result = self.0.run_getter(GET_JETTON_DATA, &[])?; let data = root_token_contract::get_jetton_data(result)?; - data.content.symbol.ok_or(JettonDataError::NotExist.into()) + Ok(data.content.symbol) } - pub fn decimals(&self) -> anyhow::Result { + pub fn decimals(&self) -> anyhow::Result> { let result = self.0.run_getter(GET_JETTON_DATA, &[])?; let data = root_token_contract::get_jetton_data(result)?; - data.content - .decimals - .ok_or(JettonDataError::NotExist.into()) + Ok(data.content.decimals) } pub fn total_supply(&self) -> anyhow::Result { @@ -100,10 +99,28 @@ impl<'a> TokenWalletContract<'a> { } } -#[derive(Error, Debug)] -pub enum JettonDataError { - #[error("Not exist")] - NotExist, +pub fn update_library_cell(state: &mut AccountState) -> anyhow::Result<()> { + if let AccountState::AccountActive { ref mut state_init } = state { + if let Some(cell) = &state_init.code { + if cell.cell_type() == CellType::LibraryReference { + let mut slice_data = SliceData::load_cell(cell.clone())?; + + // Read Library Cell Tag + let tag = slice_data.get_next_byte()?; + assert_eq!(tag, 2); + + // Read Code Hash + let mut hash = UInt256::default(); + hash.read_from(&mut slice_data)?; + + if let Some(cell) = wallets::code::get_jetton_library_cell(&hash) { + state_init.set_code(cell.clone()); + } + } + } + } + + Ok(()) } #[cfg(test)] @@ -111,15 +128,33 @@ mod tests { use std::str::FromStr; use nekoton_abi::num_bigint::BigUint; - use nekoton_abi::num_traits::FromPrimitive; + use nekoton_abi::num_traits::{FromPrimitive, ToPrimitive}; use nekoton_abi::ExecutionContext; use nekoton_utils::SimpleClock; - use ton_block::MsgAddressInt; + use ton_block::{AccountState, Deserializable, MsgAddressInt}; + use ton_types::{CellType, SliceData, UInt256}; use crate::jetton; + use crate::wallets; + + #[test] + fn usdt_root_token_contract() -> anyhow::Result<()> { + let cell = ton_types::deserialize_tree_of_cells(&mut base64::decode("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").unwrap().as_slice()).unwrap(); + let state = nekoton_utils::deserialize_account_stuff(cell)?; + + let contract = jetton::RootTokenContract(ExecutionContext { + clock: &SimpleClock, + account_stuff: &state, + }); + + let total_supply = contract.total_supply()?; + assert_eq!(total_supply, BigUint::from_u128(1229976002510000).unwrap()); + + Ok(()) + } #[test] - fn root_token_contract() -> anyhow::Result<()> { + fn tonup_root_token_contract() -> anyhow::Result<()> { let cell = ton_types::deserialize_tree_of_cells(&mut base64::decode("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").unwrap().as_slice()).unwrap(); let state = nekoton_utils::deserialize_account_stuff(cell)?; @@ -129,13 +164,13 @@ mod tests { }); let name = contract.name()?; - assert_eq!(name, "TonUP"); + assert_eq!(name.unwrap(), "TonUP"); let symbol = contract.symbol()?; - assert_eq!(symbol, "UP"); + assert_eq!(symbol.unwrap(), "UP"); let decimals = contract.decimals()?; - assert_eq!(decimals, 9); + assert_eq!(decimals.unwrap(), 9); let total_supply = contract.total_supply()?; assert_eq!(total_supply, BigUint::from_u128(56837335582855498).unwrap()); @@ -148,7 +183,41 @@ mod tests { } #[test] - fn wallet_token_contract() -> anyhow::Result<()> { + fn usdt_wallet_token_contract() -> anyhow::Result<()> { + let cell = ton_types::deserialize_tree_of_cells(&mut base64::decode("te6ccgEBAwEAqAACbIAXsqVXAuRG6+GFp/25WVl2IsmatSkX0jbrXVjoBOwsnEQNAdiGdFv5kAABdRDp2cQZrn10JgIBAJEFJFfQYxaABHulQdJwYfnHP5r0FXhq3wjit36+D+zzx7bkE76OQgrwAsROplLUCShZxn2kTkyjrdZWWw4ol9ZAosUb+zcNiHf6CEICj0Utek39dAZraCNlF3JZ7QVzRDW+drX9S9XYryt8PWg=").unwrap().as_slice()).unwrap(); + let mut state = nekoton_utils::deserialize_account_stuff(cell)?; + + if let AccountState::AccountActive { state_init } = &mut state.storage.state { + if let Some(cell) = &state_init.code { + if cell.cell_type() == CellType::LibraryReference { + let mut slice_data = SliceData::load_cell(cell.clone())?; + + let tag = slice_data.get_next_byte()?; + assert_eq!(tag, 2); + + let mut hash = UInt256::default(); + hash.read_from(&mut slice_data)?; + + if let Some(cell) = wallets::code::get_jetton_library_cell(&hash) { + state_init.set_code(cell.clone()); + } + } + } + } + + let contract = jetton::TokenWalletContract(ExecutionContext { + clock: &SimpleClock, + account_stuff: &state, + }); + + let balance = contract.balance()?; + assert_eq!(balance.to_u128().unwrap(), 156092097302); + + Ok(()) + } + + #[test] + fn tonup_wallet_token_contract() -> anyhow::Result<()> { let cell = ton_types::deserialize_tree_of_cells(&mut base64::decode("te6ccgECEwEAA6kAAm6ADhM4pof2jbUfk8bYdFfLChAB6cUG0z98g+xOrlf4LCzkTQz7BmMVjoAAAVA6z3tMKgC1Y0MmAgEBj0dzWUAIAc+mHGAhdljL3SZo8QcvtpqlV+kMrO8+wlbsMF9hxLGVACvaf0JMrv6BOvPpAgee08uQM/uRpUd//fhA7Nm8twzvYAIBFP8A9KQT9LzyyAsDAgFiBQQAG6D2BdqJofQB9IH0gahhAgLMEAYCASAIBwCD1AEGuQ9qJofQB9IH0gahgCaY/BCAvGooypEF1BCD3uy+8J3QlY+XFi6Z+Y/QAYCdAoEeQoAn0BLGeLAOeLZmT2qkAgEgDgkCASALCgDXO1E0PoA+kD6QNQwB9M/+gD6QDBRUaFSSccF8uLBJ8L/8uLCBYIJMS0AoBa88uLDghB73ZfeyMsfFcs/UAP6AiLPFgHPFslxgBjIywUkzxZw+gLLaszJgED7AEATyFAE+gJYzxYBzxbMye1UgAvc7UTQ+gD6QPpA1DAI0z/6AFFRoAX6QPpAU1vHBVRzbXBUIBNUFAPIUAT6AljPFgHPFszJIsjLARL0APQAywDJ+QBwdMjLAsoHy//J0FANxwUcsfLiwwr6AFGooYIImJaAZrYIoYIImJaAoBihJ5cQSRA4N18E4w0l1wsBgDQwAfMMAI8IAsI4hghDVMnbbcIAQyMsFUAjPFlAE+gIWy2oSyx8Syz/JcvsAkzVsIeIDyFAE+gJYzxYBzxbMye1UAHBSeaAYoYIQc2LQnMjLH1Iwyz9Y+gJQB88WUAfPFslxgBDIywUkzxZQBvoCFctqFMzJcfsAECQQIwHxUD0z/6APpAIfAB7UTQ+gD6QPpA1DBRNqFSKscF8uLBKML/8uLCVDRCcFQgE1QUA8hQBPoCWM8WAc8WzMkiyMsBEvQA9ADLAMkg+QBwdMjLAsoHy//J0AT6QPQEMfoAINdJwgDy4sR3gBjIywVQCM8WcPoCF8trE8yA8AnoIQF41FGcjLHxnLP1AH+gIizxZQBs8WJfoCUAPPFslQBcwjkXKRceJQCKgToIIJycOAoBS88uLFBMmAQPsAECPIUAT6AljPFgHPFszJ7VQCAdQSEQARPpEMHC68uFNgAMMIMcAkl8E4AHQ0wMBcbCVE18D8Azg+kD6QDH6ADFx1yH6ADH6ADBzqbQAAtMfghAPin6lUiC6lTE0WfAJ4IIQF41FGVIgupYxREQD8ArgNYIQWV8HvLqTWfAL4F8EhA/y8IA==").unwrap().as_slice()).unwrap(); let state = nekoton_utils::deserialize_account_stuff(cell)?; diff --git a/nekoton-contracts/src/wallets/code/jetton_wallet_governed.boc b/nekoton-contracts/src/wallets/code/jetton_wallet_governed.boc new file mode 100644 index 0000000000000000000000000000000000000000..002baf68e67ba2a8a64040e876abdac5d6d7f0bc GIT binary patch literal 1023 zcmaiyT}V@57{}k|J?HE=KXSRWPw<>vGb(b^=!>9JD5Mdb*@D@PQqhH(oXtq6h;6K> zsMRVcLU8s0-Bc5+iy^eS+13%9j#yC1vg;xayEwf<_8zsE-UNpi-iP!4pa1jwKd*ia z_`c#wK+FU}Ymg0Wuoc_!Z)SjXbM2-!({p|c@8U1;4`IbDDm=CpMzvHKU^s>utY;vT zWxyK?AFy^Zejz7*Z>9x;h(x3bHd_m!N-=0zd81`@90x7uzucROR}E_gzmyXRa#Ten zE0aSV`+BI@#bI-jvw33)Bo1rO+BYXTxpY{|x4No_gm<&r3w@J&!!5<# z#agQuTM<(#SPQ7CfK-$gej(*dO2zMabvB)0iDbAFOgOWCVY+ar@hCO5jZWLq$&bdv zAt%s#BX#{_a(gx&Zg7Axb(WbZHrM3ZpX9WBo7A+pIpxqprLw;2>+#Q_O6_Nf8SV?Y zGV~yKFaeN&m^K`7OsD3%gIX(>g1T2OA-HZ1q@7@yr#w^sRiJV*J6@v@EFJHc8THiI zSh{5gk6wxqU&K4D?-F|N1Z?#YVOs3H>kMq{i@>y8_tYBg3owrqeNPlC`u3_Q(36%} zbI@Ww34TT25gpZHtY4l%o~7QILh+1szE}clr}_DmT>GLFACV5nlN@D#BL83Zi_pvE zNp3C8ugy%5c)OGRR$9|^KoUj{qp_V3_r $source:literal ($const_bytes:ident)),*$(,)?) => {$( @@ -23,8 +24,26 @@ declare_tvc! { wallet_v5r1 => "./wallet_v5r1_code.boc" (WALLET_V5R1_CODE), highload_wallet_v2 => "./highload_wallet_v2_code.boc" (HIGHLOAD_WALLET_V2_CODE), ever_wallet => "./ever_wallet_code.boc" (EVER_WALLET_CODE), + jetton_wallet_governed => "./jetton_wallet_governed.boc" (JETTON_WALLET_GOVERNED), } fn load(mut data: &[u8]) -> Cell { ton_types::deserialize_tree_of_cells(&mut data).expect("Trust me") } + +static JETTON_LIBRARY_CELLS: once_cell::sync::Lazy> = + once_cell::sync::Lazy::new(|| { + let mut m = HashMap::new(); + + let codes = [jetton_wallet_governed()]; + + for code in codes { + m.insert(code.repr_hash(), code); + } + + m + }); + +pub fn get_jetton_library_cell(hash: &UInt256) -> Option<&'static Cell> { + JETTON_LIBRARY_CELLS.get(hash) +} diff --git a/src/core/contract_subscription/mod.rs b/src/core/contract_subscription/mod.rs index 8f1de0620..484b3737a 100644 --- a/src/core/contract_subscription/mod.rs +++ b/src/core/contract_subscription/mod.rs @@ -387,7 +387,7 @@ impl ContractSubscription { prev_trans_lt: Option, on_contract_state: OnContractState<'_>, ) -> Result { - let contract_state = match prev_trans_lt { + let mut contract_state = match prev_trans_lt { Some(last_lt) => { let poll = self .transport @@ -412,7 +412,7 @@ impl ContractSubscription { }; if updated { - on_contract_state(&contract_state); + on_contract_state(&mut contract_state); self.contract_state = new_contract_state; self.transactions_synced = false; } else { @@ -459,7 +459,7 @@ impl ContractSubscription { } } -type OnContractState<'a> = &'a mut (dyn FnMut(&RawContractState) + Send + Sync); +type OnContractState<'a> = &'a mut (dyn FnMut(&mut RawContractState) + Send + Sync); type OnTransactionsFound<'a> = &'a mut (dyn FnMut(Vec, TransactionsBatchInfo) + Send + Sync); type OnMessageSent<'a> = &'a mut (dyn FnMut(PendingTransaction, RawTransaction) + Send + Sync); diff --git a/src/core/generic_contract/mod.rs b/src/core/generic_contract/mod.rs index 5936949e7..f2fc0ee67 100644 --- a/src/core/generic_contract/mod.rs +++ b/src/core/generic_contract/mod.rs @@ -141,7 +141,7 @@ impl GenericContract { fn make_contract_state_handler( handler: &dyn GenericContractSubscriptionHandler, -) -> impl FnMut(&RawContractState) + '_ { +) -> impl FnMut(&mut RawContractState) + '_ { move |contract_state| handler.on_state_changed(contract_state.brief()) } diff --git a/src/core/jetton_wallet/mod.rs b/src/core/jetton_wallet/mod.rs index 08bae4e4c..5eb6ae22e 100644 --- a/src/core/jetton_wallet/mod.rs +++ b/src/core/jetton_wallet/mod.rs @@ -1,6 +1,10 @@ use std::convert::TryFrom; use std::sync::Arc; +use crate::core::models::*; +use crate::core::parsing::*; +use crate::transport::models::{RawContractState, RawTransaction}; +use crate::transport::Transport; use anyhow::Result; use nekoton_abi::num_traits::ToPrimitive; use nekoton_abi::*; @@ -10,11 +14,6 @@ use num_bigint::{BigInt, BigUint, ToBigInt}; use ton_block::{MsgAddressInt, Serializable}; use ton_types::{BuilderData, IBitstring, SliceData}; -use crate::core::models::*; -use crate::core::parsing::*; -use crate::transport::models::{RawContractState, RawTransaction}; -use crate::transport::Transport; - use super::{ContractSubscription, InternalMessage}; pub const JETTON_TRANSFER_OPCODE: u32 = 0x0f8a7ea5; @@ -209,11 +208,11 @@ impl JettonWallet { if let Some(data) = &data { match data { - JettonWalletTransaction::Transfer(tokens) => { - balance -= tokens.clone().to_bigint().trust_me(); + JettonWalletTransaction::Transfer(transfer) => { + balance -= transfer.tokens.clone().to_bigint().trust_me(); } - JettonWalletTransaction::Notify(tokens) => { - balance += tokens.clone().to_bigint().trust_me(); + JettonWalletTransaction::Notify(transfer) => { + balance += transfer.tokens.clone().to_bigint().trust_me(); } } } @@ -267,12 +266,15 @@ pub async fn get_token_wallet_details( transport: &dyn Transport, token_wallet: &MsgAddressInt, ) -> Result<(JettonWalletData, JettonRootData)> { - let token_wallet_state = match transport.get_contract_state(token_wallet).await? { + let mut token_wallet_state = match transport.get_contract_state(token_wallet).await? { RawContractState::Exists(state) => state, RawContractState::NotExists { .. } => { return Err(JettonWalletError::InvalidTokenWalletContract.into()) } }; + + nekoton_contracts::jetton::update_library_cell(&mut token_wallet_state.account.storage.state)?; + let token_wallet_state = nekoton_contracts::jetton::TokenWalletContract(token_wallet_state.as_context(clock)); @@ -313,13 +315,15 @@ pub async fn get_token_root_details_from_token_wallet( transport: &dyn Transport, token_wallet_address: &MsgAddressInt, ) -> Result<(MsgAddressInt, JettonRootData)> { - let state = match transport.get_contract_state(token_wallet_address).await? { + let mut state = match transport.get_contract_state(token_wallet_address).await? { RawContractState::Exists(state) => state, RawContractState::NotExists { .. } => { return Err(JettonWalletError::WalletNotDeployed.into()) } }; + nekoton_contracts::jetton::update_library_cell(&mut state.account.storage.state)?; + let root_token_contract = nekoton_contracts::jetton::TokenWalletContract(state.as_context(clock)).root()?; @@ -338,15 +342,17 @@ pub async fn get_token_root_details_from_token_wallet( fn make_contract_state_handler( clock: Arc, balance: &'_ mut BigUint, -) -> impl FnMut(&RawContractState) + '_ { +) -> impl FnMut(&mut RawContractState) + '_ { move |contract_state| { if let RawContractState::Exists(state) = contract_state { - if let Ok(new_balance) = - nekoton_contracts::jetton::TokenWalletContract(state.as_context(clock.as_ref())) - .balance() - { - *balance = new_balance; - } + nekoton_contracts::jetton::update_library_cell(&mut state.account.storage.state) + .ok() + .and_then(|_| { + nekoton_contracts::jetton::TokenWalletContract(state.as_context(clock.as_ref())) + .balance() + .ok() + .map(|new_balance| *balance = new_balance) + }); } } } diff --git a/src/core/nft_wallet/mod.rs b/src/core/nft_wallet/mod.rs index 91d75fa81..b6fdf152e 100644 --- a/src/core/nft_wallet/mod.rs +++ b/src/core/nft_wallet/mod.rs @@ -363,7 +363,7 @@ fn make_contract_state_handler<'a>( owner: &'a mut MsgAddressInt, manager: &'a mut MsgAddressInt, handler: Option<&'a dyn NftSubscriptionHandler>, -) -> impl FnMut(&RawContractState) + 'a { +) -> impl FnMut(&mut RawContractState) + 'a { move |contract_state| { if let RawContractState::Exists(state) = contract_state { if let Ok(info) = NftContractState(state).get_info(clock) { diff --git a/src/core/parsing.rs b/src/core/parsing.rs index fb9d0f83a..16cec6ffb 100644 --- a/src/core/parsing.rs +++ b/src/core/parsing.rs @@ -659,9 +659,18 @@ pub fn parse_jetton_transaction( let amount = BigUint::from(grams.as_u128()); + let mut addr = MsgAddressInt::default(); + addr.read_from(&mut body).ok()?; + match opcode { - JETTON_TRANSFER_OPCODE => Some(JettonWalletTransaction::Transfer(amount)), - JETTON_NOTIFY_OPCODE => Some(JettonWalletTransaction::Notify(amount)), + JETTON_TRANSFER_OPCODE => Some(JettonWalletTransaction::Transfer(JettonOutgoingTransfer { + to: addr, + tokens: amount, + })), + JETTON_NOTIFY_OPCODE => Some(JettonWalletTransaction::Notify(JettonIncomingTransfer { + from: addr, + tokens: amount, + })), _ => None, } } @@ -1103,8 +1112,15 @@ mod tests { let wallet_transaction = parse_jetton_transaction(&tx, &description); assert!(wallet_transaction.is_some()); - if let Some(JettonWalletTransaction::Notify(amount)) = wallet_transaction { - assert_eq!(amount.to_u128().unwrap(), 100000000000); + if let Some(JettonWalletTransaction::Notify(transfer)) = wallet_transaction { + assert_eq!(transfer.tokens.to_u128().unwrap(), 100000000000); + assert_eq!( + transfer.from, + MsgAddressInt::from_str( + "0:6ca35273892588b4c5f4ae898dc1983eec9662dffebeacdbe82103a1d1dcac60" + ) + .unwrap() + ); } } } diff --git a/src/core/token_wallet/mod.rs b/src/core/token_wallet/mod.rs index 59a16a09f..0aa3b461e 100644 --- a/src/core/token_wallet/mod.rs +++ b/src/core/token_wallet/mod.rs @@ -475,7 +475,7 @@ fn make_contract_state_handler( clock: Arc, version: TokenWalletVersion, balance: &'_ mut BigUint, -) -> impl FnMut(&RawContractState) + '_ { +) -> impl FnMut(&mut RawContractState) + '_ { move |contract_state| { if let RawContractState::Exists(state) = contract_state { if let Ok(new_balance) = diff --git a/src/core/ton_wallet/mod.rs b/src/core/ton_wallet/mod.rs index 60430b5aa..ee09d82fc 100644 --- a/src/core/ton_wallet/mod.rs +++ b/src/core/ton_wallet/mod.rs @@ -801,7 +801,7 @@ fn make_contract_state_handler<'a>( public_key: &'a PublicKey, wallet_type: WalletType, wallet_data: &'a mut WalletData, -) -> impl FnMut(&RawContractState) + 'a { +) -> impl FnMut(&mut RawContractState) + 'a { move |contract_state| { if let RawContractState::Exists(contract_state) = contract_state { if let Err(e) = wallet_data.update( diff --git a/src/models.rs b/src/models.rs index cb8d31604..a5c891408 100644 --- a/src/models.rs +++ b/src/models.rs @@ -279,10 +279,8 @@ pub enum TokenWalletTransaction { #[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "snake_case", tag = "type", content = "data")] pub enum JettonWalletTransaction { - #[serde(with = "serde_string")] - Transfer(BigUint), - #[serde(with = "serde_string")] - Notify(BigUint), + Transfer(JettonOutgoingTransfer), + Notify(JettonIncomingTransfer), } #[derive(Clone, Debug, Serialize, Deserialize)] @@ -313,6 +311,14 @@ pub struct TokenOutgoingTransfer { pub payload: ton_types::Cell, } +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct JettonIncomingTransfer { + #[serde(with = "serde_string")] + pub from: MsgAddressInt, + #[serde(with = "serde_string")] + pub tokens: BigUint, +} + #[derive(Clone, Debug, Serialize, Deserialize)] pub struct JettonOutgoingTransfer { #[serde(with = "serde_string")] From 7de3176d3f93bfb55874e6c517c1e74702066964 Mon Sep 17 00:00:00 2001 From: Alexey Pashinov Date: Wed, 27 Nov 2024 15:12:27 +0100 Subject: [PATCH 08/38] TON WalletV4 --- nekoton-contracts/src/jetton/mod.rs | 60 ++- .../src/jetton/root_token_contract.rs | 3 +- .../src/wallets/code/jetton_wallet_v2.boc | Bin 0 -> 942 bytes nekoton-contracts/src/wallets/code/mod.rs | 5 +- .../src/wallets/code/wallet_v4r1_code.boc | Bin 0 -> 769 bytes .../src/wallets/code/wallet_v4r2_code.boc | Bin 0 -> 736 bytes src/core/jetton_wallet/mod.rs | 121 +++++- src/core/parsing.rs | 85 +++- src/core/ton_wallet/mod.rs | 64 ++- src/core/ton_wallet/wallet_v4.rs | 397 ++++++++++++++++++ src/core/ton_wallet/wallet_v5r1.rs | 15 +- src/models.rs | 4 +- 12 files changed, 697 insertions(+), 57 deletions(-) create mode 100644 nekoton-contracts/src/wallets/code/jetton_wallet_v2.boc create mode 100644 nekoton-contracts/src/wallets/code/wallet_v4r1_code.boc create mode 100644 nekoton-contracts/src/wallets/code/wallet_v4r2_code.boc create mode 100644 src/core/ton_wallet/wallet_v4.rs diff --git a/nekoton-contracts/src/jetton/mod.rs b/nekoton-contracts/src/jetton/mod.rs index 34772ec4c..12c64a579 100644 --- a/nekoton-contracts/src/jetton/mod.rs +++ b/nekoton-contracts/src/jetton/mod.rs @@ -131,11 +131,9 @@ mod tests { use nekoton_abi::num_traits::{FromPrimitive, ToPrimitive}; use nekoton_abi::ExecutionContext; use nekoton_utils::SimpleClock; - use ton_block::{AccountState, Deserializable, MsgAddressInt}; - use ton_types::{CellType, SliceData, UInt256}; + use ton_block::MsgAddressInt; use crate::jetton; - use crate::wallets; #[test] fn usdt_root_token_contract() -> anyhow::Result<()> { @@ -179,6 +177,12 @@ mod tests { let expected_code = ton_types::deserialize_tree_of_cells(&mut base64::decode("te6cckECEQEAAyMAART/APSkE/S88sgLAQIBYgIDAgLMBAUAG6D2BdqJofQB9IH0gahhAgHUBgcCASAICQDDCDHAJJfBOAB0NMDAXGwlRNfA/AM4PpA+kAx+gAxcdch+gAx+gAwc6m0AALTH4IQD4p+pVIgupUxNFnwCeCCEBeNRRlSILqWMUREA/AK4DWCEFlfB7y6k1nwC+BfBIQP8vCAAET6RDBwuvLhTYAIBIAoLAIPUAQa5D2omh9AH0gfSBqGAJpj8EIC8aijKkQXUEIPe7L7wndCVj5cWLpn5j9ABgJ0CgR5CgCfQEsZ4sA54tmZPaqQB8VA9M/+gD6QCHwAe1E0PoA+kD6QNQwUTahUirHBfLiwSjC//LiwlQ0QnBUIBNUFAPIUAT6AljPFgHPFszJIsjLARL0APQAywDJIPkAcHTIywLKB8v/ydAE+kD0BDH6ACDXScIA8uLEd4AYyMsFUAjPFnD6AhfLaxPMgMAgEgDQ4AnoIQF41FGcjLHxnLP1AH+gIizxZQBs8WJfoCUAPPFslQBcwjkXKRceJQCKgToIIJycOAoBS88uLFBMmAQPsAECPIUAT6AljPFgHPFszJ7VQC9ztRND6APpA+kDUMAjTP/oAUVGgBfpA+kBTW8cFVHNtcFQgE1QUA8hQBPoCWM8WAc8WzMkiyMsBEvQA9ADLAMn5AHB0yMsCygfL/8nQUA3HBRyx8uLDCvoAUaihggiYloBmtgihggiYloCgGKEnlxBJEDg3XwTjDSXXCwGAPEADXO1E0PoA+kD6QNQwB9M/+gD6QDBRUaFSSccF8uLBJ8L/8uLCBYIJMS0AoBa88uLDghB73ZfeyMsfFcs/UAP6AiLPFgHPFslxgBjIywUkzxZw+gLLaszJgED7AEATyFAE+gJYzxYBzxbMye1UgAHBSeaAYoYIQc2LQnMjLH1Iwyz9Y+gJQB88WUAfPFslxgBDIywUkzxZQBvoCFctqFMzJcfsAECQQIwB8wwAjwgCwjiGCENUydttwgBDIywVQCM8WUAT6AhbLahLLHxLLP8ly+wCTNWwh4gPIUAT6AljPFgHPFszJ7VSV6u3X")?.as_slice())?; assert_eq!(wallet_code, expected_code); + let token_address = contract.get_wallet_address(&MsgAddressInt::default())?; + assert_eq!( + token_address.to_string(), + "0:0c6a835483369275c9ae76e7e31d9eda0845368045a8ec2ed78609d96bb0a087" + ); + Ok(()) } @@ -187,23 +191,25 @@ mod tests { let cell = ton_types::deserialize_tree_of_cells(&mut base64::decode("te6ccgEBAwEAqAACbIAXsqVXAuRG6+GFp/25WVl2IsmatSkX0jbrXVjoBOwsnEQNAdiGdFv5kAABdRDp2cQZrn10JgIBAJEFJFfQYxaABHulQdJwYfnHP5r0FXhq3wjit36+D+zzx7bkE76OQgrwAsROplLUCShZxn2kTkyjrdZWWw4ol9ZAosUb+zcNiHf6CEICj0Utek39dAZraCNlF3JZ7QVzRDW+drX9S9XYryt8PWg=").unwrap().as_slice()).unwrap(); let mut state = nekoton_utils::deserialize_account_stuff(cell)?; - if let AccountState::AccountActive { state_init } = &mut state.storage.state { - if let Some(cell) = &state_init.code { - if cell.cell_type() == CellType::LibraryReference { - let mut slice_data = SliceData::load_cell(cell.clone())?; + jetton::update_library_cell(&mut state.storage.state)?; - let tag = slice_data.get_next_byte()?; - assert_eq!(tag, 2); + let contract = jetton::TokenWalletContract(ExecutionContext { + clock: &SimpleClock, + account_stuff: &state, + }); - let mut hash = UInt256::default(); - hash.read_from(&mut slice_data)?; + let balance = contract.balance()?; + assert_eq!(balance.to_u128().unwrap(), 156092097302); - if let Some(cell) = wallets::code::get_jetton_library_cell(&hash) { - state_init.set_code(cell.clone()); - } - } - } - } + Ok(()) + } + + #[test] + fn notcoin_wallet_token_contract() -> anyhow::Result<()> { + let cell = ton_types::deserialize_tree_of_cells(&mut base64::decode("te6ccgEBAwEAqgACbIAX5XxfY9N6rJiyOS4NGQc01nd0dzEnWBk87cdqg9bLTwQNAeCGdH/3UAABdXbIjToZrn5eJgIBAJUHFxcOBj4fBYAfGfo6PQWliRZGmmqpYpA1QxmYkyLZonLf41f59x68XdAAvlWFDxGF2lXm67y4yzC17wYKD9A0guwPkMs1gOsM//IIQgK6KRjIlH6bJa+awbiDNXdUFz5YEvgHo9bmQqFHCVlTlQ==").unwrap().as_slice()).unwrap(); + let mut state = nekoton_utils::deserialize_account_stuff(cell)?; + + jetton::update_library_cell(&mut state.storage.state)?; let contract = jetton::TokenWalletContract(ExecutionContext { clock: &SimpleClock, @@ -211,7 +217,7 @@ mod tests { }); let balance = contract.balance()?; - assert_eq!(balance.to_u128().unwrap(), 156092097302); + assert_eq!(balance.to_u128().unwrap(), 6499273466060549); Ok(()) } @@ -265,4 +271,22 @@ mod tests { Ok(()) } + + #[test] + fn mintless_points_root_token_contract() -> anyhow::Result<()> { + let cell = + ton_types::deserialize_tree_of_cells(&mut base64::decode("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").unwrap().as_slice()) + .unwrap(); + let state = nekoton_utils::deserialize_account_stuff(cell)?; + + let contract = jetton::RootTokenContract(ExecutionContext { + clock: &SimpleClock, + account_stuff: &state, + }); + + let details = contract.get_details()?; + assert_eq!(details.admin_address, MsgAddressInt::default()); + + Ok(()) + } } diff --git a/nekoton-contracts/src/jetton/root_token_contract.rs b/nekoton-contracts/src/jetton/root_token_contract.rs index 77f3c8971..96cb16e6a 100644 --- a/nekoton-contracts/src/jetton/root_token_contract.rs +++ b/nekoton-contracts/src/jetton/root_token_contract.rs @@ -39,7 +39,8 @@ pub fn get_jetton_data(res: VmGetterOutput) -> Result { let mintable = stack[1].as_bool()?; let mut address_data = stack[2].as_slice()?.clone(); - let admin_address = MsgAddressInt::construct_from(&mut address_data)?; + + let admin_address = MsgAddressInt::construct_from(&mut address_data).unwrap_or_default(); let content = stack[3].as_cell()?; let content = MetaDataContent::parse(content)?; diff --git a/nekoton-contracts/src/wallets/code/jetton_wallet_v2.boc b/nekoton-contracts/src/wallets/code/jetton_wallet_v2.boc new file mode 100644 index 0000000000000000000000000000000000000000..3794284ea7c54d304775569d63735b8efbcdcf8d GIT binary patch literal 942 zcmaiyZAep57{||Z?%n2m+j6O8f%p0v61AD6h2R2(UI^~ipc#Q;FCQ%AI-`Ohwy^@G zRx_grq1g*WU(}*Lj6thDxOD}mBNkaQ^-EujxQw`+L3XY$;rbFB4xESc{GaFf{hwFg zhh6j70ElrTREDac3U=W->}Lj<2>U|wTwACe(2i<9>kw>SM(HP}bXbn&0fsKjU?mRm z1OvTe{)46t#?2-9rB)gsh**hrnoX30&%k?)r8nEQhOpOo)Yc3YD?LFS^M z)x<<4#z` z#kgNcybzV}2c5JWi?hV4cEfSWJoF%WBmxkDF?l>-UeMx`6>IbPef1}3TMcyH_6}V* z4G2tq{5&GgY$Uk!Q@H@Ji0aCe;3sQ@aLvZEF_%#FzI!Ynx-U5C}I*WlK z0h5n9sc2tUe3*(>t7fnSQ^{3TBXyyQKInR`s=7C25V|ZlMwH#zU|;K9860)133QFA zYa&S9UgO95i?eIp#yrTIL4de_v7_2IpGv!HuHP?@>t=K_+Wv=c?Rp>L!@{i%(E(+9 J|IXu&{s8Z@f`b46 literal 0 HcmV?d00001 diff --git a/nekoton-contracts/src/wallets/code/mod.rs b/nekoton-contracts/src/wallets/code/mod.rs index 6cc1f3d4c..309189d55 100644 --- a/nekoton-contracts/src/wallets/code/mod.rs +++ b/nekoton-contracts/src/wallets/code/mod.rs @@ -21,10 +21,13 @@ declare_tvc! { multisig2_1 => "./Multisig2_1.tvc" (MULTISIG2_1_CODE), surf_wallet => "./Surf.tvc" (SURF_WALLET_CODE), wallet_v3 => "./wallet_v3_code.boc" (WALLET_V3_CODE), + wallet_v4r1 => "./wallet_v4r1_code.boc" (WALLET_V4R1_CODE), + wallet_v4r2 => "./wallet_v4r2_code.boc" (WALLET_V4R2_CODE), wallet_v5r1 => "./wallet_v5r1_code.boc" (WALLET_V5R1_CODE), highload_wallet_v2 => "./highload_wallet_v2_code.boc" (HIGHLOAD_WALLET_V2_CODE), ever_wallet => "./ever_wallet_code.boc" (EVER_WALLET_CODE), jetton_wallet_governed => "./jetton_wallet_governed.boc" (JETTON_WALLET_GOVERNED), + jetton_wallet_v2 => "./jetton_wallet_v2.boc" (JETTON_WALLET_V2), } fn load(mut data: &[u8]) -> Cell { @@ -35,7 +38,7 @@ static JETTON_LIBRARY_CELLS: once_cell::sync::Lazy> = once_cell::sync::Lazy::new(|| { let mut m = HashMap::new(); - let codes = [jetton_wallet_governed()]; + let codes = [jetton_wallet_governed(), jetton_wallet_v2()]; for code in codes { m.insert(code.repr_hash(), code); diff --git a/nekoton-contracts/src/wallets/code/wallet_v4r1_code.boc b/nekoton-contracts/src/wallets/code/wallet_v4r1_code.boc new file mode 100644 index 0000000000000000000000000000000000000000..542d420ca47f234c94593c4e194a46a9282b2742 GIT binary patch literal 769 zcmY+CUr19?9LIm>>|VE~%j-7BBx-kSCeh0beY9Ol=nnuL}}=)+~kcK5K(-4+su!}c{&%MZJFFi}EGq?;pj+`}) z@?eeQJfOHL;>hb%ids)Bq|%}{7*%^IluE16_`EFs5%`s`G z*k0=tjdbR9_FA`S9+(EThH^Nh1s9Tm3Tsuf$2I~Xvx_BcN7Cd{$+!S|a+}_iBTn!X zJN$J&|JB)LY$RSom4_248d#{gjXv&X6hi}!x64MBU-M^{O0!e?(2*(5 z_~>ElQs*2-{kp@@@*#0Qb43@}C_>NX%d)$R7hK)qG-`s)(=$><3c`?Zwg*4oxjaM< zY388q&DXmtw*-E4{`Z4Q9NOL<{X^4nfifDGzS`jZ>QSef;XrU?!o6d;r{Vi}Vp)Lmk z33Y`jqkeX%%&IR-(6K5j-ae6RK{5JR4N`mkr6!cM@<~`44J>l`i%+uF9In8wP~elO z2z(M30VSygq3EDY+NpRIgKc)U;GGQ0WGVjUdOSoIeEAEXIrD(CUVes+jNZDo<(pxL zCZgk8^Xz6I!CrI{X{*zjeYS8TAjY!NR(F)Uux&DAA^8P^fpo$tdlvhJnV{AGOyE>q3meNL1S0uHzPEn9T04YQ#g-57LRf z0fJi{$HT*&b2hu>ebWnmE|Rm^dRG^xjx99q%%52*&rUjH2PawAgZr&ZU2_->G&-R9 zUFvS`vLmulg6_@dmB2|(lupT0s1Y_#Osj?(g+cLbH-5T%X^@!gZ-Ta0UvIBm7rBx7 zpZAO;w7ov`o0P0sK2E~ecdHJ$oUaTFNw?LE_f9nCkrhV4zO-9nbnH?_oZH)}i3-S~ z%bY!+u3?CfQ{JNkiM*fdtzXZuW8vu4$X@};LKJ9IVc7h7zo~&?^dBkob(mmKU8}|R RA7ltf)njwkK(JR+{sYpxENTD% literal 0 HcmV?d00001 diff --git a/src/core/jetton_wallet/mod.rs b/src/core/jetton_wallet/mod.rs index 5eb6ae22e..0d7ef090e 100644 --- a/src/core/jetton_wallet/mod.rs +++ b/src/core/jetton_wallet/mod.rs @@ -1,10 +1,6 @@ use std::convert::TryFrom; use std::sync::Arc; -use crate::core::models::*; -use crate::core::parsing::*; -use crate::transport::models::{RawContractState, RawTransaction}; -use crate::transport::Transport; use anyhow::Result; use nekoton_abi::num_traits::ToPrimitive; use nekoton_abi::*; @@ -14,10 +10,16 @@ use num_bigint::{BigInt, BigUint, ToBigInt}; use ton_block::{MsgAddressInt, Serializable}; use ton_types::{BuilderData, IBitstring, SliceData}; +use crate::core::models::*; +use crate::core::parsing::*; +use crate::core::transactions_tree::TransactionsTreeStream; +use crate::transport::models::{RawContractState, RawTransaction}; +use crate::transport::Transport; + use super::{ContractSubscription, InternalMessage}; pub const JETTON_TRANSFER_OPCODE: u32 = 0x0f8a7ea5; -pub const JETTON_NOTIFY_OPCODE: u32 = 0x7362d09c; +pub const JETTON_INTERNAL_TRANSFER_OPCODE: u32 = 0x178d4519; pub struct JettonWallet { clock: Arc, @@ -88,14 +90,99 @@ impl JettonWallet { pub async fn estimate_min_attached_amount( &self, - _destination: TransferRecipient, - _tokens: BigUint, - _notify_receiver: bool, - _payload: ton_types::Cell, + amount: BigUint, + destination: MsgAddressInt, + remaining_gas_to: MsgAddressInt, + custom_payload: Option, + callback_value: BigUint, + callback_payload: Option, ) -> Result { - // TODO: estimate? - const ATTACHED_AMOUNT: u64 = 100_000_000; // 0.1 TON - Ok(ATTACHED_AMOUNT) + const FEE_MULTIPLIER: u128 = 2; + + // Prepare internal message + let internal_message = self.prepare_transfer( + amount, + destination, + remaining_gas_to, + custom_payload, + callback_value, + callback_payload, + 0, + )?; + + let mut message = ton_block::Message::with_int_header(ton_block::InternalMessageHeader { + src: ton_block::MsgAddressIntOrNone::Some( + internal_message + .source + .unwrap_or_else(|| self.owner.clone()), + ), + dst: internal_message.destination, + ..Default::default() + }); + + message.set_body(internal_message.body.clone()); + + // Prepare executor + let transport = self.contract_subscription.transport().clone(); + let config = transport + .get_blockchain_config(self.clock.as_ref(), true) + .await?; + + let mut tree = TransactionsTreeStream::new(message, config, transport, self.clock.clone()); + tree.unlimited_account_balance(); + tree.unlimited_message_balance(); + + type Err = fn(Option) -> JettonWalletError; + let check_exit_code = |tx: &ton_block::Transaction, err: Err| -> Result<()> { + let descr = tx.read_description()?; + if descr.is_aborted() { + let exit_code = match descr { + ton_block::TransactionDescr::Ordinary(descr) => match descr.compute_ph { + ton_block::TrComputePhase::Vm(phase) => Some(phase.exit_code), + ton_block::TrComputePhase::Skipped(_) => None, + }, + _ => None, + }; + Err(err(exit_code).into()) + } else { + Ok(()) + } + }; + + let mut attached_amount: u128 = 0; + + // Simulate source transaction + let source_tx = tree.next().await?.ok_or(JettonWalletError::NoSourceTx)?; + check_exit_code(&source_tx, JettonWalletError::SourceTxFailed)?; + attached_amount += source_tx.total_fees.grams.as_u128(); + + if source_tx.outmsg_cnt == 0 { + return Err(JettonWalletError::NoDestTx.into()); + } + + if let Some(message) = tree.peek() { + if message.state_init().is_some() && message.src_ref() == Some(self.address()) { + // Simulate first deploy transaction + // NOTE: we don't need to count attached amount here because of separate `initial_balance` + let _ = tree.next().await?.ok_or(JettonWalletError::NoDestTx)?; + //also we ignore non zero exit code for deploy transactions + } + } + + tree.retain_message_queue(|message| { + message.state_init().is_none() && message.src_ref() == Some(self.address()) + }); + + if tree.message_queue().len() != 1 { + return Err(JettonWalletError::NoDestTx.into()); + } + + // Simulate destination transaction + let dest_tx = tree.next().await?.ok_or(JettonWalletError::NoDestTx)?; + check_exit_code(&dest_tx, JettonWalletError::DestinationTxFailed)?; + attached_amount += dest_tx.total_fees.grams.as_u128(); + + Ok((attached_amount * FEE_MULTIPLIER) as u64) } pub fn prepare_transfer( @@ -211,7 +298,7 @@ impl JettonWallet { JettonWalletTransaction::Transfer(transfer) => { balance -= transfer.tokens.clone().to_bigint().trust_me(); } - JettonWalletTransaction::Notify(transfer) => { + JettonWalletTransaction::InternalTransfer(transfer) => { balance += transfer.tokens.clone().to_bigint().trust_me(); } } @@ -392,4 +479,12 @@ enum JettonWalletError { WalletNotDeployed, #[error("Failed to convert grams")] TryFromGrams, + #[error("No source transaction produced")] + NoSourceTx, + #[error("No destination transaction produced")] + NoDestTx, + #[error("Source transaction failed with exit code {0:?}")] + SourceTxFailed(Option), + #[error("Destination transaction failed with exit code {0:?}")] + DestinationTxFailed(Option), } diff --git a/src/core/parsing.rs b/src/core/parsing.rs index 16cec6ffb..be33d2bc3 100644 --- a/src/core/parsing.rs +++ b/src/core/parsing.rs @@ -10,7 +10,7 @@ use nekoton_abi::*; use nekoton_contracts::tip4_1::nft_contract; use nekoton_contracts::{old_tip3, tip3_1}; -use crate::core::jetton_wallet::{JETTON_NOTIFY_OPCODE, JETTON_TRANSFER_OPCODE}; +use crate::core::jetton_wallet::{JETTON_INTERNAL_TRANSFER_OPCODE, JETTON_TRANSFER_OPCODE}; use crate::core::models::*; use crate::core::ton_wallet::{MultisigType, WalletType}; @@ -65,7 +65,7 @@ pub fn parse_payload(payload: ton_types::SliceData) -> Option { None } -pub fn parse_payload_wallet_v5r1(payload: ton_types::SliceData) -> Option { +pub fn parse_jetton_payload(payload: ton_types::SliceData) -> Option { let mut payload = payload; let opcode = payload.get_next_u32().ok()?; @@ -121,7 +121,7 @@ pub fn parse_transaction_additional_info( WalletInteractionMethod::WalletV3Transfer, ) } - WalletType::WalletV5R1 => { + WalletType::WalletV4R1 | WalletType::WalletV4R2 | WalletType::WalletV5R1 => { let mut out_msg = None; tx.out_msgs .iterate(|item| { @@ -136,12 +136,12 @@ pub fn parse_transaction_additional_info( _ => return None, }; - let known_payload = out_msg.body().and_then(parse_payload_wallet_v5r1); + let known_payload = out_msg.body().and_then(parse_jetton_payload); ( Some(recipient.clone()), known_payload, - WalletInteractionMethod::WalletV5R1Transfer, + WalletInteractionMethod::TonWalletTransfer, ) } WalletType::Multisig(multisig_type) => { @@ -637,17 +637,17 @@ pub fn parse_token_transaction( pub fn parse_jetton_transaction( tx: &ton_block::Transaction, - _description: &ton_block::TransactionDescrOrdinary, + description: &ton_block::TransactionDescrOrdinary, ) -> Option { - // if description.aborted { - // return None; - // } + if description.aborted { + return None; + } let in_msg = tx.in_msg.as_ref()?.read_struct().ok()?; let mut body = in_msg.body()?; let opcode = body.get_next_u32().ok()?; - if opcode != JETTON_TRANSFER_OPCODE && opcode != JETTON_NOTIFY_OPCODE { + if opcode != JETTON_TRANSFER_OPCODE && opcode != JETTON_INTERNAL_TRANSFER_OPCODE { return None; } @@ -667,10 +667,12 @@ pub fn parse_jetton_transaction( to: addr, tokens: amount, })), - JETTON_NOTIFY_OPCODE => Some(JettonWalletTransaction::Notify(JettonIncomingTransfer { - from: addr, - tokens: amount, - })), + JETTON_INTERNAL_TRANSFER_OPCODE => Some(JettonWalletTransaction::InternalTransfer( + JettonIncomingTransfer { + from: addr, + tokens: amount, + }, + )), _ => None, } } @@ -1106,16 +1108,59 @@ mod tests { } #[test] - fn test_jetton_tokens_incoming_transfer() { - let (tx, description) = parse_transaction("te6ccgECBgEAAUEAA7F5WehEBzKkG99WEqFRkzi98nCWmpXQJBUBmsu+62sSSaAAAujDMA9YPbDkegBLk39wTvj+oYkQn/oiUxWp8d52BTFTfDVpe66gAALouYkW/BZ0Hn3gAAAoSAMCAQARDFCJAT9rXEEgAIJyP0XywcegmfnHNRlyzBk1fCDnCH7vhvlGb9ZcIR/c1sVcySdx1adc9HfHMJmGQfZqbrhnFk2erivzrCxhpNhIwQEBoAQBsUgBCW497xW4vg6Jw7WmJUrLC2JRQDOPb6GH1xJsclEw3tkAJWehEBzKkG99WEqFRkzi98nCWmpXQJBUBmsu+62sSSaQE/a1xAYMRbIAAF0YZgHrBM6Dz7zABQBmc2LQnAAAAZNZcVp9UXSHboAIANlGpOcSSxFpi+ldExuDMH3ZLMW//X1Zt9BCB0OjuVjA"); + fn test_parse_wallet_v5r1_transfer() { + let (tx, description) = parse_transaction("te6ccgECDgEAAroAA7V6PzxB5ur5JLcojkw57D91dcch0SdJBkRg11onChvcQxAAAuqQo7KQGfOICr+MryG/HTeCGLoHvR2QzQp8l/VW7Jy5KteDKoNgAALqkJdMvBZ0b1RwADRmUxQIBQQBAg8MQoYY8SmEQAMCAG/JhfBQTA/WGAAAAAAAAgAAAAAAAxZIaTNMW1cxmByM5WsWV9cxExzB5+1s+b7Uz5613xWmQNAtXACdQmljE4gAAAAAAAAAACPAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIACCcp/sV0iKg0YadasmKflOuBQl9+BT1AMGK8jDUAHzabPWEAUiOhXPDSZMLX8X/WQ0jZRy1Ef+OW9TZFgZ7OoiSM4CAeAIBgEB3wcBsWgBR+eIPN1fJJblEcmHPYfurrjkOiTpIMiMGutE4UN7iGMABaelQoOWDtcjd5wKID6i0sUbGwEsUr2tFotsGs7AiEdQUQ/0AAYP1jQAAF1SFHZSBM6N6o7ACwHliAFH54g83V8kluURyYc9h+6uuOQ6JOkgyIwa60ThQ3uIYgObSztz///4izo3vDAAACqUsU/LVK+ma1KSpaW5p+h9917oIw6a7Txpn/VJg/WB7C5dJQYSdVOvNFZvNMz1vvv5wMwo33jnWdrh1jaHQJXGBQkCCg7DyG0DDQoBaGIAC09KhQcsHa5G7zgUQH1FpYo2NgJYpXtaLRbYNZ2BEI6goh/oAAAAAAAAAAAAAAAAAAELAbIPin6lAAAAAAAAAABUUC0RQAgAcgwTrCsIXFRhmQTVWMIpgapb1R1i6mXzRjfAhiAa+x8AKPzxB5ur5JLcojkw57D91dcch0SdJBkRg11onChvcQxIHJw4AQwACW7J3GUgAAA="); + assert!(!description.aborted); - let wallet_transaction = parse_jetton_transaction(&tx, &description); + let wallet_transaction = parse_transaction_additional_info(&tx, WalletType::WalletV5R1); assert!(wallet_transaction.is_some()); - if let Some(JettonWalletTransaction::Notify(transfer)) = wallet_transaction { - assert_eq!(transfer.tokens.to_u128().unwrap(), 100000000000); + if let Some(TransactionAdditionalInfo::WalletInteraction(WalletInteractionInfo { + recipient, + known_payload, + .. + })) = wallet_transaction + { + assert_eq!( + recipient.unwrap(), + MsgAddressInt::from_str( + "0:169e950a0e583b5c8dde702880fa8b4b146c6c04b14af6b45a2db06b3b02211d" + ) + .unwrap() + ); + + assert!(known_payload.is_some()); + + let payload = known_payload.unwrap(); + if let KnownPayload::JettonOutgoingTransfer(JettonOutgoingTransfer { to, tokens }) = + payload + { + assert_eq!( + to, + MsgAddressInt::from_str( + "0:390609d615842e2a30cc826aac6114c0d52dea8eb17532f9a31be043100d7d8f" + ) + .unwrap() + ); + + assert_eq!(tokens.to_u128().unwrap(), 296400000000); + } + } + } + + #[test] + fn test_jetton_incoming_transfer() { + let (tx, description) = parse_transaction("te6ccgECEQEAA18AA7VyIr+K0006cnz63RzDYQCVEwPdJSutXyVs8FkkuI/aUhAAAuqVnc5wMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAZ0cDmQAFxhZIHoBQQBAhUECQF3khgYYMNQEQMCAG/Jh45gTBQmPAAAAAAABgACAAAABCVSGbb14fUjN0CNk0Gb357AgRdmQAzjvhXKQxQsysyEQNA6FACeQH0MDwXYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACCcpCuyJZa+rsW68PLm0COuucbYY14eIvIDQmENZPKyY2kdcLRm1JwTMPyj/1uPSmUxtxqBm0adw5aQV4xl7KOWKwCAeAMBgIB3QkHAQEgCADJSABEV/FaaadOT59bo5hsIBKiYHukpXWr5K2eCySXEftKQwAbKNSc4kliLTF9K6JjcGYPuyWYt/+vqzb6CEDodHcrGBAVsEtIBggjWgAAXVKzuc4Kzo4HMmqZO22AAADJtrLp10ABASAKAatIAERX8Vppp05Pn1ujmGwgEqJge6SldavkrZ4LJJcR+0pDACVnoRAcypBvfVhKhUZM4vfJwlpqV0CQVAZrLvutrEkmhAQGDAMIAABdUrO5zgjOjgcywAsAXnNi0JwAAAGTbWXTrhZIANlGpOcSSxFpi+ldExuDMH3ZLMW//X1Zt9BCB0OjuVjAArFoAFvJbBqvg228UqryRCckJdyJhdWLi/oZ3qOObCPE9jQBAAiK/itNNOnJ8+t0cw2EAlRMD3SUrrV8lbPBZJLiP2lIUBd5IYAGF1LiAABdUrO5zgTOjgcz4A4NAKMXjUUZAAABk21l064WSADZRqTnEksRaYvpXRMbgzB92SzFv/19WbfQQgdDo7lYwQAbKNSc4kliLTF9K6JjcGYPuyWYt/+vqzb6CEDodHcrGAQFAgE0EA8AhwCAErPQiA5lSDe+rCVCoyZxe+ThLTUroEgqAzWXfdbWJJNQAsROplLUCShZxn2kTkyjrdZWWw4ol9ZAosUb+zcNiHf6CEICj0Utek39dAZraCNlF3JZ7QVzRDW+drX9S9XYryt8PWg="); + assert!(!description.aborted); + + let jetton_transaction = parse_jetton_transaction(&tx, &description).unwrap(); + + if let JettonWalletTransaction::InternalTransfer(JettonIncomingTransfer { from, tokens }) = + jetton_transaction + { + assert_eq!(tokens.to_u128().unwrap(), 100); assert_eq!( - transfer.from, + from, MsgAddressInt::from_str( "0:6ca35273892588b4c5f4ae898dc1983eec9662dffebeacdbe82103a1d1dcac60" ) diff --git a/src/core/ton_wallet/mod.rs b/src/core/ton_wallet/mod.rs index ee09d82fc..7db0a2cfa 100644 --- a/src/core/ton_wallet/mod.rs +++ b/src/core/ton_wallet/mod.rs @@ -30,6 +30,7 @@ pub mod ever_wallet; pub mod highload_wallet_v2; pub mod multisig; pub mod wallet_v3; +pub mod wallet_v4; pub mod wallet_v5r1; pub const DEFAULT_WORKCHAIN: i8 = 0; @@ -229,6 +230,20 @@ impl TonWallet { self.workchain(), expiration, ), + WalletType::WalletV4R1 => wallet_v4::prepare_deploy( + self.clock.as_ref(), + &self.public_key, + self.workchain(), + expiration, + wallet_v4::WalletV4Version::R1, + ), + WalletType::WalletV4R2 => wallet_v4::prepare_deploy( + self.clock.as_ref(), + &self.public_key, + self.workchain(), + expiration, + wallet_v4::WalletV4Version::R2, + ), WalletType::WalletV5R1 => wallet_v5r1::prepare_deploy( self.clock.as_ref(), &self.public_key, @@ -325,6 +340,24 @@ impl TonWallet { vec![gift], expiration, ), + WalletType::WalletV4R1 => wallet_v4::prepare_transfer( + self.clock.as_ref(), + public_key, + current_state, + 0, + vec![gift], + expiration, + wallet_v4::WalletV4Version::R1, + ), + WalletType::WalletV4R2 => wallet_v4::prepare_transfer( + self.clock.as_ref(), + public_key, + current_state, + 0, + vec![gift], + expiration, + wallet_v4::WalletV4Version::R2, + ), WalletType::WalletV5R1 => wallet_v5r1::prepare_transfer( self.clock.as_ref(), public_key, @@ -650,6 +683,12 @@ pub fn extract_wallet_init_data(contract: &ExistingContract) -> Result<(PublicKe } else if wallet_v3::is_wallet_v3(&code_hash) { let public_key = PublicKey::from_bytes(wallet_v3::InitData::try_from(data)?.public_key())?; Ok((public_key, WalletType::WalletV3)) + } else if wallet_v4::is_wallet_v4r1(&code_hash) { + let public_key = PublicKey::from_bytes(wallet_v4::InitData::try_from(data)?.public_key())?; + Ok((public_key, WalletType::WalletV4R1)) + } else if wallet_v4::is_wallet_v4r2(&code_hash) { + let public_key = PublicKey::from_bytes(wallet_v4::InitData::try_from(data)?.public_key())?; + Ok((public_key, WalletType::WalletV4R2)) } else if wallet_v5r1::is_wallet_v5r1(&code_hash) { let public_key = PublicKey::from_bytes(wallet_v5r1::InitData::try_from(data)?.public_key())?; @@ -887,6 +926,8 @@ pub enum TransferAction { pub enum WalletType { Multisig(MultisigType), WalletV3, + WalletV4R1, + WalletV4R2, WalletV5R1, HighloadWalletV2, EverWallet, @@ -900,6 +941,7 @@ impl WalletType { Self::WalletV5R1 => wallet_v5r1::DETAILS, Self::HighloadWalletV2 => highload_wallet_v2::DETAILS, Self::EverWallet => ever_wallet::DETAILS, + Self::WalletV4R1 | Self::WalletV4R2 => wallet_v4::DETAILS, } } @@ -916,6 +958,8 @@ impl WalletType { match self { Self::Multisig(multisig_type) => multisig_type.code_hash(), Self::WalletV3 => wallet_v3::CODE_HASH, + Self::WalletV4R1 => wallet_v4::CODE_HASH_R1, + Self::WalletV4R2 => wallet_v4::CODE_HASH_R2, Self::WalletV5R1 => wallet_v5r1::CODE_HASH, Self::HighloadWalletV2 => highload_wallet_v2::CODE_HASH, Self::EverWallet => ever_wallet::CODE_HASH, @@ -927,6 +971,8 @@ impl WalletType { match self { Self::Multisig(multisig_type) => multisig_type.code(), Self::WalletV3 => wallets::code::wallet_v3(), + Self::WalletV4R1 => wallets::code::wallet_v4r1(), + Self::WalletV4R2 => wallets::code::wallet_v4r2(), Self::WalletV5R1 => wallets::code::wallet_v5r1(), Self::HighloadWalletV2 => wallets::code::highload_wallet_v2(), Self::EverWallet => wallets::code::ever_wallet(), @@ -940,6 +986,8 @@ impl FromStr for WalletType { fn from_str(s: &str) -> Result { Ok(match s { "WalletV3" => Self::WalletV3, + "WalletV4R1" => Self::WalletV4R1, + "WalletV4R2" => Self::WalletV4R2, "WalletV5R1" => Self::WalletV5R1, "HighloadWalletV2" => Self::HighloadWalletV2, "EverWallet" => Self::EverWallet, @@ -962,7 +1010,9 @@ impl TryInto for WalletType { WalletType::Multisig(MultisigType::SurfWallet) => 6, WalletType::Multisig(MultisigType::Multisig2) => 7, WalletType::Multisig(MultisigType::Multisig2_1) => 8, - WalletType::WalletV5R1 => 9, + WalletType::WalletV4R1 => 9, + WalletType::WalletV4R2 => 10, + WalletType::WalletV5R1 => 11, _ => anyhow::bail!("Unimplemented wallet type"), }; @@ -975,6 +1025,8 @@ impl std::fmt::Display for WalletType { match self { Self::Multisig(multisig_type) => multisig_type.fmt(f), Self::WalletV3 => f.write_str("WalletV3"), + Self::WalletV4R1 => f.write_str("WalletV4R1"), + Self::WalletV4R2 => f.write_str("WalletV4R2"), Self::WalletV5R1 => f.write_str("WalletV5R1"), Self::HighloadWalletV2 => f.write_str("HighloadWalletV2"), Self::EverWallet => f.write_str("EverWallet"), @@ -997,6 +1049,16 @@ pub fn compute_address( WalletType::HighloadWalletV2 => { highload_wallet_v2::compute_contract_address(public_key, workchain_id) } + WalletType::WalletV4R1 => wallet_v4::compute_contract_address( + public_key, + workchain_id, + wallet_v4::WalletV4Version::R1, + ), + WalletType::WalletV4R2 => wallet_v4::compute_contract_address( + public_key, + workchain_id, + wallet_v4::WalletV4Version::R2, + ), } } diff --git a/src/core/ton_wallet/wallet_v4.rs b/src/core/ton_wallet/wallet_v4.rs new file mode 100644 index 000000000..4479c8403 --- /dev/null +++ b/src/core/ton_wallet/wallet_v4.rs @@ -0,0 +1,397 @@ +use std::convert::TryFrom; + +use anyhow::Result; +use ed25519_dalek::PublicKey; +use ton_block::{MsgAddrStd, MsgAddressInt, Serializable}; +use ton_types::{BuilderData, Cell, IBitstring, SliceData, UInt256}; + +use nekoton_utils::*; + +use super::{Gift, TonWalletDetails, TransferAction}; +use crate::core::models::{Expiration, ExpireAt}; +use crate::crypto::{SignedMessage, UnsignedMessage}; + +pub fn prepare_deploy( + clock: &dyn Clock, + public_key: &PublicKey, + workchain: i8, + expiration: Expiration, + version: WalletV4Version, +) -> Result> { + let init_data = InitData::from_key(public_key).with_subwallet_id(SUBWALLET_ID); + let dst = compute_contract_address(public_key, workchain, version); + let mut message = + ton_block::Message::with_ext_in_header(ton_block::ExternalInboundMessageHeader { + dst, + ..Default::default() + }); + + message.set_state_init(init_data.make_state_init(version)?); + + let expire_at = ExpireAt::new(clock, expiration); + let (hash, payload) = init_data.make_transfer_payload(None, expire_at.timestamp)?; + + Ok(Box::new(UnsignedWalletV4 { + init_data, + gifts: Vec::new(), + payload, + message, + expire_at, + hash, + })) +} + +pub fn prepare_transfer( + clock: &dyn Clock, + public_key: &PublicKey, + current_state: &ton_block::AccountStuff, + seqno_offset: u32, + gifts: Vec, + expiration: Expiration, + version: WalletV4Version, +) -> Result { + if gifts.len() > MAX_MESSAGES { + return Err(WalletV4Error::TooManyGifts.into()); + } + + let (mut init_data, with_state_init) = match ¤t_state.storage.state { + ton_block::AccountState::AccountActive { state_init, .. } => match &state_init.data { + Some(data) => (InitData::try_from(data)?, false), + None => return Err(WalletV4Error::InvalidInitData.into()), + }, + ton_block::AccountState::AccountFrozen { .. } => { + return Err(WalletV4Error::AccountIsFrozen.into()) + } + ton_block::AccountState::AccountUninit => ( + InitData::from_key(public_key).with_subwallet_id(SUBWALLET_ID), + true, + ), + }; + + init_data.seqno += seqno_offset; + + let mut message = + ton_block::Message::with_ext_in_header(ton_block::ExternalInboundMessageHeader { + dst: current_state.addr.clone(), + ..Default::default() + }); + + if with_state_init { + message.set_state_init(init_data.make_state_init(version)?); + } + + let expire_at = ExpireAt::new(clock, expiration); + let (hash, payload) = init_data.make_transfer_payload(gifts.clone(), expire_at.timestamp)?; + + Ok(TransferAction::Sign(Box::new(UnsignedWalletV4 { + init_data, + gifts, + payload, + hash, + expire_at, + message, + }))) +} + +#[derive(Clone)] +struct UnsignedWalletV4 { + init_data: InitData, + gifts: Vec, + payload: BuilderData, + hash: UInt256, + expire_at: ExpireAt, + message: ton_block::Message, +} + +impl UnsignedMessage for UnsignedWalletV4 { + fn refresh_timeout(&mut self, clock: &dyn Clock) { + if !self.expire_at.refresh(clock) { + return; + } + + let (hash, payload) = self + .init_data + .make_transfer_payload(self.gifts.clone(), self.expire_at()) + .trust_me(); + self.hash = hash; + self.payload = payload; + } + + fn expire_at(&self) -> u32 { + self.expire_at.timestamp + } + + fn hash(&self) -> &[u8] { + self.hash.as_slice() + } + + fn sign(&self, signature: &[u8; ed25519_dalek::SIGNATURE_LENGTH]) -> Result { + let mut payload = self.payload.clone(); + payload.prepend_raw(signature, signature.len() * 8)?; + + let mut message = self.message.clone(); + message.set_body(SliceData::load_builder(payload)?); + + Ok(SignedMessage { + message, + expire_at: self.expire_at(), + }) + } + + fn sign_with_pruned_payload( + &self, + signature: &[u8; ed25519_dalek::SIGNATURE_LENGTH], + prune_after_depth: u16, + ) -> Result { + let mut payload = self.payload.clone(); + payload.append_raw(signature, signature.len() * 8)?; + let body = payload.into_cell()?; + + let mut message = self.message.clone(); + message.set_body(prune_deep_cells(&body, prune_after_depth)?); + + Ok(SignedMessage { + message, + expire_at: self.expire_at(), + }) + } +} + +pub static CODE_HASH_R1: &[u8; 32] = &[ + 0x64, 0xDD, 0x54, 0x80, 0x55, 0x22, 0xC5, 0xBE, 0x8A, 0x9D, 0xB5, 0x9C, 0xEA, 0x01, 0x05, 0xCC, + 0xF0, 0xD0, 0x87, 0x86, 0xCA, 0x79, 0xBE, 0xB8, 0xCB, 0x79, 0xE8, 0x80, 0xA8, 0xD7, 0x32, 0x2D, +]; + +pub static CODE_HASH_R2: &[u8; 32] = &[ + 0xFE, 0xB5, 0xFF, 0x68, 0x20, 0xE2, 0xFF, 0x0D, 0x94, 0x83, 0xE7, 0xE0, 0xD6, 0x2C, 0x81, 0x7D, + 0x84, 0x67, 0x89, 0xFB, 0x4A, 0xE5, 0x80, 0xC8, 0x78, 0x86, 0x6D, 0x95, 0x9D, 0xAB, 0xD5, 0xC0, +]; + +pub fn is_wallet_v4r1(code_hash: &UInt256) -> bool { + code_hash.as_slice() == CODE_HASH_R1 +} + +pub fn is_wallet_v4r2(code_hash: &UInt256) -> bool { + code_hash.as_slice() == CODE_HASH_R2 +} + +pub fn compute_contract_address( + public_key: &PublicKey, + workchain_id: i8, + version: WalletV4Version, +) -> MsgAddressInt { + InitData::from_key(public_key) + .with_subwallet_id(SUBWALLET_ID) + .compute_addr(workchain_id, version) + .trust_me() +} + +pub static DETAILS: TonWalletDetails = TonWalletDetails { + requires_separate_deploy: false, + min_amount: 1, // 0.000000001 TON + max_messages: MAX_MESSAGES, + supports_payload: true, + supports_state_init: true, + supports_multiple_owners: false, + supports_code_update: false, + expiration_time: 0, + required_confirmations: None, +}; + +const MAX_MESSAGES: usize = 4; + +/// `WalletV5` init data +#[derive(Clone, Copy)] +pub struct InitData { + pub seqno: u32, + pub subwallet_id: i32, + pub public_key: UInt256, +} + +impl InitData { + pub fn public_key(&self) -> &[u8; 32] { + self.public_key.as_slice() + } + + pub fn from_key(key: &PublicKey) -> Self { + Self { + seqno: 0, + subwallet_id: 0, + public_key: key.as_bytes().into(), + } + } + + pub fn with_subwallet_id(mut self, id: i32) -> Self { + self.subwallet_id = id; + self + } + + pub fn compute_addr( + &self, + workchain_id: i8, + version: WalletV4Version, + ) -> Result { + let init_state = self.make_state_init(version)?.serialize()?; + let hash = init_state.repr_hash(); + Ok(MsgAddressInt::AddrStd(MsgAddrStd { + anycast: None, + workchain_id, + address: hash.into(), + })) + } + + pub fn make_state_init(&self, version: WalletV4Version) -> Result { + let code = match version { + WalletV4Version::R1 => nekoton_contracts::wallets::code::wallet_v4r1(), + WalletV4Version::R2 => nekoton_contracts::wallets::code::wallet_v4r2(), + }; + + Ok(ton_block::StateInit { + code: Some(code), + data: Some(self.serialize()?), + ..Default::default() + }) + } + + pub fn serialize(&self) -> Result { + let mut data = BuilderData::new(); + data.append_u32(self.seqno)? + .append_i32(self.subwallet_id)? + .append_raw(self.public_key.as_slice(), 256)?; + + // empty plugin dict + data.append_bit_zero()?; + + data.into_cell() + } + + pub fn make_transfer_payload( + &self, + gifts: impl IntoIterator, + expire_at: u32, + ) -> Result<(UInt256, BuilderData)> { + let mut payload = BuilderData::new(); + + // insert prefix + payload + .append_i32(self.subwallet_id)? + .append_u32(expire_at)? + .append_u32(self.seqno)?; + + // Opcode + payload.append_u8(0)?; + + for gift in gifts { + let mut internal_message = + ton_block::Message::with_int_header(ton_block::InternalMessageHeader { + ihr_disabled: true, + bounce: gift.bounce, + dst: gift.destination, + value: gift.amount.into(), + ..Default::default() + }); + + if let Some(body) = gift.body { + internal_message.set_body(body); + } + + if let Some(state_init) = gift.state_init { + internal_message.set_state_init(state_init); + } + + // append it to the body + payload + .append_u8(gift.flags)? + .checked_append_reference(internal_message.serialize()?)?; + } + + let hash = payload.clone().into_cell()?.repr_hash(); + + Ok((hash, payload)) + } +} + +impl TryFrom<&Cell> for InitData { + type Error = anyhow::Error; + + fn try_from(data: &Cell) -> Result { + let mut cs = SliceData::load_cell_ref(data)?; + Ok(Self { + seqno: cs.get_next_u32()?, + subwallet_id: cs.get_next_i32()?, + public_key: UInt256::from_be_bytes(&cs.get_next_bytes(32)?), + }) + } +} + +const SUBWALLET_ID: i32 = 0x29A9A317; + +#[derive(Clone, Copy, PartialEq, Eq)] +pub enum WalletV4Version { + R1, + R2, +} + +#[derive(thiserror::Error, Debug)] +enum WalletV4Error { + #[error("Invalid init data")] + InvalidInitData, + #[error("Account is frozen")] + AccountIsFrozen, + #[error("Too many outgoing messages")] + TooManyGifts, +} + +#[cfg(test)] +mod tests { + use std::str::FromStr; + use ton_block::Deserializable; + use ton_types::UInt256; + + use nekoton_contracts::wallets; + + use crate::core::ton_wallet::wallet_v4::{ + is_wallet_v4r1, is_wallet_v4r2, InitData, WalletV4Version, SUBWALLET_ID, + }; + + #[test] + fn code_hash_v4r1() -> anyhow::Result<()> { + let code_cell = wallets::code::wallet_v4r1(); + + let is_wallet_v4r1 = is_wallet_v4r1(&code_cell.repr_hash()); + assert!(is_wallet_v4r1); + + Ok(()) + } + + #[test] + fn code_hash_v4r2() -> anyhow::Result<()> { + let code_cell = wallets::code::wallet_v4r2(); + + let is_wallet_v4r2 = is_wallet_v4r2(&code_cell.repr_hash()); + assert!(is_wallet_v4r2); + + Ok(()) + } + + #[test] + fn state_init_v4r2() -> anyhow::Result<()> { + let state_init_base64 = "te6ccgECFgEAAwQAAgE0AQIBFP8A9KQT9LzyyAsDAFEAAAAAKamjF2dW1vNw/It5bDWN3jVo5dxzZVk+Q11lVLs3LamPSWAVQAIBIAQFAgFIBgcE+PKDCNcYINMf0x/THwL4I7vyZO1E0NMf0x/T//QE0VFDuvKhUVG68qIF+QFUEGT5EPKj+AAkpMjLH1JAyx9SMMv/UhD0AMntVPgPAdMHIcAAn2xRkyDXSpbTB9QC+wDoMOAhwAHjACHAAuMAAcADkTDjDQOkyMsfEssfy/8SExQVAubQAdDTAyFxsJJfBOAi10nBIJJfBOAC0x8hghBwbHVnvSKCEGRzdHK9sJJfBeAD+kAwIPpEAcjKB8v/ydDtRNCBAUDXIfQEMFyBAQj0Cm+hMbOSXwfgBdM/yCWCEHBsdWe6kjgw4w0DghBkc3RyupJfBuMNCAkCASAKCwB4AfoA9AQw+CdvIjBQCqEhvvLgUIIQcGx1Z4MesXCAGFAEywUmzxZY+gIZ9ADLaRfLH1Jgyz8gyYBA+wAGAIpQBIEBCPRZMO1E0IEBQNcgyAHPFvQAye1UAXKwjiOCEGRzdHKDHrFwgBhQBcsFUAPPFiP6AhPLassfyz/JgED7AJJfA+ICASAMDQBZvSQrb2omhAgKBrkPoCGEcNQICEekk30pkQzmkD6f+YN4EoAbeBAUiYcVnzGEAgFYDg8AEbjJftRNDXCx+AA9sp37UTQgQFA1yH0BDACyMoHy//J0AGBAQj0Cm+hMYAIBIBARABmtznaiaEAga5Drhf/AABmvHfaiaEAQa5DrhY/AAG7SB/oA1NQi+QAFyMoHFcv/ydB3dIAYyMsFywIizxZQBfoCFMtrEszMyXP7AMhAFIEBCPRR8qcCAHCBAQjXGPoA0z/IVCBHgQEI9FHyp4IQbm90ZXB0gBjIywXLAlAGzxZQBPoCFMtqEssfyz/Jc/sAAgBsgQEI1xj6ANM/MFIkgQEI9Fnyp4IQZHN0cnB0gBjIywXLAlAFzxZQA/oCE8tqyx8Syz/Jc/sAAAr0AMntVA=="; + + let state_init = ton_block::StateInit::construct_from_base64(state_init_base64)?; + + let init_data_clone = InitData { + seqno: 0, + subwallet_id: SUBWALLET_ID, + public_key: UInt256::from_str( + "6756d6f370fc8b796c358dde3568e5dc7365593e435d6554bb372da98f496015", + )?, + }; + + let state_init_clone = init_data_clone.make_state_init(WalletV4Version::R2)?; + + assert_eq!(state_init, state_init_clone); + + Ok(()) + } +} diff --git a/src/core/ton_wallet/wallet_v5r1.rs b/src/core/ton_wallet/wallet_v5r1.rs index d970b09b5..b75b5f298 100644 --- a/src/core/ton_wallet/wallet_v5r1.rs +++ b/src/core/ton_wallet/wallet_v5r1.rs @@ -359,9 +359,12 @@ enum WalletV5Error { #[cfg(test)] mod tests { use ed25519_dalek::PublicKey; + use nekoton_contracts::wallets; use ton_block::AccountState; - use crate::core::ton_wallet::wallet_v5r1::{compute_contract_address, InitData, WALLET_ID}; + use crate::core::ton_wallet::wallet_v5r1::{ + compute_contract_address, is_wallet_v5r1, InitData, WALLET_ID, + }; #[test] fn state_init() -> anyhow::Result<()> { @@ -388,4 +391,14 @@ mod tests { Ok(()) } + + #[test] + fn code_hash() -> anyhow::Result<()> { + let code_cell = wallets::code::wallet_v5r1(); + + let is_wallet_v5r1 = is_wallet_v5r1(&code_cell.repr_hash()); + assert!(is_wallet_v5r1); + + Ok(()) + } } diff --git a/src/models.rs b/src/models.rs index a5c891408..cdab59ab2 100644 --- a/src/models.rs +++ b/src/models.rs @@ -60,7 +60,7 @@ pub enum KnownPayload { #[serde(rename_all = "snake_case", tag = "type", content = "data")] pub enum WalletInteractionMethod { WalletV3Transfer, - WalletV5R1Transfer, + TonWalletTransfer, Multisig(Box), } @@ -280,7 +280,7 @@ pub enum TokenWalletTransaction { #[serde(rename_all = "snake_case", tag = "type", content = "data")] pub enum JettonWalletTransaction { Transfer(JettonOutgoingTransfer), - Notify(JettonIncomingTransfer), + InternalTransfer(JettonIncomingTransfer), } #[derive(Clone, Debug, Serialize, Deserialize)] From 11178486e08bc0bf0f688c5205c234dfd575178a Mon Sep 17 00:00:00 2001 From: Alexey Pashinov Date: Tue, 17 Dec 2024 17:23:28 +0100 Subject: [PATCH 09/38] Fix derive wallet address for jetton mintless tokens --- nekoton-abi/Cargo.toml | 2 +- nekoton-contracts/src/jetton/mod.rs | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/nekoton-abi/Cargo.toml b/nekoton-abi/Cargo.toml index 70208fab6..92d512c56 100644 --- a/nekoton-abi/Cargo.toml +++ b/nekoton-abi/Cargo.toml @@ -29,7 +29,7 @@ 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" } -ton_vm = { git = "https://github.com/broxus/ton-labs-vm.git" } +ton_vm = { git = "https://github.com/broxus/ton-labs-vm.git", branch = "feature/jetton" } nekoton-derive = { path = "../nekoton-derive", optional = true } nekoton-utils = { path = "../nekoton-utils" } diff --git a/nekoton-contracts/src/jetton/mod.rs b/nekoton-contracts/src/jetton/mod.rs index 12c64a579..457dc0a74 100644 --- a/nekoton-contracts/src/jetton/mod.rs +++ b/nekoton-contracts/src/jetton/mod.rs @@ -287,6 +287,19 @@ mod tests { let details = contract.get_details()?; assert_eq!(details.admin_address, MsgAddressInt::default()); + let owner = nekoton_utils::unpack_std_smc_addr( + "UQA8aeJrWO-5DZ-1Zs2juDYfT4V_ud2KY8gegMd33gHjeUaF", + true, + )?; + + let address = contract.get_wallet_address(&owner)?; + assert_eq!( + address, + MsgAddressInt::from_str( + "0:3d97d11909a20de878c4400ed241a714065d3a0f4d4f0d60ecaf0dbe11cdd1bc" + )? + ); + Ok(()) } } From 74dbc70b6a6a98e55866c1736fdd5b6bff6b4802 Mon Sep 17 00:00:00 2001 From: Alexey Pashinov Date: Tue, 17 Dec 2024 17:39:06 +0100 Subject: [PATCH 10/38] JettonnWallet optional transaction preload --- src/core/jetton_wallet/mod.rs | 37 ++++++++++++++++++++++++++--------- 1 file changed, 28 insertions(+), 9 deletions(-) diff --git a/src/core/jetton_wallet/mod.rs b/src/core/jetton_wallet/mod.rs index 0d7ef090e..354e46e0f 100644 --- a/src/core/jetton_wallet/mod.rs +++ b/src/core/jetton_wallet/mod.rs @@ -36,6 +36,7 @@ impl JettonWallet { owner: MsgAddressInt, root_token_contract: MsgAddressInt, handler: Arc, + preload_transactions: bool, ) -> Result { let state = match transport.get_contract_state(&root_token_contract).await? { RawContractState::Exists(state) => state, @@ -46,16 +47,34 @@ impl JettonWallet { let state = nekoton_contracts::jetton::RootTokenContract(state.as_context(clock.as_ref())); let address = state.get_wallet_address(&owner)?; - let mut balance = Default::default(); - let contract_subscription = ContractSubscription::subscribe( - clock.clone(), - transport, - address, - &mut make_contract_state_handler(clock.clone(), &mut balance), - Some(&mut make_transactions_handler(handler.as_ref())), - ) - .await?; + + let contract_subscription = { + let handler = handler.as_ref(); + // NOTE: create handler beforehead to prevent lifetime issues + let mut on_transactions_found = match preload_transactions { + true => Some(make_transactions_handler(handler)), + false => None, + }; + + // Manual map is used here due to unsoundness + // See issue: https://github.com/rust-lang/rust/issues/84305 + #[allow(trivial_casts)] + #[allow(clippy::manual_map)] + let on_transactions_found = match &mut on_transactions_found { + Some(handler) => Some(handler as _), + None => None, + }; + + ContractSubscription::subscribe( + clock.clone(), + transport, + address, + &mut make_contract_state_handler(clock.clone(), &mut balance), + on_transactions_found, + ) + .await? + }; handler.on_balance_changed(balance.clone()); From 7aeea57df0b7a315f5c46e0124e164a82240bc89 Mon Sep 17 00:00:00 2001 From: Alexey Pashinov Date: Tue, 17 Dec 2024 18:17:29 +0100 Subject: [PATCH 11/38] Downgrade lazy_static version --- nekoton-jetton/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nekoton-jetton/Cargo.toml b/nekoton-jetton/Cargo.toml index 3a7060df0..f3a4d5b66 100644 --- a/nekoton-jetton/Cargo.toml +++ b/nekoton-jetton/Cargo.toml @@ -11,7 +11,7 @@ edition = "2021" [dependencies] anyhow = "1.0" -lazy_static = "1.5.0" +lazy_static = "1.4" serde = { version = "1.0.183", features = ["derive"] } sha2 = "0.10.8" From ebf6eeba41bd2079ec4c198e68e03f38c82985f3 Mon Sep 17 00:00:00 2001 From: Alexey Pashinov Date: Wed, 18 Dec 2024 13:17:55 +0100 Subject: [PATCH 12/38] JettonWallet contract in case of using this code as library cells --- nekoton-contracts/src/jetton/mod.rs | 20 ++++++++++++++++++ .../src/wallets/code/jetton_wallet.boc | Bin 0 -> 1344 bytes nekoton-contracts/src/wallets/code/mod.rs | 9 ++++++-- nekoton-jetton/Cargo.toml | 4 ++-- 4 files changed, 29 insertions(+), 4 deletions(-) create mode 100644 nekoton-contracts/src/wallets/code/jetton_wallet.boc diff --git a/nekoton-contracts/src/jetton/mod.rs b/nekoton-contracts/src/jetton/mod.rs index 457dc0a74..0d0246498 100644 --- a/nekoton-contracts/src/jetton/mod.rs +++ b/nekoton-contracts/src/jetton/mod.rs @@ -302,4 +302,24 @@ mod tests { Ok(()) } + + #[test] + fn mintless_points_token_wallet_contract() -> anyhow::Result<()> { + let cell = + ton_types::deserialize_tree_of_cells(&mut base64::decode("te6ccgEBAwEAyQACbIAMC6d7f4iHKlXHXBfufxF6w/5pIENHdpy1yJnyM+lsrQQNAl2Gc+Ll0AABc7gAAbghs2ElpgIBANQFAlQL5ACADZRqTnEksRaYvpXRMbgzB92SzFv/19WbfQQgdDo7lYwQA+mfQx3OTMfvDyPCOAYxl9HdjYWqWkQCtdgoLLcHjaDKvtRVlwuLLP8LwzhcDJNm1TPewFBFqmlIYet7ln0NupwfCEICDvGeG/QPK6SS/KrDhu7KWb9oJ6OFBwjZ/NmttoOrwzY=").unwrap().as_slice()) + .unwrap(); + let mut state = nekoton_utils::deserialize_account_stuff(cell)?; + + jetton::update_library_cell(&mut state.storage.state)?; + + let contract = jetton::TokenWalletContract(ExecutionContext { + clock: &SimpleClock, + account_stuff: &state, + }); + + let data = contract.get_details()?; + assert_eq!(data.balance.to_u128().unwrap(), 10000000000); + + Ok(()) + } } diff --git a/nekoton-contracts/src/wallets/code/jetton_wallet.boc b/nekoton-contracts/src/wallets/code/jetton_wallet.boc new file mode 100644 index 0000000000000000000000000000000000000000..09f021b5670603e200fb874898fb9eefed8fcb27 GIT binary patch literal 1344 zcmZ{keM}o=9LJyM?%KO6lshQ2fKjecy3HXgP#haX%3x+gaOK6MD6tbGf8YXzEvSgt zc9q39GRERIvt%Vfqgm9UK>^9^hT4`Rv;!PQbm1j2^^YmJRB~KN>Qk6+3Coi_x!-fS z-{<@N{+`^(@0UB4v2+NCO#sq?4&;I&XoQc!X>1z*lxUTu%GIQb>?Fg~Ug{9_Ej2@J z(mMJe-9@|T>$C`yqaZw_3Qq{WIEcX}49Bo3LWV(3t$}~eljT+MvhQymI8oi508Tfa z8Bvv2F(Ldnw2p0(%V(-|gTW zfMd(#GNsANQJPdC`uP$IS@O)jXwI%(BXy@yH{cBcZ;(V=2oT>*E`=0w3^pgJaMxT0 z2@ixGcug4YH>}hg z8RTw8h0lE3GUF9-#OWen_EgBr^Z~U^!eG0FZ<+SviQq#645HCES>-x*dt4aH7CPq* zV%hGs95JgFzb<_BwJ@6OCfbK10MKc8wPq};hr}*Qi(h54ot9#MO!I4Y*Z{v z6dz_cM*x1`W*D?2r+_`P0Y@W6S>1K7F&FT*p!`zLrB_@at8k`!G%b9?V*{I`>4nDV zGzStvx8l9riP7}qp!k~~H7)wyiy1+3mYkJe5t+$^i$|DA^_y32)Y2~Wmr6|og`;A~=6)|v<)v%?0>BR(6#xJL literal 0 HcmV?d00001 diff --git a/nekoton-contracts/src/wallets/code/mod.rs b/nekoton-contracts/src/wallets/code/mod.rs index 309189d55..3fb3de93e 100644 --- a/nekoton-contracts/src/wallets/code/mod.rs +++ b/nekoton-contracts/src/wallets/code/mod.rs @@ -26,8 +26,9 @@ declare_tvc! { wallet_v5r1 => "./wallet_v5r1_code.boc" (WALLET_V5R1_CODE), highload_wallet_v2 => "./highload_wallet_v2_code.boc" (HIGHLOAD_WALLET_V2_CODE), ever_wallet => "./ever_wallet_code.boc" (EVER_WALLET_CODE), - jetton_wallet_governed => "./jetton_wallet_governed.boc" (JETTON_WALLET_GOVERNED), + jetton_wallet => "./jetton_wallet.boc" (JETTON_WALLET), jetton_wallet_v2 => "./jetton_wallet_v2.boc" (JETTON_WALLET_V2), + jetton_wallet_governed => "./jetton_wallet_governed.boc" (JETTON_WALLET_GOVERNED), } fn load(mut data: &[u8]) -> Cell { @@ -38,7 +39,11 @@ static JETTON_LIBRARY_CELLS: once_cell::sync::Lazy> = once_cell::sync::Lazy::new(|| { let mut m = HashMap::new(); - let codes = [jetton_wallet_governed(), jetton_wallet_v2()]; + let codes = [ + jetton_wallet(), + jetton_wallet_v2(), + jetton_wallet_governed(), + ]; for code in codes { m.insert(code.repr_hash(), code); diff --git a/nekoton-jetton/Cargo.toml b/nekoton-jetton/Cargo.toml index f3a4d5b66..8bae8aa6f 100644 --- a/nekoton-jetton/Cargo.toml +++ b/nekoton-jetton/Cargo.toml @@ -20,8 +20,8 @@ ton_types = { git = "https://github.com/broxus/ton-labs-types.git" } ton_abi = { git = "https://github.com/broxus/ton-labs-abi" } nekoton-utils = { path = "../nekoton-utils" } -num-bigint = "0.4.6" -num-traits = "0.2.19" +num-bigint = "0.4" +num-traits = "0.2" [features] web = ["ton_abi/web"] From 961c7a38e04545ef158902bf99584ae62dbd5969 Mon Sep 17 00:00:00 2001 From: Alexey Pashinov Date: Wed, 18 Dec 2024 15:18:41 +0100 Subject: [PATCH 13/38] Parse JettonNotify event --- src/core/parsing.rs | 28 ++++++++++++++++++++++++++++ src/models.rs | 2 ++ 2 files changed, 30 insertions(+) diff --git a/src/core/parsing.rs b/src/core/parsing.rs index be33d2bc3..38a83132b 100644 --- a/src/core/parsing.rs +++ b/src/core/parsing.rs @@ -222,6 +222,8 @@ pub fn parse_transaction_additional_info( TokenWalletDeployedNotification::try_from(InputMessage(inputs)) .map(TransactionAdditionalInfo::TokenWalletDeployed) .ok() + } else if function_id == JettonNotify::FUNCTION_ID { + JettonNotify::decode_body(body).map(|x| TransactionAdditionalInfo::JettonNotify(x)) } else { None } @@ -263,6 +265,32 @@ impl WalletNotificationFunctions { } } +struct JettonNotify; + +impl JettonNotify { + const FUNCTION_ID: u32 = 0x7362d09c; + + fn decode_body(data: ton_types::SliceData) -> Option { + let mut payload = data; + + let function_id = payload.get_next_u32().ok()?; + assert_eq!(function_id, Self::FUNCTION_ID); + + let _query_id = payload.get_next_u64().ok()?; + + let mut amount = ton_block::Grams::default(); + amount.read_from(&mut payload).ok()?; + + let mut sender = MsgAddressInt::default(); + sender.read_from(&mut payload).ok()?; + + Some(JettonIncomingTransfer { + from: sender, + tokens: BigUint::from(amount.as_u128()), + }) + } +} + impl TryFrom for DePoolOnRoundCompleteNotification { type Error = UnpackerError; diff --git a/src/models.rs b/src/models.rs index cdab59ab2..5de00239d 100644 --- a/src/models.rs +++ b/src/models.rs @@ -28,6 +28,8 @@ pub enum TransactionAdditionalInfo { TokenWalletDeployed(TokenWalletDeployedNotification), /// User interaction with wallet contract WalletInteraction(WalletInteractionInfo), + /// Jetton wallet notification + JettonNotify(JettonIncomingTransfer), } #[derive(Clone, Debug, Serialize, Deserialize)] From eea908ad707e8cd59bbd1978193937a34f4e19e1 Mon Sep 17 00:00:00 2001 From: Alexey Pashinov Date: Thu, 19 Dec 2024 14:54:12 +0100 Subject: [PATCH 14/38] Fix the attached amount in jetton transfer --- src/core/jetton_wallet/mod.rs | 100 +++------------------------------- 1 file changed, 8 insertions(+), 92 deletions(-) diff --git a/src/core/jetton_wallet/mod.rs b/src/core/jetton_wallet/mod.rs index 354e46e0f..7de602522 100644 --- a/src/core/jetton_wallet/mod.rs +++ b/src/core/jetton_wallet/mod.rs @@ -109,99 +109,15 @@ impl JettonWallet { pub async fn estimate_min_attached_amount( &self, - amount: BigUint, - destination: MsgAddressInt, - remaining_gas_to: MsgAddressInt, - custom_payload: Option, - callback_value: BigUint, - callback_payload: Option, + _amount: BigUint, + _destination: MsgAddressInt, + _remaining_gas_to: MsgAddressInt, + _custom_payload: Option, + _callback_value: BigUint, + _callback_payload: Option, ) -> Result { - const FEE_MULTIPLIER: u128 = 2; - - // Prepare internal message - let internal_message = self.prepare_transfer( - amount, - destination, - remaining_gas_to, - custom_payload, - callback_value, - callback_payload, - 0, - )?; - - let mut message = ton_block::Message::with_int_header(ton_block::InternalMessageHeader { - src: ton_block::MsgAddressIntOrNone::Some( - internal_message - .source - .unwrap_or_else(|| self.owner.clone()), - ), - dst: internal_message.destination, - ..Default::default() - }); - - message.set_body(internal_message.body.clone()); - - // Prepare executor - let transport = self.contract_subscription.transport().clone(); - let config = transport - .get_blockchain_config(self.clock.as_ref(), true) - .await?; - - let mut tree = TransactionsTreeStream::new(message, config, transport, self.clock.clone()); - tree.unlimited_account_balance(); - tree.unlimited_message_balance(); - - type Err = fn(Option) -> JettonWalletError; - let check_exit_code = |tx: &ton_block::Transaction, err: Err| -> Result<()> { - let descr = tx.read_description()?; - if descr.is_aborted() { - let exit_code = match descr { - ton_block::TransactionDescr::Ordinary(descr) => match descr.compute_ph { - ton_block::TrComputePhase::Vm(phase) => Some(phase.exit_code), - ton_block::TrComputePhase::Skipped(_) => None, - }, - _ => None, - }; - Err(err(exit_code).into()) - } else { - Ok(()) - } - }; - - let mut attached_amount: u128 = 0; - - // Simulate source transaction - let source_tx = tree.next().await?.ok_or(JettonWalletError::NoSourceTx)?; - check_exit_code(&source_tx, JettonWalletError::SourceTxFailed)?; - attached_amount += source_tx.total_fees.grams.as_u128(); - - if source_tx.outmsg_cnt == 0 { - return Err(JettonWalletError::NoDestTx.into()); - } - - if let Some(message) = tree.peek() { - if message.state_init().is_some() && message.src_ref() == Some(self.address()) { - // Simulate first deploy transaction - // NOTE: we don't need to count attached amount here because of separate `initial_balance` - let _ = tree.next().await?.ok_or(JettonWalletError::NoDestTx)?; - //also we ignore non zero exit code for deploy transactions - } - } - - tree.retain_message_queue(|message| { - message.state_init().is_none() && message.src_ref() == Some(self.address()) - }); - - if tree.message_queue().len() != 1 { - return Err(JettonWalletError::NoDestTx.into()); - } - - // Simulate destination transaction - let dest_tx = tree.next().await?.ok_or(JettonWalletError::NoDestTx)?; - check_exit_code(&dest_tx, JettonWalletError::DestinationTxFailed)?; - attached_amount += dest_tx.total_fees.grams.as_u128(); - - Ok((attached_amount * FEE_MULTIPLIER) as u64) + const ATTACHED_AMOUNT: u64 = 50_000_000; // 0.05 TON + Ok(ATTACHED_AMOUNT) } pub fn prepare_transfer( From 85a0ed4e6cb98263d5c3983a461d09fd3e264024 Mon Sep 17 00:00:00 2001 From: Alexey Pashinov Date: Thu, 19 Dec 2024 17:29:14 +0100 Subject: [PATCH 15/38] Rework fee estimation for jetton transfers --- src/core/jetton_wallet/mod.rs | 42 ++++++++++++++++++----------------- src/core/parsing.rs | 42 +++++++++++++++++++++++++++++++++-- 2 files changed, 62 insertions(+), 22 deletions(-) diff --git a/src/core/jetton_wallet/mod.rs b/src/core/jetton_wallet/mod.rs index 7de602522..1441d1247 100644 --- a/src/core/jetton_wallet/mod.rs +++ b/src/core/jetton_wallet/mod.rs @@ -12,7 +12,6 @@ use ton_types::{BuilderData, IBitstring, SliceData}; use crate::core::models::*; use crate::core::parsing::*; -use crate::core::transactions_tree::TransactionsTreeStream; use crate::transport::models::{RawContractState, RawTransaction}; use crate::transport::Transport; @@ -25,6 +24,7 @@ pub struct JettonWallet { clock: Arc, contract_subscription: ContractSubscription, handler: Arc, + root: MsgAddressInt, owner: MsgAddressInt, balance: BigUint, } @@ -84,6 +84,7 @@ impl JettonWallet { handler, owner, balance, + root: root_token_contract, }) } @@ -107,17 +108,26 @@ impl JettonWallet { self.contract_subscription.contract_state() } - pub async fn estimate_min_attached_amount( - &self, - _amount: BigUint, - _destination: MsgAddressInt, - _remaining_gas_to: MsgAddressInt, - _custom_payload: Option, - _callback_value: BigUint, - _callback_payload: Option, - ) -> Result { - const ATTACHED_AMOUNT: u64 = 50_000_000; // 0.05 TON - Ok(ATTACHED_AMOUNT) + pub async fn estimate_min_attached_amount(&self, destination: MsgAddressInt) -> Result { + let transport = self.contract_subscription.transport(); + + let state = match transport.get_contract_state(&self.root).await? { + RawContractState::Exists(state) => state, + RawContractState::NotExists { .. } => { + return Err(JettonWalletError::InvalidRootTokenContract.into()) + } + }; + let state = + nekoton_contracts::jetton::RootTokenContract(state.as_context(self.clock.as_ref())); + + let token_wallet = state.get_wallet_address(&destination)?; + + let attached_amount = match transport.get_contract_state(&token_wallet).await? { + RawContractState::Exists(_) => 50_000_000, // 0.05 TON + RawContractState::NotExists { .. } => 100_000_000, // 0.1 TON + }; + + Ok(attached_amount) } pub fn prepare_transfer( @@ -414,12 +424,4 @@ enum JettonWalletError { WalletNotDeployed, #[error("Failed to convert grams")] TryFromGrams, - #[error("No source transaction produced")] - NoSourceTx, - #[error("No destination transaction produced")] - NoDestTx, - #[error("Source transaction failed with exit code {0:?}")] - SourceTxFailed(Option), - #[error("Destination transaction failed with exit code {0:?}")] - DestinationTxFailed(Option), } diff --git a/src/core/parsing.rs b/src/core/parsing.rs index 38a83132b..b2508bda0 100644 --- a/src/core/parsing.rs +++ b/src/core/parsing.rs @@ -4,7 +4,7 @@ use anyhow::Result; use num_bigint::BigUint; use once_cell::race::OnceBox; use ton_block::{Deserializable, MsgAddressInt}; -use ton_types::UInt256; +use ton_types::{SliceData, UInt256}; use nekoton_abi::*; use nekoton_contracts::tip4_1::nft_contract; @@ -667,12 +667,29 @@ pub fn parse_jetton_transaction( tx: &ton_block::Transaction, description: &ton_block::TransactionDescrOrdinary, ) -> Option { + const STANDART_JETTON_CELLS: usize = 0; + const BROXUS_JETTON_CELLS: usize = 2; + if description.aborted { return None; } let in_msg = tx.in_msg.as_ref()?.read_struct().ok()?; - let mut body = in_msg.body()?; + + let mut body = { + let body = in_msg.body()?; + let refs = body.remaining_references(); + + match refs { + BROXUS_JETTON_CELLS => { + let cell = body.reference_opt(1)?; + SliceData::load_cell(cell).ok()? + } + STANDART_JETTON_CELLS => body, + _ => return None, + } + }; + let opcode = body.get_next_u32().ok()?; if opcode != JETTON_TRANSFER_OPCODE && opcode != JETTON_INTERNAL_TRANSFER_OPCODE { @@ -1196,4 +1213,25 @@ mod tests { ); } } + + #[test] + fn test_jetton_dai() { + let (tx, description) = parse_transaction("te6ccgECMwEACSYAE7X6OrhdT/i8Xg5V59GCudDIUbfPGKdcsPDkBnz5sIW2KwANc8U0Lbg0OqOeC1k7UXbTLixtVq4SI7H/omNX5HX5P6tAAAL0iVD+5BlH1wTijl995XiSwIUamhbAfUS7wPTkzLgkcK+YXBRzcAAC6yrXI3QWdi33AAA0aZo8yAUEAQIbBMMKt8kAklVUGGexJBEDAgBvyYSs4EwMd5wAAAAAAAQAAgAAAAJeYRuqD+MYW6CcXXPMdGrn9t4c/56IAT6BUHkIdCYe6kCQI4wAnkTsTAXadAAAAAAAAAAAwwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgnJxmvJY1QBTQnReXi0IzeVfBKM3EmbwB66VehPHyjVmS14lhxOkukViJ2a2JhA539HQH7HMCshzxLct0ZeLetD2AgHgCQYBAd8HAbFIAHimhbcGh1RzwWsnai7aZcWNqtXCRHY/9Exq/I6/J/VpABso1JziSWItMX0romNwZg+7JZi3/6+rNvoIQOh0dysYEAgJycAGDHeyAABekSof3ITOxb7gwAgAbHNi0JxUbeTvq2fsF4AWNFeF2KAACABZ6outPe5mOMb5eXVGsY+rBO9DauJvEObaxIa79AoZ2gKxaAHly5UvnpC63rNiDsQtIAxFgAG8/zqu4NLbRFJP7NzIRQAPFNC24NDqjngtZO1F20y4sbVauEiOx/6JjV+R1+T+rRAJJVVABpGyNAAAXpEpxE8EzsW+zeAnCgIBwAwLALEXjUUZVG3k76tn7BeAFjRXhdigAAgAWeqLrT3uZjjG+Xl1RrGPqwTvQ2ribxDm2sSGu/QKGdsACz1Rdae9zMcY3y8uqNYx9WCd6G1cTeIc21iQ136BQztEBQFLAAAAAYAFnqi6097mY4xvl5dUaxj6sE70Nq4m8Q5trEhrv0ChnbANART/APSkE/S88sgLDgIBYhIPAgFmERAAI7dgXaiaH0AfSB9IGpqaY+YLcAAltxSdqJofQB9IH0gamppj5g2IUAICxRUTAfKqgjHtRND6APpA+kDU1NMfMAnTP/oAUXGgB/pA+kD6AFRzhnDIyVQTAyMQRgHIUAb6AlAEzxZYzxbMzMsfyXAgyMsBE/QA9ADLAMn5AHB0yMsCygfL/8nQU57HBQ/HBR6x8uLDUbqhggiYloBctgihggiYloCgG6EKFAH4ggiYloC2CXL7AiqOMDA4OIIQc2LQnMjLH8s/UAf6AlAFzxZQBs8WyXGAEMjLBSPPFnD6AstqzMmBAIL7AI44Ols4JtcLAcMABsIAFrCOIoIQ1TJ223CAEMjLBVAIzxYn+gIXy2oWyx8Wyz/JgQCC+wCSNTXiECPiBFAzBSACAcsZFgIBzhgXAJk7UTQ+gD6QPpA1NTTHzAGgCDXIdMfghAXjUUZUiC6ghB73ZfeE7oSsfLixYBA1yH6ADAVoAUQNEEwyFAG+gJQBM8WWM8WzMzLH8ntVIACLO1E0PoA+kD6QNTU0x8wEEVfBQHHBfLiwYIImJaAcPsC0z+CENUydttwgBDIywUD+kAwE88WIvoCEstqyx/LP8mBAIL7AIAIBICMaAgFYHhsCASAdHACtO1E0PoA+kD6QNTU0x8wMFImxwXy4sEF0z/6QNQB+wTTHzBHYMhQBvoCUATPFljPFszMyx/J7VSCENUydttwgBDIywVQA88WIvoCEstqyx/LP8mAQvsAgAJc7UTQ+gD6QPpA1NTTHzA1W1IUxwXy4sED0z/6QDCCEBT9raDIyx8Syz9QBM8WUAPPFhLLH8lxgBDIywVQA88WcPoCEstqzMmAQPsAgAgEgIR8B8TtRND6APpA+kDU1NMfMAnTP/oA+kD0BCDXSYEBC75RpKFSnscF8uLBLML/8uLCCoIJMS0AoBu88uLDghB73ZfeyMsfE8s/AfoCJc8WAc8WF/QABJgE+kAwE88WApE04gLJcYAYyMsFJM8WcPoCy2rMyYBA+wAEUDWAgACbIUAb6AlAEzxZYzxbMzMsfye1UAfcA9M/+gD6QCHwAe1E0PoA+kD6QNTU0x8wUVihUkzHBfLiwSrC//LiwlQ2JnDIyVQTAyMQRgHIUAb6AlAEzxZYzxbMzMsfyXAgyMsBE/QA9ADLAMkg+QBwdMjLAsoHy//J0Ab6QPQEMfoAINdJwgDy4sSCEBeNRRnIyx8cgIgDayz9QCvoCJc8WIc8WKfoCUArPFsklyMsfUArPFlIgzMnIzBn0AMl3gBjIywVQB88WcPoCFstrGMwUzMklkXKRceJQCqgVoIIJycOAoBa88uLFBoBA+wBQBAUDyFAG+gJQBM8WWM8WzMzLH8ntVAIB1CUkABE+kQwcLry4U2AB9ztou37IMcAkl8E4AHQ0wMBcbCVE18D8BHg+kD6QDH6ADFx1yH6ADH6ADBzqbQAItdJwAGOEALUMfQEMCBulF8F2zHg0ALeAtMfghAPin6lUiC6lTE0WfAM4IIQF41FGVIgupcxREQD8QaC4DWCEFlfB7xSELqUMFnwDeCAmAFpsIoIQfW/yVFIQupMw8A7gggs2kZlSELqTMPAP4IIQHqYO/7qS8BDgW4QP8vACATQqKAKPCADZRqTnEksRaYvpXRMbgzB92SzFv/19WbfQQgdDo7lYwQAn0OvZ2MFmsP6fKK8J/1CZcTuNw7+lIXVRq1uB6p0gVUAAAAAgKSoAAAEU/wD0pBP0vPLICysCAsgtLAAQqoJfBYQP8vACAc0vLgAptuEAIZGWCrGeLEP0BZbVkwIBQfYBAVvZBjgEkvgnAA6GmBmP0gAWoA6Gppj/0gGAH6Ahh2omh9AH0gfSBqGOoYKjwIE8MAL+XccFk18Ef45MIG6TXwRw4NDTHzHTPzH6ADH6QDBZcMjJVBMDIxBGAchQBvoCUATPFljPFszMyx/JcCDIywET9AD0AMsAyfkAcHTIywLKB8v/ydDHBeKWEHtfC/AL4TdANFRFd8hQBvoCUATPFljPFszMyx/J7VQg+wQhbuMC0DIxAEbtHu1TAvpAMfoAMXHXIfoAMfoAMHOptAAC0NMfMUREA/EGggAEXwY="); + assert!(!description.aborted); + + let jetton_transaction = parse_jetton_transaction(&tx, &description).unwrap(); + + if let JettonWalletTransaction::InternalTransfer(JettonIncomingTransfer { from, tokens }) = + jetton_transaction + { + assert_eq!(tokens.to_u128().unwrap(), 100000000000000000); + assert_eq!( + from, + MsgAddressInt::from_str( + "0:2cf545d69ef7331c637cbcbaa358c7d58277a1b5713788736d62435dfa050ced" + ) + .unwrap() + ); + } + } } From caa992a4632ffec32bf7530be645d5b60b394427 Mon Sep 17 00:00:00 2001 From: Alexey Pashinov Date: Thu, 19 Dec 2024 19:30:47 +0100 Subject: [PATCH 16/38] Add hamster combat code contract --- nekoton-contracts/src/jetton/mod.rs | 20 ++++++++++++++++++ .../code/jetton_wallet_hamster_combat.boc | Bin 0 -> 1347 bytes nekoton-contracts/src/wallets/code/mod.rs | 2 ++ 3 files changed, 22 insertions(+) create mode 100644 nekoton-contracts/src/wallets/code/jetton_wallet_hamster_combat.boc diff --git a/nekoton-contracts/src/jetton/mod.rs b/nekoton-contracts/src/jetton/mod.rs index 0d0246498..0f9967f10 100644 --- a/nekoton-contracts/src/jetton/mod.rs +++ b/nekoton-contracts/src/jetton/mod.rs @@ -322,4 +322,24 @@ mod tests { Ok(()) } + + #[test] + fn hamster_token_wallet_contract() -> anyhow::Result<()> { + let cell = + ton_types::deserialize_tree_of_cells(&mut base64::decode("te6ccgEBAwEAyQACbIAKqccjBo+00V2Pb7qZhRYSHX52cx1iP9tpON3cdZrkP8QNAl2GdhkS0AABegbul1whs2ElpgIBANQFGHJ82gCACGZPh6infgRlai2q2zEzj6/XTCUYYz5sBXNuHUXFkiawACfLlnexAarJqUlmkXX/yPvEfPlx8Id4LDSocvlK3az1CNK1yFN5P0+WKSDutZY4tqmGqAE7w+lQchEcy4oOjEQUCEICDxrT2KRr0oMyHd5jkZX7cmAumzGxcn/swl4u3BCWbfQ=").unwrap().as_slice()) + .unwrap(); + let mut state = nekoton_utils::deserialize_account_stuff(cell)?; + + jetton::update_library_cell(&mut state.storage.state)?; + + let contract = jetton::TokenWalletContract(ExecutionContext { + clock: &SimpleClock, + account_stuff: &state, + }); + + let data = contract.get_details()?; + assert_eq!(data.balance.to_u128().unwrap(), 105000000000); + + Ok(()) + } } diff --git a/nekoton-contracts/src/wallets/code/jetton_wallet_hamster_combat.boc b/nekoton-contracts/src/wallets/code/jetton_wallet_hamster_combat.boc new file mode 100644 index 0000000000000000000000000000000000000000..f9fe2fcfb30e6aa863e871003c3f44aa7721382d GIT binary patch literal 1347 zcmZ`(e@q)y9Dncb+Upg{9TZwX9ajpBO=Ja%V?v||GYr9%f(a-Q#K<4GKw$=TGHg3! zaZ|F6#cgKEN`i@I{umk%D$#DJZ8@@bfWwF`6cSVan378+$91v3mia4gdCANBzL)p? z{Cas4zh3AKVCfJL5dhLb7RU!BP!Au4^VmH8G0`R2A-$hWBl}5FRv@dE&B>N1jM_~Z zC<}Fh`kML!CPzSMOc|QtJ#i3&?HG<>bA$wgtV#`kUm`1O<0ao--FK+2KLH$TIXux)11SDg-@eeudy-Bt>g@<3m~eMt+`i&g`QsqX zZ;&zy-y{~xArygV(1=w!7vKYRx2pV$cyvcJEJmR4+#&^q_Uoqm+!B8EfUsMrc#zrK zyi(SksbjZ*`6UmB&5>M$5Dg*1V$AG1U0AxFAQu74F2{j)m!EJ!=z^E|al3A6Wne(qV@#il5c^q%Ma4FLJazYr?EU1!1KQAwIo$5Q zfIP^KT0q2dQ^H`!6jFMPvh)Ji7`L3{U|*HHwz^(7p-Pl z)J$X_W-*2Ve$QqYY)Vc6yB2)b7MZNo(QuSLi+B5_=LgQeItaAI*ZQZ@LRVa7aC<7f zSRa{ZK_cjvzmq>RmEHzQzxrO?nKN`Q!%r@d3(|`MJ)3Z@nx0j?dGU&g8blveY8oh> q8r`u}r_hx<=rMXsQNEEe^nmVE#?V748zrE4`iE=xIy~BzJNYM{GaX$3 literal 0 HcmV?d00001 diff --git a/nekoton-contracts/src/wallets/code/mod.rs b/nekoton-contracts/src/wallets/code/mod.rs index 3fb3de93e..393d70aeb 100644 --- a/nekoton-contracts/src/wallets/code/mod.rs +++ b/nekoton-contracts/src/wallets/code/mod.rs @@ -29,6 +29,7 @@ declare_tvc! { jetton_wallet => "./jetton_wallet.boc" (JETTON_WALLET), jetton_wallet_v2 => "./jetton_wallet_v2.boc" (JETTON_WALLET_V2), jetton_wallet_governed => "./jetton_wallet_governed.boc" (JETTON_WALLET_GOVERNED), + jetton_wallet_hamster_combat => "./jetton_wallet_hamster_combat.boc" (JETTON_WALLET_HAMSTER_COMBAT), } fn load(mut data: &[u8]) -> Cell { @@ -43,6 +44,7 @@ static JETTON_LIBRARY_CELLS: once_cell::sync::Lazy> = jetton_wallet(), jetton_wallet_v2(), jetton_wallet_governed(), + jetton_wallet_hamster_combat(), ]; for code in codes { From 9d9581ca053301c3b7b97214470a867e0c0d6c73 Mon Sep 17 00:00:00 2001 From: Alexey Pashinov Date: Thu, 19 Dec 2024 22:11:56 +0100 Subject: [PATCH 17/38] JettonWalletData method for standalone client --- src/core/jetton_wallet/mod.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/core/jetton_wallet/mod.rs b/src/core/jetton_wallet/mod.rs index 1441d1247..1d8caa1db 100644 --- a/src/core/jetton_wallet/mod.rs +++ b/src/core/jetton_wallet/mod.rs @@ -328,6 +328,21 @@ pub async fn get_token_wallet_details( Ok((token_wallet_details, root_contract_details)) } +pub fn get_wallet_data(account: ton_block::AccountStuff) -> Result { + let mut account = account; + + nekoton_contracts::jetton::update_library_cell(&mut account.storage.state)?; + + let token_wallet_state = nekoton_contracts::jetton::TokenWalletContract(ExecutionContext { + clock: &SimpleClock, + account_stuff: &account, + }); + + let token_wallet_details = token_wallet_state.get_details()?; + + Ok(token_wallet_details) +} + pub async fn get_token_root_details( clock: &dyn Clock, transport: &dyn Transport, From 6af81e4a7385954ba943efc26ed885123df8d32d Mon Sep 17 00:00:00 2001 From: Alexey Pashinov Date: Fri, 20 Dec 2024 12:58:23 +0100 Subject: [PATCH 18/38] Download contract code for accounts with library cells --- Cargo.toml | 3 +- nekoton-contracts/src/jetton/mod.rs | 251 +------------- .../src/wallets/code/jetton_wallet.boc | Bin 1344 -> 0 bytes .../wallets/code/jetton_wallet_governed.boc | Bin 1023 -> 0 bytes .../code/jetton_wallet_hamster_combat.boc | Bin 1347 -> 0 bytes .../src/wallets/code/jetton_wallet_v2.boc | Bin 942 -> 0 bytes nekoton-contracts/src/wallets/code/mod.rs | 29 +- src/core/jetton_wallet/mod.rs | 309 +++++++++++++++++- src/core/parsing.rs | 2 +- 9 files changed, 304 insertions(+), 290 deletions(-) delete mode 100644 nekoton-contracts/src/wallets/code/jetton_wallet.boc delete mode 100644 nekoton-contracts/src/wallets/code/jetton_wallet_governed.boc delete mode 100644 nekoton-contracts/src/wallets/code/jetton_wallet_hamster_combat.boc delete mode 100644 nekoton-contracts/src/wallets/code/jetton_wallet_v2.boc diff --git a/Cargo.toml b/Cargo.toml index a246ea3fb..873b4ada7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,6 +42,7 @@ parking_lot = "0.12.0" pbkdf2 = { version = "0.12.2", optional = true } quick_cache = "0.4.1" rand = { version = "0.8", features = ["getrandom"], optional = true } +reqwest = { version = "0.11.8", features = ["blocking", "json"], optional = true } secstr = { version = "0.5.0", features = ["serde"], optional = true } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" @@ -87,7 +88,7 @@ proto_transport = ["dep:nekoton-proto"] extended_models = [] non_threadsafe = [] wallet_core = ["dep:pbkdf2", "dep:chacha20poly1305", "dep:zeroize", "dep:secstr", "dep:hmac", "dep:ed25519-dalek", - "dep:tiny-bip39", "dep:tiny-hderive", "dep:sha2", "dep:getrandom", "dep:rand", "dep:curve25519-dalek-ng", "nekoton-utils/encryption"] + "dep:tiny-bip39", "dep:tiny-hderive", "dep:sha2", "dep:getrandom", "dep:rand", "dep:curve25519-dalek-ng", "dep:reqwest", "nekoton-utils/encryption"] [package.metadata.docs.rs] all-features = true diff --git a/nekoton-contracts/src/jetton/mod.rs b/nekoton-contracts/src/jetton/mod.rs index 0f9967f10..86cee124b 100644 --- a/nekoton-contracts/src/jetton/mod.rs +++ b/nekoton-contracts/src/jetton/mod.rs @@ -1,13 +1,11 @@ use nekoton_abi::num_bigint::BigUint; use nekoton_abi::{ExecutionContext, StackItem}; -use ton_block::{AccountState, Deserializable, Serializable}; -use ton_types::{CellType, SliceData, UInt256}; +use ton_block::Serializable; +use ton_types::SliceData; pub use root_token_contract::JettonRootData; pub use token_wallet_contract::JettonWalletData; -use crate::wallets; - mod root_token_contract; mod token_wallet_contract; @@ -98,248 +96,3 @@ impl<'a> TokenWalletContract<'a> { Ok(data) } } - -pub fn update_library_cell(state: &mut AccountState) -> anyhow::Result<()> { - if let AccountState::AccountActive { ref mut state_init } = state { - if let Some(cell) = &state_init.code { - if cell.cell_type() == CellType::LibraryReference { - let mut slice_data = SliceData::load_cell(cell.clone())?; - - // Read Library Cell Tag - let tag = slice_data.get_next_byte()?; - assert_eq!(tag, 2); - - // Read Code Hash - let mut hash = UInt256::default(); - hash.read_from(&mut slice_data)?; - - if let Some(cell) = wallets::code::get_jetton_library_cell(&hash) { - state_init.set_code(cell.clone()); - } - } - } - } - - Ok(()) -} - -#[cfg(test)] -mod tests { - use std::str::FromStr; - - use nekoton_abi::num_bigint::BigUint; - use nekoton_abi::num_traits::{FromPrimitive, ToPrimitive}; - use nekoton_abi::ExecutionContext; - use nekoton_utils::SimpleClock; - use ton_block::MsgAddressInt; - - use crate::jetton; - - #[test] - fn usdt_root_token_contract() -> anyhow::Result<()> { - let cell = ton_types::deserialize_tree_of_cells(&mut base64::decode("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").unwrap().as_slice()).unwrap(); - let state = nekoton_utils::deserialize_account_stuff(cell)?; - - let contract = jetton::RootTokenContract(ExecutionContext { - clock: &SimpleClock, - account_stuff: &state, - }); - - let total_supply = contract.total_supply()?; - assert_eq!(total_supply, BigUint::from_u128(1229976002510000).unwrap()); - - Ok(()) - } - - #[test] - fn tonup_root_token_contract() -> anyhow::Result<()> { - let cell = ton_types::deserialize_tree_of_cells(&mut base64::decode("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").unwrap().as_slice()).unwrap(); - let state = nekoton_utils::deserialize_account_stuff(cell)?; - - let contract = jetton::RootTokenContract(ExecutionContext { - clock: &SimpleClock, - account_stuff: &state, - }); - - let name = contract.name()?; - assert_eq!(name.unwrap(), "TonUP"); - - let symbol = contract.symbol()?; - assert_eq!(symbol.unwrap(), "UP"); - - let decimals = contract.decimals()?; - assert_eq!(decimals.unwrap(), 9); - - let total_supply = contract.total_supply()?; - assert_eq!(total_supply, BigUint::from_u128(56837335582855498).unwrap()); - - let wallet_code = contract.wallet_code()?; - let expected_code = ton_types::deserialize_tree_of_cells(&mut base64::decode("te6cckECEQEAAyMAART/APSkE/S88sgLAQIBYgIDAgLMBAUAG6D2BdqJofQB9IH0gahhAgHUBgcCASAICQDDCDHAJJfBOAB0NMDAXGwlRNfA/AM4PpA+kAx+gAxcdch+gAx+gAwc6m0AALTH4IQD4p+pVIgupUxNFnwCeCCEBeNRRlSILqWMUREA/AK4DWCEFlfB7y6k1nwC+BfBIQP8vCAAET6RDBwuvLhTYAIBIAoLAIPUAQa5D2omh9AH0gfSBqGAJpj8EIC8aijKkQXUEIPe7L7wndCVj5cWLpn5j9ABgJ0CgR5CgCfQEsZ4sA54tmZPaqQB8VA9M/+gD6QCHwAe1E0PoA+kD6QNQwUTahUirHBfLiwSjC//LiwlQ0QnBUIBNUFAPIUAT6AljPFgHPFszJIsjLARL0APQAywDJIPkAcHTIywLKB8v/ydAE+kD0BDH6ACDXScIA8uLEd4AYyMsFUAjPFnD6AhfLaxPMgMAgEgDQ4AnoIQF41FGcjLHxnLP1AH+gIizxZQBs8WJfoCUAPPFslQBcwjkXKRceJQCKgToIIJycOAoBS88uLFBMmAQPsAECPIUAT6AljPFgHPFszJ7VQC9ztRND6APpA+kDUMAjTP/oAUVGgBfpA+kBTW8cFVHNtcFQgE1QUA8hQBPoCWM8WAc8WzMkiyMsBEvQA9ADLAMn5AHB0yMsCygfL/8nQUA3HBRyx8uLDCvoAUaihggiYloBmtgihggiYloCgGKEnlxBJEDg3XwTjDSXXCwGAPEADXO1E0PoA+kD6QNQwB9M/+gD6QDBRUaFSSccF8uLBJ8L/8uLCBYIJMS0AoBa88uLDghB73ZfeyMsfFcs/UAP6AiLPFgHPFslxgBjIywUkzxZw+gLLaszJgED7AEATyFAE+gJYzxYBzxbMye1UgAHBSeaAYoYIQc2LQnMjLH1Iwyz9Y+gJQB88WUAfPFslxgBDIywUkzxZQBvoCFctqFMzJcfsAECQQIwB8wwAjwgCwjiGCENUydttwgBDIywVQCM8WUAT6AhbLahLLHxLLP8ly+wCTNWwh4gPIUAT6AljPFgHPFszJ7VSV6u3X")?.as_slice())?; - assert_eq!(wallet_code, expected_code); - - let token_address = contract.get_wallet_address(&MsgAddressInt::default())?; - assert_eq!( - token_address.to_string(), - "0:0c6a835483369275c9ae76e7e31d9eda0845368045a8ec2ed78609d96bb0a087" - ); - - Ok(()) - } - - #[test] - fn usdt_wallet_token_contract() -> anyhow::Result<()> { - let cell = ton_types::deserialize_tree_of_cells(&mut base64::decode("te6ccgEBAwEAqAACbIAXsqVXAuRG6+GFp/25WVl2IsmatSkX0jbrXVjoBOwsnEQNAdiGdFv5kAABdRDp2cQZrn10JgIBAJEFJFfQYxaABHulQdJwYfnHP5r0FXhq3wjit36+D+zzx7bkE76OQgrwAsROplLUCShZxn2kTkyjrdZWWw4ol9ZAosUb+zcNiHf6CEICj0Utek39dAZraCNlF3JZ7QVzRDW+drX9S9XYryt8PWg=").unwrap().as_slice()).unwrap(); - let mut state = nekoton_utils::deserialize_account_stuff(cell)?; - - jetton::update_library_cell(&mut state.storage.state)?; - - let contract = jetton::TokenWalletContract(ExecutionContext { - clock: &SimpleClock, - account_stuff: &state, - }); - - let balance = contract.balance()?; - assert_eq!(balance.to_u128().unwrap(), 156092097302); - - Ok(()) - } - - #[test] - fn notcoin_wallet_token_contract() -> anyhow::Result<()> { - let cell = ton_types::deserialize_tree_of_cells(&mut base64::decode("te6ccgEBAwEAqgACbIAX5XxfY9N6rJiyOS4NGQc01nd0dzEnWBk87cdqg9bLTwQNAeCGdH/3UAABdXbIjToZrn5eJgIBAJUHFxcOBj4fBYAfGfo6PQWliRZGmmqpYpA1QxmYkyLZonLf41f59x68XdAAvlWFDxGF2lXm67y4yzC17wYKD9A0guwPkMs1gOsM//IIQgK6KRjIlH6bJa+awbiDNXdUFz5YEvgHo9bmQqFHCVlTlQ==").unwrap().as_slice()).unwrap(); - let mut state = nekoton_utils::deserialize_account_stuff(cell)?; - - jetton::update_library_cell(&mut state.storage.state)?; - - let contract = jetton::TokenWalletContract(ExecutionContext { - clock: &SimpleClock, - account_stuff: &state, - }); - - let balance = contract.balance()?; - assert_eq!(balance.to_u128().unwrap(), 6499273466060549); - - Ok(()) - } - - #[test] - fn tonup_wallet_token_contract() -> anyhow::Result<()> { - let cell = ton_types::deserialize_tree_of_cells(&mut base64::decode("te6ccgECEwEAA6kAAm6ADhM4pof2jbUfk8bYdFfLChAB6cUG0z98g+xOrlf4LCzkTQz7BmMVjoAAAVA6z3tMKgC1Y0MmAgEBj0dzWUAIAc+mHGAhdljL3SZo8QcvtpqlV+kMrO8+wlbsMF9hxLGVACvaf0JMrv6BOvPpAgee08uQM/uRpUd//fhA7Nm8twzvYAIBFP8A9KQT9LzyyAsDAgFiBQQAG6D2BdqJofQB9IH0gahhAgLMEAYCASAIBwCD1AEGuQ9qJofQB9IH0gahgCaY/BCAvGooypEF1BCD3uy+8J3QlY+XFi6Z+Y/QAYCdAoEeQoAn0BLGeLAOeLZmT2qkAgEgDgkCASALCgDXO1E0PoA+kD6QNQwB9M/+gD6QDBRUaFSSccF8uLBJ8L/8uLCBYIJMS0AoBa88uLDghB73ZfeyMsfFcs/UAP6AiLPFgHPFslxgBjIywUkzxZw+gLLaszJgED7AEATyFAE+gJYzxYBzxbMye1UgAvc7UTQ+gD6QPpA1DAI0z/6AFFRoAX6QPpAU1vHBVRzbXBUIBNUFAPIUAT6AljPFgHPFszJIsjLARL0APQAywDJ+QBwdMjLAsoHy//J0FANxwUcsfLiwwr6AFGooYIImJaAZrYIoYIImJaAoBihJ5cQSRA4N18E4w0l1wsBgDQwAfMMAI8IAsI4hghDVMnbbcIAQyMsFUAjPFlAE+gIWy2oSyx8Syz/JcvsAkzVsIeIDyFAE+gJYzxYBzxbMye1UAHBSeaAYoYIQc2LQnMjLH1Iwyz9Y+gJQB88WUAfPFslxgBDIywUkzxZQBvoCFctqFMzJcfsAECQQIwHxUD0z/6APpAIfAB7UTQ+gD6QPpA1DBRNqFSKscF8uLBKML/8uLCVDRCcFQgE1QUA8hQBPoCWM8WAc8WzMkiyMsBEvQA9ADLAMkg+QBwdMjLAsoHy//J0AT6QPQEMfoAINdJwgDy4sR3gBjIywVQCM8WcPoCF8trE8yA8AnoIQF41FGcjLHxnLP1AH+gIizxZQBs8WJfoCUAPPFslQBcwjkXKRceJQCKgToIIJycOAoBS88uLFBMmAQPsAECPIUAT6AljPFgHPFszJ7VQCAdQSEQARPpEMHC68uFNgAMMIMcAkl8E4AHQ0wMBcbCVE18D8Azg+kD6QDH6ADFx1yH6ADH6ADBzqbQAAtMfghAPin6lUiC6lTE0WfAJ4IIQF41FGVIgupYxREQD8ArgNYIQWV8HvLqTWfAL4F8EhA/y8IA==").unwrap().as_slice()).unwrap(); - let state = nekoton_utils::deserialize_account_stuff(cell)?; - - let contract = jetton::TokenWalletContract(ExecutionContext { - clock: &SimpleClock, - account_stuff: &state, - }); - - let balance = contract.balance()?; - assert_eq!(balance, BigUint::from_u128(2000000000).unwrap()); - - let root = contract.root()?; - assert_eq!( - root, - MsgAddressInt::from_str( - "0:af69fd0932bbfa04ebcfa4081e7b4f2e40cfee46951dfff7e103b366f2dc33bd" - )? - ); - - Ok(()) - } - - #[test] - fn wallet_address() -> anyhow::Result<()> { - let cell = ton_types::deserialize_tree_of_cells(&mut base64::decode("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").unwrap().as_slice()).unwrap(); - let state = nekoton_utils::deserialize_account_stuff(cell)?; - - let contract = jetton::RootTokenContract(ExecutionContext { - clock: &SimpleClock, - account_stuff: &state, - }); - - let owner = nekoton_utils::unpack_std_smc_addr( - "EQC-D0YPvNUq92FeG7_ZGFQY-L-lZ0wayn8arc4AKElbSo6v", - true, - )?; - - let expected = nekoton_utils::unpack_std_smc_addr( - "EQBWqBJJQriSjGTOBXPPSZjZoTnESO3RqPLrO6enXSq--yes", - true, - )?; - - let address = contract.get_wallet_address(&owner)?; - assert_eq!(address, expected); - - Ok(()) - } - - #[test] - fn mintless_points_root_token_contract() -> anyhow::Result<()> { - let cell = - ton_types::deserialize_tree_of_cells(&mut base64::decode("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").unwrap().as_slice()) - .unwrap(); - let state = nekoton_utils::deserialize_account_stuff(cell)?; - - let contract = jetton::RootTokenContract(ExecutionContext { - clock: &SimpleClock, - account_stuff: &state, - }); - - let details = contract.get_details()?; - assert_eq!(details.admin_address, MsgAddressInt::default()); - - let owner = nekoton_utils::unpack_std_smc_addr( - "UQA8aeJrWO-5DZ-1Zs2juDYfT4V_ud2KY8gegMd33gHjeUaF", - true, - )?; - - let address = contract.get_wallet_address(&owner)?; - assert_eq!( - address, - MsgAddressInt::from_str( - "0:3d97d11909a20de878c4400ed241a714065d3a0f4d4f0d60ecaf0dbe11cdd1bc" - )? - ); - - Ok(()) - } - - #[test] - fn mintless_points_token_wallet_contract() -> anyhow::Result<()> { - let cell = - ton_types::deserialize_tree_of_cells(&mut base64::decode("te6ccgEBAwEAyQACbIAMC6d7f4iHKlXHXBfufxF6w/5pIENHdpy1yJnyM+lsrQQNAl2Gc+Ll0AABc7gAAbghs2ElpgIBANQFAlQL5ACADZRqTnEksRaYvpXRMbgzB92SzFv/19WbfQQgdDo7lYwQA+mfQx3OTMfvDyPCOAYxl9HdjYWqWkQCtdgoLLcHjaDKvtRVlwuLLP8LwzhcDJNm1TPewFBFqmlIYet7ln0NupwfCEICDvGeG/QPK6SS/KrDhu7KWb9oJ6OFBwjZ/NmttoOrwzY=").unwrap().as_slice()) - .unwrap(); - let mut state = nekoton_utils::deserialize_account_stuff(cell)?; - - jetton::update_library_cell(&mut state.storage.state)?; - - let contract = jetton::TokenWalletContract(ExecutionContext { - clock: &SimpleClock, - account_stuff: &state, - }); - - let data = contract.get_details()?; - assert_eq!(data.balance.to_u128().unwrap(), 10000000000); - - Ok(()) - } - - #[test] - fn hamster_token_wallet_contract() -> anyhow::Result<()> { - let cell = - ton_types::deserialize_tree_of_cells(&mut base64::decode("te6ccgEBAwEAyQACbIAKqccjBo+00V2Pb7qZhRYSHX52cx1iP9tpON3cdZrkP8QNAl2GdhkS0AABegbul1whs2ElpgIBANQFGHJ82gCACGZPh6infgRlai2q2zEzj6/XTCUYYz5sBXNuHUXFkiawACfLlnexAarJqUlmkXX/yPvEfPlx8Id4LDSocvlK3az1CNK1yFN5P0+WKSDutZY4tqmGqAE7w+lQchEcy4oOjEQUCEICDxrT2KRr0oMyHd5jkZX7cmAumzGxcn/swl4u3BCWbfQ=").unwrap().as_slice()) - .unwrap(); - let mut state = nekoton_utils::deserialize_account_stuff(cell)?; - - jetton::update_library_cell(&mut state.storage.state)?; - - let contract = jetton::TokenWalletContract(ExecutionContext { - clock: &SimpleClock, - account_stuff: &state, - }); - - let data = contract.get_details()?; - assert_eq!(data.balance.to_u128().unwrap(), 105000000000); - - Ok(()) - } -} diff --git a/nekoton-contracts/src/wallets/code/jetton_wallet.boc b/nekoton-contracts/src/wallets/code/jetton_wallet.boc deleted file mode 100644 index 09f021b5670603e200fb874898fb9eefed8fcb27..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1344 zcmZ{keM}o=9LJyM?%KO6lshQ2fKjecy3HXgP#haX%3x+gaOK6MD6tbGf8YXzEvSgt zc9q39GRERIvt%Vfqgm9UK>^9^hT4`Rv;!PQbm1j2^^YmJRB~KN>Qk6+3Coi_x!-fS z-{<@N{+`^(@0UB4v2+NCO#sq?4&;I&XoQc!X>1z*lxUTu%GIQb>?Fg~Ug{9_Ej2@J z(mMJe-9@|T>$C`yqaZw_3Qq{WIEcX}49Bo3LWV(3t$}~eljT+MvhQymI8oi508Tfa z8Bvv2F(Ldnw2p0(%V(-|gTW zfMd(#GNsANQJPdC`uP$IS@O)jXwI%(BXy@yH{cBcZ;(V=2oT>*E`=0w3^pgJaMxT0 z2@ixGcug4YH>}hg z8RTw8h0lE3GUF9-#OWen_EgBr^Z~U^!eG0FZ<+SviQq#645HCES>-x*dt4aH7CPq* zV%hGs95JgFzb<_BwJ@6OCfbK10MKc8wPq};hr}*Qi(h54ot9#MO!I4Y*Z{v z6dz_cM*x1`W*D?2r+_`P0Y@W6S>1K7F&FT*p!`zLrB_@at8k`!G%b9?V*{I`>4nDV zGzStvx8l9riP7}qp!k~~H7)wyiy1+3mYkJe5t+$^i$|DA^_y32)Y2~Wmr6|og`;A~=6)|v<)v%?0>BR(6#xJL diff --git a/nekoton-contracts/src/wallets/code/jetton_wallet_governed.boc b/nekoton-contracts/src/wallets/code/jetton_wallet_governed.boc deleted file mode 100644 index 002baf68e67ba2a8a64040e876abdac5d6d7f0bc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1023 zcmaiyT}V@57{}k|J?HE=KXSRWPw<>vGb(b^=!>9JD5Mdb*@D@PQqhH(oXtq6h;6K> zsMRVcLU8s0-Bc5+iy^eS+13%9j#yC1vg;xayEwf<_8zsE-UNpi-iP!4pa1jwKd*ia z_`c#wK+FU}Ymg0Wuoc_!Z)SjXbM2-!({p|c@8U1;4`IbDDm=CpMzvHKU^s>utY;vT zWxyK?AFy^Zejz7*Z>9x;h(x3bHd_m!N-=0zd81`@90x7uzucROR}E_gzmyXRa#Ten zE0aSV`+BI@#bI-jvw33)Bo1rO+BYXTxpY{|x4No_gm<&r3w@J&!!5<# z#agQuTM<(#SPQ7CfK-$gej(*dO2zMabvB)0iDbAFOgOWCVY+ar@hCO5jZWLq$&bdv zAt%s#BX#{_a(gx&Zg7Axb(WbZHrM3ZpX9WBo7A+pIpxqprLw;2>+#Q_O6_Nf8SV?Y zGV~yKFaeN&m^K`7OsD3%gIX(>g1T2OA-HZ1q@7@yr#w^sRiJV*J6@v@EFJHc8THiI zSh{5gk6wxqU&K4D?-F|N1Z?#YVOs3H>kMq{i@>y8_tYBg3owrqeNPlC`u3_Q(36%} zbI@Ww34TT25gpZHtY4l%o~7QILh+1szE}clr}_DmT>GLFACV5nlN@D#BL83Zi_pvE zNp3C8ugy%5c)OGRR$9|^KoUj{qp_V3_rN1jM_~Z zC<}Fh`kML!CPzSMOc|QtJ#i3&?HG<>bA$wgtV#`kUm`1O<0ao--FK+2KLH$TIXux)11SDg-@eeudy-Bt>g@<3m~eMt+`i&g`QsqX zZ;&zy-y{~xArygV(1=w!7vKYRx2pV$cyvcJEJmR4+#&^q_Uoqm+!B8EfUsMrc#zrK zyi(SksbjZ*`6UmB&5>M$5Dg*1V$AG1U0AxFAQu74F2{j)m!EJ!=z^E|al3A6Wne(qV@#il5c^q%Ma4FLJazYr?EU1!1KQAwIo$5Q zfIP^KT0q2dQ^H`!6jFMPvh)Ji7`L3{U|*HHwz^(7p-Pl z)J$X_W-*2Ve$QqYY)Vc6yB2)b7MZNo(QuSLi+B5_=LgQeItaAI*ZQZ@LRVa7aC<7f zSRa{ZK_cjvzmq>RmEHzQzxrO?nKN`Q!%r@d3(|`MJ)3Z@nx0j?dGU&g8blveY8oh> q8r`u}r_hx<=rMXsQNEEe^nmVE#?V748zrE4`iE=xIy~BzJNYM{GaX$3 diff --git a/nekoton-contracts/src/wallets/code/jetton_wallet_v2.boc b/nekoton-contracts/src/wallets/code/jetton_wallet_v2.boc deleted file mode 100644 index 3794284ea7c54d304775569d63735b8efbcdcf8d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 942 zcmaiyZAep57{||Z?%n2m+j6O8f%p0v61AD6h2R2(UI^~ipc#Q;FCQ%AI-`Ohwy^@G zRx_grq1g*WU(}*Lj6thDxOD}mBNkaQ^-EujxQw`+L3XY$;rbFB4xESc{GaFf{hwFg zhh6j70ElrTREDac3U=W->}Lj<2>U|wTwACe(2i<9>kw>SM(HP}bXbn&0fsKjU?mRm z1OvTe{)46t#?2-9rB)gsh**hrnoX30&%k?)r8nEQhOpOo)Yc3YD?LFS^M z)x<<4#z` z#kgNcybzV}2c5JWi?hV4cEfSWJoF%WBmxkDF?l>-UeMx`6>IbPef1}3TMcyH_6}V* z4G2tq{5&GgY$Uk!Q@H@Ji0aCe;3sQ@aLvZEF_%#FzI!Ynx-U5C}I*WlK z0h5n9sc2tUe3*(>t7fnSQ^{3TBXyyQKInR`s=7C25V|ZlMwH#zU|;K9860)133QFA zYa&S9UgO95i?eIp#yrTIL4de_v7_2IpGv!HuHP?@>t=K_+Wv=c?Rp>L!@{i%(E(+9 J|IXu&{s8Z@f`b46 diff --git a/nekoton-contracts/src/wallets/code/mod.rs b/nekoton-contracts/src/wallets/code/mod.rs index 393d70aeb..e7da28e68 100644 --- a/nekoton-contracts/src/wallets/code/mod.rs +++ b/nekoton-contracts/src/wallets/code/mod.rs @@ -1,5 +1,4 @@ -use std::collections::HashMap; -use ton_types::{Cell, UInt256}; +use ton_types::Cell; macro_rules! declare_tvc { ($($contract:ident => $source:literal ($const_bytes:ident)),*$(,)?) => {$( @@ -26,34 +25,8 @@ declare_tvc! { wallet_v5r1 => "./wallet_v5r1_code.boc" (WALLET_V5R1_CODE), highload_wallet_v2 => "./highload_wallet_v2_code.boc" (HIGHLOAD_WALLET_V2_CODE), ever_wallet => "./ever_wallet_code.boc" (EVER_WALLET_CODE), - jetton_wallet => "./jetton_wallet.boc" (JETTON_WALLET), - jetton_wallet_v2 => "./jetton_wallet_v2.boc" (JETTON_WALLET_V2), - jetton_wallet_governed => "./jetton_wallet_governed.boc" (JETTON_WALLET_GOVERNED), - jetton_wallet_hamster_combat => "./jetton_wallet_hamster_combat.boc" (JETTON_WALLET_HAMSTER_COMBAT), } fn load(mut data: &[u8]) -> Cell { ton_types::deserialize_tree_of_cells(&mut data).expect("Trust me") } - -static JETTON_LIBRARY_CELLS: once_cell::sync::Lazy> = - once_cell::sync::Lazy::new(|| { - let mut m = HashMap::new(); - - let codes = [ - jetton_wallet(), - jetton_wallet_v2(), - jetton_wallet_governed(), - jetton_wallet_hamster_combat(), - ]; - - for code in codes { - m.insert(code.repr_hash(), code); - } - - m - }); - -pub fn get_jetton_library_cell(hash: &UInt256) -> Option<&'static Cell> { - JETTON_LIBRARY_CELLS.get(hash) -} diff --git a/src/core/jetton_wallet/mod.rs b/src/core/jetton_wallet/mod.rs index 1d8caa1db..9143fab1f 100644 --- a/src/core/jetton_wallet/mod.rs +++ b/src/core/jetton_wallet/mod.rs @@ -1,19 +1,19 @@ use std::convert::TryFrom; use std::sync::Arc; +use crate::core::models::*; +use crate::core::parsing::*; +use crate::transport::models::{RawContractState, RawTransaction}; +use crate::transport::Transport; use anyhow::Result; use nekoton_abi::num_traits::ToPrimitive; use nekoton_abi::*; use nekoton_contracts::jetton::{JettonRootData, JettonWalletData}; use nekoton_utils::*; use num_bigint::{BigInt, BigUint, ToBigInt}; -use ton_block::{MsgAddressInt, Serializable}; -use ton_types::{BuilderData, IBitstring, SliceData}; - -use crate::core::models::*; -use crate::core::parsing::*; -use crate::transport::models::{RawContractState, RawTransaction}; -use crate::transport::Transport; +use serde::Deserialize; +use ton_block::{AccountState, Deserializable, MsgAddressInt, Serializable}; +use ton_types::{BuilderData, CellType, IBitstring, SliceData, UInt256}; use super::{ContractSubscription, InternalMessage}; @@ -305,7 +305,7 @@ pub async fn get_token_wallet_details( } }; - nekoton_contracts::jetton::update_library_cell(&mut token_wallet_state.account.storage.state)?; + update_library_cell(&mut token_wallet_state.account.storage.state)?; let token_wallet_state = nekoton_contracts::jetton::TokenWalletContract(token_wallet_state.as_context(clock)); @@ -331,7 +331,7 @@ pub async fn get_token_wallet_details( pub fn get_wallet_data(account: ton_block::AccountStuff) -> Result { let mut account = account; - nekoton_contracts::jetton::update_library_cell(&mut account.storage.state)?; + update_library_cell(&mut account.storage.state)?; let token_wallet_state = nekoton_contracts::jetton::TokenWalletContract(ExecutionContext { clock: &SimpleClock, @@ -369,7 +369,7 @@ pub async fn get_token_root_details_from_token_wallet( } }; - nekoton_contracts::jetton::update_library_cell(&mut state.account.storage.state)?; + update_library_cell(&mut state.account.storage.state)?; let root_token_contract = nekoton_contracts::jetton::TokenWalletContract(state.as_context(clock)).root()?; @@ -392,7 +392,7 @@ fn make_contract_state_handler( ) -> impl FnMut(&mut RawContractState) + '_ { move |contract_state| { if let RawContractState::Exists(state) = contract_state { - nekoton_contracts::jetton::update_library_cell(&mut state.account.storage.state) + update_library_cell(&mut state.account.storage.state) .ok() .and_then(|_| { nekoton_contracts::jetton::TokenWalletContract(state.as_context(clock.as_ref())) @@ -429,6 +429,71 @@ fn make_transactions_handler( } } +pub fn update_library_cell(state: &mut AccountState) -> Result<()> { + if let AccountState::AccountActive { ref mut state_init } = state { + if let Some(cell) = &state_init.code { + if cell.cell_type() == CellType::LibraryReference { + let mut slice_data = SliceData::load_cell(cell.clone())?; + + // Read Library Cell Tag + let tag = slice_data.get_next_byte()?; + assert_eq!(tag, 2); + + // Read Code Hash + let mut hash = UInt256::default(); + hash.read_from(&mut slice_data)?; + + let cell = download_lib(hash)?; + state_init.set_code(cell); + } + } + } + + Ok(()) +} + +fn download_lib(hash: UInt256) -> Result { + static URL: &str = "https://dton.io/graphql/graphql"; + + let mut headers = reqwest::header::HeaderMap::new(); + headers.insert( + reqwest::header::CONTENT_TYPE, + reqwest::header::HeaderValue::from_static("application/json"), + ); + + let client = reqwest::blocking::ClientBuilder::new() + .default_headers(headers) + .build()?; + + let query = serde_json::json!({ + "query": format!("{{ + get_lib( + lib_hash: \"{}\" + ) + }}", hash.to_hex_string().to_uppercase()) + }) + .to_string(); + + let response = client.post(URL).body(query).send()?; + + #[derive(Deserialize)] + struct GqlResponse { + data: Data, + } + + #[derive(Deserialize)] + struct Data { + get_lib: String, + } + + let parsed: GqlResponse = response.json()?; + + let bytes = base64::decode(parsed.data.get_lib)?; + let cell = ton_types::deserialize_tree_of_cells(&mut bytes.as_slice())?; + + Ok(cell) +} + #[derive(thiserror::Error, Debug)] enum JettonWalletError { #[error("Invalid root token contract")] @@ -440,3 +505,225 @@ enum JettonWalletError { #[error("Failed to convert grams")] TryFromGrams, } + +#[cfg(test)] +mod tests { + use std::str::FromStr; + + use nekoton_abi::num_bigint::BigUint; + use nekoton_abi::num_traits::{FromPrimitive, ToPrimitive}; + use nekoton_abi::ExecutionContext; + use nekoton_contracts::jetton; + use nekoton_utils::SimpleClock; + use ton_block::MsgAddressInt; + + use crate::core::jetton_wallet::update_library_cell; + + #[test] + fn usdt_root_token_contract() -> anyhow::Result<()> { + let cell = ton_types::deserialize_tree_of_cells(&mut base64::decode("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").unwrap().as_slice()).unwrap(); + let state = nekoton_utils::deserialize_account_stuff(cell)?; + + let contract = jetton::RootTokenContract(ExecutionContext { + clock: &SimpleClock, + account_stuff: &state, + }); + + let total_supply = contract.total_supply()?; + assert_eq!(total_supply, BigUint::from_u128(1229976002510000).unwrap()); + + Ok(()) + } + + #[test] + fn tonup_root_token_contract() -> anyhow::Result<()> { + let cell = ton_types::deserialize_tree_of_cells(&mut base64::decode("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").unwrap().as_slice()).unwrap(); + let state = nekoton_utils::deserialize_account_stuff(cell)?; + + let contract = jetton::RootTokenContract(ExecutionContext { + clock: &SimpleClock, + account_stuff: &state, + }); + + let name = contract.name()?; + assert_eq!(name.unwrap(), "TonUP"); + + let symbol = contract.symbol()?; + assert_eq!(symbol.unwrap(), "UP"); + + let decimals = contract.decimals()?; + assert_eq!(decimals.unwrap(), 9); + + let total_supply = contract.total_supply()?; + assert_eq!(total_supply, BigUint::from_u128(56837335582855498).unwrap()); + + let wallet_code = contract.wallet_code()?; + let expected_code = ton_types::deserialize_tree_of_cells(&mut base64::decode("te6cckECEQEAAyMAART/APSkE/S88sgLAQIBYgIDAgLMBAUAG6D2BdqJofQB9IH0gahhAgHUBgcCASAICQDDCDHAJJfBOAB0NMDAXGwlRNfA/AM4PpA+kAx+gAxcdch+gAx+gAwc6m0AALTH4IQD4p+pVIgupUxNFnwCeCCEBeNRRlSILqWMUREA/AK4DWCEFlfB7y6k1nwC+BfBIQP8vCAAET6RDBwuvLhTYAIBIAoLAIPUAQa5D2omh9AH0gfSBqGAJpj8EIC8aijKkQXUEIPe7L7wndCVj5cWLpn5j9ABgJ0CgR5CgCfQEsZ4sA54tmZPaqQB8VA9M/+gD6QCHwAe1E0PoA+kD6QNQwUTahUirHBfLiwSjC//LiwlQ0QnBUIBNUFAPIUAT6AljPFgHPFszJIsjLARL0APQAywDJIPkAcHTIywLKB8v/ydAE+kD0BDH6ACDXScIA8uLEd4AYyMsFUAjPFnD6AhfLaxPMgMAgEgDQ4AnoIQF41FGcjLHxnLP1AH+gIizxZQBs8WJfoCUAPPFslQBcwjkXKRceJQCKgToIIJycOAoBS88uLFBMmAQPsAECPIUAT6AljPFgHPFszJ7VQC9ztRND6APpA+kDUMAjTP/oAUVGgBfpA+kBTW8cFVHNtcFQgE1QUA8hQBPoCWM8WAc8WzMkiyMsBEvQA9ADLAMn5AHB0yMsCygfL/8nQUA3HBRyx8uLDCvoAUaihggiYloBmtgihggiYloCgGKEnlxBJEDg3XwTjDSXXCwGAPEADXO1E0PoA+kD6QNQwB9M/+gD6QDBRUaFSSccF8uLBJ8L/8uLCBYIJMS0AoBa88uLDghB73ZfeyMsfFcs/UAP6AiLPFgHPFslxgBjIywUkzxZw+gLLaszJgED7AEATyFAE+gJYzxYBzxbMye1UgAHBSeaAYoYIQc2LQnMjLH1Iwyz9Y+gJQB88WUAfPFslxgBDIywUkzxZQBvoCFctqFMzJcfsAECQQIwB8wwAjwgCwjiGCENUydttwgBDIywVQCM8WUAT6AhbLahLLHxLLP8ly+wCTNWwh4gPIUAT6AljPFgHPFszJ7VSV6u3X")?.as_slice())?; + assert_eq!(wallet_code, expected_code); + + let token_address = contract.get_wallet_address(&MsgAddressInt::default())?; + assert_eq!( + token_address.to_string(), + "0:0c6a835483369275c9ae76e7e31d9eda0845368045a8ec2ed78609d96bb0a087" + ); + + Ok(()) + } + + #[test] + fn usdt_wallet_token_contract() -> anyhow::Result<()> { + let cell = ton_types::deserialize_tree_of_cells(&mut base64::decode("te6ccgEBAwEAqAACbIAXsqVXAuRG6+GFp/25WVl2IsmatSkX0jbrXVjoBOwsnEQNAdiGdFv5kAABdRDp2cQZrn10JgIBAJEFJFfQYxaABHulQdJwYfnHP5r0FXhq3wjit36+D+zzx7bkE76OQgrwAsROplLUCShZxn2kTkyjrdZWWw4ol9ZAosUb+zcNiHf6CEICj0Utek39dAZraCNlF3JZ7QVzRDW+drX9S9XYryt8PWg=").unwrap().as_slice()).unwrap(); + let mut state = nekoton_utils::deserialize_account_stuff(cell)?; + + update_library_cell(&mut state.storage.state)?; + + let contract = jetton::TokenWalletContract(ExecutionContext { + clock: &SimpleClock, + account_stuff: &state, + }); + + let balance = contract.balance()?; + assert_eq!(balance.to_u128().unwrap(), 156092097302); + + Ok(()) + } + + #[test] + fn notcoin_wallet_token_contract() -> anyhow::Result<()> { + let cell = ton_types::deserialize_tree_of_cells(&mut base64::decode("te6ccgEBAwEAqgACbIAX5XxfY9N6rJiyOS4NGQc01nd0dzEnWBk87cdqg9bLTwQNAeCGdH/3UAABdXbIjToZrn5eJgIBAJUHFxcOBj4fBYAfGfo6PQWliRZGmmqpYpA1QxmYkyLZonLf41f59x68XdAAvlWFDxGF2lXm67y4yzC17wYKD9A0guwPkMs1gOsM//IIQgK6KRjIlH6bJa+awbiDNXdUFz5YEvgHo9bmQqFHCVlTlQ==").unwrap().as_slice()).unwrap(); + let mut state = nekoton_utils::deserialize_account_stuff(cell)?; + + update_library_cell(&mut state.storage.state)?; + + let contract = jetton::TokenWalletContract(ExecutionContext { + clock: &SimpleClock, + account_stuff: &state, + }); + + let balance = contract.balance()?; + assert_eq!(balance.to_u128().unwrap(), 6499273466060549); + + Ok(()) + } + + #[test] + fn tonup_wallet_token_contract() -> anyhow::Result<()> { + let cell = ton_types::deserialize_tree_of_cells(&mut base64::decode("te6ccgECEwEAA6kAAm6ADhM4pof2jbUfk8bYdFfLChAB6cUG0z98g+xOrlf4LCzkTQz7BmMVjoAAAVA6z3tMKgC1Y0MmAgEBj0dzWUAIAc+mHGAhdljL3SZo8QcvtpqlV+kMrO8+wlbsMF9hxLGVACvaf0JMrv6BOvPpAgee08uQM/uRpUd//fhA7Nm8twzvYAIBFP8A9KQT9LzyyAsDAgFiBQQAG6D2BdqJofQB9IH0gahhAgLMEAYCASAIBwCD1AEGuQ9qJofQB9IH0gahgCaY/BCAvGooypEF1BCD3uy+8J3QlY+XFi6Z+Y/QAYCdAoEeQoAn0BLGeLAOeLZmT2qkAgEgDgkCASALCgDXO1E0PoA+kD6QNQwB9M/+gD6QDBRUaFSSccF8uLBJ8L/8uLCBYIJMS0AoBa88uLDghB73ZfeyMsfFcs/UAP6AiLPFgHPFslxgBjIywUkzxZw+gLLaszJgED7AEATyFAE+gJYzxYBzxbMye1UgAvc7UTQ+gD6QPpA1DAI0z/6AFFRoAX6QPpAU1vHBVRzbXBUIBNUFAPIUAT6AljPFgHPFszJIsjLARL0APQAywDJ+QBwdMjLAsoHy//J0FANxwUcsfLiwwr6AFGooYIImJaAZrYIoYIImJaAoBihJ5cQSRA4N18E4w0l1wsBgDQwAfMMAI8IAsI4hghDVMnbbcIAQyMsFUAjPFlAE+gIWy2oSyx8Syz/JcvsAkzVsIeIDyFAE+gJYzxYBzxbMye1UAHBSeaAYoYIQc2LQnMjLH1Iwyz9Y+gJQB88WUAfPFslxgBDIywUkzxZQBvoCFctqFMzJcfsAECQQIwHxUD0z/6APpAIfAB7UTQ+gD6QPpA1DBRNqFSKscF8uLBKML/8uLCVDRCcFQgE1QUA8hQBPoCWM8WAc8WzMkiyMsBEvQA9ADLAMkg+QBwdMjLAsoHy//J0AT6QPQEMfoAINdJwgDy4sR3gBjIywVQCM8WcPoCF8trE8yA8AnoIQF41FGcjLHxnLP1AH+gIizxZQBs8WJfoCUAPPFslQBcwjkXKRceJQCKgToIIJycOAoBS88uLFBMmAQPsAECPIUAT6AljPFgHPFszJ7VQCAdQSEQARPpEMHC68uFNgAMMIMcAkl8E4AHQ0wMBcbCVE18D8Azg+kD6QDH6ADFx1yH6ADH6ADBzqbQAAtMfghAPin6lUiC6lTE0WfAJ4IIQF41FGVIgupYxREQD8ArgNYIQWV8HvLqTWfAL4F8EhA/y8IA==").unwrap().as_slice()).unwrap(); + let state = nekoton_utils::deserialize_account_stuff(cell)?; + + let contract = jetton::TokenWalletContract(ExecutionContext { + clock: &SimpleClock, + account_stuff: &state, + }); + + let balance = contract.balance()?; + assert_eq!(balance, BigUint::from_u128(2000000000).unwrap()); + + let root = contract.root()?; + assert_eq!( + root, + MsgAddressInt::from_str( + "0:af69fd0932bbfa04ebcfa4081e7b4f2e40cfee46951dfff7e103b366f2dc33bd" + )? + ); + + Ok(()) + } + + #[test] + fn wallet_address() -> anyhow::Result<()> { + let cell = ton_types::deserialize_tree_of_cells(&mut base64::decode("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").unwrap().as_slice()).unwrap(); + let state = nekoton_utils::deserialize_account_stuff(cell)?; + + let contract = jetton::RootTokenContract(ExecutionContext { + clock: &SimpleClock, + account_stuff: &state, + }); + + let owner = nekoton_utils::unpack_std_smc_addr( + "EQC-D0YPvNUq92FeG7_ZGFQY-L-lZ0wayn8arc4AKElbSo6v", + true, + )?; + + let expected = nekoton_utils::unpack_std_smc_addr( + "EQBWqBJJQriSjGTOBXPPSZjZoTnESO3RqPLrO6enXSq--yes", + true, + )?; + + let address = contract.get_wallet_address(&owner)?; + assert_eq!(address, expected); + + Ok(()) + } + + #[test] + fn mintless_points_root_token_contract() -> anyhow::Result<()> { + let cell = + ton_types::deserialize_tree_of_cells(&mut base64::decode("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").unwrap().as_slice()) + .unwrap(); + let state = nekoton_utils::deserialize_account_stuff(cell)?; + + let contract = jetton::RootTokenContract(ExecutionContext { + clock: &SimpleClock, + account_stuff: &state, + }); + + let details = contract.get_details()?; + assert_eq!(details.admin_address, MsgAddressInt::default()); + + let owner = nekoton_utils::unpack_std_smc_addr( + "UQA8aeJrWO-5DZ-1Zs2juDYfT4V_ud2KY8gegMd33gHjeUaF", + true, + )?; + + let address = contract.get_wallet_address(&owner)?; + assert_eq!( + address, + MsgAddressInt::from_str( + "0:3d97d11909a20de878c4400ed241a714065d3a0f4d4f0d60ecaf0dbe11cdd1bc" + )? + ); + + Ok(()) + } + + #[test] + fn mintless_points_token_wallet_contract() -> anyhow::Result<()> { + let cell = + ton_types::deserialize_tree_of_cells(&mut base64::decode("te6ccgEBAwEAyQACbIAMC6d7f4iHKlXHXBfufxF6w/5pIENHdpy1yJnyM+lsrQQNAl2Gc+Ll0AABc7gAAbghs2ElpgIBANQFAlQL5ACADZRqTnEksRaYvpXRMbgzB92SzFv/19WbfQQgdDo7lYwQA+mfQx3OTMfvDyPCOAYxl9HdjYWqWkQCtdgoLLcHjaDKvtRVlwuLLP8LwzhcDJNm1TPewFBFqmlIYet7ln0NupwfCEICDvGeG/QPK6SS/KrDhu7KWb9oJ6OFBwjZ/NmttoOrwzY=").unwrap().as_slice()) + .unwrap(); + let mut state = nekoton_utils::deserialize_account_stuff(cell)?; + + update_library_cell(&mut state.storage.state)?; + + let contract = jetton::TokenWalletContract(ExecutionContext { + clock: &SimpleClock, + account_stuff: &state, + }); + + let data = contract.get_details()?; + assert_eq!(data.balance.to_u128().unwrap(), 10000000000); + + Ok(()) + } + + #[test] + fn hamster_token_wallet_contract() -> anyhow::Result<()> { + let cell = + ton_types::deserialize_tree_of_cells(&mut base64::decode("te6ccgEBAwEAyQACbIAKqccjBo+00V2Pb7qZhRYSHX52cx1iP9tpON3cdZrkP8QNAl2GdhkS0AABegbul1whs2ElpgIBANQFGHJ82gCACGZPh6infgRlai2q2zEzj6/XTCUYYz5sBXNuHUXFkiawACfLlnexAarJqUlmkXX/yPvEfPlx8Id4LDSocvlK3az1CNK1yFN5P0+WKSDutZY4tqmGqAE7w+lQchEcy4oOjEQUCEICDxrT2KRr0oMyHd5jkZX7cmAumzGxcn/swl4u3BCWbfQ=").unwrap().as_slice()) + .unwrap(); + let mut state = nekoton_utils::deserialize_account_stuff(cell)?; + + update_library_cell(&mut state.storage.state)?; + + let contract = jetton::TokenWalletContract(ExecutionContext { + clock: &SimpleClock, + account_stuff: &state, + }); + + let data = contract.get_details()?; + assert_eq!(data.balance.to_u128().unwrap(), 105000000000); + + Ok(()) + } +} diff --git a/src/core/parsing.rs b/src/core/parsing.rs index b2508bda0..b605ad241 100644 --- a/src/core/parsing.rs +++ b/src/core/parsing.rs @@ -223,7 +223,7 @@ pub fn parse_transaction_additional_info( .map(TransactionAdditionalInfo::TokenWalletDeployed) .ok() } else if function_id == JettonNotify::FUNCTION_ID { - JettonNotify::decode_body(body).map(|x| TransactionAdditionalInfo::JettonNotify(x)) + JettonNotify::decode_body(body).map(TransactionAdditionalInfo::JettonNotify) } else { None } From f2793e27a069c69ef36141a18d58e6c758964820 Mon Sep 17 00:00:00 2001 From: Alexey Pashinov Date: Fri, 20 Dec 2024 17:43:14 +0100 Subject: [PATCH 19/38] Make update_library_cell async --- Cargo.toml | 2 +- src/core/contract_subscription/mod.rs | 17 +++- src/core/generic_contract/mod.rs | 3 +- src/core/jetton_wallet/mod.rs | 123 ++++++-------------------- src/core/nft_wallet/mod.rs | 3 +- src/core/token_wallet/mod.rs | 3 +- src/core/ton_wallet/mod.rs | 5 +- src/core/utils.rs | 70 ++++++++++++++- 8 files changed, 120 insertions(+), 106 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 873b4ada7..ab5a97f9a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,7 +42,7 @@ parking_lot = "0.12.0" pbkdf2 = { version = "0.12.2", optional = true } quick_cache = "0.4.1" rand = { version = "0.8", features = ["getrandom"], optional = true } -reqwest = { version = "0.11.8", features = ["blocking", "json"], optional = true } +reqwest = { version = "0.11.8", features = ["json"], optional = true } secstr = { version = "0.5.0", features = ["serde"], optional = true } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" diff --git a/src/core/contract_subscription/mod.rs b/src/core/contract_subscription/mod.rs index 484b3737a..454b92800 100644 --- a/src/core/contract_subscription/mod.rs +++ b/src/core/contract_subscription/mod.rs @@ -2,17 +2,17 @@ use std::sync::Arc; use anyhow::Result; use futures_util::StreamExt; -use serde::{Deserialize, Serialize}; -use ton_block::MsgAddressInt; - use nekoton_abi::{Executor, LastTransactionId}; use nekoton_utils::*; +use serde::{Deserialize, Serialize}; +use ton_block::MsgAddressInt; use super::models::{ ContractState, PendingTransaction, ReliableBehavior, TransactionsBatchInfo, TransactionsBatchType, }; use super::{utils, PollingMethod}; + use crate::core::utils::{MessageContext, PendingTransactionsExt}; use crate::transport::models::{RawContractState, RawTransaction}; use crate::transport::Transport; @@ -26,6 +26,7 @@ pub struct ContractSubscription { latest_known_lt: Option, pending_transactions: Vec, transactions_synced: bool, + supports_library_cells: bool, } impl ContractSubscription { @@ -35,11 +36,13 @@ impl ContractSubscription { address: MsgAddressInt, on_contract_state: OnContractState<'_>, on_transactions_found: Option>, + supports_library_cells: bool, ) -> Result { let mut result = Self { clock, transport, address, + supports_library_cells, contract_state: Default::default(), latest_known_lt: None, pending_transactions: Vec::new(), @@ -412,6 +415,12 @@ impl ContractSubscription { }; if updated { + if self.supports_library_cells { + if let RawContractState::Exists(state) = &mut contract_state { + utils::update_library_cell(&mut state.account.storage.state).await?; + } + } + on_contract_state(&mut contract_state); self.contract_state = new_contract_state; self.transactions_synced = false; @@ -459,7 +468,7 @@ impl ContractSubscription { } } -type OnContractState<'a> = &'a mut (dyn FnMut(&mut RawContractState) + Send + Sync); +type OnContractState<'a> = &'a mut (dyn FnMut(&RawContractState) + Send + Sync); type OnTransactionsFound<'a> = &'a mut (dyn FnMut(Vec, TransactionsBatchInfo) + Send + Sync); type OnMessageSent<'a> = &'a mut (dyn FnMut(PendingTransaction, RawTransaction) + Send + Sync); diff --git a/src/core/generic_contract/mod.rs b/src/core/generic_contract/mod.rs index f2fc0ee67..2998fe2b3 100644 --- a/src/core/generic_contract/mod.rs +++ b/src/core/generic_contract/mod.rs @@ -49,6 +49,7 @@ impl GenericContract { address, &mut make_contract_state_handler(handler), on_transactions_found, + false, ) .await? }; @@ -141,7 +142,7 @@ impl GenericContract { fn make_contract_state_handler( handler: &dyn GenericContractSubscriptionHandler, -) -> impl FnMut(&mut RawContractState) + '_ { +) -> impl FnMut(&RawContractState) + '_ { move |contract_state| handler.on_state_changed(contract_state.brief()) } diff --git a/src/core/jetton_wallet/mod.rs b/src/core/jetton_wallet/mod.rs index 9143fab1f..67671b4b3 100644 --- a/src/core/jetton_wallet/mod.rs +++ b/src/core/jetton_wallet/mod.rs @@ -11,11 +11,10 @@ use nekoton_abi::*; use nekoton_contracts::jetton::{JettonRootData, JettonWalletData}; use nekoton_utils::*; use num_bigint::{BigInt, BigUint, ToBigInt}; -use serde::Deserialize; -use ton_block::{AccountState, Deserializable, MsgAddressInt, Serializable}; -use ton_types::{BuilderData, CellType, IBitstring, SliceData, UInt256}; +use ton_block::{MsgAddressInt, Serializable}; +use ton_types::{BuilderData, IBitstring, SliceData}; -use super::{ContractSubscription, InternalMessage}; +use super::{utils, ContractSubscription, InternalMessage}; pub const JETTON_TRANSFER_OPCODE: u32 = 0x0f8a7ea5; pub const JETTON_INTERNAL_TRANSFER_OPCODE: u32 = 0x178d4519; @@ -72,6 +71,7 @@ impl JettonWallet { address, &mut make_contract_state_handler(clock.clone(), &mut balance), on_transactions_found, + true, ) .await? }; @@ -305,7 +305,7 @@ pub async fn get_token_wallet_details( } }; - update_library_cell(&mut token_wallet_state.account.storage.state)?; + utils::update_library_cell(&mut token_wallet_state.account.storage.state).await?; let token_wallet_state = nekoton_contracts::jetton::TokenWalletContract(token_wallet_state.as_context(clock)); @@ -328,10 +328,10 @@ pub async fn get_token_wallet_details( Ok((token_wallet_details, root_contract_details)) } -pub fn get_wallet_data(account: ton_block::AccountStuff) -> Result { +pub async fn get_wallet_data(account: ton_block::AccountStuff) -> Result { let mut account = account; - update_library_cell(&mut account.storage.state)?; + utils::update_library_cell(&mut account.storage.state).await?; let token_wallet_state = nekoton_contracts::jetton::TokenWalletContract(ExecutionContext { clock: &SimpleClock, @@ -369,7 +369,7 @@ pub async fn get_token_root_details_from_token_wallet( } }; - update_library_cell(&mut state.account.storage.state)?; + utils::update_library_cell(&mut state.account.storage.state).await?; let root_token_contract = nekoton_contracts::jetton::TokenWalletContract(state.as_context(clock)).root()?; @@ -389,17 +389,15 @@ pub async fn get_token_root_details_from_token_wallet( fn make_contract_state_handler( clock: Arc, balance: &'_ mut BigUint, -) -> impl FnMut(&mut RawContractState) + '_ { +) -> impl FnMut(&RawContractState) + '_ { move |contract_state| { if let RawContractState::Exists(state) = contract_state { - update_library_cell(&mut state.account.storage.state) - .ok() - .and_then(|_| { - nekoton_contracts::jetton::TokenWalletContract(state.as_context(clock.as_ref())) - .balance() - .ok() - .map(|new_balance| *balance = new_balance) - }); + if let Ok(new_balance) = + nekoton_contracts::jetton::TokenWalletContract(state.as_context(clock.as_ref())) + .balance() + { + *balance = new_balance + } } } } @@ -429,71 +427,6 @@ fn make_transactions_handler( } } -pub fn update_library_cell(state: &mut AccountState) -> Result<()> { - if let AccountState::AccountActive { ref mut state_init } = state { - if let Some(cell) = &state_init.code { - if cell.cell_type() == CellType::LibraryReference { - let mut slice_data = SliceData::load_cell(cell.clone())?; - - // Read Library Cell Tag - let tag = slice_data.get_next_byte()?; - assert_eq!(tag, 2); - - // Read Code Hash - let mut hash = UInt256::default(); - hash.read_from(&mut slice_data)?; - - let cell = download_lib(hash)?; - state_init.set_code(cell); - } - } - } - - Ok(()) -} - -fn download_lib(hash: UInt256) -> Result { - static URL: &str = "https://dton.io/graphql/graphql"; - - let mut headers = reqwest::header::HeaderMap::new(); - headers.insert( - reqwest::header::CONTENT_TYPE, - reqwest::header::HeaderValue::from_static("application/json"), - ); - - let client = reqwest::blocking::ClientBuilder::new() - .default_headers(headers) - .build()?; - - let query = serde_json::json!({ - "query": format!("{{ - get_lib( - lib_hash: \"{}\" - ) - }}", hash.to_hex_string().to_uppercase()) - }) - .to_string(); - - let response = client.post(URL).body(query).send()?; - - #[derive(Deserialize)] - struct GqlResponse { - data: Data, - } - - #[derive(Deserialize)] - struct Data { - get_lib: String, - } - - let parsed: GqlResponse = response.json()?; - - let bytes = base64::decode(parsed.data.get_lib)?; - let cell = ton_types::deserialize_tree_of_cells(&mut bytes.as_slice())?; - - Ok(cell) -} - #[derive(thiserror::Error, Debug)] enum JettonWalletError { #[error("Invalid root token contract")] @@ -517,7 +450,7 @@ mod tests { use nekoton_utils::SimpleClock; use ton_block::MsgAddressInt; - use crate::core::jetton_wallet::update_library_cell; + use crate::core::utils::update_library_cell; #[test] fn usdt_root_token_contract() -> anyhow::Result<()> { @@ -570,12 +503,12 @@ mod tests { Ok(()) } - #[test] - fn usdt_wallet_token_contract() -> anyhow::Result<()> { + #[tokio::test] + async fn usdt_wallet_token_contract() -> anyhow::Result<()> { let cell = ton_types::deserialize_tree_of_cells(&mut base64::decode("te6ccgEBAwEAqAACbIAXsqVXAuRG6+GFp/25WVl2IsmatSkX0jbrXVjoBOwsnEQNAdiGdFv5kAABdRDp2cQZrn10JgIBAJEFJFfQYxaABHulQdJwYfnHP5r0FXhq3wjit36+D+zzx7bkE76OQgrwAsROplLUCShZxn2kTkyjrdZWWw4ol9ZAosUb+zcNiHf6CEICj0Utek39dAZraCNlF3JZ7QVzRDW+drX9S9XYryt8PWg=").unwrap().as_slice()).unwrap(); let mut state = nekoton_utils::deserialize_account_stuff(cell)?; - update_library_cell(&mut state.storage.state)?; + update_library_cell(&mut state.storage.state).await?; let contract = jetton::TokenWalletContract(ExecutionContext { clock: &SimpleClock, @@ -588,12 +521,12 @@ mod tests { Ok(()) } - #[test] - fn notcoin_wallet_token_contract() -> anyhow::Result<()> { + #[tokio::test] + async fn notcoin_wallet_token_contract() -> anyhow::Result<()> { let cell = ton_types::deserialize_tree_of_cells(&mut base64::decode("te6ccgEBAwEAqgACbIAX5XxfY9N6rJiyOS4NGQc01nd0dzEnWBk87cdqg9bLTwQNAeCGdH/3UAABdXbIjToZrn5eJgIBAJUHFxcOBj4fBYAfGfo6PQWliRZGmmqpYpA1QxmYkyLZonLf41f59x68XdAAvlWFDxGF2lXm67y4yzC17wYKD9A0guwPkMs1gOsM//IIQgK6KRjIlH6bJa+awbiDNXdUFz5YEvgHo9bmQqFHCVlTlQ==").unwrap().as_slice()).unwrap(); let mut state = nekoton_utils::deserialize_account_stuff(cell)?; - update_library_cell(&mut state.storage.state)?; + update_library_cell(&mut state.storage.state).await?; let contract = jetton::TokenWalletContract(ExecutionContext { clock: &SimpleClock, @@ -687,14 +620,14 @@ mod tests { Ok(()) } - #[test] - fn mintless_points_token_wallet_contract() -> anyhow::Result<()> { + #[tokio::test] + async fn mintless_points_token_wallet_contract() -> anyhow::Result<()> { let cell = ton_types::deserialize_tree_of_cells(&mut base64::decode("te6ccgEBAwEAyQACbIAMC6d7f4iHKlXHXBfufxF6w/5pIENHdpy1yJnyM+lsrQQNAl2Gc+Ll0AABc7gAAbghs2ElpgIBANQFAlQL5ACADZRqTnEksRaYvpXRMbgzB92SzFv/19WbfQQgdDo7lYwQA+mfQx3OTMfvDyPCOAYxl9HdjYWqWkQCtdgoLLcHjaDKvtRVlwuLLP8LwzhcDJNm1TPewFBFqmlIYet7ln0NupwfCEICDvGeG/QPK6SS/KrDhu7KWb9oJ6OFBwjZ/NmttoOrwzY=").unwrap().as_slice()) .unwrap(); let mut state = nekoton_utils::deserialize_account_stuff(cell)?; - update_library_cell(&mut state.storage.state)?; + update_library_cell(&mut state.storage.state).await?; let contract = jetton::TokenWalletContract(ExecutionContext { clock: &SimpleClock, @@ -707,14 +640,14 @@ mod tests { Ok(()) } - #[test] - fn hamster_token_wallet_contract() -> anyhow::Result<()> { + #[tokio::test] + async fn hamster_token_wallet_contract() -> anyhow::Result<()> { let cell = ton_types::deserialize_tree_of_cells(&mut base64::decode("te6ccgEBAwEAyQACbIAKqccjBo+00V2Pb7qZhRYSHX52cx1iP9tpON3cdZrkP8QNAl2GdhkS0AABegbul1whs2ElpgIBANQFGHJ82gCACGZPh6infgRlai2q2zEzj6/XTCUYYz5sBXNuHUXFkiawACfLlnexAarJqUlmkXX/yPvEfPlx8Id4LDSocvlK3az1CNK1yFN5P0+WKSDutZY4tqmGqAE7w+lQchEcy4oOjEQUCEICDxrT2KRr0oMyHd5jkZX7cmAumzGxcn/swl4u3BCWbfQ=").unwrap().as_slice()) .unwrap(); let mut state = nekoton_utils::deserialize_account_stuff(cell)?; - update_library_cell(&mut state.storage.state)?; + update_library_cell(&mut state.storage.state).await?; let contract = jetton::TokenWalletContract(ExecutionContext { clock: &SimpleClock, diff --git a/src/core/nft_wallet/mod.rs b/src/core/nft_wallet/mod.rs index b6fdf152e..d0303e7d2 100644 --- a/src/core/nft_wallet/mod.rs +++ b/src/core/nft_wallet/mod.rs @@ -166,6 +166,7 @@ impl Nft { None, ), Some(&mut make_transactions_handler(handler.as_ref())), + false, ) .await?; @@ -363,7 +364,7 @@ fn make_contract_state_handler<'a>( owner: &'a mut MsgAddressInt, manager: &'a mut MsgAddressInt, handler: Option<&'a dyn NftSubscriptionHandler>, -) -> impl FnMut(&mut RawContractState) + 'a { +) -> impl FnMut(&RawContractState) + 'a { move |contract_state| { if let RawContractState::Exists(state) = contract_state { if let Ok(info) = NftContractState(state).get_info(clock) { diff --git a/src/core/token_wallet/mod.rs b/src/core/token_wallet/mod.rs index 0aa3b461e..edb3f01ce 100644 --- a/src/core/token_wallet/mod.rs +++ b/src/core/token_wallet/mod.rs @@ -79,6 +79,7 @@ impl TokenWallet { address, &mut make_contract_state_handler(clock.clone(), version, &mut balance), on_transactions_found, + false, ) .await? }; @@ -475,7 +476,7 @@ fn make_contract_state_handler( clock: Arc, version: TokenWalletVersion, balance: &'_ mut BigUint, -) -> impl FnMut(&mut RawContractState) + '_ { +) -> impl FnMut(&RawContractState) + '_ { move |contract_state| { if let RawContractState::Exists(state) = contract_state { if let Ok(new_balance) = diff --git a/src/core/ton_wallet/mod.rs b/src/core/ton_wallet/mod.rs index 7db0a2cfa..f14d7fe4e 100644 --- a/src/core/ton_wallet/mod.rs +++ b/src/core/ton_wallet/mod.rs @@ -72,6 +72,7 @@ impl TonWallet { handler.as_ref(), wallet_type, )), + false, ) .await?; @@ -115,6 +116,7 @@ impl TonWallet { handler.as_ref(), wallet_type, )), + false, ) .await?; @@ -151,6 +153,7 @@ impl TonWallet { handler.as_ref(), existing_wallet.wallet_type, )), + false, ) .await?; @@ -840,7 +843,7 @@ fn make_contract_state_handler<'a>( public_key: &'a PublicKey, wallet_type: WalletType, wallet_data: &'a mut WalletData, -) -> impl FnMut(&mut RawContractState) + 'a { +) -> impl FnMut(&RawContractState) + 'a { move |contract_state| { if let RawContractState::Exists(contract_state) = contract_state { if let Err(e) = wallet_data.update( diff --git a/src/core/utils.rs b/src/core/utils.rs index a579f36f7..17c2e4258 100644 --- a/src/core/utils.rs +++ b/src/core/utils.rs @@ -7,10 +7,11 @@ use std::task::{Context, Poll}; use anyhow::Result; use ed25519_dalek::PublicKey; use futures_util::{Future, FutureExt, Stream}; -use ton_block::{MsgAddressInt, Serializable}; - use nekoton_abi::{GenTimings, LastTransactionId, TransactionId}; use nekoton_utils::*; +use serde::Deserialize; +use ton_block::{AccountState, Deserializable, MsgAddressInt, Serializable}; +use ton_types::{CellType, SliceData, UInt256}; use crate::core::models::*; #[cfg(feature = "wallet_core")] @@ -492,3 +493,68 @@ pub fn default_headers( } type HeadersMap = HashMap; + +pub async fn update_library_cell(state: &mut AccountState) -> Result<()> { + if let AccountState::AccountActive { ref mut state_init } = state { + if let Some(cell) = &state_init.code { + if cell.cell_type() == CellType::LibraryReference { + let mut slice_data = SliceData::load_cell(cell.clone())?; + + // Read Library Cell Tag + let tag = slice_data.get_next_byte()?; + assert_eq!(tag, 2); + + // Read Code Hash + let mut hash = UInt256::default(); + hash.read_from(&mut slice_data)?; + + let cell = download_lib(hash).await?; + state_init.set_code(cell); + } + } + } + + Ok(()) +} + +async fn download_lib(hash: UInt256) -> Result { + static URL: &str = "https://dton.io/graphql/graphql"; + + let mut headers = reqwest::header::HeaderMap::new(); + headers.insert( + reqwest::header::CONTENT_TYPE, + reqwest::header::HeaderValue::from_static("application/json"), + ); + + let client = reqwest::ClientBuilder::new() + .default_headers(headers) + .build()?; + + let query = serde_json::json!({ + "query": format!("{{ + get_lib( + lib_hash: \"{}\" + ) + }}", hash.to_hex_string().to_uppercase()) + }) + .to_string(); + + let response = client.post(URL).body(query).send().await?; + + #[derive(Deserialize)] + struct GqlResponse { + data: Data, + } + + #[derive(Deserialize)] + struct Data { + get_lib: String, + } + + let parsed: GqlResponse = response.json().await?; + + let bytes = base64::decode(parsed.data.get_lib)?; + let cell = ton_types::deserialize_tree_of_cells(&mut bytes.as_slice())?; + + Ok(cell) +} From 03d88b2b4c6369fd9bc5f183daa03d123e125844 Mon Sep 17 00:00:00 2001 From: Alexey Pashinov Date: Sat, 21 Dec 2024 03:12:00 +0100 Subject: [PATCH 20/38] Move reqwest from core to external transport impl --- Cargo.toml | 3 +- nekoton-transport/Cargo.toml | 3 + nekoton-transport/src/gql_ext.rs | 212 ++++++++++++++++++++++++++ nekoton-transport/src/lib.rs | 2 + src/core/contract_subscription/mod.rs | 15 +- src/core/generic_contract/mod.rs | 2 +- src/core/jetton_wallet/mod.rs | 105 +++---------- src/core/nft_wallet/mod.rs | 2 +- src/core/token_wallet/mod.rs | 2 +- src/core/ton_wallet/mod.rs | 6 +- src/core/utils.rs | 34 ++--- 11 files changed, 270 insertions(+), 116 deletions(-) create mode 100644 nekoton-transport/src/gql_ext.rs diff --git a/Cargo.toml b/Cargo.toml index ab5a97f9a..a246ea3fb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,7 +42,6 @@ parking_lot = "0.12.0" pbkdf2 = { version = "0.12.2", optional = true } quick_cache = "0.4.1" rand = { version = "0.8", features = ["getrandom"], optional = true } -reqwest = { version = "0.11.8", features = ["json"], optional = true } secstr = { version = "0.5.0", features = ["serde"], optional = true } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" @@ -88,7 +87,7 @@ proto_transport = ["dep:nekoton-proto"] extended_models = [] non_threadsafe = [] wallet_core = ["dep:pbkdf2", "dep:chacha20poly1305", "dep:zeroize", "dep:secstr", "dep:hmac", "dep:ed25519-dalek", - "dep:tiny-bip39", "dep:tiny-hderive", "dep:sha2", "dep:getrandom", "dep:rand", "dep:curve25519-dalek-ng", "dep:reqwest", "nekoton-utils/encryption"] + "dep:tiny-bip39", "dep:tiny-hderive", "dep:sha2", "dep:getrandom", "dep:rand", "dep:curve25519-dalek-ng", "nekoton-utils/encryption"] [package.metadata.docs.rs] all-features = true diff --git a/nekoton-transport/Cargo.toml b/nekoton-transport/Cargo.toml index 18b5b3f97..3a57fae79 100644 --- a/nekoton-transport/Cargo.toml +++ b/nekoton-transport/Cargo.toml @@ -24,8 +24,11 @@ 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" } + [features] default = ["gql_transport"] gql_transport = ["nekoton/gql_transport"] diff --git a/nekoton-transport/src/gql_ext.rs b/nekoton-transport/src/gql_ext.rs new file mode 100644 index 000000000..7f180e4b4 --- /dev/null +++ b/nekoton-transport/src/gql_ext.rs @@ -0,0 +1,212 @@ +use std::convert::TryInto; +use std::sync::Arc; + +use anyhow::{Context, Result}; +use reqwest::Url; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct GqlExtNetworkSettings { + pub endpoint: String, + /// Gql node type + pub local: bool, +} + +pub struct GqlExtClient { + client: reqwest::Client, + endpoint: Endpoint, + local: bool, +} + +impl GqlExtClient { + pub fn new(settings: GqlExtNetworkSettings) -> Result> { + let endpoint = &settings.endpoint; + let endpoint = Endpoint::new(endpoint) + .with_context(|| format!("failed to parse endpoint: {}", endpoint))?; + + let mut headers = reqwest::header::HeaderMap::new(); + headers.insert( + reqwest::header::CONTENT_TYPE, + reqwest::header::HeaderValue::from_static("application/json"), + ); + + let client = reqwest::ClientBuilder::new() + .default_headers(headers) + .build() + .context("failed to build http client")?; + + Ok(Arc::new(Self { + client, + endpoint, + local: settings.local, + })) + } +} + +#[cfg_attr(not(feature = "non_threadsafe"), async_trait::async_trait)] +#[cfg_attr(feature = "non_threadsafe", async_trait::async_trait(?Send))] +impl nekoton::external::GqlConnection for GqlExtClient { + fn is_local(&self) -> bool { + self.local + } + + async fn post(&self, req: nekoton::external::GqlRequest) -> Result { + let response = self + .client + .post(self.endpoint.gql.clone()) + .body(req.data) + .send() + .await?; + Ok(response.text().await?) + } +} + +struct Endpoint { + gql: Url, +} + +impl Endpoint { + fn new(url: &str) -> Result { + let gql = expand_address(url); + Ok(Self { + gql: gql.as_str().try_into()?, + }) + } +} + +fn expand_address(base_url: &str) -> String { + match base_url.trim_end_matches('/') { + url if base_url.starts_with("http://") || base_url.starts_with("https://") => { + format!("{}/graphql", url) + } + url @ ("localhost" | "127.0.0.1") => format!("http://{}/graphql", url), + url => format!("https://{}/graphql", url), + } +} + +#[cfg(test)] +mod tests { + use nekoton::abi::num_traits::ToPrimitive; + use nekoton::abi::ExecutionContext; + use nekoton::contracts::jetton; + use nekoton::core::utils::update_library_cell; + use nekoton::external::{GqlConnection, GqlRequest}; + use nekoton_utils::*; + + use super::*; + + #[tokio::test] + async fn gql_client_works() -> Result<()> { + let client = GqlExtClient::new(GqlExtNetworkSettings { + endpoint: "https://dton.io/graphql".to_string(), + local: false, + })?; + + const QUERY: &str = r#"{"query":"{\n get_lib(\n lib_hash: \"0F1AD3D8A46BD283321DDE639195FB72602E9B31B1727FECC25E2EDC10966DF4\"\n )\n}"}"#; + + let _response = client + .post(GqlRequest { + data: QUERY.to_string(), + long_query: false, + }) + .await?; + + Ok(()) + } + + #[tokio::test] + async fn usdt_wallet_token_contract() -> Result<()> { + let client = GqlExtClient::new(GqlExtNetworkSettings { + endpoint: "https://dton.io/graphql".to_string(), + local: false, + })?; + + let cell = ton_types::deserialize_tree_of_cells(&mut base64::decode("te6ccgEBAwEAqAACbIAXsqVXAuRG6+GFp/25WVl2IsmatSkX0jbrXVjoBOwsnEQNAdiGdFv5kAABdRDp2cQZrn10JgIBAJEFJFfQYxaABHulQdJwYfnHP5r0FXhq3wjit36+D+zzx7bkE76OQgrwAsROplLUCShZxn2kTkyjrdZWWw4ol9ZAosUb+zcNiHf6CEICj0Utek39dAZraCNlF3JZ7QVzRDW+drX9S9XYryt8PWg=").unwrap().as_slice()).unwrap(); + let mut state = deserialize_account_stuff(cell)?; + + update_library_cell(client.as_ref(), &mut state.storage.state).await?; + + let contract = jetton::TokenWalletContract(ExecutionContext { + clock: &SimpleClock, + account_stuff: &state, + }); + + let balance = contract.balance()?; + assert_eq!(balance.to_u128().unwrap(), 156092097302); + + Ok(()) + } + + #[tokio::test] + async fn notcoin_wallet_token_contract() -> Result<()> { + let client = GqlExtClient::new(GqlExtNetworkSettings { + endpoint: "https://dton.io/graphql".to_string(), + local: false, + })?; + + let cell = ton_types::deserialize_tree_of_cells(&mut base64::decode("te6ccgEBAwEAqgACbIAX5XxfY9N6rJiyOS4NGQc01nd0dzEnWBk87cdqg9bLTwQNAeCGdH/3UAABdXbIjToZrn5eJgIBAJUHFxcOBj4fBYAfGfo6PQWliRZGmmqpYpA1QxmYkyLZonLf41f59x68XdAAvlWFDxGF2lXm67y4yzC17wYKD9A0guwPkMs1gOsM//IIQgK6KRjIlH6bJa+awbiDNXdUFz5YEvgHo9bmQqFHCVlTlQ==").unwrap().as_slice()).unwrap(); + let mut state = deserialize_account_stuff(cell)?; + + update_library_cell(client.as_ref(), &mut state.storage.state).await?; + + let contract = jetton::TokenWalletContract(ExecutionContext { + clock: &SimpleClock, + account_stuff: &state, + }); + + let balance = contract.balance()?; + assert_eq!(balance.to_u128().unwrap(), 6499273466060549); + + Ok(()) + } + + #[tokio::test] + async fn mintless_points_token_wallet_contract() -> Result<()> { + let client = GqlExtClient::new(GqlExtNetworkSettings { + endpoint: "https://dton.io/graphql".to_string(), + local: false, + })?; + + let cell = + ton_types::deserialize_tree_of_cells(&mut base64::decode("te6ccgEBAwEAyQACbIAMC6d7f4iHKlXHXBfufxF6w/5pIENHdpy1yJnyM+lsrQQNAl2Gc+Ll0AABc7gAAbghs2ElpgIBANQFAlQL5ACADZRqTnEksRaYvpXRMbgzB92SzFv/19WbfQQgdDo7lYwQA+mfQx3OTMfvDyPCOAYxl9HdjYWqWkQCtdgoLLcHjaDKvtRVlwuLLP8LwzhcDJNm1TPewFBFqmlIYet7ln0NupwfCEICDvGeG/QPK6SS/KrDhu7KWb9oJ6OFBwjZ/NmttoOrwzY=").unwrap().as_slice()) + .unwrap(); + let mut state = deserialize_account_stuff(cell)?; + + update_library_cell(client.as_ref(), &mut state.storage.state).await?; + + let contract = jetton::TokenWalletContract(ExecutionContext { + clock: &SimpleClock, + account_stuff: &state, + }); + + let data = contract.get_details()?; + assert_eq!(data.balance.to_u128().unwrap(), 10000000000); + + Ok(()) + } + + #[tokio::test] + async fn hamster_token_wallet_contract() -> Result<()> { + let client = GqlExtClient::new(GqlExtNetworkSettings { + endpoint: "https://dton.io/graphql".to_string(), + local: false, + })?; + + let cell = + ton_types::deserialize_tree_of_cells(&mut base64::decode("te6ccgEBAwEAyQACbIAKqccjBo+00V2Pb7qZhRYSHX52cx1iP9tpON3cdZrkP8QNAl2GdhkS0AABegbul1whs2ElpgIBANQFGHJ82gCACGZPh6infgRlai2q2zEzj6/XTCUYYz5sBXNuHUXFkiawACfLlnexAarJqUlmkXX/yPvEfPlx8Id4LDSocvlK3az1CNK1yFN5P0+WKSDutZY4tqmGqAE7w+lQchEcy4oOjEQUCEICDxrT2KRr0oMyHd5jkZX7cmAumzGxcn/swl4u3BCWbfQ=").unwrap().as_slice()) + .unwrap(); + let mut state = deserialize_account_stuff(cell)?; + + update_library_cell(client.as_ref(), &mut state.storage.state).await?; + + let contract = jetton::TokenWalletContract(ExecutionContext { + clock: &SimpleClock, + account_stuff: &state, + }); + + let data = contract.get_details()?; + assert_eq!(data.balance.to_u128().unwrap(), 105000000000); + + Ok(()) + } +} diff --git a/nekoton-transport/src/lib.rs b/nekoton-transport/src/lib.rs index 6e3595139..90e49de83 100644 --- a/nekoton-transport/src/lib.rs +++ b/nekoton-transport/src/lib.rs @@ -54,6 +54,8 @@ #[cfg(feature = "gql_transport")] pub mod gql; +#[cfg(feature = "gql_transport")] +pub mod gql_ext; #[cfg(feature = "jrpc_transport")] pub mod jrpc; #[cfg(feature = "proto_transport")] diff --git a/src/core/contract_subscription/mod.rs b/src/core/contract_subscription/mod.rs index 454b92800..c67463ba4 100644 --- a/src/core/contract_subscription/mod.rs +++ b/src/core/contract_subscription/mod.rs @@ -14,6 +14,7 @@ use super::models::{ use super::{utils, PollingMethod}; use crate::core::utils::{MessageContext, PendingTransactionsExt}; +use crate::external::GqlConnection; use crate::transport::models::{RawContractState, RawTransaction}; use crate::transport::Transport; @@ -21,28 +22,28 @@ use crate::transport::Transport; pub struct ContractSubscription { clock: Arc, transport: Arc, + gql_connection: Option>, address: MsgAddressInt, contract_state: ContractState, latest_known_lt: Option, pending_transactions: Vec, transactions_synced: bool, - supports_library_cells: bool, } impl ContractSubscription { pub async fn subscribe( clock: Arc, transport: Arc, + gql_connection: Option>, address: MsgAddressInt, on_contract_state: OnContractState<'_>, on_transactions_found: Option>, - supports_library_cells: bool, ) -> Result { let mut result = Self { clock, transport, + gql_connection, address, - supports_library_cells, contract_state: Default::default(), latest_known_lt: None, pending_transactions: Vec::new(), @@ -415,9 +416,13 @@ impl ContractSubscription { }; if updated { - if self.supports_library_cells { + if let Some(connection) = self.gql_connection.as_ref() { if let RawContractState::Exists(state) = &mut contract_state { - utils::update_library_cell(&mut state.account.storage.state).await?; + utils::update_library_cell( + connection.as_ref(), + &mut state.account.storage.state, + ) + .await?; } } diff --git a/src/core/generic_contract/mod.rs b/src/core/generic_contract/mod.rs index 2998fe2b3..7e9c46e18 100644 --- a/src/core/generic_contract/mod.rs +++ b/src/core/generic_contract/mod.rs @@ -46,10 +46,10 @@ impl GenericContract { ContractSubscription::subscribe( clock, transport, + None, address, &mut make_contract_state_handler(handler), on_transactions_found, - false, ) .await? }; diff --git a/src/core/jetton_wallet/mod.rs b/src/core/jetton_wallet/mod.rs index 67671b4b3..d9df5196d 100644 --- a/src/core/jetton_wallet/mod.rs +++ b/src/core/jetton_wallet/mod.rs @@ -3,6 +3,7 @@ use std::sync::Arc; use crate::core::models::*; use crate::core::parsing::*; +use crate::external::GqlConnection; use crate::transport::models::{RawContractState, RawTransaction}; use crate::transport::Transport; use anyhow::Result; @@ -32,6 +33,7 @@ impl JettonWallet { pub async fn subscribe( clock: Arc, transport: Arc, + gql_connection: Option>, owner: MsgAddressInt, root_token_contract: MsgAddressInt, handler: Arc, @@ -68,10 +70,10 @@ impl JettonWallet { ContractSubscription::subscribe( clock.clone(), transport, + gql_connection, address, &mut make_contract_state_handler(clock.clone(), &mut balance), on_transactions_found, - true, ) .await? }; @@ -295,7 +297,8 @@ pub trait JettonWalletSubscriptionHandler: Send + Sync { pub async fn get_token_wallet_details( clock: &dyn Clock, - transport: &dyn Transport, + transport: Arc, + gql_connection: Arc, token_wallet: &MsgAddressInt, ) -> Result<(JettonWalletData, JettonRootData)> { let mut token_wallet_state = match transport.get_contract_state(token_wallet).await? { @@ -305,7 +308,11 @@ pub async fn get_token_wallet_details( } }; - utils::update_library_cell(&mut token_wallet_state.account.storage.state).await?; + utils::update_library_cell( + gql_connection.as_ref(), + &mut token_wallet_state.account.storage.state, + ) + .await?; let token_wallet_state = nekoton_contracts::jetton::TokenWalletContract(token_wallet_state.as_context(clock)); @@ -328,10 +335,13 @@ pub async fn get_token_wallet_details( Ok((token_wallet_details, root_contract_details)) } -pub async fn get_wallet_data(account: ton_block::AccountStuff) -> Result { +pub async fn get_wallet_data( + gql_connection: Arc, + account: ton_block::AccountStuff, +) -> Result { let mut account = account; - utils::update_library_cell(&mut account.storage.state).await?; + utils::update_library_cell(gql_connection.as_ref(), &mut account.storage.state).await?; let token_wallet_state = nekoton_contracts::jetton::TokenWalletContract(ExecutionContext { clock: &SimpleClock, @@ -359,7 +369,8 @@ pub async fn get_token_root_details( pub async fn get_token_root_details_from_token_wallet( clock: &dyn Clock, - transport: &dyn Transport, + transport: Arc, + gql_connection: Arc, token_wallet_address: &MsgAddressInt, ) -> Result<(MsgAddressInt, JettonRootData)> { let mut state = match transport.get_contract_state(token_wallet_address).await? { @@ -369,7 +380,7 @@ pub async fn get_token_root_details_from_token_wallet( } }; - utils::update_library_cell(&mut state.account.storage.state).await?; + utils::update_library_cell(gql_connection.as_ref(), &mut state.account.storage.state).await?; let root_token_contract = nekoton_contracts::jetton::TokenWalletContract(state.as_context(clock)).root()?; @@ -444,14 +455,12 @@ mod tests { use std::str::FromStr; use nekoton_abi::num_bigint::BigUint; - use nekoton_abi::num_traits::{FromPrimitive, ToPrimitive}; + use nekoton_abi::num_traits::FromPrimitive; use nekoton_abi::ExecutionContext; use nekoton_contracts::jetton; use nekoton_utils::SimpleClock; use ton_block::MsgAddressInt; - use crate::core::utils::update_library_cell; - #[test] fn usdt_root_token_contract() -> anyhow::Result<()> { let cell = ton_types::deserialize_tree_of_cells(&mut base64::decode("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").unwrap().as_slice()).unwrap(); @@ -503,42 +512,6 @@ mod tests { Ok(()) } - #[tokio::test] - async fn usdt_wallet_token_contract() -> anyhow::Result<()> { - let cell = ton_types::deserialize_tree_of_cells(&mut base64::decode("te6ccgEBAwEAqAACbIAXsqVXAuRG6+GFp/25WVl2IsmatSkX0jbrXVjoBOwsnEQNAdiGdFv5kAABdRDp2cQZrn10JgIBAJEFJFfQYxaABHulQdJwYfnHP5r0FXhq3wjit36+D+zzx7bkE76OQgrwAsROplLUCShZxn2kTkyjrdZWWw4ol9ZAosUb+zcNiHf6CEICj0Utek39dAZraCNlF3JZ7QVzRDW+drX9S9XYryt8PWg=").unwrap().as_slice()).unwrap(); - let mut state = nekoton_utils::deserialize_account_stuff(cell)?; - - update_library_cell(&mut state.storage.state).await?; - - let contract = jetton::TokenWalletContract(ExecutionContext { - clock: &SimpleClock, - account_stuff: &state, - }); - - let balance = contract.balance()?; - assert_eq!(balance.to_u128().unwrap(), 156092097302); - - Ok(()) - } - - #[tokio::test] - async fn notcoin_wallet_token_contract() -> anyhow::Result<()> { - let cell = ton_types::deserialize_tree_of_cells(&mut base64::decode("te6ccgEBAwEAqgACbIAX5XxfY9N6rJiyOS4NGQc01nd0dzEnWBk87cdqg9bLTwQNAeCGdH/3UAABdXbIjToZrn5eJgIBAJUHFxcOBj4fBYAfGfo6PQWliRZGmmqpYpA1QxmYkyLZonLf41f59x68XdAAvlWFDxGF2lXm67y4yzC17wYKD9A0guwPkMs1gOsM//IIQgK6KRjIlH6bJa+awbiDNXdUFz5YEvgHo9bmQqFHCVlTlQ==").unwrap().as_slice()).unwrap(); - let mut state = nekoton_utils::deserialize_account_stuff(cell)?; - - update_library_cell(&mut state.storage.state).await?; - - let contract = jetton::TokenWalletContract(ExecutionContext { - clock: &SimpleClock, - account_stuff: &state, - }); - - let balance = contract.balance()?; - assert_eq!(balance.to_u128().unwrap(), 6499273466060549); - - Ok(()) - } - #[test] fn tonup_wallet_token_contract() -> anyhow::Result<()> { let cell = ton_types::deserialize_tree_of_cells(&mut base64::decode("te6ccgECEwEAA6kAAm6ADhM4pof2jbUfk8bYdFfLChAB6cUG0z98g+xOrlf4LCzkTQz7BmMVjoAAAVA6z3tMKgC1Y0MmAgEBj0dzWUAIAc+mHGAhdljL3SZo8QcvtpqlV+kMrO8+wlbsMF9hxLGVACvaf0JMrv6BOvPpAgee08uQM/uRpUd//fhA7Nm8twzvYAIBFP8A9KQT9LzyyAsDAgFiBQQAG6D2BdqJofQB9IH0gahhAgLMEAYCASAIBwCD1AEGuQ9qJofQB9IH0gahgCaY/BCAvGooypEF1BCD3uy+8J3QlY+XFi6Z+Y/QAYCdAoEeQoAn0BLGeLAOeLZmT2qkAgEgDgkCASALCgDXO1E0PoA+kD6QNQwB9M/+gD6QDBRUaFSSccF8uLBJ8L/8uLCBYIJMS0AoBa88uLDghB73ZfeyMsfFcs/UAP6AiLPFgHPFslxgBjIywUkzxZw+gLLaszJgED7AEATyFAE+gJYzxYBzxbMye1UgAvc7UTQ+gD6QPpA1DAI0z/6AFFRoAX6QPpAU1vHBVRzbXBUIBNUFAPIUAT6AljPFgHPFszJIsjLARL0APQAywDJ+QBwdMjLAsoHy//J0FANxwUcsfLiwwr6AFGooYIImJaAZrYIoYIImJaAoBihJ5cQSRA4N18E4w0l1wsBgDQwAfMMAI8IAsI4hghDVMnbbcIAQyMsFUAjPFlAE+gIWy2oSyx8Syz/JcvsAkzVsIeIDyFAE+gJYzxYBzxbMye1UAHBSeaAYoYIQc2LQnMjLH1Iwyz9Y+gJQB88WUAfPFslxgBDIywUkzxZQBvoCFctqFMzJcfsAECQQIwHxUD0z/6APpAIfAB7UTQ+gD6QPpA1DBRNqFSKscF8uLBKML/8uLCVDRCcFQgE1QUA8hQBPoCWM8WAc8WzMkiyMsBEvQA9ADLAMkg+QBwdMjLAsoHy//J0AT6QPQEMfoAINdJwgDy4sR3gBjIywVQCM8WcPoCF8trE8yA8AnoIQF41FGcjLHxnLP1AH+gIizxZQBs8WJfoCUAPPFslQBcwjkXKRceJQCKgToIIJycOAoBS88uLFBMmAQPsAECPIUAT6AljPFgHPFszJ7VQCAdQSEQARPpEMHC68uFNgAMMIMcAkl8E4AHQ0wMBcbCVE18D8Azg+kD6QDH6ADFx1yH6ADH6ADBzqbQAAtMfghAPin6lUiC6lTE0WfAJ4IIQF41FGVIgupYxREQD8ArgNYIQWV8HvLqTWfAL4F8EhA/y8IA==").unwrap().as_slice()).unwrap(); @@ -619,44 +592,4 @@ mod tests { Ok(()) } - - #[tokio::test] - async fn mintless_points_token_wallet_contract() -> anyhow::Result<()> { - let cell = - ton_types::deserialize_tree_of_cells(&mut base64::decode("te6ccgEBAwEAyQACbIAMC6d7f4iHKlXHXBfufxF6w/5pIENHdpy1yJnyM+lsrQQNAl2Gc+Ll0AABc7gAAbghs2ElpgIBANQFAlQL5ACADZRqTnEksRaYvpXRMbgzB92SzFv/19WbfQQgdDo7lYwQA+mfQx3OTMfvDyPCOAYxl9HdjYWqWkQCtdgoLLcHjaDKvtRVlwuLLP8LwzhcDJNm1TPewFBFqmlIYet7ln0NupwfCEICDvGeG/QPK6SS/KrDhu7KWb9oJ6OFBwjZ/NmttoOrwzY=").unwrap().as_slice()) - .unwrap(); - let mut state = nekoton_utils::deserialize_account_stuff(cell)?; - - update_library_cell(&mut state.storage.state).await?; - - let contract = jetton::TokenWalletContract(ExecutionContext { - clock: &SimpleClock, - account_stuff: &state, - }); - - let data = contract.get_details()?; - assert_eq!(data.balance.to_u128().unwrap(), 10000000000); - - Ok(()) - } - - #[tokio::test] - async fn hamster_token_wallet_contract() -> anyhow::Result<()> { - let cell = - ton_types::deserialize_tree_of_cells(&mut base64::decode("te6ccgEBAwEAyQACbIAKqccjBo+00V2Pb7qZhRYSHX52cx1iP9tpON3cdZrkP8QNAl2GdhkS0AABegbul1whs2ElpgIBANQFGHJ82gCACGZPh6infgRlai2q2zEzj6/XTCUYYz5sBXNuHUXFkiawACfLlnexAarJqUlmkXX/yPvEfPlx8Id4LDSocvlK3az1CNK1yFN5P0+WKSDutZY4tqmGqAE7w+lQchEcy4oOjEQUCEICDxrT2KRr0oMyHd5jkZX7cmAumzGxcn/swl4u3BCWbfQ=").unwrap().as_slice()) - .unwrap(); - let mut state = nekoton_utils::deserialize_account_stuff(cell)?; - - update_library_cell(&mut state.storage.state).await?; - - let contract = jetton::TokenWalletContract(ExecutionContext { - clock: &SimpleClock, - account_stuff: &state, - }); - - let data = contract.get_details()?; - assert_eq!(data.balance.to_u128().unwrap(), 105000000000); - - Ok(()) - } } diff --git a/src/core/nft_wallet/mod.rs b/src/core/nft_wallet/mod.rs index d0303e7d2..f8c2ba620 100644 --- a/src/core/nft_wallet/mod.rs +++ b/src/core/nft_wallet/mod.rs @@ -158,6 +158,7 @@ impl Nft { let contract_subscription = ContractSubscription::subscribe( clock.clone(), transport, + None, nft_address.clone(), &mut make_contract_state_handler( clock.as_ref(), @@ -166,7 +167,6 @@ impl Nft { None, ), Some(&mut make_transactions_handler(handler.as_ref())), - false, ) .await?; diff --git a/src/core/token_wallet/mod.rs b/src/core/token_wallet/mod.rs index edb3f01ce..8bb4a37f1 100644 --- a/src/core/token_wallet/mod.rs +++ b/src/core/token_wallet/mod.rs @@ -76,10 +76,10 @@ impl TokenWallet { ContractSubscription::subscribe( clock.clone(), transport, + None, address, &mut make_contract_state_handler(clock.clone(), version, &mut balance), on_transactions_found, - false, ) .await? }; diff --git a/src/core/ton_wallet/mod.rs b/src/core/ton_wallet/mod.rs index f14d7fe4e..d36ab3408 100644 --- a/src/core/ton_wallet/mod.rs +++ b/src/core/ton_wallet/mod.rs @@ -60,6 +60,7 @@ impl TonWallet { let contract_subscription = ContractSubscription::subscribe( clock.clone(), transport, + None, address, &mut make_contract_state_handler( clock.as_ref(), @@ -72,7 +73,6 @@ impl TonWallet { handler.as_ref(), wallet_type, )), - false, ) .await?; @@ -104,6 +104,7 @@ impl TonWallet { let contract_subscription = ContractSubscription::subscribe( clock.clone(), transport, + None, address, &mut make_contract_state_handler( clock.as_ref(), @@ -116,7 +117,6 @@ impl TonWallet { handler.as_ref(), wallet_type, )), - false, ) .await?; @@ -141,6 +141,7 @@ impl TonWallet { let contract_subscription = ContractSubscription::subscribe( clock.clone(), transport, + None, existing_wallet.address, &mut make_contract_state_handler( clock.as_ref(), @@ -153,7 +154,6 @@ impl TonWallet { handler.as_ref(), existing_wallet.wallet_type, )), - false, ) .await?; diff --git a/src/core/utils.rs b/src/core/utils.rs index 17c2e4258..2276f9cf2 100644 --- a/src/core/utils.rs +++ b/src/core/utils.rs @@ -16,6 +16,7 @@ use ton_types::{CellType, SliceData, UInt256}; use crate::core::models::*; #[cfg(feature = "wallet_core")] use crate::crypto::{SignedMessage, UnsignedMessage}; +use crate::external::{GqlConnection, GqlRequest}; use crate::transport::models::RawTransaction; use crate::transport::Transport; @@ -494,7 +495,10 @@ pub fn default_headers( type HeadersMap = HashMap; -pub async fn update_library_cell(state: &mut AccountState) -> Result<()> { +pub async fn update_library_cell<'a>( + connection: &'a dyn GqlConnection, + state: &mut AccountState, +) -> Result<()> { if let AccountState::AccountActive { ref mut state_init } = state { if let Some(cell) = &state_init.code { if cell.cell_type() == CellType::LibraryReference { @@ -508,7 +512,7 @@ pub async fn update_library_cell(state: &mut AccountState) -> Result<()> { let mut hash = UInt256::default(); hash.read_from(&mut slice_data)?; - let cell = download_lib(hash).await?; + let cell = download_lib(connection, hash).await?; state_init.set_code(cell); } } @@ -517,19 +521,10 @@ pub async fn update_library_cell(state: &mut AccountState) -> Result<()> { Ok(()) } -async fn download_lib(hash: UInt256) -> Result { - static URL: &str = "https://dton.io/graphql/graphql"; - - let mut headers = reqwest::header::HeaderMap::new(); - headers.insert( - reqwest::header::CONTENT_TYPE, - reqwest::header::HeaderValue::from_static("application/json"), - ); - - let client = reqwest::ClientBuilder::new() - .default_headers(headers) - .build()?; - +async fn download_lib<'a>( + connection: &'a dyn GqlConnection, + hash: UInt256, +) -> Result { let query = serde_json::json!({ "query": format!("{{ get_lib( @@ -539,7 +534,12 @@ async fn download_lib(hash: UInt256) -> Result { }) .to_string(); - let response = client.post(URL).body(query).send().await?; + let response = connection + .post(GqlRequest { + data: query, + long_query: false, + }) + .await?; #[derive(Deserialize)] struct GqlResponse { @@ -551,7 +551,7 @@ async fn download_lib(hash: UInt256) -> Result { get_lib: String, } - let parsed: GqlResponse = response.json().await?; + let parsed: GqlResponse = serde_json::from_str(&response)?; let bytes = base64::decode(parsed.data.get_lib)?; let cell = ton_types::deserialize_tree_of_cells(&mut bytes.as_slice())?; From a4cecaeb7e0c93f5e9b00f6f09932789f5e94f5e Mon Sep 17 00:00:00 2001 From: Alexey Pashinov Date: Tue, 24 Dec 2024 20:27:45 +0100 Subject: [PATCH 21/38] Getting jetton meta --- nekoton-contracts/src/jetton/mod.rs | 10 ++- .../src/jetton/root_token_contract.rs | 50 +++++++++++- src/core/jetton_wallet/mod.rs | 81 ++++++++++++++----- src/core/parsing.rs | 5 +- 4 files changed, 122 insertions(+), 24 deletions(-) diff --git a/nekoton-contracts/src/jetton/mod.rs b/nekoton-contracts/src/jetton/mod.rs index 86cee124b..5d88c323a 100644 --- a/nekoton-contracts/src/jetton/mod.rs +++ b/nekoton-contracts/src/jetton/mod.rs @@ -3,7 +3,7 @@ use nekoton_abi::{ExecutionContext, StackItem}; use ton_block::Serializable; use ton_types::SliceData; -pub use root_token_contract::JettonRootData; +pub use root_token_contract::{JettonRootData, JettonRootMeta}; pub use token_wallet_contract::JettonWalletData; mod root_token_contract; @@ -13,6 +13,7 @@ mod token_wallet_contract; pub struct RootTokenContract<'a>(pub ExecutionContext<'a>); pub const GET_JETTON_DATA: &str = "get_jetton_data"; +pub const GET_JETTON_META: &str = "get_jetton_meta"; pub const GET_WALLET_DATA: &str = "get_wallet_data"; pub const GET_WALLET_ADDRESS: &str = "get_wallet_address"; @@ -69,6 +70,13 @@ impl RootTokenContract<'_> { let data = root_token_contract::get_jetton_data(result)?; Ok(data) } + + pub fn get_meta(&self) -> anyhow::Result { + let result = self.0.run_getter(GET_JETTON_META, &[])?; + + let meta = root_token_contract::get_jetton_meta(result)?; + Ok(meta) + } } #[derive(Copy, Clone)] diff --git a/nekoton-contracts/src/jetton/root_token_contract.rs b/nekoton-contracts/src/jetton/root_token_contract.rs index 96cb16e6a..27561630c 100644 --- a/nekoton-contracts/src/jetton/root_token_contract.rs +++ b/nekoton-contracts/src/jetton/root_token_contract.rs @@ -4,7 +4,7 @@ use nekoton_abi::VmGetterOutput; use nekoton_jetton::{JettonMetaData, MetaDataContent}; use thiserror::Error; use ton_block::{Deserializable, MsgAddressInt}; -use ton_types::Cell; +use ton_types::{Cell, SliceData}; #[derive(PartialEq, Eq, Debug, Clone)] pub struct JettonRootData { @@ -15,6 +15,15 @@ pub struct JettonRootData { pub wallet_code: Cell, } +#[derive(PartialEq, Eq, Debug, Clone)] +pub struct JettonRootMeta { + pub name: String, + pub symbol: String, + pub decimals: u8, + pub base_chain_id: String, + pub base_token: String, +} + pub fn get_jetton_data(res: VmGetterOutput) -> Result { if !res.is_ok { return Err(RootContractError::ExecutionFailed { @@ -63,6 +72,45 @@ pub fn get_jetton_data(res: VmGetterOutput) -> Result { }) } +pub fn get_jetton_meta(res: VmGetterOutput) -> Result { + if !res.is_ok { + return Err(RootContractError::ExecutionFailed { + exit_code: res.exit_code, + } + .into()); + } + + const JETTON_META_STACK_ELEMENTS: usize = 5; + + let stack = res.stack; + if stack.len() != JETTON_META_STACK_ELEMENTS { + return Err(RootContractError::InvalidMethodResultStackSize { + actual: stack.len(), + expected: JETTON_META_STACK_ELEMENTS, + } + .into()); + } + + let slice = SliceData::load_cell_ref(stack[0].as_cell()?)?; + let name = String::from_utf8_lossy(slice.remaining_data().data()).to_string(); + + let slice = SliceData::load_cell_ref(stack[1].as_cell()?)?; + let symbol = String::from_utf8_lossy(slice.remaining_data().data()).to_string(); + + let decimals = stack[2].as_integer()?.into(0..=u8::MAX)?; + + let base_chain_id = stack[3].as_integer()?.to_string(); + let base_token = stack[4].as_integer()?.to_string(); + + Ok(JettonRootMeta { + name, + symbol, + decimals, + base_chain_id, + base_token, + }) +} + pub fn get_wallet_address(res: VmGetterOutput) -> Result { if !res.is_ok { return Err(RootContractError::ExecutionFailed { diff --git a/src/core/jetton_wallet/mod.rs b/src/core/jetton_wallet/mod.rs index d9df5196d..2d47a3b85 100644 --- a/src/core/jetton_wallet/mod.rs +++ b/src/core/jetton_wallet/mod.rs @@ -9,7 +9,7 @@ use crate::transport::Transport; use anyhow::Result; use nekoton_abi::num_traits::ToPrimitive; use nekoton_abi::*; -use nekoton_contracts::jetton::{JettonRootData, JettonWalletData}; +use nekoton_contracts::jetton::{JettonRootData, JettonRootMeta, JettonWalletData}; use nekoton_utils::*; use num_bigint::{BigInt, BigUint, ToBigInt}; use ton_block::{MsgAddressInt, Serializable}; @@ -295,6 +295,40 @@ pub trait JettonWalletSubscriptionHandler: Send + Sync { ); } +pub async fn get_wallet_data( + gql_connection: Arc, + account: ton_block::AccountStuff, +) -> Result { + let mut account = account; + + utils::update_library_cell(gql_connection.as_ref(), &mut account.storage.state).await?; + + let token_wallet_state = nekoton_contracts::jetton::TokenWalletContract(ExecutionContext { + clock: &SimpleClock, + account_stuff: &account, + }); + + let token_wallet_details = token_wallet_state.get_details()?; + Ok(token_wallet_details) +} + +pub async fn get_token_root_meta( + gql_connection: Arc, + account: ton_block::AccountStuff, +) -> Result { + let mut account = account; + + utils::update_library_cell(gql_connection.as_ref(), &mut account.storage.state).await?; + + let token_root_state = nekoton_contracts::jetton::RootTokenContract(ExecutionContext { + clock: &SimpleClock, + account_stuff: &account, + }); + + let token_root_meta = token_root_state.get_meta()?; + Ok(token_root_meta) +} + pub async fn get_token_wallet_details( clock: &dyn Clock, transport: Arc, @@ -335,35 +369,21 @@ pub async fn get_token_wallet_details( Ok((token_wallet_details, root_contract_details)) } -pub async fn get_wallet_data( - gql_connection: Arc, - account: ton_block::AccountStuff, -) -> Result { - let mut account = account; - - utils::update_library_cell(gql_connection.as_ref(), &mut account.storage.state).await?; - - let token_wallet_state = nekoton_contracts::jetton::TokenWalletContract(ExecutionContext { - clock: &SimpleClock, - account_stuff: &account, - }); - - let token_wallet_details = token_wallet_state.get_details()?; - - Ok(token_wallet_details) -} - pub async fn get_token_root_details( clock: &dyn Clock, transport: &dyn Transport, + gql_connection: Arc, root_token_contract: &MsgAddressInt, ) -> Result { - let state = match transport.get_contract_state(root_token_contract).await? { + let mut state = match transport.get_contract_state(root_token_contract).await? { RawContractState::Exists(state) => state, RawContractState::NotExists { .. } => { return Err(JettonWalletError::InvalidRootTokenContract.into()) } }; + + utils::update_library_cell(gql_connection.as_ref(), &mut state.account.storage.state).await?; + nekoton_contracts::jetton::RootTokenContract(state.as_context(clock)).get_details() } @@ -592,4 +612,25 @@ mod tests { Ok(()) } + + #[test] + fn dai_root_token_contract() -> anyhow::Result<()> { + let cell = + ton_types::deserialize_tree_of_cells(&mut base64::decode("te6ccgECRQEADYYAAm6AE+h17Oxgs1h/T5RXhP+oTLidxuHf0pC6qNWtwPVOkCqlFTHghnZCD2AAAXqAUR18GiVwJ0+mKAEDW4JK7BCRgnTXuADXSFAQWkxQ4t3cWOUGSbo/BQeScFYTTzcZwUuAG969LgAAAAMlCwIBFP8A9KQT9LzyyAsDAgLIBQQAEKqCXwWED/LwAgHNBwYAKbbhACGRlgqxnixD9AWW1ZMCAUH2AQFb2QY4BJL4JwAOhpgZj9IAFqAOhqaY/9IBgB+gIYdqJofQB9IH0gahjqGCo8CBPAgC/l3HBZNfBH+OTCBuk18EcODQ0x8x0z8x+gAx+kAwWXDIyVQTAyMQRgHIUAb6AlAEzxZYzxbMzMsfyXAgyMsBE/QA9ADLAMn5AHB0yMsCygfL/8nQxwXilhB7XwvwC+E3QDRURXfIUAb6AlAEzxZYzxbMzMsfye1UIPsEIW7jAtAKCQBG7R7tUwL6QDH6ADFx1yH6ADH6ADBzqbQAAtDTHzFERAPxBoIABF8GART/APSkE/S88sgLDAIBYhANAgFmDw4AI7dgXaiaH0AfSB9IGpqaY+YLcAAltxSdqJofQB9IH0gamppj5g2IUAICxRMRAfKqgjHtRND6APpA+kDU1NMfMAnTP/oAUXGgB/pA+kD6AFRzhnDIyVQTAyMQRgHIUAb6AlAEzxZYzxbMzMsfyXAgyMsBE/QA9ADLAMn5AHB0yMsCygfL/8nQU57HBQ/HBR6x8uLDUbqhggiYloBctgihggiYloCgG6EKEgH4ggiYloC2CXL7AiqOMDA4OIIQc2LQnMjLH8s/UAf6AlAFzxZQBs8WyXGAEMjLBSPPFnD6AstqzMmBAIL7AI44Ols4JtcLAcMABsIAFrCOIoIQ1TJ223CAEMjLBVAIzxYn+gIXy2oWyx8Wyz/JgQCC+wCSNTXiECPiBFAzBR4CAcsXFAIBzhYVAJk7UTQ+gD6QPpA1NTTHzAGgCDXIdMfghAXjUUZUiC6ghB73ZfeE7oSsfLixYBA1yH6ADAVoAUQNEEwyFAG+gJQBM8WWM8WzMzLH8ntVIACLO1E0PoA+kD6QNTU0x8wEEVfBQHHBfLiwYIImJaAcPsC0z+CENUydttwgBDIywUD+kAwE88WIvoCEstqyx/LP8mBAIL7AIAIBICEYAgFYHBkCASAbGgCtO1E0PoA+kD6QNTU0x8wMFImxwXy4sEF0z/6QNQB+wTTHzBHYMhQBvoCUATPFljPFszMyx/J7VSCENUydttwgBDIywVQA88WIvoCEstqyx/LP8mAQvsAgAJc7UTQ+gD6QPpA1NTTHzA1W1IUxwXy4sED0z/6QDCCEBT9raDIyx8Syz9QBM8WUAPPFhLLH8lxgBDIywVQA88WcPoCEstqzMmAQPsAgAgEgHx0B8TtRND6APpA+kDU1NMfMAnTP/oA+kD0BCDXSYEBC75RpKFSnscF8uLBLML/8uLCCoIJMS0AoBu88uLDghB73ZfeyMsfE8s/AfoCJc8WAc8WF/QABJgE+kAwE88WApE04gLJcYAYyMsFJM8WcPoCy2rMyYBA+wAEUDWAeACbIUAb6AlAEzxZYzxbMzMsfye1UAfcA9M/+gD6QCHwAe1E0PoA+kD6QNTU0x8wUVihUkzHBfLiwSrC//LiwlQ2JnDIyVQTAyMQRgHIUAb6AlAEzxZYzxbMzMsfyXAgyMsBE/QA9ADLAMkg+QBwdMjLAsoHy//J0Ab6QPQEMfoAINdJwgDy4sSCEBeNRRnIyx8cgIADayz9QCvoCJc8WIc8WKfoCUArPFsklyMsfUArPFlIgzMnIzBn0AMl3gBjIywVQB88WcPoCFstrGMwUzMklkXKRceJQCqgVoIIJycOAoBa88uLFBoBA+wBQBAUDyFAG+gJQBM8WWM8WzMzLH8ntVAIB1CMiABE+kQwcLry4U2AB9ztou37IMcAkl8E4AHQ0wMBcbCVE18D8BHg+kD6QDH6ADFx1yH6ADH6ADBzqbQAItdJwAGOEALUMfQEMCBulF8F2zHg0ALeAtMfghAPin6lUiC6lTE0WfAM4IIQF41FGVIgupcxREQD8QaC4DWCEFlfB7xSELqUMFnwDeCAkAFpsIoIQfW/yVFIQupMw8A7gggs2kZlSELqTMPAP4IIQHqYO/7qS8BDgW4QP8vACghIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOAAAAAAAAAAAAAAAABrz8ynovhVAdNh2nR/6TuBYsdvDJyYABkRBSQASRGFpIFRva2VuART/APSkE/S88sgLKQIBYjMqAgFYLCsAI7ilXtRND6APpA1NTU0x8wbEKAIBIC4tADu2M12omh9AH0gampqaY+YCBqvguhqammD6f/p/5hACAVgyLwHvrxb2omh9AH0gampqaY+YLYDoamppg+n/6f+YLYDoAL14A/wUfSIAwAh4A4DACHgDwCB4BGRGhM0Ojo4OZ0Xl7o3txa6N7WytzmWsLg0lzEzFzu3uTW5l7S2sLOyl8GeLLGeLRYnUZ4sA54tFoXObszxni2ToNoDAMAHScMjLBwHPFsmC8GEF1sx2r0ADJelNWIzlEb5b/btztDfcUeykORfXpD49WIMH9BcBcMjLBwHPFsmC8O6A/S8eA0gOIoI2NZbudS17sn9Qd2uVCGoCeRiWdZI+WIMH9BcC0HDIywcBzxbJMQDEgvCCo1N/8NvOfuw11p7cOhie5vF9gvNTpVP5qpbLC+POiVgDgwf0FwFwyMsHAc8WyYLwt2p8oVPCRnFlgzW70IlGNQ/8Yh+hxRbnEjCV1P/VxYFYgwf0F3DIywf0AMl/QxMAla289qJofQB9IGpqammPmAqvgvwUALhkZKoJgZGIIwDkKAN9ASgCZ4ssZ4tmZmWP5LgQZGWAifoAegBlgGT8gDg6ZGWBZQPl/+ToQAICzDk0AgEgODUCASA3NgBRSCCzaRmcjLHxTLPwHPFszLH8lxgBDIywVQA88WcPoCEstqzMmAQPsAgAv1+ChGBHDIyVQTAyMQRgHIUAb6AlAEzxZYzxbMzMsfyXAgyMsBE/QA9ADLAMkg+QBwdMjLAsoHy//J0AXIyx9QBM8WzMnIzPQAyXeAGMjLBSTPFnD6AstrEszMyYBA+wCABF0Q66TVgVCQYQBHCuQQzEAYAOWDgNKA8mQZZ4uA54tk6HAYQCASA7OgBn9kODeARwuoodSGEGEEyVMryVMYcQq3xgDSEmAACXMZmZFOEVKpEDfAgOWDgVKBcjYQ5OhAH30QY4BJL4HwAOhpgYC42EkvgfB9IH0gGP0AGLjrkP0AGP0AGAFpj+mf9qJofQB9IGpqammPmEAKqUhdRx8bm5wcKahjgvlwJIF9IH0AGOoYEGhAMGuQ/QAYKhOwKjS9eAaYKAJQAoGiImQoA30BKAJniwlmZmZlj+T2qnBDwC/oIQe92X3lKQuo7zODk5A/oA+kD4KFRisXDIyVQTAyMQRgHIUAb6AlAEzxZYzxbMzMsfyXAgyMsBE/QA9ADLAMn5AHB0yMsCygfL/8nQKccF8uBKUUKhBQNKFFCXyFAG+gJQBM8WEszMzMsfye1UAfpA9AQi1wsBwwCSXwfjDeBEPQTwghAsdrlzUpC6jtEVXwUzggiYloAVoBW88uBLAvpA0wAwlcghzxbJkW3ighDRc1QAcIAYyMsFUAXPFiT6AhTLahPLHxTLPyP6RDBwupZsInABywHjDfQAyYBA+wDgghAT5cEaUpC64wI6OoIQFP2toFJwuuMCJsADQ0JBPgL+jiE1NRXHBfLgSfpAMBA1QTTIUAb6AlAEzxYSzMzMyx/J7VTgJsAEjiMxNDRRQ8cF8uBJ1DAQNUQzAshQBvoCUATPFhLMzMzLH8ntVOCCEBcjDDpScLqOERAjXwM1NVsBxwXy4EnUMPsE4IIQce3jjFJwuuMCMTKCEG+PxQFSUEA/AMy6jhswbCIh+kJvE9cL/8AA8tBMAfpAMEQUA23wDTDgMTU1ghAepg7/ErqOMwLHBfLgSYIQO5rKAHD7AoIQ1TJ223CAEMjLBQT6QDAUzxYj+gITy2oSyx/LP8mBAIL7AOBfBIQP8vAASDA0NFFDxwXy4EnUMASkEDVEMMhQBvoCUATPFhLMzMzLH8ntVADuNl8D+kD4KFAHcMjJVBMDIxBGAchQBvoCUATPFljPFszMyx/JcCDIywET9AD0AMsAyfkAcHTIywLKB8v/ydAjxwXy4EoE+kDTHzBSQLyVQQQD8A6OIWwxghDVMnbbcIAQyMsFUAPPFiL6AhLLassfyz/JgEL7AOIAkBA2XwYyggiYloAUoBS88uBLAtDU1NMH0//T/zCCEJPlwRpwgBjIywVQCc8WKPoCGMtqF8sfFcs/ywcTy/8Ty/8SzMzJgED7AAB2+ChEA3DIyVQTAyMQRgHIUAb6AlAEzxZYzxbMzMsfyXAgyMsBE/QA9ADLAMn5AHB0yMsCygfL/8nQzxYAsnCAEMjLBVAEzxYj+gITy2ohbp80W2wighDVMnbbWMsfyz+OLzMh10mBAQu+lAH6QDCSMSPiyAHPFszJyFADzxYSzMmCEGgIjZtYyx8Ty38BzxbM4smAQvsA").unwrap().as_slice()) + .unwrap(); + let state = nekoton_utils::deserialize_account_stuff(cell)?; + + let contract = jetton::RootTokenContract(ExecutionContext { + clock: &SimpleClock, + account_stuff: &state, + }); + + let meta = contract.get_meta()?; + assert_eq!(meta.name, "Dai Token"); + assert_eq!(meta.symbol, "DAI"); + assert_eq!(meta.decimals, 18); + assert_eq!(meta.base_chain_id, "56"); + + Ok(()) + } } diff --git a/src/core/parsing.rs b/src/core/parsing.rs index b605ad241..59cdb7e4e 100644 --- a/src/core/parsing.rs +++ b/src/core/parsing.rs @@ -668,7 +668,7 @@ pub fn parse_jetton_transaction( description: &ton_block::TransactionDescrOrdinary, ) -> Option { const STANDART_JETTON_CELLS: usize = 0; - const BROXUS_JETTON_CELLS: usize = 2; + const MINTLESS_JETTON_CELLS: usize = 2; if description.aborted { return None; @@ -676,12 +676,13 @@ pub fn parse_jetton_transaction( let in_msg = tx.in_msg.as_ref()?.read_struct().ok()?; + // Workaround to extract body since we don`t store the type in message let mut body = { let body = in_msg.body()?; let refs = body.remaining_references(); match refs { - BROXUS_JETTON_CELLS => { + MINTLESS_JETTON_CELLS => { let cell = body.reference_opt(1)?; SliceData::load_cell(cell).ok()? } From b449026648448b232169366196111e3039a7d9da Mon Sep 17 00:00:00 2001 From: Stanislav Eliseev Date: Fri, 17 Jan 2025 15:48:14 +0100 Subject: [PATCH 22/38] Update initial storage fee --- src/core/token_wallet/mod.rs | 66 +++++++++++++++++++++++++++++------- 1 file changed, 54 insertions(+), 12 deletions(-) diff --git a/src/core/token_wallet/mod.rs b/src/core/token_wallet/mod.rs index 8bb4a37f1..7d95418b4 100644 --- a/src/core/token_wallet/mod.rs +++ b/src/core/token_wallet/mod.rs @@ -2,13 +2,13 @@ use std::convert::TryFrom; use std::sync::Arc; use anyhow::Result; -use num_bigint::{BigInt, BigUint, ToBigInt}; -use ton_block::MsgAddressInt; - use nekoton_abi::*; use nekoton_contracts::tip3_any::{RootTokenContractState, TokenWalletContractState}; use nekoton_contracts::{old_tip3, tip3_1}; use nekoton_utils::*; +use num_bigint::{BigInt, BigUint, ToBigInt}; +use ton_block::MsgAddressInt; +use ton_executor::BlockchainConfig; use crate::core::models::*; use crate::core::parsing::*; @@ -142,8 +142,9 @@ impl TokenWallet { const FEE_MULTIPLIER: u128 = 2; // Prepare internal message - let internal_message = - self.prepare_transfer(destination, tokens, notify_receiver, payload, 0)?; + let internal_message = self + .prepare_transfer(destination, tokens, notify_receiver, payload, 0) + .await?; let mut message = ton_block::Message::with_int_header(ton_block::InternalMessageHeader { src: ton_block::MsgAddressIntOrNone::Some( @@ -220,7 +221,35 @@ impl TokenWallet { Ok((attached_amount * FEE_MULTIPLIER) as u64) } - pub fn prepare_transfer( + async fn calculate_initial_balance( + &self, + config: &BlockchainConfig, + address: &MsgAddressInt, + ) -> Result { + let gas_config = config.get_gas_config(address.is_masterchain()); + + let contract_state = self + .contract_subscription() + .transport() + .get_contract_state(address) + .await?; + + let storage_fees = match contract_state.into_contract() { + Some(contract) => { + let storage_fees = config.calc_storage_fee( + &contract.account.storage_stat, + address.is_masterchain(), + contract.timings.current_utime(self.clock.as_ref()), + )?; + storage_fees.as_u128() as u64 + } + None => 0, + }; + + Ok(30000 * gas_config.gas_price + 100_000_000 * storage_fees / 1) // ever_storage_fee = 1) + } + + pub async fn prepare_transfer( &self, destination: TransferRecipient, tokens: BigUint, @@ -228,8 +257,17 @@ impl TokenWallet { payload: ton_types::Cell, mut attached_amount: u64, ) -> Result { - if matches!(&destination, TransferRecipient::OwnerWallet(_)) { - attached_amount += INITIAL_BALANCE; + let transport = self.contract_subscription.transport().clone(); + + let config = transport + .get_blockchain_config(self.clock.as_ref(), true) + .await?; + + match &destination { + TransferRecipient::OwnerWallet(address) => { + attached_amount += self.calculate_initial_balance(&config, &address).await?; + } + _ => (), } let (function, input) = match self.version { @@ -242,11 +280,14 @@ impl TokenWallet { .arg(BigUint128(tokens)) // tokens } TransferRecipient::OwnerWallet(owner_wallet) => { + let initial_balance = self + .calculate_initial_balance(&config, &owner_wallet) + .await?; MessageBuilder::new(token_wallet_contract::transfer_to_recipient()) .arg(BigUint256(Default::default())) // recipient_public_key .arg(owner_wallet) // recipient_address .arg(BigUint128(tokens)) // tokens - .arg(BigUint128(INITIAL_BALANCE.into())) // deploy_grams + .arg(BigUint128(initial_balance.into())) // deploy_grams } } .arg(BigUint128(Default::default())) // grams / transfer_grams @@ -264,10 +305,13 @@ impl TokenWallet { .arg(token_wallet) // recipient token wallet } TransferRecipient::OwnerWallet(owner_wallet) => { + let initial_balance = self + .calculate_initial_balance(&config, &owner_wallet) + .await?; MessageBuilder::new(token_wallet_contract::transfer()) .arg(BigUint128(tokens)) // amount .arg(owner_wallet) // recipient - .arg(BigUint128(INITIAL_BALANCE.into())) // deployWalletValue + .arg(BigUint128(initial_balance.into())) // deployWalletValue } } .arg(&self.owner) // remainingGasTo @@ -470,8 +514,6 @@ pub async fn get_token_root_details_from_token_wallet( Ok((root_token_contract, details)) } -const INITIAL_BALANCE: u64 = 100_000_000; // 0.1 TON - fn make_contract_state_handler( clock: Arc, version: TokenWalletVersion, From 3a4813d325ce2492383bbd76b3ae58ca7258915e Mon Sep 17 00:00:00 2001 From: Stanislav Eliseev Date: Fri, 17 Jan 2025 15:58:09 +0100 Subject: [PATCH 23/38] Remove multiple get_contract_state calls --- src/core/token_wallet/mod.rs | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/core/token_wallet/mod.rs b/src/core/token_wallet/mod.rs index 7d95418b4..ded55a748 100644 --- a/src/core/token_wallet/mod.rs +++ b/src/core/token_wallet/mod.rs @@ -258,6 +258,7 @@ impl TokenWallet { mut attached_amount: u64, ) -> Result { let transport = self.contract_subscription.transport().clone(); + let initial_balance: u64; let config = transport .get_blockchain_config(self.clock.as_ref(), true) @@ -265,9 +266,12 @@ impl TokenWallet { match &destination { TransferRecipient::OwnerWallet(address) => { - attached_amount += self.calculate_initial_balance(&config, &address).await?; + initial_balance = self.calculate_initial_balance(&config, &address).await?; + attached_amount += initial_balance; + } + TransferRecipient::TokenWallet(address) => { + initial_balance = self.calculate_initial_balance(&config, &address).await?; } - _ => (), } let (function, input) = match self.version { @@ -280,9 +284,6 @@ impl TokenWallet { .arg(BigUint128(tokens)) // tokens } TransferRecipient::OwnerWallet(owner_wallet) => { - let initial_balance = self - .calculate_initial_balance(&config, &owner_wallet) - .await?; MessageBuilder::new(token_wallet_contract::transfer_to_recipient()) .arg(BigUint256(Default::default())) // recipient_public_key .arg(owner_wallet) // recipient_address @@ -305,9 +306,6 @@ impl TokenWallet { .arg(token_wallet) // recipient token wallet } TransferRecipient::OwnerWallet(owner_wallet) => { - let initial_balance = self - .calculate_initial_balance(&config, &owner_wallet) - .await?; MessageBuilder::new(token_wallet_contract::transfer()) .arg(BigUint128(tokens)) // amount .arg(owner_wallet) // recipient From e3937b46c458c1a5c072859d7152d97fed2e3a23 Mon Sep 17 00:00:00 2001 From: Stanislav Eliseev Date: Fri, 17 Jan 2025 23:30:47 +0100 Subject: [PATCH 24/38] Change amounts to const fee prices --- src/core/token_wallet/mod.rs | 38 +++++++++++++++++++----------------- 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/src/core/token_wallet/mod.rs b/src/core/token_wallet/mod.rs index ded55a748..8b2293496 100644 --- a/src/core/token_wallet/mod.rs +++ b/src/core/token_wallet/mod.rs @@ -221,29 +221,31 @@ impl TokenWallet { Ok((attached_amount * FEE_MULTIPLIER) as u64) } - async fn calculate_initial_balance( + fn calculate_initial_balance( &self, config: &BlockchainConfig, address: &MsgAddressInt, ) -> Result { let gas_config = config.get_gas_config(address.is_masterchain()); - let contract_state = self - .contract_subscription() - .transport() - .get_contract_state(address) - .await?; - - let storage_fees = match contract_state.into_contract() { - Some(contract) => { - let storage_fees = config.calc_storage_fee( - &contract.account.storage_stat, - address.is_masterchain(), - contract.timings.current_utime(self.clock.as_ref()), - )?; - storage_fees.as_u128() as u64 + let prices = config.raw_config().storage_prices()?; + let mut most_recent_bit_price = 0; + let mut most_recent_mc_bit_price = 0; + let mut most_recent_time = 0; + + for i in 0..prices.len()? { + let price = prices.get(i as u32)?; + if most_recent_time < price.utime_since { + most_recent_time = price.utime_since; + most_recent_bit_price = price.bit_price_ps; + most_recent_mc_bit_price = price.mc_bit_price_ps; } - None => 0, + } + + let storage_fees = if address.is_masterchain() { + most_recent_mc_bit_price + } else { + most_recent_bit_price }; Ok(30000 * gas_config.gas_price + 100_000_000 * storage_fees / 1) // ever_storage_fee = 1) @@ -266,11 +268,11 @@ impl TokenWallet { match &destination { TransferRecipient::OwnerWallet(address) => { - initial_balance = self.calculate_initial_balance(&config, &address).await?; + initial_balance = self.calculate_initial_balance(&config, &address)?; attached_amount += initial_balance; } TransferRecipient::TokenWallet(address) => { - initial_balance = self.calculate_initial_balance(&config, &address).await?; + initial_balance = self.calculate_initial_balance(&config, &address)?; } } From c404651cb8222300b6671352451642f955e69301 Mon Sep 17 00:00:00 2001 From: Stanislav Eliseev Date: Sun, 19 Jan 2025 22:13:38 +0100 Subject: [PATCH 25/38] Slightly optimize code --- nekoton-abi/src/lib.rs | 55 ++++++++++++++++++++++++++++++++++++ src/core/token_wallet/mod.rs | 40 +++++++++++++------------- 2 files changed, 74 insertions(+), 21 deletions(-) diff --git a/nekoton-abi/src/lib.rs b/nekoton-abi/src/lib.rs index ec6036b48..73e1bf7ec 100644 --- a/nekoton-abi/src/lib.rs +++ b/nekoton-abi/src/lib.rs @@ -1331,4 +1331,59 @@ mod tests { assert_eq!(read_function_id(&remaining_body).unwrap(), 1290691692); // sendTransaction input id } + + fn parse_cell(boc: &str) -> anyhow::Result { + let boc = boc.trim(); + if boc.is_empty() { + Ok(ton_types::Cell::default()) + } else { + let body = base64::decode(boc)?; + ton_types::deserialize_tree_of_cells(&mut body.as_slice()) + } + } + #[test] + fn hello() -> Result<()> { + let account = "te6ccgECCAEAAWMAAnHABjAbLHVZbm5Wmm0Trk7HDJTxd+zgvhn5aN3Oc9ROevxyEIJBQznnq6AAAMmJtAYVEUBKXTw4E0ACAQBQyLDWxgjLA6yhKJfmGLWfXdvRC34pWEXEek1ncgteNXUAAAGTRh7KRgEU/wD0pBP0vPLICwMCASAHBALm8nHXAQHAAPJ6gwjXGO1E0IMH1wHXCz/I+CjPFiPPFsn5AANx1wEBwwCagwfXAVETuvLgZN6AQNcBgCDXAYAg1wFUFnX5EPKo+CO78nlmvvgjgQcIoIED6KhSILyx8nQCIIIQTO5kbLrjDwHIy//LP8ntVAYFAD6CEBaePhG6jhH4AAKTINdKl3jXAdQC+wDo0ZMy8jziAJgwAtdM0PpAgwbXAXHXAXjXAddM+ABwgBAEqgIUscjLBVAFzxZQA/oCy2ki0CHPMSHXSaCECbmYM3ABywBYzxaXMHEBywASzOLJAfsAAATSMA=="; + let tx = "te6ccgECDAEAAnAAA7V2MBssdVlublaabROuTscMlPF37OC+Gflo3c5z1E56/HAAAyYm8XlAEKy5RXcT3fqNI58hfr0g4fdhYXBqSMX+HjpeTjGvKFBwAAMmJtAYVDZzz11gADRs0o5IBQQBAg8MQoYagXrEQAMCAG/Jg9CQTAosIAAAAAAAAgAAAAAAAiQUizx+r6MbUjovm5x2fP/W4DVTBrBq54SSuX2h3AswQFAWDACdQpDjE4gAAAAAAAAAAB3AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIACCcrqCn0Efu7Vm1b+COCiqvWs4TNx2oxYmPkhulZEkArnZw95XLR5CFFs3X75wm4iqv+Nca0J1tAkWz6i3WmL0YEACAeAIBgEB3wcAsUgAxgNljqstzcrTTaJ1ydjhkp4u/ZwXwz8tG7nOeonPX48AGMBssdVlublaabROuTscMlPF37OC+Gflo3c5z1E56/HQ7msoAAYKLDAAAGTE3i8oBM5566xAAUWIAMYDZY6rLc3K002idcnY4ZKeLv2cF8M/LRu5znqJz1+ODAkB4Zrdv8UI48I32K2mO07+dwrfKPTgLSPJw+wxQdfU6xu1vMGDMUCrW17rxr2ihaAfQ9JTGcJz6WdXxAVsWl2YLYPyLDWxgjLA6yhKJfmGLWfXdvRC34pWEXEek1ncgteNXUAAAGTRiBG/mc89hBM7mRsgCgFlgAxgNljqstzcrTTaJ1ydjhkp4u/ZwXwz8tG7nOeonPX44AAAAAAAAAAAAAAAB3NZQAA4CwAA"; + + let config = "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"; + + let mut root_cell = parse_cell(account)?; + let account = ton_block::Account::construct_from_cell(root_cell.clone())?; + let tx = ton_block::Transaction::construct_from_base64(tx)?; + let in_msg = tx.read_in_msg()?; + + let config = ton_block::ConfigParams::construct_from_base64(config)?; + let executor = ton_executor::OrdinaryTransactionExecutor::new( + ton_executor::BlockchainConfig::with_config(config, 42)?, + ); + + let last_trans_lt = match account.stuff() { + Some(state) => state.storage.last_trans_lt, + None => 0, + }; + + let params = ton_executor::ExecuteParams { + block_unixtime: tx.now, + block_lt: tx.lt, + last_tr_lt: Arc::new(AtomicU64::new(last_trans_lt)), + trace_callback: Some(Arc::new(move |args, info| { + //actions.push(info); + println!( + "MUUU {:?}", + args.ctrl(7).unwrap().as_tuple().unwrap().last().unwrap() + ); + println!("\n ============ \n CMD: {}", info.cmd_str,); + for item in &info.stack.storage { + println!("{item}"); + } + })), + ..Default::default() + }; + + let transaction = + executor.execute_with_libs_and_params(in_msg.as_ref(), &mut root_cell, params)?; + + Ok(()) + } } diff --git a/src/core/token_wallet/mod.rs b/src/core/token_wallet/mod.rs index 8b2293496..f8d379a80 100644 --- a/src/core/token_wallet/mod.rs +++ b/src/core/token_wallet/mod.rs @@ -1,6 +1,11 @@ use std::convert::TryFrom; use std::sync::Arc; +use crate::core::models::*; +use crate::core::parsing::*; +use crate::core::transactions_tree::*; +use crate::transport::models::{RawContractState, RawTransaction}; +use crate::transport::Transport; use anyhow::Result; use nekoton_abi::*; use nekoton_contracts::tip3_any::{RootTokenContractState, TokenWalletContractState}; @@ -10,12 +15,6 @@ use num_bigint::{BigInt, BigUint, ToBigInt}; use ton_block::MsgAddressInt; use ton_executor::BlockchainConfig; -use crate::core::models::*; -use crate::core::parsing::*; -use crate::core::transactions_tree::*; -use crate::transport::models::{RawContractState, RawTransaction}; -use crate::transport::Transport; - use super::{ContractSubscription, InternalMessage}; pub struct TokenWallet { @@ -233,14 +232,14 @@ impl TokenWallet { let mut most_recent_mc_bit_price = 0; let mut most_recent_time = 0; - for i in 0..prices.len()? { - let price = prices.get(i as u32)?; + prices.map.iterate(|price| { if most_recent_time < price.utime_since { most_recent_time = price.utime_since; most_recent_bit_price = price.bit_price_ps; most_recent_mc_bit_price = price.mc_bit_price_ps; } - } + Ok(true) + })?; let storage_fees = if address.is_masterchain() { most_recent_mc_bit_price @@ -248,7 +247,9 @@ impl TokenWallet { most_recent_bit_price }; - Ok(30000 * gas_config.gas_price + 100_000_000 * storage_fees / 1) // ever_storage_fee = 1) + Ok(30000u64 + .saturating_mul(gas_config.gas_price) + .saturating_add(100_000_000u64.saturating_mul(storage_fees))) } pub async fn prepare_transfer( @@ -259,21 +260,18 @@ impl TokenWallet { payload: ton_types::Cell, mut attached_amount: u64, ) -> Result { - let transport = self.contract_subscription.transport().clone(); - let initial_balance: u64; - - let config = transport - .get_blockchain_config(self.clock.as_ref(), true) - .await?; + let mut initial_balance: u64 = 0; match &destination { - TransferRecipient::OwnerWallet(address) => { - initial_balance = self.calculate_initial_balance(&config, &address)?; - attached_amount += initial_balance; - } TransferRecipient::TokenWallet(address) => { - initial_balance = self.calculate_initial_balance(&config, &address)?; + let transport = self.contract_subscription.transport(); + let config = transport + .get_blockchain_config(self.clock.as_ref(), true) + .await?; + initial_balance = self.calculate_initial_balance(&config, address)?; + attached_amount += initial_balance } + _ => (), } let (function, input) = match self.version { From 0b7387a1ce6be57d320e09fe286e49d6ed7c9fd0 Mon Sep 17 00:00:00 2001 From: Stanislav Eliseev Date: Sun, 19 Jan 2025 23:51:43 +0100 Subject: [PATCH 26/38] Add non-zero stub balance for workchains --- src/core/token_wallet/mod.rs | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/src/core/token_wallet/mod.rs b/src/core/token_wallet/mod.rs index f8d379a80..0bc03be66 100644 --- a/src/core/token_wallet/mod.rs +++ b/src/core/token_wallet/mod.rs @@ -252,6 +252,14 @@ impl TokenWallet { .saturating_add(100_000_000u64.saturating_mul(storage_fees))) } + fn stub_balance(address: &MsgAddressInt) -> u64 { + if address.is_masterchain() { + 1000 + } else { + 1 + } + } + pub async fn prepare_transfer( &self, destination: TransferRecipient, @@ -260,19 +268,22 @@ impl TokenWallet { payload: ton_types::Cell, mut attached_amount: u64, ) -> Result { - let mut initial_balance: u64 = 0; - - match &destination { - TransferRecipient::TokenWallet(address) => { + let initial_balance = match &destination { + TransferRecipient::OwnerWallet(address) => { let transport = self.contract_subscription.transport(); let config = transport .get_blockchain_config(self.clock.as_ref(), true) .await?; - initial_balance = self.calculate_initial_balance(&config, address)?; - attached_amount += initial_balance + + let initial_balance = match self.calculate_initial_balance(&config, address) { + Ok(value) => value + Self::stub_balance(address), + Err(_) => Self::stub_balance(address), + }; + attached_amount += initial_balance; + initial_balance } - _ => (), - } + TransferRecipient::TokenWallet(address) => Self::stub_balance(address), + }; let (function, input) = match self.version { TokenWalletVersion::OldTip3v4 => { From d7337b9d0070edfd1804f09360a98a333f4ea44d Mon Sep 17 00:00:00 2001 From: Stanislav Eliseev Date: Sun, 19 Jan 2025 23:56:47 +0100 Subject: [PATCH 27/38] Add non-zero stub balance for workchains --- src/core/token_wallet/mod.rs | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/core/token_wallet/mod.rs b/src/core/token_wallet/mod.rs index 0bc03be66..c8240dc53 100644 --- a/src/core/token_wallet/mod.rs +++ b/src/core/token_wallet/mod.rs @@ -252,14 +252,6 @@ impl TokenWallet { .saturating_add(100_000_000u64.saturating_mul(storage_fees))) } - fn stub_balance(address: &MsgAddressInt) -> u64 { - if address.is_masterchain() { - 1000 - } else { - 1 - } - } - pub async fn prepare_transfer( &self, destination: TransferRecipient, @@ -268,6 +260,14 @@ impl TokenWallet { payload: ton_types::Cell, mut attached_amount: u64, ) -> Result { + fn stub_balance(address: &MsgAddressInt) -> u64 { + if address.is_masterchain() { + 1000 + } else { + 1 + } + } + let initial_balance = match &destination { TransferRecipient::OwnerWallet(address) => { let transport = self.contract_subscription.transport(); @@ -276,13 +276,13 @@ impl TokenWallet { .await?; let initial_balance = match self.calculate_initial_balance(&config, address) { - Ok(value) => value + Self::stub_balance(address), - Err(_) => Self::stub_balance(address), + Ok(value) => value + stub_balance(address), + Err(_) => stub_balance(address), }; attached_amount += initial_balance; initial_balance } - TransferRecipient::TokenWallet(address) => Self::stub_balance(address), + TransferRecipient::TokenWallet(address) => stub_balance(address), }; let (function, input) = match self.version { From 0694383e93e466fa0605bba199da3a089dc23756 Mon Sep 17 00:00:00 2001 From: Stanislav Eliseev Date: Tue, 21 Jan 2025 19:07:41 +0100 Subject: [PATCH 28/38] fix: add temporary solution for fees --- src/core/token_wallet/mod.rs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/core/token_wallet/mod.rs b/src/core/token_wallet/mod.rs index c8240dc53..d30d94481 100644 --- a/src/core/token_wallet/mod.rs +++ b/src/core/token_wallet/mod.rs @@ -1,4 +1,5 @@ use std::convert::TryFrom; +use std::ops::Shr; use std::sync::Arc; use crate::core::models::*; @@ -247,9 +248,15 @@ impl TokenWallet { most_recent_bit_price }; - Ok(30000u64 - .saturating_mul(gas_config.gas_price) - .saturating_add(100_000_000u64.saturating_mul(storage_fees))) + let fees = if storage_fees > 1 { + 600_000_000u64 + } else { + 30000u64 + .saturating_mul(gas_config.gas_price.shr(16)) + .saturating_add(100_000_000u64.saturating_mul(storage_fees)) + }; + + Ok(fees) } pub async fn prepare_transfer( From 1f7359944215a3d250b6c4325f43b243b11c41c6 Mon Sep 17 00:00:00 2001 From: Stanislav Eliseev Date: Wed, 22 Jan 2025 12:25:57 +0100 Subject: [PATCH 29/38] fix: fix initial balance calculation --- src/core/token_wallet/mod.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/core/token_wallet/mod.rs b/src/core/token_wallet/mod.rs index d30d94481..08d07fb39 100644 --- a/src/core/token_wallet/mod.rs +++ b/src/core/token_wallet/mod.rs @@ -248,14 +248,14 @@ impl TokenWallet { most_recent_bit_price }; - let fees = if storage_fees > 1 { + let fee_base = if storage_fees > 1 { 600_000_000u64 } else { - 30000u64 - .saturating_mul(gas_config.gas_price.shr(16)) - .saturating_add(100_000_000u64.saturating_mul(storage_fees)) + 100_000_000u64 }; + let fees = 30000u64.saturating_mul(gas_config.gas_price.shr(16)) + fee_base; + Ok(fees) } From 6c2e23404a420c8bea7074c0b8619e9a549049ce Mon Sep 17 00:00:00 2001 From: Stanislav Eliseev Date: Wed, 22 Jan 2025 13:18:26 +0100 Subject: [PATCH 30/38] fix: fix initial balance calculation --- src/core/token_wallet/mod.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/core/token_wallet/mod.rs b/src/core/token_wallet/mod.rs index 08d07fb39..5fe174f72 100644 --- a/src/core/token_wallet/mod.rs +++ b/src/core/token_wallet/mod.rs @@ -231,11 +231,11 @@ impl TokenWallet { let prices = config.raw_config().storage_prices()?; let mut most_recent_bit_price = 0; let mut most_recent_mc_bit_price = 0; - let mut most_recent_time = 0; + let mut most_recent_time: i32 = -1; prices.map.iterate(|price| { - if most_recent_time < price.utime_since { - most_recent_time = price.utime_since; + if most_recent_time < price.utime_since as i32 { + most_recent_time = price.utime_since as i32; most_recent_bit_price = price.bit_price_ps; most_recent_mc_bit_price = price.mc_bit_price_ps; } From f3c22a334cacc8788abf2590d750baf6f9473866 Mon Sep 17 00:00:00 2001 From: Stanislav Eliseev Date: Thu, 23 Jan 2025 13:07:49 +0100 Subject: [PATCH 31/38] fix: change non-existent contract state to uninit in local execution if its balance is overridden --- src/core/contract_subscription/mod.rs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/core/contract_subscription/mod.rs b/src/core/contract_subscription/mod.rs index c67463ba4..24f65db31 100644 --- a/src/core/contract_subscription/mod.rs +++ b/src/core/contract_subscription/mod.rs @@ -5,8 +5,7 @@ use futures_util::StreamExt; use nekoton_abi::{Executor, LastTransactionId}; use nekoton_utils::*; use serde::{Deserialize, Serialize}; -use ton_block::MsgAddressInt; - +use ton_block::{AccountStuff, MsgAddressInt}; use super::models::{ ContractState, PendingTransaction, ReliableBehavior, TransactionsBatchInfo, TransactionsBatchType, @@ -252,11 +251,17 @@ impl ContractSubscription { .transport .get_blockchain_config(self.clock.as_ref(), true) .await?; + let mut account = match self.transport.get_contract_state(&self.address).await? { RawContractState::Exists(state) => ton_block::Account::Account(state.account), - RawContractState::NotExists { .. } => ton_block::Account::AccountNone, + RawContractState::NotExists { .. } if options.override_balance.is_some() => { + let stuff = AccountStuff { addr: self.address.clone(), ..Default::default() }; + ton_block::Account::Account(stuff) + } + RawContractState::NotExists { .. } => ton_block::Account::AccountNone }; + if let Some(balance) = options.override_balance { account.set_balance(balance.into()); } From e0d7085f200e377ee226250dbe63ada46f973273 Mon Sep 17 00:00:00 2001 From: Stanislav Eliseev Date: Tue, 21 Jan 2025 13:24:20 +0100 Subject: [PATCH 32/38] feat: Remove ton gql from nekoton. Use transport native lib support --- nekoton-proto/src/protos/rpc.rs | 18 ++- nekoton-transport/src/gql_ext.rs | 212 -------------------------- src/core/contract_subscription/mod.rs | 17 +-- src/core/generic_contract/mod.rs | 1 - src/core/jetton_wallet/mod.rs | 20 +-- src/core/nft_wallet/mod.rs | 1 - src/core/token_wallet/mod.rs | 1 - src/core/ton_wallet/mod.rs | 3 - src/core/utils.rs | 48 +----- src/transport/gql/mod.rs | 6 +- src/transport/jrpc/mod.rs | 45 +++++- src/transport/jrpc/models.rs | 5 + src/transport/mod.rs | 4 +- src/transport/proto/mod.rs | 48 +++++- 14 files changed, 137 insertions(+), 292 deletions(-) delete mode 100644 nekoton-transport/src/gql_ext.rs diff --git a/nekoton-proto/src/protos/rpc.rs b/nekoton-proto/src/protos/rpc.rs index bcf308c06..d7c3006ec 100644 --- a/nekoton-proto/src/protos/rpc.rs +++ b/nekoton-proto/src/protos/rpc.rs @@ -52,6 +52,13 @@ pub mod request { #[prost(bytes = "bytes", tag = "1")] pub message: ::prost::bytes::Bytes, } + #[allow(clippy::derive_partial_eq_without_eq)] + #[derive(Clone, PartialEq, ::prost::Message)] + pub struct GetLibraryCell { + #[prost(bytes = "bytes", tag = "1")] + pub hash: ::prost::bytes::Bytes, + } + #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Oneof)] pub enum Call { @@ -77,12 +84,14 @@ pub mod request { GetAccountsByCodeHash(GetAccountsByCodeHash), #[prost(message, tag = "11")] SendMessage(SendMessage), + #[prost(message, tag = "12")] + GetLibraryCell(GetLibraryCell), } } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct Response { - #[prost(oneof = "response::Result", tags = "1, 2, 3, 4, 5, 6, 7, 8, 9, 10")] + #[prost(oneof = "response::Result", tags = "1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11")] pub result: ::core::option::Option, } /// Nested message and enum types in `Response`. @@ -156,6 +165,11 @@ pub mod response { #[prost(oneof = "get_contract_state::State", tags = "1, 2, 3")] pub state: ::core::option::Option, } + #[derive(Clone, PartialEq, ::prost::Message)] + pub struct GetLibraryCell { + #[prost(bytes = "bytes", optional, tag = "1")] + pub cell: ::core::option::Option<::prost::bytes::Bytes>, + } /// Nested message and enum types in `GetContractState`. pub mod get_contract_state { #[allow(clippy::derive_partial_eq_without_eq)] @@ -252,6 +266,8 @@ pub mod response { GetContractState(GetContractState), #[prost(message, tag = "10")] SendMessage(()), + #[prost(message, tag = "11")] + GetLibraryCell(GetLibraryCell) } } #[allow(clippy::derive_partial_eq_without_eq)] diff --git a/nekoton-transport/src/gql_ext.rs b/nekoton-transport/src/gql_ext.rs deleted file mode 100644 index 7f180e4b4..000000000 --- a/nekoton-transport/src/gql_ext.rs +++ /dev/null @@ -1,212 +0,0 @@ -use std::convert::TryInto; -use std::sync::Arc; - -use anyhow::{Context, Result}; -use reqwest::Url; -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct GqlExtNetworkSettings { - pub endpoint: String, - /// Gql node type - pub local: bool, -} - -pub struct GqlExtClient { - client: reqwest::Client, - endpoint: Endpoint, - local: bool, -} - -impl GqlExtClient { - pub fn new(settings: GqlExtNetworkSettings) -> Result> { - let endpoint = &settings.endpoint; - let endpoint = Endpoint::new(endpoint) - .with_context(|| format!("failed to parse endpoint: {}", endpoint))?; - - let mut headers = reqwest::header::HeaderMap::new(); - headers.insert( - reqwest::header::CONTENT_TYPE, - reqwest::header::HeaderValue::from_static("application/json"), - ); - - let client = reqwest::ClientBuilder::new() - .default_headers(headers) - .build() - .context("failed to build http client")?; - - Ok(Arc::new(Self { - client, - endpoint, - local: settings.local, - })) - } -} - -#[cfg_attr(not(feature = "non_threadsafe"), async_trait::async_trait)] -#[cfg_attr(feature = "non_threadsafe", async_trait::async_trait(?Send))] -impl nekoton::external::GqlConnection for GqlExtClient { - fn is_local(&self) -> bool { - self.local - } - - async fn post(&self, req: nekoton::external::GqlRequest) -> Result { - let response = self - .client - .post(self.endpoint.gql.clone()) - .body(req.data) - .send() - .await?; - Ok(response.text().await?) - } -} - -struct Endpoint { - gql: Url, -} - -impl Endpoint { - fn new(url: &str) -> Result { - let gql = expand_address(url); - Ok(Self { - gql: gql.as_str().try_into()?, - }) - } -} - -fn expand_address(base_url: &str) -> String { - match base_url.trim_end_matches('/') { - url if base_url.starts_with("http://") || base_url.starts_with("https://") => { - format!("{}/graphql", url) - } - url @ ("localhost" | "127.0.0.1") => format!("http://{}/graphql", url), - url => format!("https://{}/graphql", url), - } -} - -#[cfg(test)] -mod tests { - use nekoton::abi::num_traits::ToPrimitive; - use nekoton::abi::ExecutionContext; - use nekoton::contracts::jetton; - use nekoton::core::utils::update_library_cell; - use nekoton::external::{GqlConnection, GqlRequest}; - use nekoton_utils::*; - - use super::*; - - #[tokio::test] - async fn gql_client_works() -> Result<()> { - let client = GqlExtClient::new(GqlExtNetworkSettings { - endpoint: "https://dton.io/graphql".to_string(), - local: false, - })?; - - const QUERY: &str = r#"{"query":"{\n get_lib(\n lib_hash: \"0F1AD3D8A46BD283321DDE639195FB72602E9B31B1727FECC25E2EDC10966DF4\"\n )\n}"}"#; - - let _response = client - .post(GqlRequest { - data: QUERY.to_string(), - long_query: false, - }) - .await?; - - Ok(()) - } - - #[tokio::test] - async fn usdt_wallet_token_contract() -> Result<()> { - let client = GqlExtClient::new(GqlExtNetworkSettings { - endpoint: "https://dton.io/graphql".to_string(), - local: false, - })?; - - let cell = ton_types::deserialize_tree_of_cells(&mut base64::decode("te6ccgEBAwEAqAACbIAXsqVXAuRG6+GFp/25WVl2IsmatSkX0jbrXVjoBOwsnEQNAdiGdFv5kAABdRDp2cQZrn10JgIBAJEFJFfQYxaABHulQdJwYfnHP5r0FXhq3wjit36+D+zzx7bkE76OQgrwAsROplLUCShZxn2kTkyjrdZWWw4ol9ZAosUb+zcNiHf6CEICj0Utek39dAZraCNlF3JZ7QVzRDW+drX9S9XYryt8PWg=").unwrap().as_slice()).unwrap(); - let mut state = deserialize_account_stuff(cell)?; - - update_library_cell(client.as_ref(), &mut state.storage.state).await?; - - let contract = jetton::TokenWalletContract(ExecutionContext { - clock: &SimpleClock, - account_stuff: &state, - }); - - let balance = contract.balance()?; - assert_eq!(balance.to_u128().unwrap(), 156092097302); - - Ok(()) - } - - #[tokio::test] - async fn notcoin_wallet_token_contract() -> Result<()> { - let client = GqlExtClient::new(GqlExtNetworkSettings { - endpoint: "https://dton.io/graphql".to_string(), - local: false, - })?; - - let cell = ton_types::deserialize_tree_of_cells(&mut base64::decode("te6ccgEBAwEAqgACbIAX5XxfY9N6rJiyOS4NGQc01nd0dzEnWBk87cdqg9bLTwQNAeCGdH/3UAABdXbIjToZrn5eJgIBAJUHFxcOBj4fBYAfGfo6PQWliRZGmmqpYpA1QxmYkyLZonLf41f59x68XdAAvlWFDxGF2lXm67y4yzC17wYKD9A0guwPkMs1gOsM//IIQgK6KRjIlH6bJa+awbiDNXdUFz5YEvgHo9bmQqFHCVlTlQ==").unwrap().as_slice()).unwrap(); - let mut state = deserialize_account_stuff(cell)?; - - update_library_cell(client.as_ref(), &mut state.storage.state).await?; - - let contract = jetton::TokenWalletContract(ExecutionContext { - clock: &SimpleClock, - account_stuff: &state, - }); - - let balance = contract.balance()?; - assert_eq!(balance.to_u128().unwrap(), 6499273466060549); - - Ok(()) - } - - #[tokio::test] - async fn mintless_points_token_wallet_contract() -> Result<()> { - let client = GqlExtClient::new(GqlExtNetworkSettings { - endpoint: "https://dton.io/graphql".to_string(), - local: false, - })?; - - let cell = - ton_types::deserialize_tree_of_cells(&mut base64::decode("te6ccgEBAwEAyQACbIAMC6d7f4iHKlXHXBfufxF6w/5pIENHdpy1yJnyM+lsrQQNAl2Gc+Ll0AABc7gAAbghs2ElpgIBANQFAlQL5ACADZRqTnEksRaYvpXRMbgzB92SzFv/19WbfQQgdDo7lYwQA+mfQx3OTMfvDyPCOAYxl9HdjYWqWkQCtdgoLLcHjaDKvtRVlwuLLP8LwzhcDJNm1TPewFBFqmlIYet7ln0NupwfCEICDvGeG/QPK6SS/KrDhu7KWb9oJ6OFBwjZ/NmttoOrwzY=").unwrap().as_slice()) - .unwrap(); - let mut state = deserialize_account_stuff(cell)?; - - update_library_cell(client.as_ref(), &mut state.storage.state).await?; - - let contract = jetton::TokenWalletContract(ExecutionContext { - clock: &SimpleClock, - account_stuff: &state, - }); - - let data = contract.get_details()?; - assert_eq!(data.balance.to_u128().unwrap(), 10000000000); - - Ok(()) - } - - #[tokio::test] - async fn hamster_token_wallet_contract() -> Result<()> { - let client = GqlExtClient::new(GqlExtNetworkSettings { - endpoint: "https://dton.io/graphql".to_string(), - local: false, - })?; - - let cell = - ton_types::deserialize_tree_of_cells(&mut base64::decode("te6ccgEBAwEAyQACbIAKqccjBo+00V2Pb7qZhRYSHX52cx1iP9tpON3cdZrkP8QNAl2GdhkS0AABegbul1whs2ElpgIBANQFGHJ82gCACGZPh6infgRlai2q2zEzj6/XTCUYYz5sBXNuHUXFkiawACfLlnexAarJqUlmkXX/yPvEfPlx8Id4LDSocvlK3az1CNK1yFN5P0+WKSDutZY4tqmGqAE7w+lQchEcy4oOjEQUCEICDxrT2KRr0oMyHd5jkZX7cmAumzGxcn/swl4u3BCWbfQ=").unwrap().as_slice()) - .unwrap(); - let mut state = deserialize_account_stuff(cell)?; - - update_library_cell(client.as_ref(), &mut state.storage.state).await?; - - let contract = jetton::TokenWalletContract(ExecutionContext { - clock: &SimpleClock, - account_stuff: &state, - }); - - let data = contract.get_details()?; - assert_eq!(data.balance.to_u128().unwrap(), 105000000000); - - Ok(()) - } -} diff --git a/src/core/contract_subscription/mod.rs b/src/core/contract_subscription/mod.rs index 24f65db31..29d072272 100644 --- a/src/core/contract_subscription/mod.rs +++ b/src/core/contract_subscription/mod.rs @@ -13,7 +13,6 @@ use super::models::{ use super::{utils, PollingMethod}; use crate::core::utils::{MessageContext, PendingTransactionsExt}; -use crate::external::GqlConnection; use crate::transport::models::{RawContractState, RawTransaction}; use crate::transport::Transport; @@ -21,7 +20,6 @@ use crate::transport::Transport; pub struct ContractSubscription { clock: Arc, transport: Arc, - gql_connection: Option>, address: MsgAddressInt, contract_state: ContractState, latest_known_lt: Option, @@ -33,7 +31,6 @@ impl ContractSubscription { pub async fn subscribe( clock: Arc, transport: Arc, - gql_connection: Option>, address: MsgAddressInt, on_contract_state: OnContractState<'_>, on_transactions_found: Option>, @@ -41,7 +38,6 @@ impl ContractSubscription { let mut result = Self { clock, transport, - gql_connection, address, contract_state: Default::default(), latest_known_lt: None, @@ -421,14 +417,11 @@ impl ContractSubscription { }; if updated { - if let Some(connection) = self.gql_connection.as_ref() { - if let RawContractState::Exists(state) = &mut contract_state { - utils::update_library_cell( - connection.as_ref(), - &mut state.account.storage.state, - ) - .await?; - } + if let RawContractState::Exists(state) = &mut contract_state { + utils::update_library_cell( + self.transport.as_ref(), + &mut state.account.storage.state, + ).await?; } on_contract_state(&mut contract_state); diff --git a/src/core/generic_contract/mod.rs b/src/core/generic_contract/mod.rs index 7e9c46e18..5936949e7 100644 --- a/src/core/generic_contract/mod.rs +++ b/src/core/generic_contract/mod.rs @@ -46,7 +46,6 @@ impl GenericContract { ContractSubscription::subscribe( clock, transport, - None, address, &mut make_contract_state_handler(handler), on_transactions_found, diff --git a/src/core/jetton_wallet/mod.rs b/src/core/jetton_wallet/mod.rs index 2d47a3b85..2771bfc08 100644 --- a/src/core/jetton_wallet/mod.rs +++ b/src/core/jetton_wallet/mod.rs @@ -3,7 +3,6 @@ use std::sync::Arc; use crate::core::models::*; use crate::core::parsing::*; -use crate::external::GqlConnection; use crate::transport::models::{RawContractState, RawTransaction}; use crate::transport::Transport; use anyhow::Result; @@ -33,7 +32,6 @@ impl JettonWallet { pub async fn subscribe( clock: Arc, transport: Arc, - gql_connection: Option>, owner: MsgAddressInt, root_token_contract: MsgAddressInt, handler: Arc, @@ -70,7 +68,6 @@ impl JettonWallet { ContractSubscription::subscribe( clock.clone(), transport, - gql_connection, address, &mut make_contract_state_handler(clock.clone(), &mut balance), on_transactions_found, @@ -296,12 +293,12 @@ pub trait JettonWalletSubscriptionHandler: Send + Sync { } pub async fn get_wallet_data( - gql_connection: Arc, + transport: Arc, account: ton_block::AccountStuff, ) -> Result { let mut account = account; - utils::update_library_cell(gql_connection.as_ref(), &mut account.storage.state).await?; + utils::update_library_cell(transport.as_ref(), &mut account.storage.state).await?; let token_wallet_state = nekoton_contracts::jetton::TokenWalletContract(ExecutionContext { clock: &SimpleClock, @@ -313,12 +310,12 @@ pub async fn get_wallet_data( } pub async fn get_token_root_meta( - gql_connection: Arc, + transport: Arc, account: ton_block::AccountStuff, ) -> Result { let mut account = account; - utils::update_library_cell(gql_connection.as_ref(), &mut account.storage.state).await?; + utils::update_library_cell(transport.as_ref(), &mut account.storage.state).await?; let token_root_state = nekoton_contracts::jetton::RootTokenContract(ExecutionContext { clock: &SimpleClock, @@ -332,7 +329,6 @@ pub async fn get_token_root_meta( pub async fn get_token_wallet_details( clock: &dyn Clock, transport: Arc, - gql_connection: Arc, token_wallet: &MsgAddressInt, ) -> Result<(JettonWalletData, JettonRootData)> { let mut token_wallet_state = match transport.get_contract_state(token_wallet).await? { @@ -343,7 +339,7 @@ pub async fn get_token_wallet_details( }; utils::update_library_cell( - gql_connection.as_ref(), + transport.as_ref(), &mut token_wallet_state.account.storage.state, ) .await?; @@ -372,7 +368,6 @@ pub async fn get_token_wallet_details( pub async fn get_token_root_details( clock: &dyn Clock, transport: &dyn Transport, - gql_connection: Arc, root_token_contract: &MsgAddressInt, ) -> Result { let mut state = match transport.get_contract_state(root_token_contract).await? { @@ -382,7 +377,7 @@ pub async fn get_token_root_details( } }; - utils::update_library_cell(gql_connection.as_ref(), &mut state.account.storage.state).await?; + utils::update_library_cell(transport, &mut state.account.storage.state).await?; nekoton_contracts::jetton::RootTokenContract(state.as_context(clock)).get_details() } @@ -390,7 +385,6 @@ pub async fn get_token_root_details( pub async fn get_token_root_details_from_token_wallet( clock: &dyn Clock, transport: Arc, - gql_connection: Arc, token_wallet_address: &MsgAddressInt, ) -> Result<(MsgAddressInt, JettonRootData)> { let mut state = match transport.get_contract_state(token_wallet_address).await? { @@ -400,7 +394,7 @@ pub async fn get_token_root_details_from_token_wallet( } }; - utils::update_library_cell(gql_connection.as_ref(), &mut state.account.storage.state).await?; + utils::update_library_cell(transport.as_ref(), &mut state.account.storage.state).await?; let root_token_contract = nekoton_contracts::jetton::TokenWalletContract(state.as_context(clock)).root()?; diff --git a/src/core/nft_wallet/mod.rs b/src/core/nft_wallet/mod.rs index f8c2ba620..91d75fa81 100644 --- a/src/core/nft_wallet/mod.rs +++ b/src/core/nft_wallet/mod.rs @@ -158,7 +158,6 @@ impl Nft { let contract_subscription = ContractSubscription::subscribe( clock.clone(), transport, - None, nft_address.clone(), &mut make_contract_state_handler( clock.as_ref(), diff --git a/src/core/token_wallet/mod.rs b/src/core/token_wallet/mod.rs index 5fe174f72..c149e850e 100644 --- a/src/core/token_wallet/mod.rs +++ b/src/core/token_wallet/mod.rs @@ -76,7 +76,6 @@ impl TokenWallet { ContractSubscription::subscribe( clock.clone(), transport, - None, address, &mut make_contract_state_handler(clock.clone(), version, &mut balance), on_transactions_found, diff --git a/src/core/ton_wallet/mod.rs b/src/core/ton_wallet/mod.rs index d36ab3408..c3e31f0c7 100644 --- a/src/core/ton_wallet/mod.rs +++ b/src/core/ton_wallet/mod.rs @@ -60,7 +60,6 @@ impl TonWallet { let contract_subscription = ContractSubscription::subscribe( clock.clone(), transport, - None, address, &mut make_contract_state_handler( clock.as_ref(), @@ -104,7 +103,6 @@ impl TonWallet { let contract_subscription = ContractSubscription::subscribe( clock.clone(), transport, - None, address, &mut make_contract_state_handler( clock.as_ref(), @@ -141,7 +139,6 @@ impl TonWallet { let contract_subscription = ContractSubscription::subscribe( clock.clone(), transport, - None, existing_wallet.address, &mut make_contract_state_handler( clock.as_ref(), diff --git a/src/core/utils.rs b/src/core/utils.rs index 2276f9cf2..c9b4bc9b6 100644 --- a/src/core/utils.rs +++ b/src/core/utils.rs @@ -9,14 +9,12 @@ use ed25519_dalek::PublicKey; use futures_util::{Future, FutureExt, Stream}; use nekoton_abi::{GenTimings, LastTransactionId, TransactionId}; use nekoton_utils::*; -use serde::Deserialize; use ton_block::{AccountState, Deserializable, MsgAddressInt, Serializable}; use ton_types::{CellType, SliceData, UInt256}; use crate::core::models::*; #[cfg(feature = "wallet_core")] use crate::crypto::{SignedMessage, UnsignedMessage}; -use crate::external::{GqlConnection, GqlRequest}; use crate::transport::models::RawTransaction; use crate::transport::Transport; @@ -496,7 +494,7 @@ pub fn default_headers( type HeadersMap = HashMap; pub async fn update_library_cell<'a>( - connection: &'a dyn GqlConnection, + transport: &'a dyn Transport, state: &mut AccountState, ) -> Result<()> { if let AccountState::AccountActive { ref mut state_init } = state { @@ -512,49 +510,13 @@ pub async fn update_library_cell<'a>( let mut hash = UInt256::default(); hash.read_from(&mut slice_data)?; - let cell = download_lib(connection, hash).await?; - state_init.set_code(cell); + let cell = transport.get_library_cell(&hash).await?; + if let Some(cell) = cell { + state_init.set_code(cell); + } } } } Ok(()) } - -async fn download_lib<'a>( - connection: &'a dyn GqlConnection, - hash: UInt256, -) -> Result { - let query = serde_json::json!({ - "query": format!("{{ - get_lib( - lib_hash: \"{}\" - ) - }}", hash.to_hex_string().to_uppercase()) - }) - .to_string(); - - let response = connection - .post(GqlRequest { - data: query, - long_query: false, - }) - .await?; - - #[derive(Deserialize)] - struct GqlResponse { - data: Data, - } - - #[derive(Deserialize)] - struct Data { - get_lib: String, - } - - let parsed: GqlResponse = serde_json::from_str(&response)?; - - let bytes = base64::decode(parsed.data.get_lib)?; - let cell = ton_types::deserialize_tree_of_cells(&mut bytes.as_slice())?; - - Ok(cell) -} diff --git a/src/transport/gql/mod.rs b/src/transport/gql/mod.rs index d849d802e..b6a810c58 100644 --- a/src/transport/gql/mod.rs +++ b/src/transport/gql/mod.rs @@ -5,7 +5,7 @@ use std::time::Duration; use anyhow::{Context, Result}; use serde::Deserialize; use ton_block::{Account, Deserializable, Message, MsgAddressInt, Serializable}; - +use ton_types::{Cell, UInt256}; use nekoton_abi::{GenTimings, LastTransactionId}; use nekoton_utils::*; @@ -256,6 +256,10 @@ impl Transport for GqlTransport { } } + async fn get_library_cell(&self, _: &UInt256) -> Result> { + unimplemented!() + } + async fn poll_contract_state( &self, address: &MsgAddressInt, diff --git a/src/transport/jrpc/mod.rs b/src/transport/jrpc/mod.rs index c152d82cd..84f3ed489 100644 --- a/src/transport/jrpc/mod.rs +++ b/src/transport/jrpc/mod.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use anyhow::Result; use serde::{Deserialize, Serialize}; use ton_block::{Block, Deserializable, MsgAddressInt}; - +use ton_types::{Cell, UInt256}; use nekoton_utils::*; use crate::core::models::{NetworkCapabilities, ReliableBehavior}; @@ -102,6 +102,36 @@ impl Transport for JrpcTransport { Ok(response) } + async fn get_library_cell(&self, hash: &UInt256) -> Result> { + let req = external::JrpcRequest { + data: make_jrpc_request( + "getLibraryCell", + &GetLibraryCell { + hash + }, + ), + requires_db: false, + }; + + #[derive(Deserialize)] + struct LibraryCellResponse { + cell: Option, + } + + let data = self.connection.post(req).await?; + + let response = tiny_jsonrpc::parse_response::(&data)?; + let cell = match response.cell { + Some(boc) => { + let bytes = base64::decode(boc)?; + Some(ton_types::deserialize_tree_of_cells(&mut bytes.as_slice())?) + } + None => None + }; + + Ok(cell) + } + async fn poll_contract_state( &self, address: &MsgAddressInt, @@ -297,6 +327,19 @@ mod tests { } } + #[tokio::test] + async fn test_library_cells() { + let transport = JrpcTransport::new(Arc::new(reqwest::Client::new())); + let result = transport.get_library_cell(&UInt256::from_str("4f4f10cb9a30582792fb3c1e364de5a6fbe6fe04f4167f1f12f83468c767aeb3").unwrap()) + .await + .unwrap(); + + match result { + Some(cell) => println!("{:?}", cell.repr_hash()), + None => println!("No library cell"), + } + } + #[tokio::test] async fn test_transactions_stream() -> Result<()> { let transport = JrpcTransport::new(Arc::new(reqwest::Client::new())); diff --git a/src/transport/jrpc/models.rs b/src/transport/jrpc/models.rs index 0d176880b..825664453 100644 --- a/src/transport/jrpc/models.rs +++ b/src/transport/jrpc/models.rs @@ -2,6 +2,11 @@ use serde::{Deserialize, Serialize}; use nekoton_utils::*; +#[derive(Serialize)] +pub struct GetLibraryCell<'a> { + #[serde(with = "serde_uint256")] + pub hash: &'a ton_types::UInt256, +} #[derive(Serialize)] pub struct GetContractState<'a> { #[serde(with = "serde_address")] diff --git a/src/transport/mod.rs b/src/transport/mod.rs index 9fe187884..06f588c53 100644 --- a/src/transport/mod.rs +++ b/src/transport/mod.rs @@ -2,7 +2,7 @@ use anyhow::Result; use nekoton_utils::Clock; use serde::{Deserialize, Serialize}; use ton_block::MsgAddressInt; - +use ton_types::Cell; use crate::models::{NetworkCapabilities, ReliableBehavior}; use self::models::*; @@ -31,6 +31,8 @@ pub trait Transport: Send + Sync { async fn get_contract_state(&self, address: &MsgAddressInt) -> Result; + async fn get_library_cell(&self, hash: &ton_types::UInt256) -> Result>; + async fn poll_contract_state( &self, address: &MsgAddressInt, diff --git a/src/transport/proto/mod.rs b/src/transport/proto/mod.rs index a4494292c..03d7e5f96 100644 --- a/src/transport/proto/mod.rs +++ b/src/transport/proto/mod.rs @@ -2,9 +2,10 @@ use std::sync::Arc; use anyhow::Result; use ton_block::{Block, Deserializable, MsgAddressInt, Serializable}; - +use ton_types::{Cell, UInt256}; use nekoton_proto::prost::{bytes::Bytes, Message}; use nekoton_proto::protos::rpc; +use nekoton_proto::protos::rpc::response::GetLibraryCell; use nekoton_proto::utils; use nekoton_utils::*; @@ -137,6 +138,37 @@ impl Transport for ProtoTransport { Ok(result) } + async fn get_library_cell(&self, hash: &UInt256) -> Result> { + let data = rpc::Request { + call: Some(rpc::request::Call::GetLibraryCell( + rpc::request::GetLibraryCell { + hash: hash.into_vec().into() + }, + )), + }; + + let req = external::ProtoRequest { + data: data.encode_to_vec(), + requires_db: false, + }; + + let data = self.connection.post(req).await?; + let response = rpc::Response::decode(Bytes::from(data))?; + let result = match response.result { + Some(rpc::response::Result::GetLibraryCell(GetLibraryCell {cell})) => { + match cell { + Some(boc) => { + let cell = ton_types::deserialize_tree_of_cells(&mut boc.as_ref())?; + Some(cell) + }, + None => None + } + } + _ => return Err(ProtoClientError::InvalidResponse.into()), + }; + Ok(result) + } + async fn poll_contract_state( &self, address: &MsgAddressInt, @@ -402,7 +434,6 @@ mod tests { use std::str::FromStr; use futures_util::StreamExt; - use super::*; #[cfg_attr(not(feature = "non_threadsafe"), async_trait::async_trait)] @@ -421,6 +452,19 @@ mod tests { } } + #[tokio::test] + async fn test_library_cells_proto() { + let transport = ProtoTransport::new(Arc::new(reqwest::Client::new())); + let result = transport.get_library_cell(&UInt256::from_str("4f4f10cb9a30582792fb3c1e364de5a6fbe6fe04f4167f1f12f83468c767aeb3").unwrap()) + .await + .unwrap(); + + match result { + Some(cell) => println!("{:?}", cell.repr_hash()), + None => println!("No library cell"), + } + } + #[tokio::test] async fn test_transactions_stream() -> Result<()> { let transport = ProtoTransport::new(Arc::new(reqwest::Client::new())); From aca9e50dc991ea1c1bc4a628cbd01dba339f30d9 Mon Sep 17 00:00:00 2001 From: Stanislav Eliseev Date: Tue, 21 Jan 2025 13:26:09 +0100 Subject: [PATCH 33/38] feat: Remove ton gql from nekoton. Use transport native lib support --- nekoton-abi/src/lib.rs | 2 +- nekoton-transport/src/lib.rs | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/nekoton-abi/src/lib.rs b/nekoton-abi/src/lib.rs index 73e1bf7ec..7791ea54a 100644 --- a/nekoton-abi/src/lib.rs +++ b/nekoton-abi/src/lib.rs @@ -1381,7 +1381,7 @@ mod tests { ..Default::default() }; - let transaction = + let _ = executor.execute_with_libs_and_params(in_msg.as_ref(), &mut root_cell, params)?; Ok(()) diff --git a/nekoton-transport/src/lib.rs b/nekoton-transport/src/lib.rs index 90e49de83..6e3595139 100644 --- a/nekoton-transport/src/lib.rs +++ b/nekoton-transport/src/lib.rs @@ -54,8 +54,6 @@ #[cfg(feature = "gql_transport")] pub mod gql; -#[cfg(feature = "gql_transport")] -pub mod gql_ext; #[cfg(feature = "jrpc_transport")] pub mod jrpc; #[cfg(feature = "proto_transport")] From 496827951d11dc8d59173a380efabce7d4002755 Mon Sep 17 00:00:00 2001 From: Stanislav Eliseev Date: Tue, 21 Jan 2025 14:06:40 +0100 Subject: [PATCH 34/38] fix: fmt --- nekoton-abi/src/lib.rs | 3 +-- src/core/contract_subscription/mod.rs | 3 ++- src/transport/gql/mod.rs | 4 ++-- src/transport/jrpc/mod.rs | 19 +++++++++-------- src/transport/mod.rs | 2 +- src/transport/proto/mod.rs | 30 +++++++++++++++------------ 6 files changed, 33 insertions(+), 28 deletions(-) diff --git a/nekoton-abi/src/lib.rs b/nekoton-abi/src/lib.rs index 7791ea54a..8df35f1e9 100644 --- a/nekoton-abi/src/lib.rs +++ b/nekoton-abi/src/lib.rs @@ -1381,8 +1381,7 @@ mod tests { ..Default::default() }; - let _ = - executor.execute_with_libs_and_params(in_msg.as_ref(), &mut root_cell, params)?; + let _ = executor.execute_with_libs_and_params(in_msg.as_ref(), &mut root_cell, params)?; Ok(()) } diff --git a/src/core/contract_subscription/mod.rs b/src/core/contract_subscription/mod.rs index 29d072272..59bf0c638 100644 --- a/src/core/contract_subscription/mod.rs +++ b/src/core/contract_subscription/mod.rs @@ -421,7 +421,8 @@ impl ContractSubscription { utils::update_library_cell( self.transport.as_ref(), &mut state.account.storage.state, - ).await?; + ) + .await?; } on_contract_state(&mut contract_state); diff --git a/src/transport/gql/mod.rs b/src/transport/gql/mod.rs index b6a810c58..8e84de257 100644 --- a/src/transport/gql/mod.rs +++ b/src/transport/gql/mod.rs @@ -3,11 +3,11 @@ use std::sync::Arc; use std::time::Duration; use anyhow::{Context, Result}; +use nekoton_abi::{GenTimings, LastTransactionId}; +use nekoton_utils::*; use serde::Deserialize; use ton_block::{Account, Deserializable, Message, MsgAddressInt, Serializable}; use ton_types::{Cell, UInt256}; -use nekoton_abi::{GenTimings, LastTransactionId}; -use nekoton_utils::*; use crate::core::models::{NetworkCapabilities, ReliableBehavior}; use crate::external::{GqlConnection, GqlRequest}; diff --git a/src/transport/jrpc/mod.rs b/src/transport/jrpc/mod.rs index 84f3ed489..89831e6d5 100644 --- a/src/transport/jrpc/mod.rs +++ b/src/transport/jrpc/mod.rs @@ -1,10 +1,10 @@ use std::sync::Arc; use anyhow::Result; +use nekoton_utils::*; use serde::{Deserialize, Serialize}; use ton_block::{Block, Deserializable, MsgAddressInt}; use ton_types::{Cell, UInt256}; -use nekoton_utils::*; use crate::core::models::{NetworkCapabilities, ReliableBehavior}; use crate::external::{self, JrpcConnection}; @@ -104,12 +104,7 @@ impl Transport for JrpcTransport { async fn get_library_cell(&self, hash: &UInt256) -> Result> { let req = external::JrpcRequest { - data: make_jrpc_request( - "getLibraryCell", - &GetLibraryCell { - hash - }, - ), + data: make_jrpc_request("getLibraryCell", &GetLibraryCell { hash }), requires_db: false, }; @@ -126,7 +121,7 @@ impl Transport for JrpcTransport { let bytes = base64::decode(boc)?; Some(ton_types::deserialize_tree_of_cells(&mut bytes.as_slice())?) } - None => None + None => None, }; Ok(cell) @@ -330,7 +325,13 @@ mod tests { #[tokio::test] async fn test_library_cells() { let transport = JrpcTransport::new(Arc::new(reqwest::Client::new())); - let result = transport.get_library_cell(&UInt256::from_str("4f4f10cb9a30582792fb3c1e364de5a6fbe6fe04f4167f1f12f83468c767aeb3").unwrap()) + let result = transport + .get_library_cell( + &UInt256::from_str( + "4f4f10cb9a30582792fb3c1e364de5a6fbe6fe04f4167f1f12f83468c767aeb3", + ) + .unwrap(), + ) .await .unwrap(); diff --git a/src/transport/mod.rs b/src/transport/mod.rs index 06f588c53..8038c8303 100644 --- a/src/transport/mod.rs +++ b/src/transport/mod.rs @@ -1,9 +1,9 @@ +use crate::models::{NetworkCapabilities, ReliableBehavior}; use anyhow::Result; use nekoton_utils::Clock; use serde::{Deserialize, Serialize}; use ton_block::MsgAddressInt; use ton_types::Cell; -use crate::models::{NetworkCapabilities, ReliableBehavior}; use self::models::*; diff --git a/src/transport/proto/mod.rs b/src/transport/proto/mod.rs index 03d7e5f96..7673c2d66 100644 --- a/src/transport/proto/mod.rs +++ b/src/transport/proto/mod.rs @@ -1,12 +1,12 @@ use std::sync::Arc; use anyhow::Result; -use ton_block::{Block, Deserializable, MsgAddressInt, Serializable}; -use ton_types::{Cell, UInt256}; use nekoton_proto::prost::{bytes::Bytes, Message}; use nekoton_proto::protos::rpc; use nekoton_proto::protos::rpc::response::GetLibraryCell; use nekoton_proto::utils; +use ton_block::{Block, Deserializable, MsgAddressInt, Serializable}; +use ton_types::{Cell, UInt256}; use nekoton_utils::*; @@ -142,7 +142,7 @@ impl Transport for ProtoTransport { let data = rpc::Request { call: Some(rpc::request::Call::GetLibraryCell( rpc::request::GetLibraryCell { - hash: hash.into_vec().into() + hash: hash.into_vec().into(), }, )), }; @@ -155,15 +155,13 @@ impl Transport for ProtoTransport { let data = self.connection.post(req).await?; let response = rpc::Response::decode(Bytes::from(data))?; let result = match response.result { - Some(rpc::response::Result::GetLibraryCell(GetLibraryCell {cell})) => { - match cell { - Some(boc) => { - let cell = ton_types::deserialize_tree_of_cells(&mut boc.as_ref())?; - Some(cell) - }, - None => None + Some(rpc::response::Result::GetLibraryCell(GetLibraryCell { cell })) => match cell { + Some(boc) => { + let cell = ton_types::deserialize_tree_of_cells(&mut boc.as_ref())?; + Some(cell) } - } + None => None, + }, _ => return Err(ProtoClientError::InvalidResponse.into()), }; Ok(result) @@ -433,8 +431,8 @@ pub enum ProtoClientError { mod tests { use std::str::FromStr; - use futures_util::StreamExt; use super::*; + use futures_util::StreamExt; #[cfg_attr(not(feature = "non_threadsafe"), async_trait::async_trait)] #[cfg_attr(feature = "non_threadsafe", async_trait::async_trait(?Send))] @@ -455,7 +453,13 @@ mod tests { #[tokio::test] async fn test_library_cells_proto() { let transport = ProtoTransport::new(Arc::new(reqwest::Client::new())); - let result = transport.get_library_cell(&UInt256::from_str("4f4f10cb9a30582792fb3c1e364de5a6fbe6fe04f4167f1f12f83468c767aeb3").unwrap()) + let result = transport + .get_library_cell( + &UInt256::from_str( + "4f4f10cb9a30582792fb3c1e364de5a6fbe6fe04f4167f1f12f83468c767aeb3", + ) + .unwrap(), + ) .await .unwrap(); From 364fd9aab14c138439e6b5f13dc7c0815d850d31 Mon Sep 17 00:00:00 2001 From: Stanislav Eliseev Date: Thu, 23 Jan 2025 17:35:13 +0100 Subject: [PATCH 35/38] fix: always return None for GQL transport for library request --- src/transport/gql/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/transport/gql/mod.rs b/src/transport/gql/mod.rs index 8e84de257..b0ebb8a10 100644 --- a/src/transport/gql/mod.rs +++ b/src/transport/gql/mod.rs @@ -257,7 +257,7 @@ impl Transport for GqlTransport { } async fn get_library_cell(&self, _: &UInt256) -> Result> { - unimplemented!() + Ok(None) } async fn poll_contract_state( From 4f6cb16763f217b9e8897657ef5689345ac89c02 Mon Sep 17 00:00:00 2001 From: Stanislav Eliseev Date: Thu, 23 Jan 2025 17:43:22 +0100 Subject: [PATCH 36/38] fix: update rpc.proto --- nekoton-proto/src/protos/rpc.proto | 12 ++++++++++++ nekoton-proto/src/protos/rpc.rs | 28 ++++++++++++++-------------- 2 files changed, 26 insertions(+), 14 deletions(-) diff --git a/nekoton-proto/src/protos/rpc.proto b/nekoton-proto/src/protos/rpc.proto index b04f39ce2..a77e3a358 100644 --- a/nekoton-proto/src/protos/rpc.proto +++ b/nekoton-proto/src/protos/rpc.proto @@ -6,6 +6,11 @@ import "google/protobuf/struct.proto"; package rpc; message Request { + + message GetLibraryCell { + bytes hash = 1; + } + message GetContractState { bytes address = 1; optional uint64 last_transaction_lt = 2; @@ -48,6 +53,8 @@ message Request { GetTransactionsList get_transactions_list = 9; GetAccountsByCodeHash get_accounts_by_code_hash = 10; SendMessage send_message = 11; + + GetLibraryCell get_library_cell = 12; } } @@ -91,6 +98,10 @@ message Response { repeated bytes account= 1; } + message GetLibraryCell { + optional bytes cell = 1; + } + message GetContractState { message Timings { uint64 gen_lt = 1; @@ -141,6 +152,7 @@ message Response { GetAccountsByCodeHash get_accounts = 8; GetContractState get_contract_state = 9; google.protobuf.Empty send_message = 10; + GetLibraryCell get_library_cell = 11; } } diff --git a/nekoton-proto/src/protos/rpc.rs b/nekoton-proto/src/protos/rpc.rs index d7c3006ec..ec57e1200 100644 --- a/nekoton-proto/src/protos/rpc.rs +++ b/nekoton-proto/src/protos/rpc.rs @@ -1,11 +1,17 @@ #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct Request { - #[prost(oneof = "request::Call", tags = "1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11")] + #[prost(oneof = "request::Call", tags = "1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12")] pub call: ::core::option::Option, } /// Nested message and enum types in `Request`. pub mod request { + #[allow(clippy::derive_partial_eq_without_eq)] + #[derive(Clone, PartialEq, ::prost::Message)] + pub struct GetLibraryCell { + #[prost(bytes = "bytes", tag = "1")] + pub hash: ::prost::bytes::Bytes, + } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct GetContractState { @@ -52,13 +58,6 @@ pub mod request { #[prost(bytes = "bytes", tag = "1")] pub message: ::prost::bytes::Bytes, } - #[allow(clippy::derive_partial_eq_without_eq)] - #[derive(Clone, PartialEq, ::prost::Message)] - pub struct GetLibraryCell { - #[prost(bytes = "bytes", tag = "1")] - pub hash: ::prost::bytes::Bytes, - } - #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Oneof)] pub enum Call { @@ -161,15 +160,16 @@ pub mod response { } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] - pub struct GetContractState { - #[prost(oneof = "get_contract_state::State", tags = "1, 2, 3")] - pub state: ::core::option::Option, - } - #[derive(Clone, PartialEq, ::prost::Message)] pub struct GetLibraryCell { #[prost(bytes = "bytes", optional, tag = "1")] pub cell: ::core::option::Option<::prost::bytes::Bytes>, } + #[allow(clippy::derive_partial_eq_without_eq)] + #[derive(Clone, PartialEq, ::prost::Message)] + pub struct GetContractState { + #[prost(oneof = "get_contract_state::State", tags = "1, 2, 3")] + pub state: ::core::option::Option, + } /// Nested message and enum types in `GetContractState`. pub mod get_contract_state { #[allow(clippy::derive_partial_eq_without_eq)] @@ -267,7 +267,7 @@ pub mod response { #[prost(message, tag = "10")] SendMessage(()), #[prost(message, tag = "11")] - GetLibraryCell(GetLibraryCell) + GetLibraryCell(GetLibraryCell), } } #[allow(clippy::derive_partial_eq_without_eq)] From 60b0b3efb5c323ceea5cebdf6acc6e97cb5eea60 Mon Sep 17 00:00:00 2001 From: Ivan Kalinin Date: Tue, 28 Jan 2025 00:09:51 +0100 Subject: [PATCH 37/38] fix: use u128 for balance and message values --- src/core/contract_subscription/mod.rs | 18 ++++++++++-------- src/core/jetton_wallet/mod.rs | 2 +- src/core/mod.rs | 2 +- src/core/nft_wallet/mod.rs | 6 +++--- src/core/token_wallet/mod.rs | 8 ++++---- src/core/ton_wallet/ever_wallet.rs | 4 +++- src/core/ton_wallet/highload_wallet_v2.rs | 4 +++- src/core/ton_wallet/mod.rs | 2 +- src/core/ton_wallet/wallet_v3.rs | 4 +++- src/core/ton_wallet/wallet_v4.rs | 4 +++- src/core/ton_wallet/wallet_v5r1.rs | 4 +++- src/core/utils.rs | 2 +- src/models.rs | 8 ++++---- src/transport/models.rs | 2 +- 14 files changed, 41 insertions(+), 29 deletions(-) diff --git a/src/core/contract_subscription/mod.rs b/src/core/contract_subscription/mod.rs index 59bf0c638..6a9aa14f2 100644 --- a/src/core/contract_subscription/mod.rs +++ b/src/core/contract_subscription/mod.rs @@ -1,16 +1,16 @@ use std::sync::Arc; +use super::models::{ + ContractState, PendingTransaction, ReliableBehavior, TransactionsBatchInfo, + TransactionsBatchType, +}; +use super::{utils, PollingMethod}; use anyhow::Result; use futures_util::StreamExt; use nekoton_abi::{Executor, LastTransactionId}; use nekoton_utils::*; use serde::{Deserialize, Serialize}; use ton_block::{AccountStuff, MsgAddressInt}; -use super::models::{ - ContractState, PendingTransaction, ReliableBehavior, TransactionsBatchInfo, - TransactionsBatchType, -}; -use super::{utils, PollingMethod}; use crate::core::utils::{MessageContext, PendingTransactionsExt}; use crate::transport::models::{RawContractState, RawTransaction}; @@ -251,13 +251,15 @@ impl ContractSubscription { let mut account = match self.transport.get_contract_state(&self.address).await? { RawContractState::Exists(state) => ton_block::Account::Account(state.account), RawContractState::NotExists { .. } if options.override_balance.is_some() => { - let stuff = AccountStuff { addr: self.address.clone(), ..Default::default() }; + let stuff = AccountStuff { + addr: self.address.clone(), + ..Default::default() + }; ton_block::Account::Account(stuff) } - RawContractState::NotExists { .. } => ton_block::Account::AccountNone + RawContractState::NotExists { .. } => ton_block::Account::AccountNone, }; - if let Some(balance) = options.override_balance { account.set_balance(balance.into()); } diff --git a/src/core/jetton_wallet/mod.rs b/src/core/jetton_wallet/mod.rs index 2771bfc08..2f712a2df 100644 --- a/src/core/jetton_wallet/mod.rs +++ b/src/core/jetton_wallet/mod.rs @@ -137,7 +137,7 @@ impl JettonWallet { custom_payload: Option, callback_value: BigUint, callback_payload: Option, - attached_amount: u64, + attached_amount: u128, ) -> Result { let mut builder = BuilderData::new(); diff --git a/src/core/mod.rs b/src/core/mod.rs index cdbe5094c..a5dbd07c9 100644 --- a/src/core/mod.rs +++ b/src/core/mod.rs @@ -49,7 +49,7 @@ pub struct InternalMessage { #[serde(with = "serde_address")] pub destination: ton_block::MsgAddressInt, #[serde(with = "serde_string")] - pub amount: u64, + pub amount: u128, pub bounce: bool, #[serde(with = "serde_boc")] pub body: ton_types::SliceData, diff --git a/src/core/nft_wallet/mod.rs b/src/core/nft_wallet/mod.rs index 91d75fa81..d5e7d79af 100644 --- a/src/core/nft_wallet/mod.rs +++ b/src/core/nft_wallet/mod.rs @@ -219,7 +219,7 @@ impl Nft { send_gas_to: MsgAddressInt, callbacks: BTreeMap, ) -> Result { - const ATTACHED_AMOUNT: u64 = 1_000_000_000; // 1 TON + const ATTACHED_AMOUNT: u128 = 1_000_000_000; // 1 TON let (function, input) = MessageBuilder::new(nft_contract::transfer()) .arg(to) .arg(send_gas_to) @@ -245,7 +245,7 @@ impl Nft { send_gas_to: MsgAddressInt, callbacks: BTreeMap, ) -> Result { - const ATTACHED_AMOUNT: u64 = 1_000_000_000; // 1 TON + const ATTACHED_AMOUNT: u128 = 1_000_000_000; // 1 TON let (function, input) = MessageBuilder::new(nft_contract::change_manager()) .arg(new_manager) .arg(send_gas_to) @@ -271,7 +271,7 @@ impl Nft { send_gas_to: MsgAddressInt, callbacks: BTreeMap, ) -> Result { - const ATTACHED_AMOUNT: u64 = 1_000_000_000; // 1 TON + const ATTACHED_AMOUNT: u128 = 1_000_000_000; // 1 TON let (function, input) = MessageBuilder::new(nft_contract::change_owner()) .arg(new_owner) .arg(send_gas_to) diff --git a/src/core/token_wallet/mod.rs b/src/core/token_wallet/mod.rs index c149e850e..7c0cc168c 100644 --- a/src/core/token_wallet/mod.rs +++ b/src/core/token_wallet/mod.rs @@ -224,7 +224,7 @@ impl TokenWallet { &self, config: &BlockchainConfig, address: &MsgAddressInt, - ) -> Result { + ) -> Result { let gas_config = config.get_gas_config(address.is_masterchain()); let prices = config.raw_config().storage_prices()?; @@ -255,7 +255,7 @@ impl TokenWallet { let fees = 30000u64.saturating_mul(gas_config.gas_price.shr(16)) + fee_base; - Ok(fees) + Ok(fees as u128) } pub async fn prepare_transfer( @@ -264,9 +264,9 @@ impl TokenWallet { tokens: BigUint, notify_receiver: bool, payload: ton_types::Cell, - mut attached_amount: u64, + mut attached_amount: u128, ) -> Result { - fn stub_balance(address: &MsgAddressInt) -> u64 { + fn stub_balance(address: &MsgAddressInt) -> u128 { if address.is_masterchain() { 1000 } else { diff --git a/src/core/ton_wallet/ever_wallet.rs b/src/core/ton_wallet/ever_wallet.rs index 2089c31fa..f32d7bd09 100644 --- a/src/core/ton_wallet/ever_wallet.rs +++ b/src/core/ton_wallet/ever_wallet.rs @@ -105,7 +105,9 @@ pub fn prepare_transfer( ihr_disabled: true, bounce: gift.bounce, dst: gift.destination, - value: gift.amount.into(), + value: ton_block::CurrencyCollection::from_grams(ton_block::Grams::new( + gift.amount, + )?), ..Default::default() }); diff --git a/src/core/ton_wallet/highload_wallet_v2.rs b/src/core/ton_wallet/highload_wallet_v2.rs index 294ec0f60..458caaabb 100644 --- a/src/core/ton_wallet/highload_wallet_v2.rs +++ b/src/core/ton_wallet/highload_wallet_v2.rs @@ -273,7 +273,9 @@ impl InitData { ihr_disabled: true, bounce: gift.bounce, dst: gift.destination, - value: gift.amount.into(), + value: ton_block::CurrencyCollection::from_grams(ton_block::Grams::new( + gift.amount, + )?), ..Default::default() }); diff --git a/src/core/ton_wallet/mod.rs b/src/core/ton_wallet/mod.rs index c3e31f0c7..80cb10687 100644 --- a/src/core/ton_wallet/mod.rs +++ b/src/core/ton_wallet/mod.rs @@ -911,7 +911,7 @@ pub struct Gift { pub flags: u8, pub bounce: bool, pub destination: MsgAddressInt, - pub amount: u64, + pub amount: u128, pub body: Option, pub state_init: Option, } diff --git a/src/core/ton_wallet/wallet_v3.rs b/src/core/ton_wallet/wallet_v3.rs index a132c0284..46389fe49 100644 --- a/src/core/ton_wallet/wallet_v3.rs +++ b/src/core/ton_wallet/wallet_v3.rs @@ -301,7 +301,9 @@ impl InitData { ihr_disabled: true, bounce: gift.bounce, dst: gift.destination, - value: gift.amount.into(), + value: ton_block::CurrencyCollection::from_grams(ton_block::Grams::new( + gift.amount, + )?), ..Default::default() }); diff --git a/src/core/ton_wallet/wallet_v4.rs b/src/core/ton_wallet/wallet_v4.rs index 4479c8403..16d80dc6b 100644 --- a/src/core/ton_wallet/wallet_v4.rs +++ b/src/core/ton_wallet/wallet_v4.rs @@ -287,7 +287,9 @@ impl InitData { ihr_disabled: true, bounce: gift.bounce, dst: gift.destination, - value: gift.amount.into(), + value: ton_block::CurrencyCollection::from_grams(ton_block::Grams::new( + gift.amount, + )?), ..Default::default() }); diff --git a/src/core/ton_wallet/wallet_v5r1.rs b/src/core/ton_wallet/wallet_v5r1.rs index b75b5f298..1f235e1dd 100644 --- a/src/core/ton_wallet/wallet_v5r1.rs +++ b/src/core/ton_wallet/wallet_v5r1.rs @@ -293,7 +293,9 @@ impl InitData { ihr_disabled: true, bounce: gift.bounce, dst: gift.destination, - value: gift.amount.into(), + value: ton_block::CurrencyCollection::from_grams(ton_block::Grams::new( + gift.amount, + )?), ..Default::default() }); diff --git a/src/core/utils.rs b/src/core/utils.rs index c9b4bc9b6..3e3408884 100644 --- a/src/core/utils.rs +++ b/src/core/utils.rs @@ -140,7 +140,7 @@ pub fn parse_block( let new_contract_state = ContractState { last_lt, - balance: balance as u64, + balance: balance.try_into().unwrap_or_default(), gen_timings: GenTimings::Known { gen_lt: info.end_lt(), gen_utime: info.gen_utime().as_u32(), diff --git a/src/models.rs b/src/models.rs index 5de00239d..95a8fb07a 100644 --- a/src/models.rs +++ b/src/models.rs @@ -509,7 +509,7 @@ pub struct ContractState { /// Full account balance in nano TON #[serde(with = "serde_string")] - pub balance: u64, + pub balance: u128, /// At what point was this state obtained pub gen_timings: GenTimings, /// Last transaction id @@ -737,7 +737,7 @@ pub struct Message { pub dst: Option, /// Message value in nano TON - pub value: u64, + pub value: u128, /// Whether this message will be bounced on unsuccessful execution. pub bounce: bool, @@ -771,7 +771,7 @@ impl<'de> Deserialize<'de> for Message { #[serde(default, with = "serde_optional_address")] dst: Option, #[serde(with = "serde_string")] - value: u64, + value: u128, bounce: bool, bounced: bool, body: Option, @@ -892,7 +892,7 @@ impl TryFrom for Message { ton_block::MsgAddressIntOrNone::None => None, }, dst: Some(header.dst.clone()), - value: header.value.grams.as_u128() as u64, + value: header.value.grams.as_u128(), body, bounce: header.bounce, bounced: header.bounced, diff --git a/src/transport/models.rs b/src/transport/models.rs index 0a0036d3b..1535c33e9 100644 --- a/src/transport/models.rs +++ b/src/transport/models.rs @@ -104,7 +104,7 @@ impl ExistingContract { pub fn brief(&self) -> ContractState { ContractState { last_lt: self.account.storage.last_trans_lt, - balance: self.account.storage.balance.grams.as_u128() as u64, + balance: self.account.storage.balance.grams.as_u128(), gen_timings: self.timings, last_transaction_id: Some(self.last_transaction_id), is_deployed: matches!( From f70734151502d5ccfbd94a9acec282cf01c837e4 Mon Sep 17 00:00:00 2001 From: Ivan Kalinin Date: Mon, 3 Feb 2025 14:02:40 +0100 Subject: [PATCH 38/38] refactor: cleanup tests, make gql lib search fallible --- nekoton-abi/src/lib.rs | 54 ---------------------------------------- src/transport/gql/mod.rs | 2 +- 2 files changed, 1 insertion(+), 55 deletions(-) diff --git a/nekoton-abi/src/lib.rs b/nekoton-abi/src/lib.rs index 8df35f1e9..ec6036b48 100644 --- a/nekoton-abi/src/lib.rs +++ b/nekoton-abi/src/lib.rs @@ -1331,58 +1331,4 @@ mod tests { assert_eq!(read_function_id(&remaining_body).unwrap(), 1290691692); // sendTransaction input id } - - fn parse_cell(boc: &str) -> anyhow::Result { - let boc = boc.trim(); - if boc.is_empty() { - Ok(ton_types::Cell::default()) - } else { - let body = base64::decode(boc)?; - ton_types::deserialize_tree_of_cells(&mut body.as_slice()) - } - } - #[test] - fn hello() -> Result<()> { - let account = "te6ccgECCAEAAWMAAnHABjAbLHVZbm5Wmm0Trk7HDJTxd+zgvhn5aN3Oc9ROevxyEIJBQznnq6AAAMmJtAYVEUBKXTw4E0ACAQBQyLDWxgjLA6yhKJfmGLWfXdvRC34pWEXEek1ncgteNXUAAAGTRh7KRgEU/wD0pBP0vPLICwMCASAHBALm8nHXAQHAAPJ6gwjXGO1E0IMH1wHXCz/I+CjPFiPPFsn5AANx1wEBwwCagwfXAVETuvLgZN6AQNcBgCDXAYAg1wFUFnX5EPKo+CO78nlmvvgjgQcIoIED6KhSILyx8nQCIIIQTO5kbLrjDwHIy//LP8ntVAYFAD6CEBaePhG6jhH4AAKTINdKl3jXAdQC+wDo0ZMy8jziAJgwAtdM0PpAgwbXAXHXAXjXAddM+ABwgBAEqgIUscjLBVAFzxZQA/oCy2ki0CHPMSHXSaCECbmYM3ABywBYzxaXMHEBywASzOLJAfsAAATSMA=="; - let tx = "te6ccgECDAEAAnAAA7V2MBssdVlublaabROuTscMlPF37OC+Gflo3c5z1E56/HAAAyYm8XlAEKy5RXcT3fqNI58hfr0g4fdhYXBqSMX+HjpeTjGvKFBwAAMmJtAYVDZzz11gADRs0o5IBQQBAg8MQoYagXrEQAMCAG/Jg9CQTAosIAAAAAAAAgAAAAAAAiQUizx+r6MbUjovm5x2fP/W4DVTBrBq54SSuX2h3AswQFAWDACdQpDjE4gAAAAAAAAAAB3AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIACCcrqCn0Efu7Vm1b+COCiqvWs4TNx2oxYmPkhulZEkArnZw95XLR5CFFs3X75wm4iqv+Nca0J1tAkWz6i3WmL0YEACAeAIBgEB3wcAsUgAxgNljqstzcrTTaJ1ydjhkp4u/ZwXwz8tG7nOeonPX48AGMBssdVlublaabROuTscMlPF37OC+Gflo3c5z1E56/HQ7msoAAYKLDAAAGTE3i8oBM5566xAAUWIAMYDZY6rLc3K002idcnY4ZKeLv2cF8M/LRu5znqJz1+ODAkB4Zrdv8UI48I32K2mO07+dwrfKPTgLSPJw+wxQdfU6xu1vMGDMUCrW17rxr2ihaAfQ9JTGcJz6WdXxAVsWl2YLYPyLDWxgjLA6yhKJfmGLWfXdvRC34pWEXEek1ncgteNXUAAAGTRiBG/mc89hBM7mRsgCgFlgAxgNljqstzcrTTaJ1ydjhkp4u/ZwXwz8tG7nOeonPX44AAAAAAAAAAAAAAAB3NZQAA4CwAA"; - - let config = "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"; - - let mut root_cell = parse_cell(account)?; - let account = ton_block::Account::construct_from_cell(root_cell.clone())?; - let tx = ton_block::Transaction::construct_from_base64(tx)?; - let in_msg = tx.read_in_msg()?; - - let config = ton_block::ConfigParams::construct_from_base64(config)?; - let executor = ton_executor::OrdinaryTransactionExecutor::new( - ton_executor::BlockchainConfig::with_config(config, 42)?, - ); - - let last_trans_lt = match account.stuff() { - Some(state) => state.storage.last_trans_lt, - None => 0, - }; - - let params = ton_executor::ExecuteParams { - block_unixtime: tx.now, - block_lt: tx.lt, - last_tr_lt: Arc::new(AtomicU64::new(last_trans_lt)), - trace_callback: Some(Arc::new(move |args, info| { - //actions.push(info); - println!( - "MUUU {:?}", - args.ctrl(7).unwrap().as_tuple().unwrap().last().unwrap() - ); - println!("\n ============ \n CMD: {}", info.cmd_str,); - for item in &info.stack.storage { - println!("{item}"); - } - })), - ..Default::default() - }; - - let _ = executor.execute_with_libs_and_params(in_msg.as_ref(), &mut root_cell, params)?; - - Ok(()) - } } diff --git a/src/transport/gql/mod.rs b/src/transport/gql/mod.rs index b0ebb8a10..9e144d9f5 100644 --- a/src/transport/gql/mod.rs +++ b/src/transport/gql/mod.rs @@ -257,7 +257,7 @@ impl Transport for GqlTransport { } async fn get_library_cell(&self, _: &UInt256) -> Result> { - Ok(None) + anyhow::bail!("Library cells search is not supported by this GraphQL transport") } async fn poll_contract_state(