From c583bf40da03626ccab91f97b2616a52f62f374d Mon Sep 17 00:00:00 2001 From: Alexey Pashinov Date: Wed, 27 Nov 2024 15:12:27 +0100 Subject: [PATCH] TON WalletV4 --- nekoton-contracts/src/jetton/mod.rs | 60 ++- .../src/jetton/root_token_contract.rs | 3 +- .../src/wallets/code/jetton_wallet_v2.boc | Bin 0 -> 942 bytes nekoton-contracts/src/wallets/code/mod.rs | 5 +- .../src/wallets/code/wallet_v4r1_code.boc | Bin 0 -> 769 bytes .../src/wallets/code/wallet_v4r2_code.boc | Bin 0 -> 736 bytes src/core/jetton_wallet/mod.rs | 121 +++++- src/core/parsing.rs | 85 +++- src/core/ton_wallet/mod.rs | 58 ++- src/core/ton_wallet/wallet_v4.rs | 397 ++++++++++++++++++ src/core/ton_wallet/wallet_v5r1.rs | 15 +- src/models.rs | 4 +- 12 files changed, 691 insertions(+), 57 deletions(-) create mode 100644 nekoton-contracts/src/wallets/code/jetton_wallet_v2.boc create mode 100644 nekoton-contracts/src/wallets/code/wallet_v4r1_code.boc create mode 100644 nekoton-contracts/src/wallets/code/wallet_v4r2_code.boc create mode 100644 src/core/ton_wallet/wallet_v4.rs diff --git a/nekoton-contracts/src/jetton/mod.rs b/nekoton-contracts/src/jetton/mod.rs index 34772ec4..12c64a57 100644 --- a/nekoton-contracts/src/jetton/mod.rs +++ b/nekoton-contracts/src/jetton/mod.rs @@ -131,11 +131,9 @@ mod tests { use nekoton_abi::num_traits::{FromPrimitive, ToPrimitive}; use nekoton_abi::ExecutionContext; use nekoton_utils::SimpleClock; - use ton_block::{AccountState, Deserializable, MsgAddressInt}; - use ton_types::{CellType, SliceData, UInt256}; + use ton_block::MsgAddressInt; use crate::jetton; - use crate::wallets; #[test] fn usdt_root_token_contract() -> anyhow::Result<()> { @@ -179,6 +177,12 @@ mod tests { let expected_code = ton_types::deserialize_tree_of_cells(&mut base64::decode("te6cckECEQEAAyMAART/APSkE/S88sgLAQIBYgIDAgLMBAUAG6D2BdqJofQB9IH0gahhAgHUBgcCASAICQDDCDHAJJfBOAB0NMDAXGwlRNfA/AM4PpA+kAx+gAxcdch+gAx+gAwc6m0AALTH4IQD4p+pVIgupUxNFnwCeCCEBeNRRlSILqWMUREA/AK4DWCEFlfB7y6k1nwC+BfBIQP8vCAAET6RDBwuvLhTYAIBIAoLAIPUAQa5D2omh9AH0gfSBqGAJpj8EIC8aijKkQXUEIPe7L7wndCVj5cWLpn5j9ABgJ0CgR5CgCfQEsZ4sA54tmZPaqQB8VA9M/+gD6QCHwAe1E0PoA+kD6QNQwUTahUirHBfLiwSjC//LiwlQ0QnBUIBNUFAPIUAT6AljPFgHPFszJIsjLARL0APQAywDJIPkAcHTIywLKB8v/ydAE+kD0BDH6ACDXScIA8uLEd4AYyMsFUAjPFnD6AhfLaxPMgMAgEgDQ4AnoIQF41FGcjLHxnLP1AH+gIizxZQBs8WJfoCUAPPFslQBcwjkXKRceJQCKgToIIJycOAoBS88uLFBMmAQPsAECPIUAT6AljPFgHPFszJ7VQC9ztRND6APpA+kDUMAjTP/oAUVGgBfpA+kBTW8cFVHNtcFQgE1QUA8hQBPoCWM8WAc8WzMkiyMsBEvQA9ADLAMn5AHB0yMsCygfL/8nQUA3HBRyx8uLDCvoAUaihggiYloBmtgihggiYloCgGKEnlxBJEDg3XwTjDSXXCwGAPEADXO1E0PoA+kD6QNQwB9M/+gD6QDBRUaFSSccF8uLBJ8L/8uLCBYIJMS0AoBa88uLDghB73ZfeyMsfFcs/UAP6AiLPFgHPFslxgBjIywUkzxZw+gLLaszJgED7AEATyFAE+gJYzxYBzxbMye1UgAHBSeaAYoYIQc2LQnMjLH1Iwyz9Y+gJQB88WUAfPFslxgBDIywUkzxZQBvoCFctqFMzJcfsAECQQIwB8wwAjwgCwjiGCENUydttwgBDIywVQCM8WUAT6AhbLahLLHxLLP8ly+wCTNWwh4gPIUAT6AljPFgHPFszJ7VSV6u3X")?.as_slice())?; assert_eq!(wallet_code, expected_code); + let token_address = contract.get_wallet_address(&MsgAddressInt::default())?; + assert_eq!( + token_address.to_string(), + "0:0c6a835483369275c9ae76e7e31d9eda0845368045a8ec2ed78609d96bb0a087" + ); + Ok(()) } @@ -187,23 +191,25 @@ mod tests { let cell = ton_types::deserialize_tree_of_cells(&mut base64::decode("te6ccgEBAwEAqAACbIAXsqVXAuRG6+GFp/25WVl2IsmatSkX0jbrXVjoBOwsnEQNAdiGdFv5kAABdRDp2cQZrn10JgIBAJEFJFfQYxaABHulQdJwYfnHP5r0FXhq3wjit36+D+zzx7bkE76OQgrwAsROplLUCShZxn2kTkyjrdZWWw4ol9ZAosUb+zcNiHf6CEICj0Utek39dAZraCNlF3JZ7QVzRDW+drX9S9XYryt8PWg=").unwrap().as_slice()).unwrap(); let mut state = nekoton_utils::deserialize_account_stuff(cell)?; - if let AccountState::AccountActive { state_init } = &mut state.storage.state { - if let Some(cell) = &state_init.code { - if cell.cell_type() == CellType::LibraryReference { - let mut slice_data = SliceData::load_cell(cell.clone())?; + jetton::update_library_cell(&mut state.storage.state)?; - let tag = slice_data.get_next_byte()?; - assert_eq!(tag, 2); + let contract = jetton::TokenWalletContract(ExecutionContext { + clock: &SimpleClock, + account_stuff: &state, + }); - let mut hash = UInt256::default(); - hash.read_from(&mut slice_data)?; + let balance = contract.balance()?; + assert_eq!(balance.to_u128().unwrap(), 156092097302); - if let Some(cell) = wallets::code::get_jetton_library_cell(&hash) { - state_init.set_code(cell.clone()); - } - } - } - } + Ok(()) + } + + #[test] + fn notcoin_wallet_token_contract() -> anyhow::Result<()> { + let cell = ton_types::deserialize_tree_of_cells(&mut base64::decode("te6ccgEBAwEAqgACbIAX5XxfY9N6rJiyOS4NGQc01nd0dzEnWBk87cdqg9bLTwQNAeCGdH/3UAABdXbIjToZrn5eJgIBAJUHFxcOBj4fBYAfGfo6PQWliRZGmmqpYpA1QxmYkyLZonLf41f59x68XdAAvlWFDxGF2lXm67y4yzC17wYKD9A0guwPkMs1gOsM//IIQgK6KRjIlH6bJa+awbiDNXdUFz5YEvgHo9bmQqFHCVlTlQ==").unwrap().as_slice()).unwrap(); + let mut state = nekoton_utils::deserialize_account_stuff(cell)?; + + jetton::update_library_cell(&mut state.storage.state)?; let contract = jetton::TokenWalletContract(ExecutionContext { clock: &SimpleClock, @@ -211,7 +217,7 @@ mod tests { }); let balance = contract.balance()?; - assert_eq!(balance.to_u128().unwrap(), 156092097302); + assert_eq!(balance.to_u128().unwrap(), 6499273466060549); Ok(()) } @@ -265,4 +271,22 @@ mod tests { Ok(()) } + + #[test] + fn mintless_points_root_token_contract() -> anyhow::Result<()> { + let cell = + ton_types::deserialize_tree_of_cells(&mut base64::decode("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").unwrap().as_slice()) + .unwrap(); + let state = nekoton_utils::deserialize_account_stuff(cell)?; + + let contract = jetton::RootTokenContract(ExecutionContext { + clock: &SimpleClock, + account_stuff: &state, + }); + + let details = contract.get_details()?; + assert_eq!(details.admin_address, MsgAddressInt::default()); + + Ok(()) + } } diff --git a/nekoton-contracts/src/jetton/root_token_contract.rs b/nekoton-contracts/src/jetton/root_token_contract.rs index 77f3c897..96cb16e6 100644 --- a/nekoton-contracts/src/jetton/root_token_contract.rs +++ b/nekoton-contracts/src/jetton/root_token_contract.rs @@ -39,7 +39,8 @@ pub fn get_jetton_data(res: VmGetterOutput) -> Result { let mintable = stack[1].as_bool()?; let mut address_data = stack[2].as_slice()?.clone(); - let admin_address = MsgAddressInt::construct_from(&mut address_data)?; + + let admin_address = MsgAddressInt::construct_from(&mut address_data).unwrap_or_default(); let content = stack[3].as_cell()?; let content = MetaDataContent::parse(content)?; diff --git a/nekoton-contracts/src/wallets/code/jetton_wallet_v2.boc b/nekoton-contracts/src/wallets/code/jetton_wallet_v2.boc new file mode 100644 index 0000000000000000000000000000000000000000..3794284ea7c54d304775569d63735b8efbcdcf8d GIT binary patch literal 942 zcmaiyZAep57{||Z?%n2m+j6O8f%p0v61AD6h2R2(UI^~ipc#Q;FCQ%AI-`Ohwy^@G zRx_grq1g*WU(}*Lj6thDxOD}mBNkaQ^-EujxQw`+L3XY$;rbFB4xESc{GaFf{hwFg zhh6j70ElrTREDac3U=W->}Lj<2>U|wTwACe(2i<9>kw>SM(HP}bXbn&0fsKjU?mRm z1OvTe{)46t#?2-9rB)gsh**hrnoX30&%k?)r8nEQhOpOo)Yc3YD?LFS^M z)x<<4#z` z#kgNcybzV}2c5JWi?hV4cEfSWJoF%WBmxkDF?l>-UeMx`6>IbPef1}3TMcyH_6}V* z4G2tq{5&GgY$Uk!Q@H@Ji0aCe;3sQ@aLvZEF_%#FzI!Ynx-U5C}I*WlK z0h5n9sc2tUe3*(>t7fnSQ^{3TBXyyQKInR`s=7C25V|ZlMwH#zU|;K9860)133QFA zYa&S9UgO95i?eIp#yrTIL4de_v7_2IpGv!HuHP?@>t=K_+Wv=c?Rp>L!@{i%(E(+9 J|IXu&{s8Z@f`b46 literal 0 HcmV?d00001 diff --git a/nekoton-contracts/src/wallets/code/mod.rs b/nekoton-contracts/src/wallets/code/mod.rs index 6cc1f3d4..309189d5 100644 --- a/nekoton-contracts/src/wallets/code/mod.rs +++ b/nekoton-contracts/src/wallets/code/mod.rs @@ -21,10 +21,13 @@ declare_tvc! { multisig2_1 => "./Multisig2_1.tvc" (MULTISIG2_1_CODE), surf_wallet => "./Surf.tvc" (SURF_WALLET_CODE), wallet_v3 => "./wallet_v3_code.boc" (WALLET_V3_CODE), + wallet_v4r1 => "./wallet_v4r1_code.boc" (WALLET_V4R1_CODE), + wallet_v4r2 => "./wallet_v4r2_code.boc" (WALLET_V4R2_CODE), wallet_v5r1 => "./wallet_v5r1_code.boc" (WALLET_V5R1_CODE), highload_wallet_v2 => "./highload_wallet_v2_code.boc" (HIGHLOAD_WALLET_V2_CODE), ever_wallet => "./ever_wallet_code.boc" (EVER_WALLET_CODE), jetton_wallet_governed => "./jetton_wallet_governed.boc" (JETTON_WALLET_GOVERNED), + jetton_wallet_v2 => "./jetton_wallet_v2.boc" (JETTON_WALLET_V2), } fn load(mut data: &[u8]) -> Cell { @@ -35,7 +38,7 @@ static JETTON_LIBRARY_CELLS: once_cell::sync::Lazy> = once_cell::sync::Lazy::new(|| { let mut m = HashMap::new(); - let codes = [jetton_wallet_governed()]; + let codes = [jetton_wallet_governed(), jetton_wallet_v2()]; for code in codes { m.insert(code.repr_hash(), code); diff --git a/nekoton-contracts/src/wallets/code/wallet_v4r1_code.boc b/nekoton-contracts/src/wallets/code/wallet_v4r1_code.boc new file mode 100644 index 0000000000000000000000000000000000000000..542d420ca47f234c94593c4e194a46a9282b2742 GIT binary patch literal 769 zcmY+CUr19?9LIm>>|VE~%j-7BBx-kSCeh0beY9Ol=nnuL}}=)+~kcK5K(-4+su!}c{&%MZJFFi}EGq?;pj+`}) z@?eeQJfOHL;>hb%ids)Bq|%}{7*%^IluE16_`EFs5%`s`G z*k0=tjdbR9_FA`S9+(EThH^Nh1s9Tm3Tsuf$2I~Xvx_BcN7Cd{$+!S|a+}_iBTn!X zJN$J&|JB)LY$RSom4_248d#{gjXv&X6hi}!x64MBU-M^{O0!e?(2*(5 z_~>ElQs*2-{kp@@@*#0Qb43@}C_>NX%d)$R7hK)qG-`s)(=$><3c`?Zwg*4oxjaM< zY388q&DXmtw*-E4{`Z4Q9NOL<{X^4nfifDGzS`jZ>QSef;XrU?!o6d;r{Vi}Vp)Lmk z33Y`jqkeX%%&IR-(6K5j-ae6RK{5JR4N`mkr6!cM@<~`44J>l`i%+uF9In8wP~elO z2z(M30VSygq3EDY+NpRIgKc)U;GGQ0WGVjUdOSoIeEAEXIrD(CUVes+jNZDo<(pxL zCZgk8^Xz6I!CrI{X{*zjeYS8TAjY!NR(F)Uux&DAA^8P^fpo$tdlvhJnV{AGOyE>q3meNL1S0uHzPEn9T04YQ#g-57LRf z0fJi{$HT*&b2hu>ebWnmE|Rm^dRG^xjx99q%%52*&rUjH2PawAgZr&ZU2_->G&-R9 zUFvS`vLmulg6_@dmB2|(lupT0s1Y_#Osj?(g+cLbH-5T%X^@!gZ-Ta0UvIBm7rBx7 zpZAO;w7ov`o0P0sK2E~ecdHJ$oUaTFNw?LE_f9nCkrhV4zO-9nbnH?_oZH)}i3-S~ z%bY!+u3?CfQ{JNkiM*fdtzXZuW8vu4$X@};LKJ9IVc7h7zo~&?^dBkob(mmKU8}|R RA7ltf)njwkK(JR+{sYpxENTD% literal 0 HcmV?d00001 diff --git a/src/core/jetton_wallet/mod.rs b/src/core/jetton_wallet/mod.rs index 5eb6ae22..0d7ef090 100644 --- a/src/core/jetton_wallet/mod.rs +++ b/src/core/jetton_wallet/mod.rs @@ -1,10 +1,6 @@ use std::convert::TryFrom; use std::sync::Arc; -use crate::core::models::*; -use crate::core::parsing::*; -use crate::transport::models::{RawContractState, RawTransaction}; -use crate::transport::Transport; use anyhow::Result; use nekoton_abi::num_traits::ToPrimitive; use nekoton_abi::*; @@ -14,10 +10,16 @@ use num_bigint::{BigInt, BigUint, ToBigInt}; use ton_block::{MsgAddressInt, Serializable}; use ton_types::{BuilderData, IBitstring, SliceData}; +use crate::core::models::*; +use crate::core::parsing::*; +use crate::core::transactions_tree::TransactionsTreeStream; +use crate::transport::models::{RawContractState, RawTransaction}; +use crate::transport::Transport; + use super::{ContractSubscription, InternalMessage}; pub const JETTON_TRANSFER_OPCODE: u32 = 0x0f8a7ea5; -pub const JETTON_NOTIFY_OPCODE: u32 = 0x7362d09c; +pub const JETTON_INTERNAL_TRANSFER_OPCODE: u32 = 0x178d4519; pub struct JettonWallet { clock: Arc, @@ -88,14 +90,99 @@ impl JettonWallet { pub async fn estimate_min_attached_amount( &self, - _destination: TransferRecipient, - _tokens: BigUint, - _notify_receiver: bool, - _payload: ton_types::Cell, + amount: BigUint, + destination: MsgAddressInt, + remaining_gas_to: MsgAddressInt, + custom_payload: Option, + callback_value: BigUint, + callback_payload: Option, ) -> Result { - // TODO: estimate? - const ATTACHED_AMOUNT: u64 = 100_000_000; // 0.1 TON - Ok(ATTACHED_AMOUNT) + const FEE_MULTIPLIER: u128 = 2; + + // Prepare internal message + let internal_message = self.prepare_transfer( + amount, + destination, + remaining_gas_to, + custom_payload, + callback_value, + callback_payload, + 0, + )?; + + let mut message = ton_block::Message::with_int_header(ton_block::InternalMessageHeader { + src: ton_block::MsgAddressIntOrNone::Some( + internal_message + .source + .unwrap_or_else(|| self.owner.clone()), + ), + dst: internal_message.destination, + ..Default::default() + }); + + message.set_body(internal_message.body.clone()); + + // Prepare executor + let transport = self.contract_subscription.transport().clone(); + let config = transport + .get_blockchain_config(self.clock.as_ref(), true) + .await?; + + let mut tree = TransactionsTreeStream::new(message, config, transport, self.clock.clone()); + tree.unlimited_account_balance(); + tree.unlimited_message_balance(); + + type Err = fn(Option) -> JettonWalletError; + let check_exit_code = |tx: &ton_block::Transaction, err: Err| -> Result<()> { + let descr = tx.read_description()?; + if descr.is_aborted() { + let exit_code = match descr { + ton_block::TransactionDescr::Ordinary(descr) => match descr.compute_ph { + ton_block::TrComputePhase::Vm(phase) => Some(phase.exit_code), + ton_block::TrComputePhase::Skipped(_) => None, + }, + _ => None, + }; + Err(err(exit_code).into()) + } else { + Ok(()) + } + }; + + let mut attached_amount: u128 = 0; + + // Simulate source transaction + let source_tx = tree.next().await?.ok_or(JettonWalletError::NoSourceTx)?; + check_exit_code(&source_tx, JettonWalletError::SourceTxFailed)?; + attached_amount += source_tx.total_fees.grams.as_u128(); + + if source_tx.outmsg_cnt == 0 { + return Err(JettonWalletError::NoDestTx.into()); + } + + if let Some(message) = tree.peek() { + if message.state_init().is_some() && message.src_ref() == Some(self.address()) { + // Simulate first deploy transaction + // NOTE: we don't need to count attached amount here because of separate `initial_balance` + let _ = tree.next().await?.ok_or(JettonWalletError::NoDestTx)?; + //also we ignore non zero exit code for deploy transactions + } + } + + tree.retain_message_queue(|message| { + message.state_init().is_none() && message.src_ref() == Some(self.address()) + }); + + if tree.message_queue().len() != 1 { + return Err(JettonWalletError::NoDestTx.into()); + } + + // Simulate destination transaction + let dest_tx = tree.next().await?.ok_or(JettonWalletError::NoDestTx)?; + check_exit_code(&dest_tx, JettonWalletError::DestinationTxFailed)?; + attached_amount += dest_tx.total_fees.grams.as_u128(); + + Ok((attached_amount * FEE_MULTIPLIER) as u64) } pub fn prepare_transfer( @@ -211,7 +298,7 @@ impl JettonWallet { JettonWalletTransaction::Transfer(transfer) => { balance -= transfer.tokens.clone().to_bigint().trust_me(); } - JettonWalletTransaction::Notify(transfer) => { + JettonWalletTransaction::InternalTransfer(transfer) => { balance += transfer.tokens.clone().to_bigint().trust_me(); } } @@ -392,4 +479,12 @@ enum JettonWalletError { WalletNotDeployed, #[error("Failed to convert grams")] TryFromGrams, + #[error("No source transaction produced")] + NoSourceTx, + #[error("No destination transaction produced")] + NoDestTx, + #[error("Source transaction failed with exit code {0:?}")] + SourceTxFailed(Option), + #[error("Destination transaction failed with exit code {0:?}")] + DestinationTxFailed(Option), } diff --git a/src/core/parsing.rs b/src/core/parsing.rs index 16cec6ff..be33d2bc 100644 --- a/src/core/parsing.rs +++ b/src/core/parsing.rs @@ -10,7 +10,7 @@ use nekoton_abi::*; use nekoton_contracts::tip4_1::nft_contract; use nekoton_contracts::{old_tip3, tip3_1}; -use crate::core::jetton_wallet::{JETTON_NOTIFY_OPCODE, JETTON_TRANSFER_OPCODE}; +use crate::core::jetton_wallet::{JETTON_INTERNAL_TRANSFER_OPCODE, JETTON_TRANSFER_OPCODE}; use crate::core::models::*; use crate::core::ton_wallet::{MultisigType, WalletType}; @@ -65,7 +65,7 @@ pub fn parse_payload(payload: ton_types::SliceData) -> Option { None } -pub fn parse_payload_wallet_v5r1(payload: ton_types::SliceData) -> Option { +pub fn parse_jetton_payload(payload: ton_types::SliceData) -> Option { let mut payload = payload; let opcode = payload.get_next_u32().ok()?; @@ -121,7 +121,7 @@ pub fn parse_transaction_additional_info( WalletInteractionMethod::WalletV3Transfer, ) } - WalletType::WalletV5R1 => { + WalletType::WalletV4R1 | WalletType::WalletV4R2 | WalletType::WalletV5R1 => { let mut out_msg = None; tx.out_msgs .iterate(|item| { @@ -136,12 +136,12 @@ pub fn parse_transaction_additional_info( _ => return None, }; - let known_payload = out_msg.body().and_then(parse_payload_wallet_v5r1); + let known_payload = out_msg.body().and_then(parse_jetton_payload); ( Some(recipient.clone()), known_payload, - WalletInteractionMethod::WalletV5R1Transfer, + WalletInteractionMethod::TonWalletTransfer, ) } WalletType::Multisig(multisig_type) => { @@ -637,17 +637,17 @@ pub fn parse_token_transaction( pub fn parse_jetton_transaction( tx: &ton_block::Transaction, - _description: &ton_block::TransactionDescrOrdinary, + description: &ton_block::TransactionDescrOrdinary, ) -> Option { - // if description.aborted { - // return None; - // } + if description.aborted { + return None; + } let in_msg = tx.in_msg.as_ref()?.read_struct().ok()?; let mut body = in_msg.body()?; let opcode = body.get_next_u32().ok()?; - if opcode != JETTON_TRANSFER_OPCODE && opcode != JETTON_NOTIFY_OPCODE { + if opcode != JETTON_TRANSFER_OPCODE && opcode != JETTON_INTERNAL_TRANSFER_OPCODE { return None; } @@ -667,10 +667,12 @@ pub fn parse_jetton_transaction( to: addr, tokens: amount, })), - JETTON_NOTIFY_OPCODE => Some(JettonWalletTransaction::Notify(JettonIncomingTransfer { - from: addr, - tokens: amount, - })), + JETTON_INTERNAL_TRANSFER_OPCODE => Some(JettonWalletTransaction::InternalTransfer( + JettonIncomingTransfer { + from: addr, + tokens: amount, + }, + )), _ => None, } } @@ -1106,16 +1108,59 @@ mod tests { } #[test] - fn test_jetton_tokens_incoming_transfer() { - let (tx, description) = parse_transaction("te6ccgECBgEAAUEAA7F5WehEBzKkG99WEqFRkzi98nCWmpXQJBUBmsu+62sSSaAAAujDMA9YPbDkegBLk39wTvj+oYkQn/oiUxWp8d52BTFTfDVpe66gAALouYkW/BZ0Hn3gAAAoSAMCAQARDFCJAT9rXEEgAIJyP0XywcegmfnHNRlyzBk1fCDnCH7vhvlGb9ZcIR/c1sVcySdx1adc9HfHMJmGQfZqbrhnFk2erivzrCxhpNhIwQEBoAQBsUgBCW497xW4vg6Jw7WmJUrLC2JRQDOPb6GH1xJsclEw3tkAJWehEBzKkG99WEqFRkzi98nCWmpXQJBUBmsu+62sSSaQE/a1xAYMRbIAAF0YZgHrBM6Dz7zABQBmc2LQnAAAAZNZcVp9UXSHboAIANlGpOcSSxFpi+ldExuDMH3ZLMW//X1Zt9BCB0OjuVjA"); + fn test_parse_wallet_v5r1_transfer() { + let (tx, description) = parse_transaction("te6ccgECDgEAAroAA7V6PzxB5ur5JLcojkw57D91dcch0SdJBkRg11onChvcQxAAAuqQo7KQGfOICr+MryG/HTeCGLoHvR2QzQp8l/VW7Jy5KteDKoNgAALqkJdMvBZ0b1RwADRmUxQIBQQBAg8MQoYY8SmEQAMCAG/JhfBQTA/WGAAAAAAAAgAAAAAAAxZIaTNMW1cxmByM5WsWV9cxExzB5+1s+b7Uz5613xWmQNAtXACdQmljE4gAAAAAAAAAACPAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIACCcp/sV0iKg0YadasmKflOuBQl9+BT1AMGK8jDUAHzabPWEAUiOhXPDSZMLX8X/WQ0jZRy1Ef+OW9TZFgZ7OoiSM4CAeAIBgEB3wcBsWgBR+eIPN1fJJblEcmHPYfurrjkOiTpIMiMGutE4UN7iGMABaelQoOWDtcjd5wKID6i0sUbGwEsUr2tFotsGs7AiEdQUQ/0AAYP1jQAAF1SFHZSBM6N6o7ACwHliAFH54g83V8kluURyYc9h+6uuOQ6JOkgyIwa60ThQ3uIYgObSztz///4izo3vDAAACqUsU/LVK+ma1KSpaW5p+h9917oIw6a7Txpn/VJg/WB7C5dJQYSdVOvNFZvNMz1vvv5wMwo33jnWdrh1jaHQJXGBQkCCg7DyG0DDQoBaGIAC09KhQcsHa5G7zgUQH1FpYo2NgJYpXtaLRbYNZ2BEI6goh/oAAAAAAAAAAAAAAAAAAELAbIPin6lAAAAAAAAAABUUC0RQAgAcgwTrCsIXFRhmQTVWMIpgapb1R1i6mXzRjfAhiAa+x8AKPzxB5ur5JLcojkw57D91dcch0SdJBkRg11onChvcQxIHJw4AQwACW7J3GUgAAA="); + assert!(!description.aborted); - let wallet_transaction = parse_jetton_transaction(&tx, &description); + let wallet_transaction = parse_transaction_additional_info(&tx, WalletType::WalletV5R1); assert!(wallet_transaction.is_some()); - if let Some(JettonWalletTransaction::Notify(transfer)) = wallet_transaction { - assert_eq!(transfer.tokens.to_u128().unwrap(), 100000000000); + if let Some(TransactionAdditionalInfo::WalletInteraction(WalletInteractionInfo { + recipient, + known_payload, + .. + })) = wallet_transaction + { + assert_eq!( + recipient.unwrap(), + MsgAddressInt::from_str( + "0:169e950a0e583b5c8dde702880fa8b4b146c6c04b14af6b45a2db06b3b02211d" + ) + .unwrap() + ); + + assert!(known_payload.is_some()); + + let payload = known_payload.unwrap(); + if let KnownPayload::JettonOutgoingTransfer(JettonOutgoingTransfer { to, tokens }) = + payload + { + assert_eq!( + to, + MsgAddressInt::from_str( + "0:390609d615842e2a30cc826aac6114c0d52dea8eb17532f9a31be043100d7d8f" + ) + .unwrap() + ); + + assert_eq!(tokens.to_u128().unwrap(), 296400000000); + } + } + } + + #[test] + fn test_jetton_incoming_transfer() { + let (tx, description) = parse_transaction("te6ccgECEQEAA18AA7VyIr+K0006cnz63RzDYQCVEwPdJSutXyVs8FkkuI/aUhAAAuqVnc5wMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAZ0cDmQAFxhZIHoBQQBAhUECQF3khgYYMNQEQMCAG/Jh45gTBQmPAAAAAAABgACAAAABCVSGbb14fUjN0CNk0Gb357AgRdmQAzjvhXKQxQsysyEQNA6FACeQH0MDwXYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACCcpCuyJZa+rsW68PLm0COuucbYY14eIvIDQmENZPKyY2kdcLRm1JwTMPyj/1uPSmUxtxqBm0adw5aQV4xl7KOWKwCAeAMBgIB3QkHAQEgCADJSABEV/FaaadOT59bo5hsIBKiYHukpXWr5K2eCySXEftKQwAbKNSc4kliLTF9K6JjcGYPuyWYt/+vqzb6CEDodHcrGBAVsEtIBggjWgAAXVKzuc4Kzo4HMmqZO22AAADJtrLp10ABASAKAatIAERX8Vppp05Pn1ujmGwgEqJge6SldavkrZ4LJJcR+0pDACVnoRAcypBvfVhKhUZM4vfJwlpqV0CQVAZrLvutrEkmhAQGDAMIAABdUrO5zgjOjgcywAsAXnNi0JwAAAGTbWXTrhZIANlGpOcSSxFpi+ldExuDMH3ZLMW//X1Zt9BCB0OjuVjAArFoAFvJbBqvg228UqryRCckJdyJhdWLi/oZ3qOObCPE9jQBAAiK/itNNOnJ8+t0cw2EAlRMD3SUrrV8lbPBZJLiP2lIUBd5IYAGF1LiAABdUrO5zgTOjgcz4A4NAKMXjUUZAAABk21l064WSADZRqTnEksRaYvpXRMbgzB92SzFv/19WbfQQgdDo7lYwQAbKNSc4kliLTF9K6JjcGYPuyWYt/+vqzb6CEDodHcrGAQFAgE0EA8AhwCAErPQiA5lSDe+rCVCoyZxe+ThLTUroEgqAzWXfdbWJJNQAsROplLUCShZxn2kTkyjrdZWWw4ol9ZAosUb+zcNiHf6CEICj0Utek39dAZraCNlF3JZ7QVzRDW+drX9S9XYryt8PWg="); + assert!(!description.aborted); + + let jetton_transaction = parse_jetton_transaction(&tx, &description).unwrap(); + + if let JettonWalletTransaction::InternalTransfer(JettonIncomingTransfer { from, tokens }) = + jetton_transaction + { + assert_eq!(tokens.to_u128().unwrap(), 100); assert_eq!( - transfer.from, + from, MsgAddressInt::from_str( "0:6ca35273892588b4c5f4ae898dc1983eec9662dffebeacdbe82103a1d1dcac60" ) diff --git a/src/core/ton_wallet/mod.rs b/src/core/ton_wallet/mod.rs index ee09d82f..5caa82be 100644 --- a/src/core/ton_wallet/mod.rs +++ b/src/core/ton_wallet/mod.rs @@ -30,6 +30,7 @@ pub mod ever_wallet; pub mod highload_wallet_v2; pub mod multisig; pub mod wallet_v3; +pub mod wallet_v4; pub mod wallet_v5r1; pub const DEFAULT_WORKCHAIN: i8 = 0; @@ -229,6 +230,20 @@ impl TonWallet { self.workchain(), expiration, ), + WalletType::WalletV4R1 => wallet_v4::prepare_deploy( + self.clock.as_ref(), + &self.public_key, + self.workchain(), + expiration, + wallet_v4::WalletV4Version::R1, + ), + WalletType::WalletV4R2 => wallet_v4::prepare_deploy( + self.clock.as_ref(), + &self.public_key, + self.workchain(), + expiration, + wallet_v4::WalletV4Version::R2, + ), WalletType::WalletV5R1 => wallet_v5r1::prepare_deploy( self.clock.as_ref(), &self.public_key, @@ -325,6 +340,24 @@ impl TonWallet { vec![gift], expiration, ), + WalletType::WalletV4R1 => wallet_v4::prepare_transfer( + self.clock.as_ref(), + public_key, + current_state, + 0, + vec![gift], + expiration, + wallet_v4::WalletV4Version::R1, + ), + WalletType::WalletV4R2 => wallet_v4::prepare_transfer( + self.clock.as_ref(), + public_key, + current_state, + 0, + vec![gift], + expiration, + wallet_v4::WalletV4Version::R2, + ), WalletType::WalletV5R1 => wallet_v5r1::prepare_transfer( self.clock.as_ref(), public_key, @@ -887,6 +920,8 @@ pub enum TransferAction { pub enum WalletType { Multisig(MultisigType), WalletV3, + WalletV4R1, + WalletV4R2, WalletV5R1, HighloadWalletV2, EverWallet, @@ -900,6 +935,7 @@ impl WalletType { Self::WalletV5R1 => wallet_v5r1::DETAILS, Self::HighloadWalletV2 => highload_wallet_v2::DETAILS, Self::EverWallet => ever_wallet::DETAILS, + Self::WalletV4R1 | Self::WalletV4R2 => wallet_v4::DETAILS, } } @@ -916,6 +952,8 @@ impl WalletType { match self { Self::Multisig(multisig_type) => multisig_type.code_hash(), Self::WalletV3 => wallet_v3::CODE_HASH, + Self::WalletV4R1 => wallet_v4::CODE_HASH_R1, + Self::WalletV4R2 => wallet_v4::CODE_HASH_R2, Self::WalletV5R1 => wallet_v5r1::CODE_HASH, Self::HighloadWalletV2 => highload_wallet_v2::CODE_HASH, Self::EverWallet => ever_wallet::CODE_HASH, @@ -927,6 +965,8 @@ impl WalletType { match self { Self::Multisig(multisig_type) => multisig_type.code(), Self::WalletV3 => wallets::code::wallet_v3(), + Self::WalletV4R1 => wallets::code::wallet_v4r1(), + Self::WalletV4R2 => wallets::code::wallet_v4r2(), Self::WalletV5R1 => wallets::code::wallet_v5r1(), Self::HighloadWalletV2 => wallets::code::highload_wallet_v2(), Self::EverWallet => wallets::code::ever_wallet(), @@ -940,6 +980,8 @@ impl FromStr for WalletType { fn from_str(s: &str) -> Result { Ok(match s { "WalletV3" => Self::WalletV3, + "WalletV4R1" => Self::WalletV4R1, + "WalletV4R2" => Self::WalletV4R2, "WalletV5R1" => Self::WalletV5R1, "HighloadWalletV2" => Self::HighloadWalletV2, "EverWallet" => Self::EverWallet, @@ -962,7 +1004,9 @@ impl TryInto for WalletType { WalletType::Multisig(MultisigType::SurfWallet) => 6, WalletType::Multisig(MultisigType::Multisig2) => 7, WalletType::Multisig(MultisigType::Multisig2_1) => 8, - WalletType::WalletV5R1 => 9, + WalletType::WalletV4R1 => 9, + WalletType::WalletV4R2 => 10, + WalletType::WalletV5R1 => 11, _ => anyhow::bail!("Unimplemented wallet type"), }; @@ -975,6 +1019,8 @@ impl std::fmt::Display for WalletType { match self { Self::Multisig(multisig_type) => multisig_type.fmt(f), Self::WalletV3 => f.write_str("WalletV3"), + Self::WalletV4R1 => f.write_str("WalletV4R1"), + Self::WalletV4R2 => f.write_str("WalletV4R2"), Self::WalletV5R1 => f.write_str("WalletV5R1"), Self::HighloadWalletV2 => f.write_str("HighloadWalletV2"), Self::EverWallet => f.write_str("EverWallet"), @@ -997,6 +1043,16 @@ pub fn compute_address( WalletType::HighloadWalletV2 => { highload_wallet_v2::compute_contract_address(public_key, workchain_id) } + WalletType::WalletV4R1 => wallet_v4::compute_contract_address( + public_key, + workchain_id, + wallet_v4::WalletV4Version::R1, + ), + WalletType::WalletV4R2 => wallet_v4::compute_contract_address( + public_key, + workchain_id, + wallet_v4::WalletV4Version::R2, + ), } } diff --git a/src/core/ton_wallet/wallet_v4.rs b/src/core/ton_wallet/wallet_v4.rs new file mode 100644 index 00000000..4479c840 --- /dev/null +++ b/src/core/ton_wallet/wallet_v4.rs @@ -0,0 +1,397 @@ +use std::convert::TryFrom; + +use anyhow::Result; +use ed25519_dalek::PublicKey; +use ton_block::{MsgAddrStd, MsgAddressInt, Serializable}; +use ton_types::{BuilderData, Cell, IBitstring, SliceData, UInt256}; + +use nekoton_utils::*; + +use super::{Gift, TonWalletDetails, TransferAction}; +use crate::core::models::{Expiration, ExpireAt}; +use crate::crypto::{SignedMessage, UnsignedMessage}; + +pub fn prepare_deploy( + clock: &dyn Clock, + public_key: &PublicKey, + workchain: i8, + expiration: Expiration, + version: WalletV4Version, +) -> Result> { + let init_data = InitData::from_key(public_key).with_subwallet_id(SUBWALLET_ID); + let dst = compute_contract_address(public_key, workchain, version); + let mut message = + ton_block::Message::with_ext_in_header(ton_block::ExternalInboundMessageHeader { + dst, + ..Default::default() + }); + + message.set_state_init(init_data.make_state_init(version)?); + + let expire_at = ExpireAt::new(clock, expiration); + let (hash, payload) = init_data.make_transfer_payload(None, expire_at.timestamp)?; + + Ok(Box::new(UnsignedWalletV4 { + init_data, + gifts: Vec::new(), + payload, + message, + expire_at, + hash, + })) +} + +pub fn prepare_transfer( + clock: &dyn Clock, + public_key: &PublicKey, + current_state: &ton_block::AccountStuff, + seqno_offset: u32, + gifts: Vec, + expiration: Expiration, + version: WalletV4Version, +) -> Result { + if gifts.len() > MAX_MESSAGES { + return Err(WalletV4Error::TooManyGifts.into()); + } + + let (mut init_data, with_state_init) = match ¤t_state.storage.state { + ton_block::AccountState::AccountActive { state_init, .. } => match &state_init.data { + Some(data) => (InitData::try_from(data)?, false), + None => return Err(WalletV4Error::InvalidInitData.into()), + }, + ton_block::AccountState::AccountFrozen { .. } => { + return Err(WalletV4Error::AccountIsFrozen.into()) + } + ton_block::AccountState::AccountUninit => ( + InitData::from_key(public_key).with_subwallet_id(SUBWALLET_ID), + true, + ), + }; + + init_data.seqno += seqno_offset; + + let mut message = + ton_block::Message::with_ext_in_header(ton_block::ExternalInboundMessageHeader { + dst: current_state.addr.clone(), + ..Default::default() + }); + + if with_state_init { + message.set_state_init(init_data.make_state_init(version)?); + } + + let expire_at = ExpireAt::new(clock, expiration); + let (hash, payload) = init_data.make_transfer_payload(gifts.clone(), expire_at.timestamp)?; + + Ok(TransferAction::Sign(Box::new(UnsignedWalletV4 { + init_data, + gifts, + payload, + hash, + expire_at, + message, + }))) +} + +#[derive(Clone)] +struct UnsignedWalletV4 { + init_data: InitData, + gifts: Vec, + payload: BuilderData, + hash: UInt256, + expire_at: ExpireAt, + message: ton_block::Message, +} + +impl UnsignedMessage for UnsignedWalletV4 { + fn refresh_timeout(&mut self, clock: &dyn Clock) { + if !self.expire_at.refresh(clock) { + return; + } + + let (hash, payload) = self + .init_data + .make_transfer_payload(self.gifts.clone(), self.expire_at()) + .trust_me(); + self.hash = hash; + self.payload = payload; + } + + fn expire_at(&self) -> u32 { + self.expire_at.timestamp + } + + fn hash(&self) -> &[u8] { + self.hash.as_slice() + } + + fn sign(&self, signature: &[u8; ed25519_dalek::SIGNATURE_LENGTH]) -> Result { + let mut payload = self.payload.clone(); + payload.prepend_raw(signature, signature.len() * 8)?; + + let mut message = self.message.clone(); + message.set_body(SliceData::load_builder(payload)?); + + Ok(SignedMessage { + message, + expire_at: self.expire_at(), + }) + } + + fn sign_with_pruned_payload( + &self, + signature: &[u8; ed25519_dalek::SIGNATURE_LENGTH], + prune_after_depth: u16, + ) -> Result { + let mut payload = self.payload.clone(); + payload.append_raw(signature, signature.len() * 8)?; + let body = payload.into_cell()?; + + let mut message = self.message.clone(); + message.set_body(prune_deep_cells(&body, prune_after_depth)?); + + Ok(SignedMessage { + message, + expire_at: self.expire_at(), + }) + } +} + +pub static CODE_HASH_R1: &[u8; 32] = &[ + 0x64, 0xDD, 0x54, 0x80, 0x55, 0x22, 0xC5, 0xBE, 0x8A, 0x9D, 0xB5, 0x9C, 0xEA, 0x01, 0x05, 0xCC, + 0xF0, 0xD0, 0x87, 0x86, 0xCA, 0x79, 0xBE, 0xB8, 0xCB, 0x79, 0xE8, 0x80, 0xA8, 0xD7, 0x32, 0x2D, +]; + +pub static CODE_HASH_R2: &[u8; 32] = &[ + 0xFE, 0xB5, 0xFF, 0x68, 0x20, 0xE2, 0xFF, 0x0D, 0x94, 0x83, 0xE7, 0xE0, 0xD6, 0x2C, 0x81, 0x7D, + 0x84, 0x67, 0x89, 0xFB, 0x4A, 0xE5, 0x80, 0xC8, 0x78, 0x86, 0x6D, 0x95, 0x9D, 0xAB, 0xD5, 0xC0, +]; + +pub fn is_wallet_v4r1(code_hash: &UInt256) -> bool { + code_hash.as_slice() == CODE_HASH_R1 +} + +pub fn is_wallet_v4r2(code_hash: &UInt256) -> bool { + code_hash.as_slice() == CODE_HASH_R2 +} + +pub fn compute_contract_address( + public_key: &PublicKey, + workchain_id: i8, + version: WalletV4Version, +) -> MsgAddressInt { + InitData::from_key(public_key) + .with_subwallet_id(SUBWALLET_ID) + .compute_addr(workchain_id, version) + .trust_me() +} + +pub static DETAILS: TonWalletDetails = TonWalletDetails { + requires_separate_deploy: false, + min_amount: 1, // 0.000000001 TON + max_messages: MAX_MESSAGES, + supports_payload: true, + supports_state_init: true, + supports_multiple_owners: false, + supports_code_update: false, + expiration_time: 0, + required_confirmations: None, +}; + +const MAX_MESSAGES: usize = 4; + +/// `WalletV5` init data +#[derive(Clone, Copy)] +pub struct InitData { + pub seqno: u32, + pub subwallet_id: i32, + pub public_key: UInt256, +} + +impl InitData { + pub fn public_key(&self) -> &[u8; 32] { + self.public_key.as_slice() + } + + pub fn from_key(key: &PublicKey) -> Self { + Self { + seqno: 0, + subwallet_id: 0, + public_key: key.as_bytes().into(), + } + } + + pub fn with_subwallet_id(mut self, id: i32) -> Self { + self.subwallet_id = id; + self + } + + pub fn compute_addr( + &self, + workchain_id: i8, + version: WalletV4Version, + ) -> Result { + let init_state = self.make_state_init(version)?.serialize()?; + let hash = init_state.repr_hash(); + Ok(MsgAddressInt::AddrStd(MsgAddrStd { + anycast: None, + workchain_id, + address: hash.into(), + })) + } + + pub fn make_state_init(&self, version: WalletV4Version) -> Result { + let code = match version { + WalletV4Version::R1 => nekoton_contracts::wallets::code::wallet_v4r1(), + WalletV4Version::R2 => nekoton_contracts::wallets::code::wallet_v4r2(), + }; + + Ok(ton_block::StateInit { + code: Some(code), + data: Some(self.serialize()?), + ..Default::default() + }) + } + + pub fn serialize(&self) -> Result { + let mut data = BuilderData::new(); + data.append_u32(self.seqno)? + .append_i32(self.subwallet_id)? + .append_raw(self.public_key.as_slice(), 256)?; + + // empty plugin dict + data.append_bit_zero()?; + + data.into_cell() + } + + pub fn make_transfer_payload( + &self, + gifts: impl IntoIterator, + expire_at: u32, + ) -> Result<(UInt256, BuilderData)> { + let mut payload = BuilderData::new(); + + // insert prefix + payload + .append_i32(self.subwallet_id)? + .append_u32(expire_at)? + .append_u32(self.seqno)?; + + // Opcode + payload.append_u8(0)?; + + for gift in gifts { + let mut internal_message = + ton_block::Message::with_int_header(ton_block::InternalMessageHeader { + ihr_disabled: true, + bounce: gift.bounce, + dst: gift.destination, + value: gift.amount.into(), + ..Default::default() + }); + + if let Some(body) = gift.body { + internal_message.set_body(body); + } + + if let Some(state_init) = gift.state_init { + internal_message.set_state_init(state_init); + } + + // append it to the body + payload + .append_u8(gift.flags)? + .checked_append_reference(internal_message.serialize()?)?; + } + + let hash = payload.clone().into_cell()?.repr_hash(); + + Ok((hash, payload)) + } +} + +impl TryFrom<&Cell> for InitData { + type Error = anyhow::Error; + + fn try_from(data: &Cell) -> Result { + let mut cs = SliceData::load_cell_ref(data)?; + Ok(Self { + seqno: cs.get_next_u32()?, + subwallet_id: cs.get_next_i32()?, + public_key: UInt256::from_be_bytes(&cs.get_next_bytes(32)?), + }) + } +} + +const SUBWALLET_ID: i32 = 0x29A9A317; + +#[derive(Clone, Copy, PartialEq, Eq)] +pub enum WalletV4Version { + R1, + R2, +} + +#[derive(thiserror::Error, Debug)] +enum WalletV4Error { + #[error("Invalid init data")] + InvalidInitData, + #[error("Account is frozen")] + AccountIsFrozen, + #[error("Too many outgoing messages")] + TooManyGifts, +} + +#[cfg(test)] +mod tests { + use std::str::FromStr; + use ton_block::Deserializable; + use ton_types::UInt256; + + use nekoton_contracts::wallets; + + use crate::core::ton_wallet::wallet_v4::{ + is_wallet_v4r1, is_wallet_v4r2, InitData, WalletV4Version, SUBWALLET_ID, + }; + + #[test] + fn code_hash_v4r1() -> anyhow::Result<()> { + let code_cell = wallets::code::wallet_v4r1(); + + let is_wallet_v4r1 = is_wallet_v4r1(&code_cell.repr_hash()); + assert!(is_wallet_v4r1); + + Ok(()) + } + + #[test] + fn code_hash_v4r2() -> anyhow::Result<()> { + let code_cell = wallets::code::wallet_v4r2(); + + let is_wallet_v4r2 = is_wallet_v4r2(&code_cell.repr_hash()); + assert!(is_wallet_v4r2); + + Ok(()) + } + + #[test] + fn state_init_v4r2() -> anyhow::Result<()> { + let state_init_base64 = "te6ccgECFgEAAwQAAgE0AQIBFP8A9KQT9LzyyAsDAFEAAAAAKamjF2dW1vNw/It5bDWN3jVo5dxzZVk+Q11lVLs3LamPSWAVQAIBIAQFAgFIBgcE+PKDCNcYINMf0x/THwL4I7vyZO1E0NMf0x/T//QE0VFDuvKhUVG68qIF+QFUEGT5EPKj+AAkpMjLH1JAyx9SMMv/UhD0AMntVPgPAdMHIcAAn2xRkyDXSpbTB9QC+wDoMOAhwAHjACHAAuMAAcADkTDjDQOkyMsfEssfy/8SExQVAubQAdDTAyFxsJJfBOAi10nBIJJfBOAC0x8hghBwbHVnvSKCEGRzdHK9sJJfBeAD+kAwIPpEAcjKB8v/ydDtRNCBAUDXIfQEMFyBAQj0Cm+hMbOSXwfgBdM/yCWCEHBsdWe6kjgw4w0DghBkc3RyupJfBuMNCAkCASAKCwB4AfoA9AQw+CdvIjBQCqEhvvLgUIIQcGx1Z4MesXCAGFAEywUmzxZY+gIZ9ADLaRfLH1Jgyz8gyYBA+wAGAIpQBIEBCPRZMO1E0IEBQNcgyAHPFvQAye1UAXKwjiOCEGRzdHKDHrFwgBhQBcsFUAPPFiP6AhPLassfyz/JgED7AJJfA+ICASAMDQBZvSQrb2omhAgKBrkPoCGEcNQICEekk30pkQzmkD6f+YN4EoAbeBAUiYcVnzGEAgFYDg8AEbjJftRNDXCx+AA9sp37UTQgQFA1yH0BDACyMoHy//J0AGBAQj0Cm+hMYAIBIBARABmtznaiaEAga5Drhf/AABmvHfaiaEAQa5DrhY/AAG7SB/oA1NQi+QAFyMoHFcv/ydB3dIAYyMsFywIizxZQBfoCFMtrEszMyXP7AMhAFIEBCPRR8qcCAHCBAQjXGPoA0z/IVCBHgQEI9FHyp4IQbm90ZXB0gBjIywXLAlAGzxZQBPoCFMtqEssfyz/Jc/sAAgBsgQEI1xj6ANM/MFIkgQEI9Fnyp4IQZHN0cnB0gBjIywXLAlAFzxZQA/oCE8tqyx8Syz/Jc/sAAAr0AMntVA=="; + + let state_init = ton_block::StateInit::construct_from_base64(state_init_base64)?; + + let init_data_clone = InitData { + seqno: 0, + subwallet_id: SUBWALLET_ID, + public_key: UInt256::from_str( + "6756d6f370fc8b796c358dde3568e5dc7365593e435d6554bb372da98f496015", + )?, + }; + + let state_init_clone = init_data_clone.make_state_init(WalletV4Version::R2)?; + + assert_eq!(state_init, state_init_clone); + + Ok(()) + } +} diff --git a/src/core/ton_wallet/wallet_v5r1.rs b/src/core/ton_wallet/wallet_v5r1.rs index d970b09b..b75b5f29 100644 --- a/src/core/ton_wallet/wallet_v5r1.rs +++ b/src/core/ton_wallet/wallet_v5r1.rs @@ -359,9 +359,12 @@ enum WalletV5Error { #[cfg(test)] mod tests { use ed25519_dalek::PublicKey; + use nekoton_contracts::wallets; use ton_block::AccountState; - use crate::core::ton_wallet::wallet_v5r1::{compute_contract_address, InitData, WALLET_ID}; + use crate::core::ton_wallet::wallet_v5r1::{ + compute_contract_address, is_wallet_v5r1, InitData, WALLET_ID, + }; #[test] fn state_init() -> anyhow::Result<()> { @@ -388,4 +391,14 @@ mod tests { Ok(()) } + + #[test] + fn code_hash() -> anyhow::Result<()> { + let code_cell = wallets::code::wallet_v5r1(); + + let is_wallet_v5r1 = is_wallet_v5r1(&code_cell.repr_hash()); + assert!(is_wallet_v5r1); + + Ok(()) + } } diff --git a/src/models.rs b/src/models.rs index a5c89140..cdab59ab 100644 --- a/src/models.rs +++ b/src/models.rs @@ -60,7 +60,7 @@ pub enum KnownPayload { #[serde(rename_all = "snake_case", tag = "type", content = "data")] pub enum WalletInteractionMethod { WalletV3Transfer, - WalletV5R1Transfer, + TonWalletTransfer, Multisig(Box), } @@ -280,7 +280,7 @@ pub enum TokenWalletTransaction { #[serde(rename_all = "snake_case", tag = "type", content = "data")] pub enum JettonWalletTransaction { Transfer(JettonOutgoingTransfer), - Notify(JettonIncomingTransfer), + InternalTransfer(JettonIncomingTransfer), } #[derive(Clone, Debug, Serialize, Deserialize)]