diff --git a/Cargo.toml b/Cargo.toml index 3d4870ad7..e08fb83d1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nekoton" -version = "0.13.1" +version = "0.14.0" authors = [ "Alexey Pashinov ", "Vladimir Petrzhikovskiy ", @@ -45,7 +45,7 @@ quick_cache = "0.4.1" 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" +serde_json = { version = "1.0", features = ["raw_value"] } sha2 = { version = "0.10.8", optional = true } thiserror = "1.0" tiny-jsonrpc = { version = "0.6.0", default-features = false, optional = true } @@ -58,7 +58,7 @@ tiny-hderive = { git = "https://github.com/broxus/tiny-hderive.git", optional = 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_executor = { git = "https://github.com/broxus/ton-labs-executor.git"} ton_types = { git = "https://github.com/broxus/ton-labs-types.git" } nekoton-contracts = { path = "nekoton-contracts" } diff --git a/README.md b/README.md index f22bdc251..c8bcbfd9c 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,9 @@ -

- - Logo - -

- # nekoton   [![Workflow badge]][workflow] [![License Apache badge]][license apache] [![Docs badge]][docs] ## About Broxus SDK with TIP3 wallets support and a bunch of helpers. -## Usage - -```bash -cargo add nekoton -``` - ### Prerequisites - Rust 1.65+ diff --git a/gen-protos/Cargo.toml b/gen-protos/Cargo.toml index df361f391..79354f4e1 100644 --- a/gen-protos/Cargo.toml +++ b/gen-protos/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "gen-protos" description = "Generate Protocol Buffers definitions for nekoton-proto crate" -version = "0.13.0" +version = "0.14.0" authors = [ "Alexey Pashinov ", "Vladimir Petrzhikovskiy ", diff --git a/nekoton-abi/Cargo.toml b/nekoton-abi/Cargo.toml index 9470c53a8..4bfeea26e 100644 --- a/nekoton-abi/Cargo.toml +++ b/nekoton-abi/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nekoton-abi" -version = "0.13.0" +version = "0.14.0" authors = [ "Alexey Pashinov ", "Vladimir Petrzhikovskiy ", diff --git a/nekoton-abi/src/abi_helpers.rs b/nekoton-abi/src/abi_helpers.rs index d84316448..304f43404 100644 --- a/nekoton-abi/src/abi_helpers.rs +++ b/nekoton-abi/src/abi_helpers.rs @@ -4,6 +4,7 @@ use ton_types::UInt256; use super::{BuildTokenValue, KnownParamType, UnpackerError, UnpackerResult}; +#[derive(Clone, Debug)] pub struct BigUint128(pub BigUint); impl BuildTokenValue for BigUint128 { @@ -206,7 +207,13 @@ pub mod address_only_hash { TokenValue::Address(ton_block::MsgAddress::AddrStd(ton_block::MsgAddrStd { address, .. + })) + | TokenValue::AddressStd(ton_block::MsgAddress::AddrStd(ton_block::MsgAddrStd { + address, + .. })) => Ok(UInt256::from_be_bytes(&address.get_bytestring(0))), + TokenValue::Address(ton_block::MsgAddress::AddrNone) + | TokenValue::AddressStd(ton_block::MsgAddress::AddrNone) => Ok(UInt256::ZERO), _ => Err(UnpackerError::InvalidAbi), } } @@ -222,16 +229,7 @@ pub mod array_address_only_hash { pub fn pack(value: Vec) -> TokenValue { TokenValue::Array( param_type(), - value - .into_iter() - .map(|value| { - TokenValue::Address(ton_block::MsgAddress::AddrStd(ton_block::MsgAddrStd { - anycast: None, - workchain_id: 0, - address: value.into(), - })) - }) - .collect(), + value.into_iter().map(address_only_hash::pack).collect(), ) } @@ -240,12 +238,7 @@ pub mod array_address_only_hash { TokenValue::Array(_, values) => { let mut result = Vec::with_capacity(values.len()); for value in values { - match value { - TokenValue::Address(ton_block::MsgAddress::AddrStd( - ton_block::MsgAddrStd { address, .. }, - )) => result.push(UInt256::from_be_bytes(&address.get_bytestring(0))), - _ => return Err(UnpackerError::InvalidAbi), - } + result.push(address_only_hash::unpack(value)?); } Ok(result) } diff --git a/nekoton-abi/src/function_builder.rs b/nekoton-abi/src/function_builder.rs index 70730f5e7..0315390f2 100644 --- a/nekoton-abi/src/function_builder.rs +++ b/nekoton-abi/src/function_builder.rs @@ -24,6 +24,8 @@ pub struct FunctionBuilder { outputs: Vec, /// Whether `answer_id` is set responsible: bool, + /// Whether the function can be executed as external message. + external_msg: Option, } impl FunctionBuilder { @@ -36,11 +38,13 @@ impl FunctionBuilder { inputs: Vec::new(), outputs: Vec::new(), responsible: false, + external_msg: None, } } pub fn new_responsible(function_name: &str) -> Self { let mut function = Self::new(function_name); + // Force all responsible functions to be called as internal. function.make_responsible(); function } @@ -71,6 +75,10 @@ impl FunctionBuilder { self.responsible = true; } + pub fn external_msg(&mut self, external_msg: Option) { + self.external_msg = external_msg; + } + pub fn abi_version(mut self, abi_version: AbiVersion) -> Self { self.abi_version = abi_version; self @@ -140,6 +148,7 @@ impl FunctionBuilder { outputs: self.outputs, input_id: 0, output_id: 0, + external_msg: self.external_msg, }; match self.id { Some(id) => { diff --git a/nekoton-abi/src/lib.rs b/nekoton-abi/src/lib.rs index a1807ef45..37ac16cf2 100644 --- a/nekoton-abi/src/lib.rs +++ b/nekoton-abi/src/lib.rs @@ -65,7 +65,7 @@ use ton_block::{ Serializable, }; use ton_executor::{BlockchainConfig, OrdinaryTransactionExecutor, TransactionExecutor}; -use ton_types::{SliceData, UInt256}; +use ton_types::{HashmapE, SliceData, UInt256}; use ton_vm::executor::BehaviorModifiers; #[cfg(feature = "derive")] @@ -248,7 +248,7 @@ pub fn unpack_from_cell( ) -> Result> { let cs: Cursor = cursor.into(); let (tokens, cursor) = - TokenValue::decode_params_with_cursor(params, cs, &abi_version, allow_partial, false)?; + TokenValue::decode_params_with_cursor(params, cs, &abi_version, allow_partial, true)?; if !allow_partial && (cursor.slice.remaining_references() != 0 || cursor.slice.remaining_bits() != 0) @@ -590,11 +590,17 @@ pub fn code_to_tvc(code: ton_types::Cell) -> Result { pub struct ExecutionContext<'a> { pub clock: &'a dyn Clock, pub account_stuff: &'a AccountStuff, + pub libraries: &'a [HashmapE], } impl ExecutionContext<'_> { pub fn run_local(&self, function: &Function, input: &[Token]) -> Result { - function.run_local(self.clock, self.account_stuff.clone(), input) + function.run_local( + self.clock, + self.account_stuff.clone(), + input, + self.libraries, + ) } pub fn run_local_responsible( @@ -602,7 +608,12 @@ impl ExecutionContext<'_> { function: &Function, input: &[Token], ) -> Result { - function.run_local_responsible(self.clock, self.account_stuff.clone(), input) + function.run_local_responsible( + self.clock, + self.account_stuff.clone(), + input, + self.libraries, + ) } pub fn run_getter( @@ -643,6 +654,7 @@ impl ExecutionContext<'_> { args, config, modifier, + self.libraries, ) } } @@ -684,6 +696,7 @@ pub trait FunctionExt { clock: &dyn Clock, account_stuff: AccountStuff, input: &[Token], + libraries: &[HashmapE], ) -> Result { self.run_local_ext( clock, @@ -691,6 +704,7 @@ pub trait FunctionExt { input, false, &BriefBlockchainConfig::default(), + libraries, ) } @@ -699,6 +713,7 @@ pub trait FunctionExt { clock: &dyn Clock, account_stuff: AccountStuff, input: &[Token], + libraries: &[HashmapE], ) -> Result { self.run_local_ext( clock, @@ -706,6 +721,7 @@ pub trait FunctionExt { input, true, &BriefBlockchainConfig::default(), + libraries, ) } @@ -716,6 +732,7 @@ pub trait FunctionExt { input: &[Token], responsible: bool, config: &BriefBlockchainConfig, + libraries: &[HashmapE], ) -> Result; } @@ -734,8 +751,17 @@ where input: &[Token], responsible: bool, config: &BriefBlockchainConfig, + libraries: &[HashmapE], ) -> Result { - T::run_local_ext(self, clock, account_stuff, input, responsible, config) + T::run_local_ext( + self, + clock, + account_stuff, + input, + responsible, + config, + libraries, + ) } } @@ -752,8 +778,16 @@ impl FunctionExt for Function { input: &[Token], responsible: bool, config: &BriefBlockchainConfig, + libraries: &[HashmapE], ) -> Result { - FunctionAbi::new(self).run_local(clock, &mut account_stuff, input, responsible, config) + FunctionAbi::new(self).run_local( + clock, + &mut account_stuff, + input, + responsible, + config, + libraries, + ) } } @@ -776,11 +810,18 @@ impl<'a> FunctionAbi<'a> { clock: &dyn Clock, account_stuff: &mut AccountStuff, input: &[Token], - responsible: bool, + mut responsible: bool, config: &BriefBlockchainConfig, + libraries: &[HashmapE], ) -> Result { let function = self.abi; + if let Some(external_msg) = self.abi.external_msg { + // Force "upgrade" function to responsible when it cannot + // be executed as an external message. + responsible |= !external_msg; + } + let answer_id = if responsible { account_stuff.storage.balance.grams = ton_block::Grams::from(100_000_000_000_000u64); // 100 000 TON @@ -838,6 +879,7 @@ impl<'a> FunctionAbi<'a> { &msg, config, &Default::default(), + libraries, )?; let tokens = if let Some(answer_id) = answer_id { @@ -1171,13 +1213,23 @@ pub fn default_blockchain_config() -> &'static ton_executor::BlockchainConfig { mod tests { use std::str::FromStr; + use super::*; use ton_abi::{Param, ParamType, Uint}; use ton_block::{Deserializable, Message, Transaction}; - - use super::*; + use ton_types::deserialize_tree_of_cells; const DEFAULT_ABI_VERSION: ton_abi::contract::AbiVersion = ton_abi::contract::ABI_VERSION_2_0; + #[test] + fn string_test() { + let boc = "te6ccgEBAgEAEQABAAEAGEhlbGxvIFdvcmxkIQ=="; + let cell = deserialize_tree_of_cells(&mut base64::decode(boc).unwrap().as_slice()).unwrap(); + let slice = SliceData::load_cell(cell).unwrap(); + let params = Param::new("boc", ParamType::String); + let tokens = + unpack_from_cell(&[params], slice, false, ton_abi::contract::ABI_VERSION_2_4).unwrap(); + } + #[test] fn correct_text_payload() { let comment = create_boc_or_comment_payload("test").unwrap(); @@ -1279,6 +1331,7 @@ mod tests { let res = ExecutionContext { clock: &SimpleClock, account_stuff: &state, + libraries: Default::default(), } .run_getter("seqno", &[]) .unwrap(); diff --git a/nekoton-abi/src/token_unpacker.rs b/nekoton-abi/src/token_unpacker.rs index 7a0c293e1..7e2550d61 100644 --- a/nekoton-abi/src/token_unpacker.rs +++ b/nekoton-abi/src/token_unpacker.rs @@ -208,12 +208,17 @@ impl UnpackAbi for TokenValue { impl UnpackAbi for TokenValue { fn unpack(self) -> UnpackerResult { match self { - TokenValue::Address(ton_block::MsgAddress::AddrStd(addr)) => { + TokenValue::Address(ton_block::MsgAddress::AddrStd(addr)) + | TokenValue::AddressStd(ton_block::MsgAddress::AddrStd(addr)) => { Ok(MsgAddressInt::AddrStd(addr)) } TokenValue::Address(ton_block::MsgAddress::AddrVar(addr)) => { Ok(MsgAddressInt::AddrVar(addr)) } + TokenValue::Address(ton_block::MsgAddress::AddrNone) + | TokenValue::AddressStd(ton_block::MsgAddress::AddrNone) => { + Ok(MsgAddressInt::AddrStd(MsgAddrStd::default())) + } _ => Err(UnpackerError::InvalidAbi), } } @@ -222,7 +227,7 @@ impl UnpackAbi for TokenValue { impl UnpackAbi for TokenValue { fn unpack(self) -> UnpackerResult { match self { - TokenValue::Address(address) => Ok(address), + TokenValue::Address(address) | TokenValue::AddressStd(address) => Ok(address), _ => Err(UnpackerError::InvalidAbi), } } @@ -231,7 +236,10 @@ impl UnpackAbi for TokenValue { impl UnpackAbi for TokenValue { fn unpack(self) -> UnpackerResult { match self { - TokenValue::Address(ton_block::MsgAddress::AddrStd(addr)) => Ok(addr), + TokenValue::Address(ton_block::MsgAddress::AddrStd(addr)) + | TokenValue::AddressStd(ton_block::MsgAddress::AddrStd(addr)) => Ok(addr), + TokenValue::Address(ton_block::MsgAddress::AddrNone) + | TokenValue::AddressStd(ton_block::MsgAddress::AddrNone) => Ok(MsgAddrStd::default()), _ => Err(UnpackerError::InvalidAbi), } } diff --git a/nekoton-abi/src/tvm.rs b/nekoton-abi/src/tvm.rs index 701662196..ff4692b18 100644 --- a/nekoton-abi/src/tvm.rs +++ b/nekoton-abi/src/tvm.rs @@ -4,7 +4,7 @@ use ton_block::{ AccountStuff, CommonMsgInfo, CurrencyCollection, Deserializable, Message, MsgAddressInt, OutAction, OutActions, Serializable, }; -use ton_types::SliceData; +use ton_types::{HashmapE, SliceData}; use ton_vm::executor::gas::gas_state::Gas; use ton_vm::stack::integer::IntegerData; use ton_vm::stack::{savelist::SaveList, Stack}; @@ -50,6 +50,7 @@ pub fn call( stack: Stack, config: &BriefBlockchainConfig, modifiers: &BehaviorModifiers, + libraries: &[HashmapE], ) -> Result<(ton_vm::executor::Engine, i32, bool), ExecutionError> { let state = match &account.storage.state { ton_block::AccountState::AccountActive { state_init, .. } => Ok(state_init), @@ -82,12 +83,17 @@ pub fn call( .put(7, &mut sci.into_temp_data_item()) .map_err(|_| ExecutionError::FailedToPutSciIntoRegisters)?; - let mut engine = ton_vm::executor::Engine::with_capabilities(config.capabilities).setup( - SliceData::load_cell(code).map_err(|_| ExecutionError::FailedToPutDataIntoRegisters)?, - Some(ctrls), - Some(stack), - Some(gas), - ); + let mut enabled_capabilities = config.capabilities; + enabled_capabilities |= ton_block::GlobalCapabilities::CapSetLibCode as u64; + + let mut engine = ton_vm::executor::Engine::with_capabilities(enabled_capabilities) + .setup_with_libraries( + code, + Some(ctrls), + Some(stack), + Some(gas), + libraries.to_vec(), + ); engine.set_signature_id(config.global_id); engine.modify_behavior(modifiers.clone()); @@ -118,6 +124,7 @@ pub fn call_msg( msg: &Message, config: &BriefBlockchainConfig, modifiers: &ton_vm::executor::BehaviorModifiers, + libraries: &[HashmapE], ) -> Result { let msg_cell = msg .write_to_new_cell() @@ -141,7 +148,12 @@ pub fn call_msg( .push(StackItem::Slice(msg.body().unwrap_or_default())) // message body .push(function_selector); // function selector - let (engine, exit_code, success) = call(utime, lt, account, stack, config, modifiers)?; + let (engine, exit_code, success) = + call(utime, lt, account, stack, config, modifiers, libraries)?; + + if let Some(hash) = engine.get_missing_library() { + return Err(ExecutionError::MissingLibrary { hash }); + } if !success { return Ok(ActionPhaseOutput { messages: None, @@ -180,6 +192,7 @@ pub fn call_getter( args: &[ton_vm::stack::StackItem], config: &BriefBlockchainConfig, modifiers: &ton_vm::executor::BehaviorModifiers, + libraries: &[HashmapE], ) -> Result { let mut stack = Stack::new(); for arg in args { @@ -187,7 +200,12 @@ pub fn call_getter( } stack.push(ton_vm::int!(method_id)); - let (mut engine, exit_code, is_ok) = call(utime, lt, account, stack, config, modifiers)?; + let (mut engine, exit_code, is_ok) = + call(utime, lt, account, stack, config, modifiers, libraries)?; + + if let Some(hash) = engine.get_missing_library() { + return Err(ExecutionError::MissingLibrary { hash }); + } Ok(VmGetterOutput { stack: engine.withdraw_stack().storage, @@ -247,4 +265,6 @@ pub enum ExecutionError { FailedToParseException, #[error("Failed to retrieve actions")] FailedToRetrieveActions, + #[error("Missing library: {hash}")] + MissingLibrary { hash: ton_types::UInt256 }, } diff --git a/nekoton-contracts/Cargo.toml b/nekoton-contracts/Cargo.toml index 35de2e683..72a572c0d 100644 --- a/nekoton-contracts/Cargo.toml +++ b/nekoton-contracts/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nekoton-contracts" -version = "0.13.0" +version = "0.14.0" authors = [ "Alexey Pashinov ", "Vladimir Petrzhikovskiy ", diff --git a/nekoton-contracts/src/lib.rs b/nekoton-contracts/src/lib.rs index babad727b..7508d2dc1 100644 --- a/nekoton-contracts/src/lib.rs +++ b/nekoton-contracts/src/lib.rs @@ -120,7 +120,8 @@ mod utils { $(header: [$($header:ident),+],)? name: $name:literal, inputs: $inputs:expr, - outputs: $outputs:expr$(,)? + outputs: $outputs:expr + $(, external_msg: $external_msg:expr)?$(,)? ) => { static ABI: once_cell::race::OnceBox = once_cell::race::OnceBox::new(); @@ -133,6 +134,7 @@ mod utils { outputs: $outputs, input_id: 0, output_id: 0, + external_msg: $crate::utils::declare_function!(@external_msg $($external_msg)?), }; let id = $crate::utils::declare_function!(@function_id function $($id)?); function.input_id = id & 0x7FFFFFFF; @@ -146,6 +148,7 @@ mod utils { (@abi_version v2_1) => { ::ton_abi::contract::ABI_VERSION_2_1 }; (@abi_version v2_2) => { ::ton_abi::contract::ABI_VERSION_2_2 }; (@abi_version v2_3) => { ::ton_abi::contract::ABI_VERSION_2_3 }; + (@abi_version v2_7) => { ::ton_abi::contract::ABI_VERSION_2_7 }; (@function_id $f:ident) => { $f.get_function_id() }; (@function_id $f:ident $id:literal) => { $id }; @@ -163,6 +166,9 @@ mod utils { (@header_item expire) => { ::ton_abi::Param::new("expire", ::ton_abi::ParamType::Expire) }; + + (@external_msg) => { None }; + (@external_msg $external_msg:expr) => { $external_msg }; } pub(crate) use declare_function; diff --git a/nekoton-contracts/src/old_tip3/token_wallet_contract.rs b/nekoton-contracts/src/old_tip3/token_wallet_contract.rs index 65f9e2554..0c394c4ca 100644 --- a/nekoton-contracts/src/old_tip3/token_wallet_contract.rs +++ b/nekoton-contracts/src/old_tip3/token_wallet_contract.rs @@ -169,6 +169,14 @@ pub fn burn_by_owner() -> &'static ton_abi::Function { } } +pub fn stub() -> &'static ton_abi::Function { + declare_function! { + name: "stub", + inputs: Vec::new(), + outputs: Vec::new(), + } +} + #[derive(Debug, Clone, UnpackAbiPlain, KnownParamTypePlain)] pub struct BurnByRootInputs { #[abi(with = "uint128_number")] diff --git a/nekoton-contracts/src/tip3_1/token_wallet_contract.rs b/nekoton-contracts/src/tip3_1/token_wallet_contract.rs index b5b83bdd8..1953fae3c 100644 --- a/nekoton-contracts/src/tip3_1/token_wallet_contract.rs +++ b/nekoton-contracts/src/tip3_1/token_wallet_contract.rs @@ -165,6 +165,25 @@ pub fn accept_mint() -> &'static ton_abi::Function { } } +#[derive(Debug, Clone, KnownParamTypePlain, UnpackAbiPlain)] +pub struct InternalTransferInputs { + #[abi(uint64)] + pub query_id: u64, + #[abi(grams)] + pub amount: ton_block::Grams, + #[abi(address)] + pub from: ton_block::MsgAddressInt, +} + +pub fn internal_transfer() -> &'static ton_abi::Function { + declare_function! { + function_id: 0x178d4519, + name: "internalTransfer", + inputs: InternalTransferInputs::param_type(), + outputs: Vec::new(), + } +} + pub mod burnable { use super::*; diff --git a/nekoton-derive/Cargo.toml b/nekoton-derive/Cargo.toml index 1cef9cfd1..3f3875961 100644 --- a/nekoton-derive/Cargo.toml +++ b/nekoton-derive/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nekoton-derive" -version = "0.13.0" +version = "0.14.0" authors = [ "Alexey Pashinov ", "Vladimir Petrzhikovskiy ", diff --git a/nekoton-derive/src/attr.rs b/nekoton-derive/src/attr.rs index 8dc432bea..dd54c6557 100644 --- a/nekoton-derive/src/attr.rs +++ b/nekoton-derive/src/attr.rs @@ -372,6 +372,7 @@ pub enum TypeName { Bool, Cell, Address, + AddressStd, String, Bytes, None, @@ -391,6 +392,7 @@ impl TypeName { "bool" => TypeName::Bool, "cell" => TypeName::Cell, "address" => TypeName::Address, + "address_std" => TypeName::AddressStd, "string" => TypeName::String, "bytes" => TypeName::Bytes, _ => TypeName::None, @@ -426,6 +428,9 @@ impl TypeName { TypeName::Address => quote! { ::ton_abi::ParamType::Address }, + TypeName::AddressStd => quote! { + ::ton_abi::ParamType::AddressStd + }, TypeName::Cell => quote! { ::ton_abi::ParamType::Cell }, diff --git a/nekoton-derive/src/pack_abi.rs b/nekoton-derive/src/pack_abi.rs index c813b0cad..c0eda5dee 100644 --- a/nekoton-derive/src/pack_abi.rs +++ b/nekoton-derive/src/pack_abi.rs @@ -247,6 +247,9 @@ impl TypeName { ::ton_block::MsgAddressInt::AddrVar(addr) => ::ton_block::MsgAddress::AddrVar(addr), }) }, + TypeName::AddressStd => quote! { + :ton_abi::TokenValue::AddressStd(::ton_block::MsgAddress::AddrStd(addr)) + }, TypeName::Cell => quote! { ::ton_abi::TokenValue::Cell(value) }, diff --git a/nekoton-derive/src/unpack_abi.rs b/nekoton-derive/src/unpack_abi.rs index 644222907..90d1189e7 100644 --- a/nekoton-derive/src/unpack_abi.rs +++ b/nekoton-derive/src/unpack_abi.rs @@ -332,12 +332,27 @@ fn get_handler(type_name: &TypeName) -> proc_macro2::TokenStream { } TypeName::Address => { quote! { - ::ton_abi::TokenValue::Address(::ton_block::MsgAddress::AddrStd(addr)) => { + ::ton_abi::TokenValue::Address(::ton_block::MsgAddress::AddrStd(addr)) + | ::ton_abi::TokenValue::AddressStd(::ton_block::MsgAddress::AddrStd(addr)) => { ::ton_block::MsgAddressInt::AddrStd(addr) }, ::ton_abi::TokenValue::Address(::ton_block::MsgAddress::AddrVar(addr)) => { ::ton_block::MsgAddressInt::AddrVar(addr) }, + ::ton_abi::TokenValue::Address(::ton_block::MsgAddress::AddrNone) + | ::ton_abi::TokenValue::AddressStd(::ton_block::MsgAddress::AddrNone) => { + ::ton_block::MsgAddressInt::AddrStd(Default::default()) + }, + } + } + TypeName::AddressStd => { + quote! { + ::ton_abi::TokenValue::Address(::ton_block::MsgAddress::AddrStd(addr)) => { + addr + }, + ::ton_abi::TokenValue::Address(::ton_block::MsgAddress::AddrNone) => { + ::ton_block::MsgAddrStd::default() + }, } } TypeName::Cell => { diff --git a/nekoton-jetton/Cargo.toml b/nekoton-jetton/Cargo.toml index 8bae8aa6f..09b34263e 100644 --- a/nekoton-jetton/Cargo.toml +++ b/nekoton-jetton/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nekoton-jetton" -version = "0.13.0" +version = "0.14.0" authors = [ "Alexey Pashinov ", "Vladimir Petrzhikovskiy ", diff --git a/nekoton-transport/Cargo.toml b/nekoton-transport/Cargo.toml index 3a57fae79..6c2c149df 100644 --- a/nekoton-transport/Cargo.toml +++ b/nekoton-transport/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nekoton-transport" -version = "0.13.0" +version = "0.14.0" authors = [ "Alexey Pashinov ", "Vladimir Petrzhikovskiy ", @@ -16,6 +16,7 @@ futures-util = "0.3" log = "0.4" reqwest = { version = "0.11", features = ["json", "gzip", "rustls-tls"], default-features = false } serde = { version = "1.0", features = ["derive"] } +serde_json = {version = "1.0", features = ["raw_value"]} thiserror = "1.0" tokio = { version = "1", features = ["sync", "time"] } @@ -26,11 +27,13 @@ nekoton = { path = ".." } [dev-dependencies] base64 = "0.13" tokio = { version = "1", features = ["sync", "time", "macros"] } +ton_abi = { git = "https://github.com/broxus/ton-labs-abi" } + ton_types = { git = "https://github.com/broxus/ton-labs-types.git" } [features] -default = ["gql_transport"] +default = ["jrpc_transport"] gql_transport = ["nekoton/gql_transport"] jrpc_transport = ["nekoton/jrpc_transport"] proto_transport = ["nekoton/proto_transport"] diff --git a/nekoton-transport/src/jrpc.rs b/nekoton-transport/src/jrpc.rs index ea3b853de..a2e6931f2 100644 --- a/nekoton-transport/src/jrpc.rs +++ b/nekoton-transport/src/jrpc.rs @@ -55,11 +55,21 @@ impl nekoton::external::JrpcConnection for JrpcClient { #[cfg(test)] mod tests { - use nekoton::external::{JrpcConnection, JrpcRequest}; + use nekoton::{ + abi::{ + num_bigint::BigUint, tvm::ExecutionError, BigUint256, BriefBlockchainConfig, + BuildTokenValue, FunctionExt, TokenValueExt, + }, + external::{JrpcConnection, JrpcRequest, JrpcResponse}, + }; + use nekoton_utils::{SimpleClock, TrustMe}; + use serde::Deserialize; + use ton_types::{BuilderData, HashmapE}; use super::*; #[tokio::test] + #[ignore] async fn jrpc_client_works() { let client = JrpcClient::new("https://jrpc.everwallet.net/rpc").unwrap(); @@ -81,4 +91,247 @@ mod tests { .unwrap(); println!("{}", response); } + + #[tokio::test] + #[ignore] + async fn jrpc_client_works2() { + let client = JrpcClient::new("https://rpc-testnet.tychoprotocol.com/").unwrap(); + + const QUERY: &str = r#"{ + "jsonrpc": "2.0", + "id": 1, + "method": "getContractState", + "params": { + "address": "0:84105d39805e023053cbf2b6a30e3c495b41678eec854b0fa56c9228abd5c975" + } + }"#; + + let response = client + .post(JrpcRequest { + data: QUERY.to_owned(), + requires_db: true, + }) + .await + .unwrap(); + println!("{}", response); + } + + #[tokio::test] + #[ignore] + async fn get_state_with_retries_for_libraries_when_contract_code_is_library() { + let client = JrpcClient::new("https://rpc-testnet.tychoprotocol.com/").unwrap(); + + let contract = r#####"{ + "ABI version": 2, + "version": "2.7", + "header": ["pubkey", "time", "expire"], + "functions": [ + { + "name": "balance", + "inputs": [ + {"name":"answerId","type":"uint32"} + ], + "outputs": [ + {"name":"value0","type":"uint128"} + ] + } + ], + "data": [], + "events": [] + }"#####; + + let contract_abi = ton_abi::Contract::load(contract.as_bytes()).trust_me(); + let function = contract_abi.function("balance").trust_me(); + + let bytes = base64::decode("te6ccgEBBAEA3wACboARPmKFmZb7UI1FgPY5MbJYJKPzmu4RUDN9k8WayPe7kGQRAqUGil+P0AAAT3BmG1o6AvrwgCYDAQGTAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAIurEomVjZY/EfuvEyNXOvBmybt5bgpnmtwgyymLnInvgCAGOACOx2tye/5lfR4Ih+BBGfLQA67cTHekeUzHx9YuGo73UAAAAAAAAAAAAAAAAAAAABUAhCApZ8lwbZVdfn7LyATToKdb89MzDWfAhUcDRsBzsioiQS").unwrap(); + let stuff = ton_types::deserialize_tree_of_cells(&mut bytes.as_slice()) + .and_then(nekoton_utils::deserialize_account_stuff) + .unwrap(); + + let mut brief_blockchain_config = BriefBlockchainConfig::default(); + brief_blockchain_config.capabilities |= 0x0000_0000_0800; + println!("{brief_blockchain_config:?}"); + + let mut libraries = vec![]; + + loop { + let res = function.run_local_ext( + &SimpleClock, + stuff.clone(), + &[0u32.token_value().named("answerId")], + false, + &brief_blockchain_config, + &libraries, + ); + + match res { + Err(err) => match err.downcast::() { + Ok(ExecutionError::MissingLibrary { hash }) => { + println!("Missing library: {hash}"); + + let query = serde_json::json!({ + "jsonrpc": "2.0","id": 1,"method": "getLibraryCell","params": {"hash": hash.to_hex_string()}} ); + + #[derive(Deserialize)] + struct LibraryCellResponse { + cell: Option, + } + + let response = client + .post(JrpcRequest { + data: query.to_string(), + requires_db: true, + }) + .await + .unwrap(); + + let LibraryCellResponse { cell } = + match serde_json::from_str(&response).unwrap() { + JrpcResponse::Success(response) => response, + JrpcResponse::Err(err) => panic!("Error: {}", err), + }; + + if let Some(cell_s) = cell { + let bytes = base64::decode(cell_s).unwrap(); + let c = ton_types::deserialize_tree_of_cells(&mut bytes.as_slice()) + .unwrap(); + + let mut lib = HashmapE::with_bit_len(256); + let mut item = BuilderData::new(); + item.checked_append_reference(c).unwrap(); + lib.set_builder(hash.into(), &item).unwrap(); + libraries.push(lib); + } + } + Ok(err) => { + println!("ok {err:?}"); + break; + } + Err(err) => { + println!("err {err:?}"); + break; + } + }, + Ok(res) => { + println!("ok {res:?}"); + break; + } + } + } + } + + #[tokio::test] + #[ignore] + async fn get_state_with_retries_for_libraries_when_library_call_is_inside_contract() { + let client = JrpcClient::new("https://rpc-testnet.tychoprotocol.com/").unwrap(); + + let contract = r#####"{ + "ABI version": 2, + "version": "2.7", + "header": ["time", "expire"], + "functions": [ + { + "name": "testAddGetter", + "inputs": [ + {"name":"a","type":"uint256"}, + {"name":"b","type":"uint256"} + ], + "outputs": [ + {"name":"value0","type":"uint256"} + ] + } + ], + "data": [], + "events": [] + }"#####; + + let contract_abi = ton_abi::Contract::load(contract.as_bytes()).trust_me(); + let function = contract_abi.function("testAddGetter").trust_me(); + + let bytes = base64::decode("te6ccgECGAEAAwwAAm6AEIILpzALwEYKeX5W1GHHiStoLPHdkKlh9K2SRRV6uS6kYQo4horspSAAAFC2X/KIKlloLwAmAwEBkb1NrLsMcFKYoHq5uXkbVJzUkOEiypI/caoqHm/AFDeIAAABmOrGPYaAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACMACCEICTunDKDBYzp1WMjhzkahaIKI373nqkQCMY/dQTQhEVEoBEv8A9KQT9LzyCwQCASASBQOe8n+J+Gkh2zzTAAGOFIMI1xgg+CjIzs7J+QBY+EL5EPKo3tM/AfhDIbnytCD4I4ED6KiCCBt3QKC58rT4Y9Mf+CNYufK50x8B9KQg9KHyPBEWBgIBSAwHAgEgCggCb7qtnHw/hG8uBM0//U0dDT/9HbPCGOHCPQ0wH6QDAxyM+HIM6CEOrZx8PPC4HL/8lw+wCRMOLbPICQ8AGPhK0O0eWYEdUFUC2AJvu0pKWt+Eby4EzT/9TR0NP/0ds8IY4cI9DTAfpAMDHIz4cgzoIQ1KSlrc8Lgcv/yXD7AJEw4ts8gLFAEkcPgA+ErQ7R5aiYEyhlUD2PhrEQIBIA4NAj+7yR4cX4Qm7jAPhG8nPT/9H4AMjPhArL/3/PI/hq2zyBYUAnu6WhFNz4RvLgTNP/1NHQ0//R2zwijiIk0NMB+kAwMcjPhyDOcc8LYQLIz5IWhFNyy//L/83JcPsAkVvi2zyBAPACjtRNDT/9M/MfhDWMjL/8s/zsntVAAc+AD4StDtHlmBP/5VAtgAQ4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABADZNJw7UTQgQFA1yHXCgD4ZiLQ0wP6QDD4aak4ANwhxwDjAiHXDR/yvCHjAwH0pCD0ofI8FxcTAyqgTOUUevhG8uBM2zzT/9P/0ds82zwWFRQAKvhL+Er4Q/hCyMv/yz/Pg8zL/8ntVAAyghCy0F4AcvsC+ErQ7R5Z+EmBMoZVA9j4awAu7UTQ0//TP9MA1NP/0fhr+Gr4Zvhj+GIACvhG8uBM").unwrap(); + + let stuff = ton_types::deserialize_tree_of_cells(&mut bytes.as_slice()) + .and_then(nekoton_utils::deserialize_account_stuff) + .unwrap(); + + let mut brief_blockchain_config = BriefBlockchainConfig::default(); + brief_blockchain_config.capabilities |= 0x0000_0000_0800; + println!("{brief_blockchain_config:?}"); + + let mut libraries = vec![]; + + loop { + let res = function.run_local_ext( + &SimpleClock, + stuff.clone(), + &[ + BigUint256(BigUint::from(1u32)) + .token_value() + .token_value() + .named("a"), + BigUint256(BigUint::from(1u32)) + .token_value() + .token_value() + .named("b"), + ], + false, + &brief_blockchain_config, + &libraries, + ); + + match res { + Err(err) => match err.downcast::() { + Ok(ExecutionError::MissingLibrary { hash }) => { + println!("Missing library: {hash}"); + + let query = serde_json::json!({ + "jsonrpc": "2.0","id": 1,"method": "getLibraryCell","params": {"hash": hash.to_hex_string()}} ); + + #[derive(Deserialize)] + struct LibraryCellResponse { + cell: Option, + } + + let response = client + .post(JrpcRequest { + data: query.to_string(), + requires_db: true, + }) + .await + .unwrap(); + + let LibraryCellResponse { cell } = + match serde_json::from_str(&response).unwrap() { + JrpcResponse::Success(response) => response, + JrpcResponse::Err(err) => panic!("Error: {}", err), + }; + + if let Some(cell_s) = cell { + let bytes = base64::decode(cell_s).unwrap(); + let c = ton_types::deserialize_tree_of_cells(&mut bytes.as_slice()) + .unwrap(); + + let mut lib = HashmapE::with_bit_len(256); + let mut item = BuilderData::new(); + item.checked_append_reference(c).unwrap(); + lib.set_builder(hash.into(), &item).unwrap(); + libraries.push(lib); + } + } + Ok(err) => { + println!("ok {err:?}"); + break; + } + Err(err) => { + println!("err {err:?}"); + break; + } + }, + Ok(res) => { + println!("ok {res:?}"); + break; + } + } + } + } } diff --git a/nekoton-utils/Cargo.toml b/nekoton-utils/Cargo.toml index 0e3954660..b81cc1ac5 100644 --- a/nekoton-utils/Cargo.toml +++ b/nekoton-utils/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nekoton-utils" -version = "0.13.0" +version = "0.14.0" authors = [ "Alexey Pashinov ", "Vladimir Petrzhikovskiy ", diff --git a/nekoton-utils/src/lib.rs b/nekoton-utils/src/lib.rs index fda3d4c44..36b29a429 100644 --- a/nekoton-utils/src/lib.rs +++ b/nekoton-utils/src/lib.rs @@ -59,6 +59,7 @@ pub use self::crc::crc_16; #[cfg(feature = "encryption")] pub use self::encryption::*; pub use self::serde_helpers::*; +pub use self::signature_domain::*; pub use self::traits::*; pub use self::transaction::*; @@ -69,6 +70,7 @@ mod crc; #[cfg(feature = "encryption")] mod encryption; mod serde_helpers; +mod signature_domain; mod traits; mod transaction; diff --git a/nekoton-utils/src/signature_domain.rs b/nekoton-utils/src/signature_domain.rs new file mode 100644 index 000000000..e2efe9e50 --- /dev/null +++ b/nekoton-utils/src/signature_domain.rs @@ -0,0 +1,128 @@ +use ed25519_dalek::Signer; +use sha2::{Digest, Sha256}; +use std::borrow::Cow; + +#[derive(Debug, Clone)] +pub struct ToSign { + pub ctx: SignatureContext, + pub data: Vec, +} + +impl ToSign { + pub fn write_to_bytes(&self) -> Vec { + let mut output = Vec::new(); + + match (self.ctx.signature_type, self.ctx.global_id) { + (SignatureType::SignatureDomain, Some(global_id)) => { + let sd = SignatureDomain::L2 { global_id }; + output.extend_from_slice(&sd.hash()) + } + (SignatureType::SignatureId, Some(global_id)) => { + output.extend_from_slice(&global_id.to_be_bytes()) + } + _ => {} + } + + output.extend_from_slice(&self.data); + + output + } +} + +#[derive(Debug, Default, Clone, Copy, serde::Serialize, serde::Deserialize)] +pub struct SignatureContext { + pub global_id: Option, + pub signature_type: SignatureType, +} + +#[derive(Debug, Default, Clone, Copy, serde::Serialize, serde::Deserialize)] +pub enum SignatureType { + Empty, + SignatureId, + #[default] + SignatureDomain, +} + +impl SignatureContext { + pub fn sign<'a>( + &self, + key: &ed25519_dalek::Keypair, + data: &'a [u8], + ) -> ed25519_dalek::Signature { + let data = match self.signature_type { + SignatureType::Empty => Cow::Borrowed(data), + SignatureType::SignatureId => extend_with_signature_id(data, self.global_id), + SignatureType::SignatureDomain => extend_with_signature_domain(data, self.global_id), + }; + + key.sign(&data) + } +} + +/// Signature domain variants. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum SignatureDomain { + /// Special variant to NOT add any prefix for the verified data. + /// Can be used to verify mainnet signatures from L2 networks. + Empty, + /// Non-empty variant. Hash of its TL representation + /// is used as a prefix for the verified data. + L2 { + /// Global id of the network. + global_id: i32, + }, +} + +impl SignatureDomain { + /// Prepares arbitrary data for signing. + fn apply<'a>(&self, data: &'a [u8]) -> Cow<'a, [u8]> { + if let Self::Empty = self { + Cow::Borrowed(data) + } else { + let hash = self.hash(); + let mut result = Vec::with_capacity(32 + data.len()); + result.extend_from_slice(&hash); + result.extend_from_slice(data); + Cow::Owned(result) + } + } + + fn hash(&self) -> Vec { + Sha256::digest(self.write_to_bytes()).to_vec() + } + + fn write_to_bytes(&self) -> Vec { + let mut data = Vec::new(); + match self { + Self::Empty => { + data.extend_from_slice(&0xe1d571bu32.to_le_bytes()); //Empty variant tl tag + } + Self::L2 { global_id } => { + data.extend_from_slice(&0x71b34ee1u32.to_le_bytes()); // L2 variant tl tag + data.extend_from_slice(&global_id.to_le_bytes()); + } + } + + data + } +} + +fn extend_with_signature_id(data: &[u8], global_id: Option) -> Cow<'_, [u8]> { + match global_id { + Some(signature_id) => { + let mut extended_data = Vec::with_capacity(4 + data.len()); + extended_data.extend_from_slice(&signature_id.to_be_bytes()); + extended_data.extend_from_slice(data); + Cow::Owned(extended_data) + } + None => Cow::Borrowed(data), + } +} + +fn extend_with_signature_domain(data: &[u8], global_id: Option) -> Cow<'_, [u8]> { + let sd = match global_id { + None => SignatureDomain::Empty, + Some(global_id) => SignatureDomain::L2 { global_id }, + }; + sd.apply(data) +} diff --git a/src/core/jetton_wallet/mod.rs b/src/core/jetton_wallet/mod.rs index bd433e3da..9ee8d3fb4 100644 --- a/src/core/jetton_wallet/mod.rs +++ b/src/core/jetton_wallet/mod.rs @@ -18,6 +18,7 @@ use super::{utils, ContractSubscription, InternalMessage}; pub const JETTON_TRANSFER_OPCODE: u32 = 0x0f8a7ea5; pub const JETTON_INTERNAL_TRANSFER_OPCODE: u32 = 0x178d4519; +pub const JETTON_BURN_NOTIFICATION_OPCODE: u32 = 0x7bdd97de; pub struct JettonWallet { clock: Arc, @@ -246,6 +247,9 @@ impl JettonWallet { JettonWalletTransaction::InternalTransfer(transfer) => { balance += transfer.tokens.clone().to_bigint().trust_me(); } + JettonWalletTransaction::BurnNotification(transfer) => { + balance -= transfer.tokens.clone().to_bigint().trust_me(); + } } } @@ -480,7 +484,9 @@ mod tests { use nekoton_abi::ExecutionContext; use nekoton_contracts::jetton; use nekoton_utils::SimpleClock; - use ton_block::MsgAddressInt; + use ton_block::{Deserializable, MsgAddressInt}; + + use crate::core::parsing::parse_jetton_transaction; #[test] fn usdt_root_token_contract() -> anyhow::Result<()> { @@ -490,6 +496,7 @@ mod tests { let contract = jetton::RootTokenContract(ExecutionContext { clock: &SimpleClock, account_stuff: &state, + libraries: Default::default(), }); let total_supply = contract.total_supply()?; @@ -506,6 +513,7 @@ mod tests { let contract = jetton::RootTokenContract(ExecutionContext { clock: &SimpleClock, account_stuff: &state, + libraries: Default::default(), }); let name = contract.name()?; @@ -541,6 +549,7 @@ mod tests { let contract = jetton::TokenWalletContract(ExecutionContext { clock: &SimpleClock, account_stuff: &state, + libraries: Default::default(), }); let balance = contract.balance()?; @@ -565,6 +574,7 @@ mod tests { let contract = jetton::RootTokenContract(ExecutionContext { clock: &SimpleClock, account_stuff: &state, + libraries: Default::default(), }); let owner = nekoton_utils::unpack_std_smc_addr( @@ -593,6 +603,7 @@ mod tests { let contract = jetton::RootTokenContract(ExecutionContext { clock: &SimpleClock, account_stuff: &state, + libraries: Default::default(), }); let details = contract.get_details()?; @@ -626,6 +637,7 @@ mod tests { let contract = jetton::RootTokenContract(ExecutionContext { clock: &SimpleClock, account_stuff: &state, + libraries: Default::default(), }); let meta = contract.get_meta()?; @@ -636,4 +648,29 @@ mod tests { Ok(()) } + + #[test] + fn burn_notification_parsing() -> anyhow::Result<()> { + let cell = + ton_types::deserialize_tree_of_cells(&mut base64::decode("te6ccgECDgEAAtwAA7dw7EIKKDHhLyj/OVq4ANbffwZhE/zz7DnHy2xys54unOAAAR/PW5agVBdSn5hltw0JCSXdIfbG4agOFEZxHRdR8cMPUm14lzxAAAEeKGv9VDaSmUEwADSAIARhSAUEAQIbBMDY/ckOt5mLmH4XnREDAgBvyZIDVEwwCKwAAAAAAAQAAgAAAAK2Mbb60iBkujrVIW/+w2U+3k6Hq33EPQm1sA+bZIajxkFQNBQAnke0LDxIFAAAAAAAAAABJQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgnILJdp4X/Nif6YeRtU8WdWML176viuNHv8O2IzB0b1aN128pSHivvisB9zvx5HE4Vi8uebieCZDb7ilflucD0HBAgHgCgYBAd8HAbFIAB2IQUUGPCXlH+crVwAa2+/gzCJ/nn2HOPltjlZzxdOdAA4fK7NhiSx7wlc1oWV8DcPxu2CDM1VkTpMfHNrgA4rPkOcmingGMAj6AAAj+ety1AzSUygmwAgBa2gIjZsAAAAAAAAAAAAAAAAAAAPogAcPldmwxJY94Sua0LK+BuH43bBBmaqyJ0mPjm1wAcVn0AkBQ4Af5uyTNobDCA/FRmI9gESPFQJjZfNdNVN3T0Egw1N7kvAMAbFoAf5uyTNobDCA/FRmI9gESPFQJjZfNdNVN3T0Egw1N7kvAAOxCCigx4S8o/zlauADW338GYRP88+w5x8tscrOeLpzkOt5mLgGLSAsAAAj+ety1AjSUygmwAsBo3vdl94AAAAAAAAAACA+iABw+V2bDElj3hK5rQsr4G4fjdsEGZqrInSY+ObXABxWfQAOHyuzYYkse8JXNaFlfA3D8btggzNVZE6THxza4AOKz7AMAUOABw+V2bDElj3hK5rQsr4G4fjdsEGZqrInSY+ObXABxWfQDQAA").unwrap().as_slice()) + .unwrap(); + + let transaction = ton_block::Transaction::construct_from_cell(cell.clone()).unwrap(); + + let description = match transaction.description.read_struct().ok().unwrap() { + ton_block::TransactionDescr::Ordinary(description) => description, + _ => panic!("Unexpected transaction description"), + }; + + let data = parse_jetton_transaction(&transaction, &description).unwrap(); + + match data { + crate::models::JettonWalletTransaction::Transfer(_) => panic!("wrong data"), + crate::models::JettonWalletTransaction::InternalTransfer(_) => panic!("wrong data"), + crate::models::JettonWalletTransaction::BurnNotification(jetton_burn_notification) => { + println!("jetton_burn_notification: {:#?}", jetton_burn_notification); + Ok(()) + } + } + } } diff --git a/src/core/keystore/mod.rs b/src/core/keystore/mod.rs index be7c1562c..f92d6c339 100644 --- a/src/core/keystore/mod.rs +++ b/src/core/keystore/mod.rs @@ -14,9 +14,10 @@ use tokio::sync::RwLock; use nekoton_utils::*; use crate::crypto::{ - EncryptedData, EncryptionAlgorithm, PasswordCache, SharedSecret, Signature, SignatureId, - Signer, SignerContext, SignerEntry, SignerStorage, + EncryptedData, EncryptionAlgorithm, PasswordCache, SharedSecret, Signature, Signer, + SignerContext, SignerEntry, SignerStorage, }; + use crate::external::Storage; pub const KEYSTORE_STORAGE_KEY: &str = "__core__keystore"; @@ -290,7 +291,7 @@ impl KeyStore { pub async fn sign( &self, data: &[u8], - signature_id: Option, + signature_ctx: SignatureContext, input: T::SignInput, ) -> Result where @@ -303,7 +304,7 @@ impl KeyStore { }; state .get_signer_ref::()? - .sign(ctx, data, signature_id, input) + .sign(ctx, data, signature_ctx, input) .await } diff --git a/src/core/parsing.rs b/src/core/parsing.rs index 3238cc3ed..9ae9de60e 100644 --- a/src/core/parsing.rs +++ b/src/core/parsing.rs @@ -10,7 +10,9 @@ 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::jetton_wallet::{ + JETTON_BURN_NOTIFICATION_OPCODE, JETTON_INTERNAL_TRANSFER_OPCODE, JETTON_TRANSFER_OPCODE, +}; use crate::core::models::*; use crate::core::ton_wallet::{MultisigType, WalletType}; @@ -671,6 +673,20 @@ pub fn parse_token_transaction( TokenSwapBack::try_from((InputMessage(inputs), version)) .map(TokenWalletTransaction::SwapBack) .ok() + } else if function_id == functions.internal_transfer.input_id { + let inputs = functions + .internal_transfer + .decode_input(body, true, true) + .ok()?; + + JettonIncomingTransfer::try_from(InputMessage(inputs)) + .map(|x| { + TokenWalletTransaction::IncomingTransfer(TokenIncomingTransfer { + tokens: x.tokens, + sender_address: x.from, + }) + }) + .ok() } else { None } @@ -680,7 +696,8 @@ pub fn parse_jetton_transaction( tx: &ton_block::Transaction, description: &ton_block::TransactionDescrOrdinary, ) -> Option { - const STANDART_JETTON_CELLS: usize = 0; + const STANDARD_JETTON_CELLS: usize = 0; + const STANDARD_JETTON_BURN_CELLS: usize = 1; const MINTLESS_JETTON_CELLS: usize = 2; if description.aborted { @@ -699,14 +716,17 @@ pub fn parse_jetton_transaction( let cell = body.reference_opt(1)?; SliceData::load_cell(cell).ok()? } - STANDART_JETTON_CELLS => body, + STANDARD_JETTON_CELLS | STANDARD_JETTON_BURN_CELLS => body, _ => return None, } }; let opcode = body.get_next_u32().ok()?; - if opcode != JETTON_TRANSFER_OPCODE && opcode != JETTON_INTERNAL_TRANSFER_OPCODE { + if opcode != JETTON_TRANSFER_OPCODE + && opcode != JETTON_INTERNAL_TRANSFER_OPCODE + && opcode != JETTON_BURN_NOTIFICATION_OPCODE + { return None; } @@ -732,6 +752,12 @@ pub fn parse_jetton_transaction( tokens: amount, }, )), + JETTON_BURN_NOTIFICATION_OPCODE => Some(JettonWalletTransaction::BurnNotification( + JettonBurnNotification { + from: addr, + tokens: amount, + }, + )), _ => None, } } @@ -809,6 +835,8 @@ struct TokenWalletFunctions { // Incoming accept_transfer: &'static ton_abi::Function, // Incoming + internal_transfer: &'static ton_abi::Function, + // Incoming burn: &'static ton_abi::Function, // Outgoing accept_burn: &'static ton_abi::Function, @@ -825,6 +853,7 @@ impl TokenWalletFunctions { transfer: old_tip3::token_wallet_contract::transfer_to_recipient(), transfer_to_wallet: old_tip3::token_wallet_contract::transfer(), accept_transfer: old_tip3::token_wallet_contract::internal_transfer(), + internal_transfer: old_tip3::token_wallet_contract::stub(), burn: old_tip3::token_wallet_contract::burn_by_owner(), accept_burn: old_tip3::root_token_contract::tokens_burned(), }) @@ -838,6 +867,7 @@ impl TokenWalletFunctions { transfer: tip3_1::token_wallet_contract::transfer(), transfer_to_wallet: tip3_1::token_wallet_contract::transfer_to_wallet(), accept_transfer: tip3_1::token_wallet_contract::accept_transfer(), + internal_transfer: tip3_1::token_wallet_contract::internal_transfer(), burn: tip3_1::token_wallet_contract::burnable::burn(), accept_burn: tip3_1::root_token_contract::accept_burn(), }) @@ -875,6 +905,19 @@ impl TryFrom<(InputMessage, TokenWalletVersion)> for TokenSwapBack { } } +impl TryFrom for JettonIncomingTransfer { + type Error = UnpackerError; + + fn try_from(value: InputMessage) -> Result { + let input: tip3_1::token_wallet_contract::InternalTransferInputs = value.0.unpack()?; + + Ok(Self { + tokens: BigUint::from(input.amount.as_u128()), + from: input.from, + }) + } +} + struct Accept { tokens: BigUint, } @@ -1254,4 +1297,26 @@ mod tests { ); } } + + #[test] + fn test_tip3_jetton_incoming_transfer() { + let (tx, description) = parse_transaction("te6ccgECDAEAAlgAA7V3f+qdGMi8JjYzPKoZ9Z+bpmydkT1wisdXZ77gk1AFAhAAATx7yfqggC6iE75ppDvdzLmbyOcBCnxe+PDJ76cRgKt2Cy2ZIX6gAAE8e8n6oGaTw/dQADR5NE8IBQQBAhUECQBgwdZYeJGNEQMCAG/Jh6EgTBRYQAAAAAAABAACAAAAA8B4u2hCcp/bPN/dOsHOMIcjI4f1rR546mtVo5GE4BN0QFAVzACcRkopjFAAAAAAAAAAAPgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIJyH/HuNcxrNisZ3/ASNUm6oHt2S6OudgNcpDuLoGVKnHwuA9ejj3pxHzny2lpNan1PV91FIMUuTxfTeFZjxPvbkQIB4AgGAQHfBwCvSADv/VOjGReExsZnlUM+s/N0zZOyJ64RWOrs99wSagCgQwAOHyuzYYkse8JXNaFlfA3D8btggzNVZE6THxza4AOKz4684sQGFFhgAAAnj3k/VBLSeH7qQAGxaAHCojMY4mNubPmLxrR8IqPe3s4jRkwozPitECYHybqYEwAd/6p0YyLwmNjM8qhn1n5umbJ2RPXCKx1dnvuCTUAUCFAGDB1kBige6gAAJ495P1QK0nh+6sAJAacXjUUZAAAAAAAAAABDuaygCABw+V2bDElj3hK5rQsr4G4fjdsEGZqrInSY+ObXABxWfQAOHyuzYYkse8JXNaFlfA3D8btggzNVZE6THxza4AOKz4MKAQFACwAA"); + let tip3_transaction = + parse_token_transaction(&tx, &description, TokenWalletVersion::Tip3).unwrap(); + + if let TokenWalletTransaction::IncomingTransfer(TokenIncomingTransfer { + tokens, + sender_address, + }) = tip3_transaction + { + assert_eq!(tokens.to_u128().unwrap(), 1000000000); + assert_eq!( + sender_address, + MsgAddressInt::from_str( + "0:387caecd8624b1ef095cd68595f0370fc6ed820ccd55913a4c7c736b800e2b3e" + ) + .unwrap() + ); + } + } } diff --git a/src/core/token_wallet/mod.rs b/src/core/token_wallet/mod.rs index 710adbb7c..7910948e3 100644 --- a/src/core/token_wallet/mod.rs +++ b/src/core/token_wallet/mod.rs @@ -142,7 +142,7 @@ impl TokenWallet { // Prepare internal message let internal_message = self - .prepare_transfer(destination, tokens, notify_receiver, payload, 0) + .prepare_transfer(destination, tokens, notify_receiver, payload, 0, None) .await?; let mut message = ton_block::Message::with_int_header(ton_block::InternalMessageHeader { @@ -227,6 +227,7 @@ impl TokenWallet { notify_receiver: bool, payload: ton_types::Cell, mut attached_amount: u128, + remaining_gas_to: Option, ) -> Result { fn stub_balance(address: &MsgAddressInt) -> u128 { if address.is_masterchain() { @@ -271,7 +272,7 @@ impl TokenWallet { } } .arg(BigUint128(Default::default())) // grams / transfer_grams - .arg(&self.owner) // send_gas_to + .arg(remaining_gas_to.as_ref().unwrap_or(&self.owner)) // send_gas_to .arg(notify_receiver) // notify_receiver .arg(payload) // payload .build() @@ -291,7 +292,7 @@ impl TokenWallet { .arg(BigUint128(initial_balance.into())) // deployWalletValue } } - .arg(&self.owner) // remainingGasTo + .arg(remaining_gas_to.as_ref().unwrap_or(&self.owner)) // remainingGasTo .arg(notify_receiver) // notify .arg(payload) // payload .build() diff --git a/src/core/ton_wallet/mod.rs b/src/core/ton_wallet/mod.rs index c27cce458..c3450fe4d 100644 --- a/src/core/ton_wallet/mod.rs +++ b/src/core/ton_wallet/mod.rs @@ -8,7 +8,7 @@ use anyhow::Result; use ed25519_dalek::PublicKey; use serde::{Deserialize, Serialize}; use ton_block::MsgAddressInt; -use ton_types::{SliceData, UInt256}; +use ton_types::{BuilderData, SliceData, UInt256}; use nekoton_abi::*; use nekoton_utils::*; @@ -23,6 +23,7 @@ use super::{ContractSubscription, PollingMethod}; use crate::core::parsing::*; use crate::core::InternalMessage; use crate::crypto::UnsignedMessage; +use crate::models::ExpireAt; use crate::transport::models::{ExistingContract, RawContractState, RawTransaction}; use crate::transport::Transport; @@ -331,6 +332,56 @@ impl TonWallet { } } + pub fn make_unsigned_wallet_v5_transfer_payload( + &self, + current_state: &ton_block::AccountStuff, + public_key: &PublicKey, + gifts: Vec, + expiration: Expiration, + is_internal_flow: bool, + ) -> Result<(UInt256, BuilderData)> { + if !matches!(self.wallet_type, WalletType::WalletV5R1) { + return Err(TonWalletError::InvalidContractType.into()); + } + let (init_data, _) = wallet_v5r1::get_init_data(current_state.storage.state(), public_key)?; + + let expire_at = ExpireAt::new(self.clock.as_ref(), expiration); + let (hash, builder_data) = + init_data.make_transfer_payload(gifts, expire_at.timestamp, is_internal_flow)?; + Ok((hash, builder_data)) + } + + pub fn make_unsigned_new_wallet_v5_transfer_payload( + &self, + state_init: &ton_block::StateInit, + gifts: Vec, + expiration: Expiration, + is_internal_flow: bool, + ) -> Result<(UInt256, BuilderData)> { + if !matches!(self.wallet_type, WalletType::WalletV5R1) { + return Err(TonWalletError::InvalidContractType.into()); + } + let init_data = wallet_v5r1::get_init_data_from_state_init(state_init)?; + + let expire_at = ExpireAt::new(self.clock.as_ref(), expiration); + let (hash, builder_data) = + init_data.make_transfer_payload(gifts, expire_at.timestamp, is_internal_flow)?; + Ok((hash, builder_data)) + } + + pub fn get_wallet_v5_seqno( + &self, + current_state: &ton_block::AccountStuff, + public_key: &PublicKey, + ) -> Result { + if !matches!(self.wallet_type, WalletType::WalletV5R1) { + return Err(TonWalletError::InvalidContractType.into()); + } + + let (init_data, _) = wallet_v5r1::get_init_data(current_state.storage.state(), public_key)?; + Ok(init_data.seqno) + } + pub fn prepare_transfer( &mut self, current_state: &ton_block::AccountStuff, @@ -1070,6 +1121,7 @@ impl FromStr for WalletType { } } +// Mapping to Ledger wallet type IDs impl TryInto for WalletType { type Error = anyhow::Error; @@ -1084,9 +1136,11 @@ 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, + WalletType::WalletV5R1 => 9, + WalletType::WalletV4R1 => 10, + WalletType::WalletV4R2 => 11, + WalletType::WalletV3R1 => 12, + WalletType::WalletV3R2 => 13, _ => anyhow::bail!("Unimplemented wallet type"), }; diff --git a/src/core/ton_wallet/multisig.rs b/src/core/ton_wallet/multisig.rs index b2a84d4ed..5704d9c41 100644 --- a/src/core/ton_wallet/multisig.rs +++ b/src/core/ton_wallet/multisig.rs @@ -430,7 +430,7 @@ fn run_local( let ExecutionOutput { tokens, result_code, - } = function.run_local(clock, account_stuff, &[])?; + } = function.run_local(clock, account_stuff, &[], &[])?; tokens.ok_or_else(|| MultisigError::NonZeroResultCode(result_code).into()) } diff --git a/src/core/ton_wallet/wallet_v3v4.rs b/src/core/ton_wallet/wallet_v3v4.rs index bae849cf2..e2debc6c6 100644 --- a/src/core/ton_wallet/wallet_v3v4.rs +++ b/src/core/ton_wallet/wallet_v3v4.rs @@ -156,7 +156,7 @@ impl UnsignedMessage for UnsignedWallet { prune_after_depth: u16, ) -> Result { let mut payload = self.payload.clone(); - payload.append_raw(signature, signature.len() * 8)?; + payload.prepend_raw(signature, signature.len() * 8)?; let body = payload.into_cell()?; let mut message = self.message.clone(); diff --git a/src/core/ton_wallet/wallet_v5r1.rs b/src/core/ton_wallet/wallet_v5r1.rs index cc30bbe24..170a671d7 100644 --- a/src/core/ton_wallet/wallet_v5r1.rs +++ b/src/core/ton_wallet/wallet_v5r1.rs @@ -12,6 +12,7 @@ use crate::core::models::{Expiration, ExpireAt}; use crate::crypto::{SignedMessage, UnsignedMessage}; const SIGNED_EXTERNAL_PREFIX: u32 = 0x7369676E; +const SIGNED_INTERNAL_PREFIX: u32 = 0x73696E74; pub fn prepare_deploy( clock: &dyn Clock, @@ -19,9 +20,7 @@ pub fn prepare_deploy( workchain: i8, expiration: Expiration, ) -> Result> { - let init_data = InitData::from_key(public_key) - .with_wallet_id(WALLET_ID) - .with_is_signature_allowed(true); + let init_data = make_init_data(public_key); let dst = compute_contract_address(public_key, workchain); let mut message = ton_block::Message::with_ext_in_header(ton_block::ExternalInboundMessageHeader { @@ -32,7 +31,7 @@ pub fn prepare_deploy( 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)?; + let (hash, payload) = init_data.make_transfer_payload(None, expire_at.timestamp, false)?; Ok(Box::new(UnsignedWalletV5 { init_data, @@ -45,12 +44,37 @@ pub fn prepare_deploy( } pub fn prepare_state_init(public_key: &PublicKey) -> Result { - let init_data = InitData::from_key(public_key) - .with_wallet_id(WALLET_ID) - .with_is_signature_allowed(true); + let init_data = make_init_data(public_key); init_data.make_state_init() } +pub fn make_init_data(public_key: &PublicKey) -> InitData { + InitData::from_key(public_key) + .with_wallet_id(WALLET_ID) + .with_is_signature_allowed(true) +} + +pub fn get_init_data( + current_state: &ton_block::AccountState, + public_key: &PublicKey, +) -> Result<(InitData, bool)> { + match current_state { + ton_block::AccountState::AccountActive { state_init, .. } => match &state_init.data { + Some(data) => Ok((InitData::try_from(data)?, false)), + None => Err(WalletV5Error::InvalidInitData.into()), + }, + ton_block::AccountState::AccountFrozen { .. } => Err(WalletV5Error::AccountIsFrozen.into()), + ton_block::AccountState::AccountUninit => Ok((make_init_data(public_key), true)), + } +} + +pub fn get_init_data_from_state_init(init: &ton_block::StateInit) -> Result { + match &init.data { + Some(data) => Ok(InitData::try_from(data)?), + None => Err(WalletV5Error::InvalidInitData.into()), + } +} + pub fn prepare_transfer( clock: &dyn Clock, public_key: &PublicKey, @@ -62,22 +86,8 @@ pub fn prepare_transfer( 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, - ), - }; + let (mut init_data, with_state_init) = + get_init_data(current_state.storage.state(), public_key)?; init_data.seqno += seqno_offset; @@ -92,7 +102,8 @@ pub fn prepare_transfer( } let expire_at = ExpireAt::new(clock, expiration); - let (hash, payload) = init_data.make_transfer_payload(gifts.clone(), expire_at.timestamp)?; + let (hash, payload) = + init_data.make_transfer_payload(gifts.clone(), expire_at.timestamp, false)?; Ok(TransferAction::Sign(Box::new(UnsignedWalletV5 { init_data, @@ -122,7 +133,7 @@ impl UnsignedMessage for UnsignedWalletV5 { let (hash, payload) = self .init_data - .make_transfer_payload(self.gifts.clone(), self.expire_at()) + .make_transfer_payload(self.gifts.clone(), self.expire_at(), false) .trust_me(); self.hash = hash; self.payload = payload; @@ -178,9 +189,7 @@ pub fn is_wallet_v5r1(code_hash: &UInt256) -> bool { } 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) + make_init_data(public_key) .compute_addr(workchain_id) .trust_me() } @@ -273,6 +282,7 @@ impl InitData { &self, gifts: impl IntoIterator, expire_at: u32, + is_internal_flow: bool, ) -> Result<(UInt256, BuilderData)> { // Check if signatures are allowed if !self.is_signature_allowed { @@ -286,8 +296,13 @@ impl InitData { let mut payload = BuilderData::new(); // insert prefix + if is_internal_flow { + payload.append_u32(SIGNED_INTERNAL_PREFIX)?; + } else { + payload.append_u32(SIGNED_EXTERNAL_PREFIX)?; + }; + payload - .append_u32(SIGNED_EXTERNAL_PREFIX)? .append_u32(self.wallet_id)? .append_u32(expire_at)? .append_u32(self.seqno)?; @@ -367,13 +382,14 @@ 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, is_wallet_v5r1, InitData, WALLET_ID, }; + use ed25519_dalek::{PublicKey, Signature, Verifier}; + use nekoton_contracts::wallets; + use nekoton_utils::{SignatureContext, SignatureType, ToSign}; + use ton_block::AccountState; + use ton_types::SliceData; #[test] fn state_init() -> anyhow::Result<()> { @@ -410,4 +426,53 @@ mod tests { Ok(()) } + + #[test] + fn check_signature_test() -> anyhow::Result<()> { + let public_key_bytes = + hex::decode("6c2f9514c1c0f2ec54cffe1ac2ba0e85268e76442c14205581ebc808fe7ee52c")?; + //let payload = base64::decode("te6ccgECCQEAAWMAASFzaW50f///EWjJNSIAAAABoAECCg7DyG0DBQIB80IAEiSxvuIkjLwTZ/69OCTi5io4ZpgjPKnD56XnecGH1Q0gcJ32yAAAAAAAAAAAAAAAAABz4iFDAAAAAAAAAAAAAAAAO5rKAIAfPq6ksCQX/kNfsY8xS5PTRd4WSjwjs5C/fod9ktFK+MAAAAAAAAAAAAAAAAD39JAwAwFDgBI2HlLkTtTC7ntWgsSS4jmXMUkhy2OTDHvAO1YAIIdyCAQBCAAAAAAIAgoOw8htAwgGAdNCABIksb7iJIy8E2f+vTgk4uYqOGaYIzypw+el53nBh9UNIC+vCAAAAAAAAAAAAAAAAAAARqnX7AAAAAAAAAAAAAAAAAIA9mKAH6YK7ZtGhTyJBnq9b54dnz07z830q8r/r5MBXJdSioIQBwFDgBhcpJ/VWhGKPK44GyznIrRqKDcoivK5/ZanRrMrFKCjiAgAAA==")?; + let payload = base64::decode("te6ccgECCQEAAaMAAaFzaW50f///EWjJNSIAAAABr9SYdbfeTOkhxaWVTsB40YIzxnswT6p7oxjydvTUZ0afi8fq5F2NvuyGho+YxBUC2NPkhtL3+tuMa5CfUwJMg2ABAgoOw8htAwUCAfNCABIksb7iJIy8E2f+vTgk4uYqOGaYIzypw+el53nBh9UNIHCd9sgAAAAAAAAAAAAAAAAAc+IhQwAAAAAAAAAAAAAAADuaygCAHz6upLAkF/5DX7GPMUuT00XeFko8I7OQv36HfZLRSvjAAAAAAAAAAAAAAAAA9/SQMAMBQ4ASNh5S5E7Uwu57VoLEkuI5lzFJIctjkwx7wDtWACCHcggEAQgAAAAACAIKDsPIbQMIBgHTQgASJLG+4iSMvBNn/r04JOLmKjhmmCM8qcPnped5wYfVDSAvrwgAAAAAAAAAAAAAAAAAAEap1+wAAAAAAAAAAAAAAAACAPZigB+mCu2bRoU8iQZ6vW+eHZ89O8/N9KvK/6+TAVyXUoqCEAcBQ4AYXKSf1VoRijyuOBss5yK0aig3KIryuf2Wp0azKxSgo4gIAAA=")?; + let in_msg_body = ton_types::deserialize_tree_of_cells(&mut payload.as_slice())?; + let in_msg_body_slice = SliceData::load_cell(in_msg_body)?; + + let public_key = PublicKey::from_bytes(public_key_bytes.as_slice())?; + + let result = check_signature( + in_msg_body_slice, + public_key, + SignatureContext { + global_id: Some(2000), + signature_type: SignatureType::SignatureId, + }, + )?; + assert!(result); + Ok(()) + } + + fn check_signature( + mut in_msg_body: SliceData, + public_key: PublicKey, + ctx: SignatureContext, + ) -> anyhow::Result { + let signature_binding = in_msg_body + .get_slice(in_msg_body.remaining_bits() - 512, 512)? + .remaining_data(); + let sig = signature_binding.data(); + + let payload = in_msg_body + .shrink_data(in_msg_body.remaining_bits() - 512..) + .into_cell(); + + let hash = payload.repr_hash(); + let to_sign = ToSign { + ctx, + data: hash.into_vec(), + }; + let data = to_sign.write_to_bytes(); + + Ok(public_key + .verify(&data, &Signature::from_bytes(sig)?) + .is_ok()) + } } diff --git a/src/crypto/derived_key/mod.rs b/src/crypto/derived_key/mod.rs index 05e25ec15..17ec21d0d 100644 --- a/src/crypto/derived_key/mod.rs +++ b/src/crypto/derived_key/mod.rs @@ -2,18 +2,16 @@ use std::collections::hash_map::{self, HashMap}; use anyhow::Result; use chacha20poly1305::{ChaCha20Poly1305, KeyInit, Nonce}; -use ed25519_dalek::{Keypair, PublicKey, Signer}; +use ed25519_dalek::{Keypair, PublicKey}; use secstr::SecUtf8; use serde::{Deserialize, Serialize, Serializer}; -use nekoton_utils::*; - use super::mnemonic::*; use super::{ - default_key_name, extend_with_signature_id, Password, PasswordCache, PasswordCacheTransaction, - PubKey, SharedSecret, SignatureId, Signer as StoreSigner, SignerContext, SignerEntry, - SignerStorage, + default_key_name, Password, PasswordCache, PasswordCacheTransaction, PubKey, SharedSecret, + SignatureContext, Signer as StoreSigner, SignerContext, SignerEntry, SignerStorage, }; +use nekoton_utils::*; #[derive(Default, Clone, Debug, Eq, PartialEq)] pub struct DerivedKeySigner { @@ -355,12 +353,12 @@ impl StoreSigner for DerivedKeySigner { &self, ctx: SignerContext<'_>, data: &[u8], - signature_id: Option, + signature_ctx: SignatureContext, input: Self::SignInput, ) -> Result<[u8; 64]> { let keypair = self.use_sign_input(ctx.password_cache, input)?; - let data = extend_with_signature_id(data, signature_id); - Ok(keypair.sign(&data).to_bytes()) + let signature = signature_ctx.sign(&keypair, data); + Ok(signature.to_bytes()) } } diff --git a/src/crypto/encrypted_key/mod.rs b/src/crypto/encrypted_key/mod.rs index 2631aee97..ce951d1ec 100644 --- a/src/crypto/encrypted_key/mod.rs +++ b/src/crypto/encrypted_key/mod.rs @@ -4,19 +4,17 @@ use std::io::Read; use anyhow::Result; use chacha20poly1305::{ChaCha20Poly1305, Key, KeyInit, Nonce}; -use ed25519_dalek::{Keypair, PublicKey, SecretKey, Signer}; +use ed25519_dalek::{Keypair, PublicKey, SecretKey}; use rand::Rng; use secstr::SecUtf8; use serde::{Deserialize, Serialize}; -use nekoton_utils::*; - use super::mnemonic::*; use super::{ - default_key_name, extend_with_signature_id, Password, PasswordCache, PasswordCacheTransaction, - PubKey, SharedSecret, SignatureId, Signer as StoreSigner, SignerContext, SignerEntry, - SignerStorage, + default_key_name, Password, PasswordCache, PasswordCacheTransaction, PubKey, SharedSecret, + SignatureContext, Signer as StoreSigner, SignerContext, SignerEntry, SignerStorage, }; +use nekoton_utils::*; #[derive(Default, Clone, Debug, Eq, PartialEq)] pub struct EncryptedKeySigner { @@ -194,7 +192,7 @@ impl StoreSigner for EncryptedKeySigner { &self, ctx: SignerContext<'_>, data: &[u8], - signature_id: Option, + signature_ctx: SignatureContext, input: Self::SignInput, ) -> Result<[u8; 64]> { let key = self.get_key(&input.public_key)?; @@ -203,7 +201,7 @@ impl StoreSigner for EncryptedKeySigner { .password_cache .process_password(input.public_key.to_bytes(), input.password)?; - let signature = key.sign(data, signature_id, password.as_ref())?; + let signature = key.sign(data, password.as_ref(), signature_ctx)?; password.proceed(); Ok(signature) @@ -471,10 +469,10 @@ impl EncryptedKey { pub fn sign( &self, data: &[u8], - signature_id: Option, password: &str, + signature_ctx: SignatureContext, ) -> Result<[u8; ed25519_dalek::SIGNATURE_LENGTH]> { - self.inner.sign(data, signature_id, password) + self.inner.sign(data, password, signature_ctx) } pub fn compute_shared_keys( @@ -532,16 +530,16 @@ impl CryptoData { pub fn sign( &self, data: &[u8], - signature_id: Option, password: &str, + signature_ctx: SignatureContext, ) -> Result<[u8; ed25519_dalek::SIGNATURE_LENGTH]> { let secret = self.decrypt_secret(password)?; let pair = Keypair { secret, public: self.pubkey, }; - let data = extend_with_signature_id(data, signature_id); - Ok(pair.sign(&data).to_bytes()) + let signature = signature_ctx.sign(&pair, data); + Ok(signature.to_bytes()) } pub fn compute_shared_keys( @@ -664,7 +662,7 @@ mod tests { use std::time::Duration; use super::*; - use crate::crypto::PasswordCacheBehavior; + use crate::crypto::{PasswordCacheBehavior, SignatureType}; const TEST_PASSWORD: &str = "123"; const TEST_MNEMONIC: &str = "canyon stage apple useful bench lazy grass enact canvas like figure help pave reopen betray exotic nose fetch wagon senior acid across salon alley"; @@ -705,7 +703,11 @@ mod tests { .unwrap(); assert!(!signer.as_json().is_empty()); - let result = signer.sign(b"lol", None, "lol"); + let sig_ctx = SignatureContext { + global_id: None, + signature_type: SignatureType::Empty, + }; + let result = signer.sign(b"lol", "lol", sig_ctx); assert!(result.is_err()); } diff --git a/src/crypto/ledger_key/mod.rs b/src/crypto/ledger_key/mod.rs index 91aca3cdb..158ea3f68 100644 --- a/src/crypto/ledger_key/mod.rs +++ b/src/crypto/ledger_key/mod.rs @@ -10,8 +10,8 @@ use serde::{Deserialize, Serialize}; use nekoton_utils::*; use super::{ - default_key_name, SharedSecret, SignatureId, Signer as StoreSigner, SignerContext, SignerEntry, - SignerStorage, + default_key_name, SharedSecret, SignatureContext, Signer as StoreSigner, SignerContext, + SignerEntry, SignerStorage, }; use crate::core::ton_wallet::WalletType; use crate::external::{LedgerConnection, LedgerSignatureContext}; @@ -150,14 +150,14 @@ impl StoreSigner for LedgerKeySigner { &self, _: SignerContext<'_>, data: &[u8], - signature_id: Option, + signature_ctx: SignatureContext, input: Self::SignInput, ) -> Result<[u8; ed25519_dalek::SIGNATURE_LENGTH]> { let key = self.get_key(&input.public_key)?; let signature = match input.context { None => { self.connection - .sign(key.account_id, signature_id, data) + .sign(key.account_id, signature_ctx, data) .await? } Some(context) => { @@ -165,7 +165,7 @@ impl StoreSigner for LedgerKeySigner { .sign_transaction( key.account_id, input.wallet.try_into()?, - signature_id, + signature_ctx, data, &context, ) diff --git a/src/crypto/mod.rs b/src/crypto/mod.rs index 7deef6f40..986874339 100644 --- a/src/crypto/mod.rs +++ b/src/crypto/mod.rs @@ -1,5 +1,3 @@ -use std::borrow::Cow; - use anyhow::Result; use downcast_rs::{impl_downcast, Downcast}; use dyn_clone::DynClone; @@ -166,7 +164,7 @@ pub trait Signer: SignerStorage { &self, ctx: SignerContext<'_>, data: &[u8], - signature_id: Option, + signature_ctx: SignatureContext, input: Self::SignInput, ) -> Result; } @@ -240,18 +238,6 @@ pub fn default_key_name(public_key: &PubKey) -> String { ) } -pub fn extend_with_signature_id(data: &[u8], signature_id: Option) -> Cow<'_, [u8]> { - match signature_id { - Some(signature_id) => { - let mut extended_data = Vec::with_capacity(4 + data.len()); - extended_data.extend_from_slice(&signature_id.to_be_bytes()); - extended_data.extend_from_slice(data); - Cow::Owned(extended_data) - } - None => Cow::Borrowed(data), - } -} - pub mod x25519 { use curve25519_dalek_ng::scalar::Scalar; use zeroize::Zeroizing; diff --git a/src/external/mod.rs b/src/external/mod.rs index fc5634f6c..9431e2d82 100644 --- a/src/external/mod.rs +++ b/src/external/mod.rs @@ -2,6 +2,8 @@ use anyhow::Result; use nekoton_utils::serde_optional_hex_array; use serde::{Deserialize, Serialize}; +use nekoton_utils::SignatureContext; + #[cfg_attr(not(feature = "non_threadsafe"), async_trait::async_trait)] #[cfg_attr(feature = "non_threadsafe", async_trait::async_trait(?Send))] pub trait Storage: Sync + Send { @@ -87,7 +89,7 @@ pub trait LedgerConnection: Send + Sync { async fn sign( &self, account: u16, - signature_id: Option, + signature_ctx: SignatureContext, message: &[u8], ) -> Result<[u8; ed25519_dalek::SIGNATURE_LENGTH]>; @@ -95,8 +97,84 @@ pub trait LedgerConnection: Send + Sync { &self, account: u16, wallet: u16, - signature_id: Option, + signature_ctx: SignatureContext, message: &[u8], context: &LedgerSignatureContext, ) -> Result<[u8; ed25519_dalek::SIGNATURE_LENGTH]>; } + +#[cfg(feature = "jrpc_transport")] +pub enum JrpcResponse { + Success(T), + Err(Box), +} + +#[cfg(feature = "jrpc_transport")] +impl<'de, T> Deserialize<'de> for JrpcResponse +where + T: Deserialize<'de>, +{ + fn deserialize(de: D) -> Result + where + D: serde::Deserializer<'de>, + { + use std::marker::PhantomData; + + #[derive(Debug, Deserialize)] + #[serde(rename_all = "lowercase")] + enum Field { + Result, + Error, + #[serde(other)] + Other, + } + + enum ResponseData { + Result(T), + Error(Box), + } + + struct ResponseVisitor(PhantomData); + + impl<'de, T> serde::de::Visitor<'de> for ResponseVisitor + where + T: Deserialize<'de>, + { + type Value = ResponseData; + + fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("a JSON-RPC response object") + } + + fn visit_map(self, mut map: A) -> std::result::Result + where + A: serde::de::MapAccess<'de>, + { + let mut result = None::>; + + while let Some(key) = map.next_key()? { + match key { + Field::Result if result.is_none() => { + result = Some(map.next_value().map(ResponseData::Result)?); + } + Field::Error if result.is_none() => { + result = Some(map.next_value().map(ResponseData::Error)?); + } + Field::Other => { + map.next_value::<&serde_json::value::RawValue>()?; + } + Field::Result => return Err(serde::de::Error::duplicate_field("result")), + Field::Error => return Err(serde::de::Error::duplicate_field("error")), + } + } + + result.ok_or_else(|| serde::de::Error::missing_field("result or error")) + } + } + + Ok(match de.deserialize_map(ResponseVisitor(PhantomData))? { + ResponseData::Result(result) => JrpcResponse::Success(result), + ResponseData::Error(error) => JrpcResponse::Err(error), + }) + } +} diff --git a/src/models.rs b/src/models.rs index aa6048bc1..b04731b03 100644 --- a/src/models.rs +++ b/src/models.rs @@ -13,6 +13,7 @@ use nekoton_utils::*; pub use nekoton_contracts::tip3_any::{ RootTokenContractDetails, TokenWalletDetails, TokenWalletVersion, }; +use nekoton_utils::{SignatureContext, SignatureDomain, SignatureType}; #[non_exhaustive] #[derive(Clone, Debug, Serialize, Deserialize)] @@ -283,6 +284,7 @@ pub enum TokenWalletTransaction { pub enum JettonWalletTransaction { Transfer(JettonOutgoingTransfer), InternalTransfer(JettonIncomingTransfer), + BurnNotification(JettonBurnNotification), } #[derive(Clone, Debug, Serialize, Deserialize)] @@ -321,6 +323,14 @@ pub struct JettonIncomingTransfer { pub tokens: BigUint, } +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct JettonBurnNotification { + #[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")] @@ -411,11 +421,44 @@ pub struct NetworkCapabilities { impl NetworkCapabilities { const CAP_SIGNATURE_WITH_ID: u64 = 0x4000000; + const CAP_SIGNATURE_DOMAIN: u64 = 0x800000000; /// Returns the signature id if `CapSignatureWithId` capability is enabled. pub fn signature_id(&self) -> Option { (self.raw & Self::CAP_SIGNATURE_WITH_ID != 0).then_some(self.global_id) } + + /// Returns the signature domain type from `CapSignatureDomain` value (enabled or not) + pub fn signature_domain(&self) -> SignatureDomain { + if self.raw & Self::CAP_SIGNATURE_DOMAIN != 0 { + SignatureDomain::L2 { + global_id: self.global_id, + } + } else { + SignatureDomain::Empty + } + } + + /// Returns the signature context type which depends on enabled signature capability + pub fn signature_context(&self) -> SignatureContext { + let signature_id_enabled = self.raw & Self::CAP_SIGNATURE_WITH_ID != 0; + let signature_domain_enabled = self.raw & Self::CAP_SIGNATURE_DOMAIN != 0; + + match (signature_domain_enabled, signature_id_enabled) { + (true, true) => SignatureContext { + global_id: Some(self.global_id), + signature_type: SignatureType::SignatureDomain, + }, + (false, true) => SignatureContext { + global_id: Some(self.global_id), + signature_type: SignatureType::SignatureId, + }, + _ => SignatureContext { + global_id: None, + signature_type: SignatureType::Empty, + }, + } + } } #[derive(Debug, Copy, Clone, Eq, PartialEq, Serialize, Deserialize)] diff --git a/src/transport/models.rs b/src/transport/models.rs index 1535c33e9..379f96a88 100644 --- a/src/transport/models.rs +++ b/src/transport/models.rs @@ -124,6 +124,7 @@ impl ExistingContract { ExecutionContext { clock, account_stuff: &self.account, + libraries: Default::default(), } } }