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-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/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..5d88c323a --- /dev/null +++ b/nekoton-contracts/src/jetton/mod.rs @@ -0,0 +1,106 @@ +use nekoton_abi::num_bigint::BigUint; +use nekoton_abi::{ExecutionContext, StackItem}; +use ton_block::Serializable; +use ton_types::SliceData; + +pub use root_token_contract::{JettonRootData, JettonRootMeta}; +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_JETTON_META: &str = "get_jetton_meta"; +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)?; + Ok(data.content.name) + } + + pub fn symbol(&self) -> anyhow::Result> { + let result = self.0.run_getter(GET_JETTON_DATA, &[])?; + + let data = root_token_contract::get_jetton_data(result)?; + Ok(data.content.symbol) + } + + pub fn decimals(&self) -> anyhow::Result> { + let result = self.0.run_getter(GET_JETTON_DATA, &[])?; + + let data = root_token_contract::get_jetton_data(result)?; + Ok(data.content.decimals) + } + + 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) + } + + 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)] +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) + } +} 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..27561630c --- /dev/null +++ b/nekoton-contracts/src/jetton/root_token_contract.rs @@ -0,0 +1,147 @@ +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, SliceData}; + +#[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, +} + +#[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 { + 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).unwrap_or_default(); + + 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_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 { + 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-contracts/src/wallets/code/mod.rs b/nekoton-contracts/src/wallets/code/mod.rs index 344c05b1a..e7da28e68 100644 --- a/nekoton-contracts/src/wallets/code/mod.rs +++ b/nekoton-contracts/src/wallets/code/mod.rs @@ -20,6 +20,9 @@ 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), } 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 000000000..542d420ca Binary files /dev/null and b/nekoton-contracts/src/wallets/code/wallet_v4r1_code.boc differ diff --git a/nekoton-contracts/src/wallets/code/wallet_v4r2_code.boc b/nekoton-contracts/src/wallets/code/wallet_v4r2_code.boc new file mode 100644 index 000000000..f97fbc040 Binary files /dev/null and b/nekoton-contracts/src/wallets/code/wallet_v4r2_code.boc differ 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 000000000..56c9a34f8 Binary files /dev/null and b/nekoton-contracts/src/wallets/code/wallet_v5r1_code.boc differ diff --git a/nekoton-jetton/Cargo.toml b/nekoton-jetton/Cargo.toml new file mode 100644 index 000000000..8bae8aa6f --- /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.4" +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" +num-traits = "0.2" + +[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/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 bcf308c06..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 { @@ -77,12 +83,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`. @@ -152,6 +160,12 @@ pub mod response { } #[allow(clippy::derive_partial_eq_without_eq)] #[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, @@ -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/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/src/core/contract_subscription/mod.rs b/src/core/contract_subscription/mod.rs index 8f1de0620..6a9aa14f2 100644 --- a/src/core/contract_subscription/mod.rs +++ b/src/core/contract_subscription/mod.rs @@ -1,18 +1,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 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 crate::core::utils::{MessageContext, PendingTransactionsExt}; use crate::transport::models::{RawContractState, RawTransaction}; use crate::transport::Transport; @@ -248,8 +247,16 @@ 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 { .. } 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, }; @@ -387,7 +394,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 +419,15 @@ impl ContractSubscription { }; if updated { - on_contract_state(&contract_state); + 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); self.contract_state = new_contract_state; self.transactions_synced = false; } else { diff --git a/src/core/jetton_wallet/mod.rs b/src/core/jetton_wallet/mod.rs new file mode 100644 index 000000000..2f712a2df --- /dev/null +++ b/src/core/jetton_wallet/mod.rs @@ -0,0 +1,630 @@ +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, JettonRootMeta, JettonWalletData}; +use nekoton_utils::*; +use num_bigint::{BigInt, BigUint, ToBigInt}; +use ton_block::{MsgAddressInt, Serializable}; +use ton_types::{BuilderData, IBitstring, SliceData}; + +use super::{utils, ContractSubscription, InternalMessage}; + +pub const JETTON_TRANSFER_OPCODE: u32 = 0x0f8a7ea5; +pub const JETTON_INTERNAL_TRANSFER_OPCODE: u32 = 0x178d4519; + +pub struct JettonWallet { + clock: Arc, + contract_subscription: ContractSubscription, + handler: Arc, + root: MsgAddressInt, + owner: MsgAddressInt, + balance: BigUint, +} + +impl JettonWallet { + pub async fn subscribe( + clock: Arc, + transport: Arc, + 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, + 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 = { + 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()); + + Ok(Self { + clock, + contract_subscription, + handler, + owner, + balance, + root: root_token_contract, + }) + } + + 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: 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( + &self, + amount: BigUint, + destination: MsgAddressInt, + remaining_gas_to: MsgAddressInt, + custom_payload: Option, + callback_value: BigUint, + callback_payload: Option, + attached_amount: u128, + ) -> Result { + let mut builder = BuilderData::new(); + + // Opcode + builder.append_u32(JETTON_TRANSFER_OPCODE)?; + + // Query id + builder.append_u64(self.clock.now_ms_u64())?; + + // 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()?; + } + } + + // Callback value + let grams = ton_block::Grams::new( + callback_value + .to_u128() + .ok_or(JettonWalletError::TryFromGrams)?, + )?; + grams.write_to(&mut builder)?; + + 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)??; + + 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(transfer) => { + balance -= transfer.tokens.clone().to_bigint().trust_me(); + } + JettonWalletTransaction::InternalTransfer(transfer) => { + balance += transfer.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_wallet_data( + transport: Arc, + account: ton_block::AccountStuff, +) -> Result { + let mut account = account; + + utils::update_library_cell(transport.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( + transport: Arc, + account: ton_block::AccountStuff, +) -> Result { + let mut account = account; + + utils::update_library_cell(transport.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, + token_wallet: &MsgAddressInt, +) -> Result<(JettonWalletData, JettonRootData)> { + let mut token_wallet_state = match transport.get_contract_state(token_wallet).await? { + RawContractState::Exists(state) => state, + RawContractState::NotExists { .. } => { + return Err(JettonWalletError::InvalidTokenWalletContract.into()) + } + }; + + utils::update_library_cell( + transport.as_ref(), + &mut token_wallet_state.account.storage.state, + ) + .await?; + + 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 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(transport, &mut state.account.storage.state).await?; + + nekoton_contracts::jetton::RootTokenContract(state.as_context(clock)).get_details() +} + +pub async fn get_token_root_details_from_token_wallet( + clock: &dyn Clock, + transport: Arc, + token_wallet_address: &MsgAddressInt, +) -> Result<(MsgAddressInt, JettonRootData)> { + let mut state = match transport.get_contract_state(token_wallet_address).await? { + RawContractState::Exists(state) => state, + RawContractState::NotExists { .. } => { + return Err(JettonWalletError::WalletNotDeployed.into()) + } + }; + + 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()?; + + 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, +} + +#[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_contracts::jetton; + use nekoton_utils::SimpleClock; + use ton_block::MsgAddressInt; + + #[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 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("te6ccgECLwEAB4YAAm6AFe0/oSZXf0CdefSBA89p5cgZ/cjSo7/+/CB2bN5bhnekvRsDhnIRltAAAW6YCV7CEiEatKumIgECUXye1BbWVRSoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABEwIBFP8A9KQT9LzyyAsDAgFiBQQAG6D2BdqJofQB9IH0gahhAgLMEAYCASAIBwCD1AEGuQ9qJofQB9IH0gahgCaY/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/y8IAEDAMAUAgEgIBUCASAbFgIBIBkXAUG/XQH6XjwGkBxFBGxrLdzqWvdk/qDu1yoQ1ATyMSzrJH0YAAQAOQFBv1II3vRvWh1Pnc5mqzCfSoUTBfFm+R73nZI+9Y40+aIJGgBEACRVUCBpcyB0aGUgbmF0aXZlIHRva2VuIG9mIFRvblVQLgIBIB4cAUG/btT5QqeEjOLLBmt3oRKMah/4xD9Dii3OJGErqf+riwMdAAYAVVABQb9FRqb/4bec/dhrrT24dDE9zeL7BeanSqfzVS2WF8edEx8ADABUb25VUAFDv/CC62Y7V6ABkvSmrEZyiN8t/t252hvuKPZSHIvr0h8ewCEAtABodHRwczovL3B1YmxpYy1taWNyb2Nvc20uczMtYXAtc291dGhlYXN0LTEuYW1hem9uYXdzLmNvbS9kcm9wc2hhcmUvMTcwMjU0MzYyOS9VUC1pY29uLnBuZwEU/wD0pBP0vPLICyMCAWInJAIDemAmJQAfrxb2omh9AH0gamoYP6qQQAB9rbz2omh9AH0gamoYNhj8FAC4KhAJqgoB5CgCfQEsZ4sA54tmZJFkZYCJegB6AGWAZPyAODpkZYFlA+X/5OhAAgLMKSgAk7XwUIgG4KhAJqgoB5CgCfQEsZ4sA54tmZJFkZYCJegB6AGWAZJB8gDg6ZGWBZQPl/+ToO8AMZGWCrGeLKAJ9AQnltYlmZmS4/YBAvHZBjgEkvgfAA6GmBgLjYSS+B8H0gfSAY/QAYuOuQ/QAY/QAYAWmP6Z/2omh9AH0gamoYQAqpOF1HGZqamxsommOC+XAkgX0gfQBqGBBoQDBrkP0AGBKIGigheAUKUCgZ5CgCfQEsZ4tmZmT2qnBBCD3uy+8pOF1xgULSoBpoIQLHa5c1JwuuMCNTc3I8ADjhozUDXHBfLgSQP6QDBZyFAE+gJYzxbMzMntVOA1AsAEjhhRJMcF8uBJ1DBDAMhQBPoCWM8WzMzJ7VTgXwWED/LwKwH+Nl8DggiYloAVoBW88uBLAvpA0wAwlcghzxbJkW3ighDRc1QAcIAYyMsFUAXPFiT6AhTLahPLHxTLPyP6RDBwuo4z+ChEA3BUIBNUFAPIUAT6AljPFgHPFszJIsjLARL0APQAywDJ+QBwdMjLAsoHy//J0M8WlmwicAHLAeL0ACwACsmAQPsAAcA2NzcB+gD6QPgoVBIGcFQgE1QUA8hQBPoCWM8WAc8WzMkiyMsBEvQA9ADLAMn5AHB0yMsCygfL/8nQUAbHBfLgSqEDRUXIUAT6AljPFszMye1UAfpAMCDXCwHDAJFb4w0uAD6CENUydttwgBDIywVQA88WIvoCEstqyx/LP8mAQvsA").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("te6ccgECHwEABicAAm6AH0z6GO5yZj94eR4RwDGMvo7sbC1S0iAVrsFBZbg8bQZEfRZghnNn0JAAAXJXxpOyGjmQlkGmBQECTmE+QBlNGKCvtRVlwuLLP8LwzhcDJNm1TPewFBFqmlIYet7ln0NupwQCAeZodHRwczovL2dpc3QuZ2l0aHVidXNlcmNvbnRlbnQuY29tL0VtZWx5YW5lbmtvSy8yNzFjMGFkYTFkZTQyYjk3YzQ1NWFjOTM1Yzk3MmY0Mi9yYXcvYjdiMzBjM2U5NzBlMDc3ZTExZDA4NWNjNjcxM2JlAwAwMzE1N2M3Y2EwOC9tZXRhZGF0YS5qc29uCEICDvGeG/QPK6SS/KrDhu7KWb9oJ6OFBwjZ/NmttoOrwzYBFP8A9KQT9LzyyAsGAgFiEAcCASALCAICcQoJAIuvFvaiaH0AfSB9IGpqaf+A/DDov5noNsF4OHLr21FNnJfCg7fwrlF5Ap4rYRnDlGJxnk9G7Y90E+YseAo4ZGWD+gBkoYBAAVutvPaiaH0AfSB9IGpqaf+A/DDoii+CfBR8IIltnjeRGHyAODpkZYFlA+X/5OhAHQIBSA8MAgFqDg0ALqpn7UTQ+gD6QPpA1NTT/wH4YdFfBfhBAC6rW+1E0PoA+kD6QNTU0/8B+GHRECRfBAE/tdFdqJofQB9IH0gampp/4D8MOiKL4J8FHwgiW2eN5FAdAgLLEhEAHaI4ZGWDgOeLZIFBg/oLwAHX0MtDTAwFxsI5EMIAg1yHTHwGCEBeNRRm6kTDhgEDXIfoAMO1E0PoA+kD6QNTU0/8B+GHRUEWhQTT4QchQBvoCUATPFljPFszMy//J7VTg+kD6QDH6ADH0AfoAMfoAATFw+DoC0x8BAdM/ARKEwT87UTQ+gD6QPpA1NTT/wH4YdEmghBkK30Huo7LNTVRYccF8uBJBPpAIfpEMMAA8uFN+gDU0SDQ0x8BghAXjUUZuvLgSIBA1yH6APpAMfpAMfoAINcLAJrXS8ABAcABsPKxkTDiVEMb4DklghB73ZfeuuMCJYIQLHa5c7rjAjQkGxoZFAT+ghBlAfNUuo4lMDNRQscF8uBJAvpA0UADBPhByFAG+gJQBM8WWM8WzMzL/8ntVOAkghD7iOEZuo4kMTMD0VExxwXy4EmLAkA0+EHIUAb6AlAEzxZYzxbMzMv/ye1U4CSCEMuGKQK64wIwI4IQJQjWarrjAiOCEHQx8iG64wIQNhgXFhUAHF8GghDTchWMutyED/LwAEozUELHBfLgSQHRiwKLAkA0+EHIUAb6AlAEzxZYzxbMzMv/ye1UACI2XwMCxwXy4EnU1NEB7VT7BABONDZRRccF8uBJyFADzxbJEDQS+EHIUAb6AlAEzxZYzxbMzMv/ye1UAdI1XwM0AfpA0gABAdGVyCHPFsmRbeLIgBABywVQBM8WcPoCcAHLaoIQ0XNUAAHLH1AEAcs/I/pEMMAAjp34KPhBEDVBUNs8byIw+QBwdMjLAsoHy//J0BLPFpcxbBJwAcsB4vQAyYBQ+wAdAeY1BfoA+kD4KPhBKBA0Ads8byIw+QBwdMjLAsoHy//J0FAIxwXy4EoSoUQUUDb4QchQBvoCUATPFljPFszMy//J7VT6QNEg1wsBwACzjiLIgBABywUBzxZw+gJwActqghDVMnbbAcsfAQHLP8mAQvsAkVviHQGOIZFykXHi+DkgbpOBeC6RIOIhbpQxgX7gkQHiUCOoE6BzgQStcPg8oAJw+DYSoAFw+Dagc4EFE4IQCWYBgHD4N6C88rAlWX8cAcCCEDuaygBw+wL4KPhBEDZBUNs8byIwIPkAcHTIywLKB8v/yIAYAcsFAc8XWPoCAphYd1ADy2vMzJcwAXFYy2rM4smAEfsAUAWgQxT4QchQBvoCUATPFljPFszMy//J7VQdAfaED39wJvpEMav7UxFJRhgEyMsDUAP6AgHPFgHPFsv/IIEAysjLDwHPFyT5ACXXZSWCAgE0yMsXEssPyw/L/44pBqRcAcsJcfkEAFJwAcv/cfkEAKv7KLJTBLmTNDQjkTDiIMAgJMAAsRfmECNfAzMzInADywnJIsjLARIeABT0APQAywDJAW8C").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 dai_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 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/mod.rs b/src/core/mod.rs index 531fc761b..a5dbd07c9 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; @@ -48,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/parsing.rs b/src/core/parsing.rs index 3af574531..59cdb7e4e 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_types::UInt256; +use ton_block::{Deserializable, MsgAddressInt}; +use ton_types::{SliceData, UInt256}; use nekoton_abi::*; use nekoton_contracts::tip4_1::nft_contract; use nekoton_contracts::{old_tip3, tip3_1}; +use crate::core::jetton_wallet::{JETTON_INTERNAL_TRANSFER_OPCODE, JETTON_TRANSFER_OPCODE}; use crate::core::models::*; use crate::core::ton_wallet::{MultisigType, WalletType}; @@ -64,6 +65,30 @@ pub fn parse_payload(payload: ton_types::SliceData) -> Option { None } +pub fn parse_jetton_payload(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, @@ -96,6 +121,29 @@ pub fn parse_transaction_additional_info( WalletInteractionMethod::WalletV3Transfer, ) } + WalletType::WalletV4R1 | WalletType::WalletV4R2 | 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_jetton_payload); + + ( + Some(recipient.clone()), + known_payload, + WalletInteractionMethod::TonWalletTransfer, + ) + } WalletType::Multisig(multisig_type) => { let method = parse_multisig_transaction_impl(multisig_type, in_msg, tx)?; let (recipient, known_payload) = match &method { @@ -174,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(TransactionAdditionalInfo::JettonNotify) } else { None } @@ -215,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; @@ -587,6 +663,66 @@ pub fn parse_token_transaction( } } +pub fn parse_jetton_transaction( + tx: &ton_block::Transaction, + description: &ton_block::TransactionDescrOrdinary, +) -> Option { + const STANDART_JETTON_CELLS: usize = 0; + const MINTLESS_JETTON_CELLS: usize = 2; + + if description.aborted { + return None; + } + + 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 { + MINTLESS_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 { + return None; + } + + // Skip query id + body.move_by(64).ok()?; + + let mut grams = ton_block::Grams::default(); + grams.read_from(&mut body).ok()?; + + 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(JettonOutgoingTransfer { + to: addr, + tokens: amount, + })), + JETTON_INTERNAL_TRANSFER_OPCODE => Some(JettonWalletTransaction::InternalTransfer( + JettonIncomingTransfer { + from: addr, + tokens: amount, + }, + )), + _ => None, + } +} + struct NftFunctions { transfer: &'static ton_abi::Function, change_owner: &'static ton_abi::Function, @@ -879,11 +1015,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() { @@ -1015,4 +1152,87 @@ mod tests { TokenWalletTransaction::TransferBounced(_) )); } + + #[test] + 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_transaction_additional_info(&tx, WalletType::WalletV5R1); + assert!(wallet_transaction.is_some()); + + 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!( + from, + MsgAddressInt::from_str( + "0:6ca35273892588b4c5f4ae898dc1983eec9662dffebeacdbe82103a1d1dcac60" + ) + .unwrap() + ); + } + } + + #[test] + fn test_jetton_dai() { + let (tx, description) = parse_transaction("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"); + 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() + ); + } + } } diff --git a/src/core/token_wallet/mod.rs b/src/core/token_wallet/mod.rs index 59a16a09f..7c0cc168c 100644 --- a/src/core/token_wallet/mod.rs +++ b/src/core/token_wallet/mod.rs @@ -1,20 +1,20 @@ use std::convert::TryFrom; +use std::ops::Shr; 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 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}; +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 super::{ContractSubscription, InternalMessage}; @@ -141,8 +141,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( @@ -219,18 +220,77 @@ impl TokenWallet { Ok((attached_amount * FEE_MULTIPLIER) as u64) } - pub fn prepare_transfer( + fn calculate_initial_balance( + &self, + config: &BlockchainConfig, + address: &MsgAddressInt, + ) -> Result { + let gas_config = config.get_gas_config(address.is_masterchain()); + + 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: i32 = -1; + + prices.map.iterate(|price| { + 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; + } + Ok(true) + })?; + + let storage_fees = if address.is_masterchain() { + most_recent_mc_bit_price + } else { + most_recent_bit_price + }; + + let fee_base = if storage_fees > 1 { + 600_000_000u64 + } else { + 100_000_000u64 + }; + + let fees = 30000u64.saturating_mul(gas_config.gas_price.shr(16)) + fee_base; + + Ok(fees as u128) + } + + pub async fn prepare_transfer( &self, destination: TransferRecipient, tokens: BigUint, notify_receiver: bool, payload: ton_types::Cell, - mut attached_amount: u64, + mut attached_amount: u128, ) -> Result { - if matches!(&destination, TransferRecipient::OwnerWallet(_)) { - attached_amount += INITIAL_BALANCE; + fn stub_balance(address: &MsgAddressInt) -> u128 { + if address.is_masterchain() { + 1000 + } else { + 1 + } } + 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?; + + let initial_balance = match self.calculate_initial_balance(&config, address) { + Ok(value) => value + stub_balance(address), + Err(_) => stub_balance(address), + }; + attached_amount += initial_balance; + initial_balance + } + TransferRecipient::TokenWallet(address) => stub_balance(address), + }; + let (function, input) = match self.version { TokenWalletVersion::OldTip3v4 => { use old_tip3::token_wallet_contract; @@ -245,7 +305,7 @@ impl TokenWallet { .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 @@ -266,7 +326,7 @@ impl TokenWallet { 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 @@ -469,8 +529,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, 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 7cae9926d..80cb10687 100644 --- a/src/core/ton_wallet/mod.rs +++ b/src/core/ton_wallet/mod.rs @@ -30,6 +30,8 @@ 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; @@ -228,6 +230,26 @@ 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, + self.workchain(), + expiration, + ), WalletType::EverWallet => ever_wallet::prepare_deploy( self.clock.as_ref(), &self.public_key, @@ -318,6 +340,32 @@ 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, + current_state, + 0, + vec![gift], + expiration, + ), WalletType::EverWallet => ever_wallet::prepare_transfer( self.clock.as_ref(), public_key, @@ -635,6 +683,16 @@ 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())?; + 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)) @@ -853,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, } @@ -868,6 +926,9 @@ pub enum TransferAction { pub enum WalletType { Multisig(MultisigType), WalletV3, + WalletV4R1, + WalletV4R2, + WalletV5R1, HighloadWalletV2, EverWallet, } @@ -877,8 +938,10 @@ 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, + Self::WalletV4R1 | Self::WalletV4R2 => wallet_v4::DETAILS, } } @@ -895,6 +958,9 @@ 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, } @@ -905,6 +971,9 @@ 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(), } @@ -917,6 +986,9 @@ 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, s => Self::Multisig(MultisigType::from_str(s)?), @@ -938,6 +1010,9 @@ impl TryInto for WalletType { WalletType::Multisig(MultisigType::SurfWallet) => 6, WalletType::Multisig(MultisigType::Multisig2) => 7, WalletType::Multisig(MultisigType::Multisig2_1) => 8, + WalletType::WalletV4R1 => 9, + WalletType::WalletV4R2 => 10, + WalletType::WalletV5R1 => 11, _ => anyhow::bail!("Unimplemented wallet type"), }; @@ -950,6 +1025,9 @@ 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"), } @@ -966,10 +1044,21 @@ 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) } + 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_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 new file mode 100644 index 000000000..16d80dc6b --- /dev/null +++ b/src/core/ton_wallet/wallet_v4.rs @@ -0,0 +1,399 @@ +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: ton_block::CurrencyCollection::from_grams(ton_block::Grams::new( + gift.amount, + )?), + ..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 new file mode 100644 index 000000000..1f235e1dd --- /dev/null +++ b/src/core/ton_wallet/wallet_v5r1.rs @@ -0,0 +1,406 @@ +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; + +pub fn prepare_deploy( + clock: &dyn Clock, + public_key: &PublicKey, + workchain: i8, + expiration: Expiration, +) -> Result> { + 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 { + 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) + .with_wallet_id(WALLET_ID) + .with_is_signature_allowed(true), + 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)?; + + let mut actions = ton_block::OutActions::new(); + + 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: ton_block::CurrencyCollection::from_grams(ton_block::Grams::new( + gift.amount, + )?), + ..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, + }; + + 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)) + } +} + +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 nekoton_contracts::wallets; + use ton_block::AccountState; + + use crate::core::ton_wallet::wallet_v5r1::{ + compute_contract_address, is_wallet_v5r1, 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(()) + } + + #[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/core/utils.rs b/src/core/utils.rs index a579f36f7..3e3408884 100644 --- a/src/core/utils.rs +++ b/src/core/utils.rs @@ -7,10 +7,10 @@ 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 ton_block::{AccountState, Deserializable, MsgAddressInt, Serializable}; +use ton_types::{CellType, SliceData, UInt256}; use crate::core::models::*; #[cfg(feature = "wallet_core")] @@ -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(), @@ -492,3 +492,31 @@ pub fn default_headers( } type HeadersMap = HashMap; + +pub async fn update_library_cell<'a>( + transport: &'a dyn Transport, + 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 = transport.get_library_cell(&hash).await?; + if let Some(cell) = cell { + state_init.set_code(cell); + } + } + } + } + + Ok(()) +} diff --git a/src/models.rs b/src/models.rs index 8f1a9205d..95a8fb07a 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)] @@ -53,12 +55,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, + TonWalletTransfer, Multisig(Box), } @@ -274,6 +278,13 @@ pub enum TokenWalletTransaction { SwapBackBounced(BigUint), } +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "snake_case", tag = "type", content = "data")] +pub enum JettonWalletTransaction { + Transfer(JettonOutgoingTransfer), + InternalTransfer(JettonIncomingTransfer), +} + #[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "snake_case", tag = "type", content = "data")] pub enum NftTransaction { @@ -302,6 +313,22 @@ 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")] + pub to: MsgAddressInt, + #[serde(with = "serde_string")] + pub tokens: BigUint, +} + #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct IncomingNftTransfer { @@ -482,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 @@ -710,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, @@ -744,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, @@ -865,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/gql/mod.rs b/src/transport/gql/mod.rs index d849d802e..9e144d9f5 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 serde::Deserialize; -use ton_block::{Account, Deserializable, Message, MsgAddressInt, Serializable}; - 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 crate::core::models::{NetworkCapabilities, ReliableBehavior}; use crate::external::{GqlConnection, GqlRequest}; @@ -256,6 +256,10 @@ impl Transport for GqlTransport { } } + async fn get_library_cell(&self, _: &UInt256) -> Result> { + anyhow::bail!("Library cells search is not supported by this GraphQL transport") + } + async fn poll_contract_state( &self, address: &MsgAddressInt, diff --git a/src/transport/jrpc/mod.rs b/src/transport/jrpc/mod.rs index c152d82cd..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 nekoton_utils::*; +use ton_types::{Cell, UInt256}; use crate::core::models::{NetworkCapabilities, ReliableBehavior}; use crate::external::{self, JrpcConnection}; @@ -102,6 +102,31 @@ 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 +322,25 @@ 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..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 crate::models::{NetworkCapabilities, ReliableBehavior}; +use ton_types::Cell; 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/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!( diff --git a/src/transport/proto/mod.rs b/src/transport/proto/mod.rs index a4494292c..7673c2d66 100644 --- a/src/transport/proto/mod.rs +++ b/src/transport/proto/mod.rs @@ -1,11 +1,12 @@ use std::sync::Arc; use anyhow::Result; -use ton_block::{Block, Deserializable, MsgAddressInt, Serializable}; - 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::*; @@ -137,6 +138,35 @@ 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, @@ -401,9 +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))] @@ -421,6 +450,25 @@ 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()));