diff --git a/linker_lib/Cargo.toml b/linker_lib/Cargo.toml index 8b9edd6..eb59a61 100644 --- a/linker_lib/Cargo.toml +++ b/linker_lib/Cargo.toml @@ -1,25 +1,32 @@ [package] name = "linker-lib" -version = "0.4.1" -authors = ["Tonlabs "] -edition = "2018" +version = "0.5.0-alpha.5" +authors = ["EverX"] +edition = "2021" [lib] name = "linker_lib" crate-type = ["cdylib"] [dependencies] -base64 = "0.13.0" -ed25519-dalek = "1.0.1" -hex = "0.4.3" -lazy_static = "1.1.1" -num = "^0.2.1" -pyo3 = { version = "^0.15.0", features = ["abi3", "abi3-py36", "extension-module"] } -rand = "0.7.3" -serde = { version = "1.0.125", features = ["derive"] } -serde_json = "1.0.64" +base64 = "0.13" +crc = '3.0' +ed25519-dalek = "1.0" +ed25519 = "1.0" +failure = "0.1" +hex = "0.4" +lazy_static = "1.1" +num = "0.4" +num-format = "0.4" +pyo3 = { version = "0.15", features = ["abi3", "abi3-py36", "extension-module"] } +rand = "0.7" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" -ton_abi = { git = "https://github.com/tonlabs/ton-labs-abi.git", tag = "2.1.5" } -ton_block = { git = "https://github.com/tonlabs/ton-labs-block.git", tag = "1.7.29" } -ton_types = { git = "https://github.com/tonlabs/ton-labs-types.git", tag = "1.10.11" } -ton_vm = { git = "https://github.com/tonlabs/ton-labs-vm.git", tag = "1.8.20", default-features = false } +ton_abi = { git = 'https://github.com/tonlabs/ever-abi.git', tag = '2.3.73' } +ton_block = { git = 'https://github.com/tonlabs/ever-block.git', tag = '1.9.37' } +ton_block_json = { git = 'https://github.com/tonlabs/ever-block-json.git', tag = '0.7.105' } +ton_client = { git = 'https://github.com/tonlabs/ever-SDK.git', tag = '1.42.1' } +ton_labs_assembler = { git = 'https://github.com/tonlabs/ever-assembler.git', tag = '1.2.89'} +ton_types = { git = 'https://github.com/tonlabs/ever-types.git', tag = '1.12.7' } +ton_vm = { git = 'https://github.com/tonlabs/ever-vm.git', tag = '1.8.125' } diff --git a/linker_lib/build.sh b/linker_lib/build.sh index 706d97a..da58fbf 100755 --- a/linker_lib/build.sh +++ b/linker_lib/build.sh @@ -1,6 +1,7 @@ -# This file is part of TON OS. +#!/bin/bash +# This file is part of Ever OS. # -# TON OS is free software: you can redistribute it and/or modify +# Ever OS is free software: you can redistribute it and/or modify # it under the terms of the Apache License 2.0 (http://www.apache.org/licenses/) # # Copyright 2019-2021 (c) TON LABS @@ -9,4 +10,4 @@ if [ "${LINKER_LIB_PATH}" = "" ]; then LINKER_LIB_PATH=./target fi echo ${LINKER_LIB_PATH} -cargo build --target-dir=${LINKER_LIB_PATH} && mv ${LINKER_LIB_PATH}/debug/liblinker_lib.so ../tonos_ts4/linker_lib.so \ No newline at end of file +cargo build --target-dir=${LINKER_LIB_PATH} && mv -v ${LINKER_LIB_PATH}/debug/liblinker_lib.so ../tonos_ts4/linux/linker_lib.so \ No newline at end of file diff --git a/linker_lib/buildWin.sh b/linker_lib/buildWin.sh index 2c3bf58..850068a 100644 --- a/linker_lib/buildWin.sh +++ b/linker_lib/buildWin.sh @@ -1,6 +1,7 @@ -# This file is part of TON OS. +#!/bin/bash +# This file is part of Ever OS. # -# TON OS is free software: you can redistribute it and/or modify +# Ever OS is free software: you can redistribute it and/or modify # it under the terms of the Apache License 2.0 (http://www.apache.org/licenses/) # # Copyright 2019-2021 (c) TON LABS diff --git a/linker_lib/build_release.sh b/linker_lib/build_release.sh index bc5044f..9362c59 100755 --- a/linker_lib/build_release.sh +++ b/linker_lib/build_release.sh @@ -1,6 +1,7 @@ -# This file is part of TON OS. +#!/bin/bash +# This file is part of Ever OS. # -# TON OS is free software: you can redistribute it and/or modify +# Ever OS is free software: you can redistribute it and/or modify # it under the terms of the Apache License 2.0 (http://www.apache.org/licenses/) # # Copyright 2019-2021 (c) TON LABS diff --git a/linker_lib/src/abi.rs b/linker_lib/src/abi.rs index d1d142e..ed123e7 100644 --- a/linker_lib/src/abi.rs +++ b/linker_lib/src/abi.rs @@ -1,10 +1,10 @@ /* - This file is part of TON OS. + This file is part of Ever OS. - TON OS is free software: you can redistribute it and/or modify + Ever OS is free software: you can redistribute it and/or modify it under the terms of the Apache License 2.0 (http://www.apache.org/licenses/) - Copyright 2019-2021 (c) TON LABS + Copyright 2019-2023 (c) EverX */ use std::collections::HashMap; @@ -51,10 +51,14 @@ impl AllAbis { self.all_abis.iter().map(|pair| pair.1.text().clone()).collect() } - fn decode_function_call(&self, body: &SliceData, internal: bool) -> Option { + fn decode_function_call(&self, body: &SliceData, internal: bool, abi_str: &str) -> Option { + let res = decode_unknown_function_call(abi_str.to_owned(), body.clone(), internal, false); + if let Ok(res) = res { + return Some(res); + } for abi_str in self.values() { // println!("contract: {}", &contract.name); - let res = decode_unknown_function_call(abi_str, body.clone(), internal); + let res = decode_unknown_function_call(abi_str, body.clone(), internal, false); if let Ok(res) = res { return Some(res); } @@ -62,12 +66,14 @@ impl AllAbis { None } - pub fn from_file(&mut self, filename: &String) -> Result { - if !self.all_abis.contains_key(filename) { - let info = AbiInfo::from_file(filename.clone())?; - self.register_abi(info); + pub fn read_from_file(&mut self, filename: &String) -> Result { + if let Some(abi_info) = self.all_abis.get(filename) { + Ok(abi_info.clone()) + } else { + let abi_info = AbiInfo::from_file(filename.clone())?; + self.all_abis.insert(filename.clone(), abi_info.clone()); + Ok(abi_info) } - Ok(self.all_abis[filename].clone()) } } @@ -82,7 +88,7 @@ impl AbiInfo { fn from_file(filename: String) -> Result { let abi_str = load_abi_json_string(&filename)?; let abi_info = AbiInfo { - filename: filename, + filename, text: abi_str, }; Ok(abi_info) @@ -97,6 +103,7 @@ pub fn decode_body( abi_info: &AbiInfo, method: Option, out_msg: &TonBlockMessage, + is_debot_call: bool, ) -> MsgAbiInfo { let internal = out_msg.is_internal(); @@ -104,13 +111,14 @@ pub fn decode_body( // TODO: refactor this function - if gs.trace { - println!("decode_body {:?} {:?}", body, &method); + if gs.is_trace(3) { + println!("decode_body: {:?} {:?}", &method, body); } if body.is_none() { return MsgAbiInfo::create_empty(); } + let body = body.unwrap(); let abi_str = abi_info.text(); @@ -121,7 +129,8 @@ pub fn decode_body( abi_str.clone(), method.clone(), body.clone(), - internal + internal, + false, ); if let Ok(s) = s { return MsgAbiInfo::create_answer(s, method); @@ -129,18 +138,33 @@ pub fn decode_body( } // Check for a call to a remote method - if let Some(res) = gs.all_abis.decode_function_call(&body, internal) { - // println!(">> {} {}", res.function_name, res.params); + if let Some(res) = gs.all_abis.decode_function_call(&body, internal, abi_str) { + if gs.is_trace(5) { + println!("decode_function_call - {} {}", res.function_name, res.params); + } return MsgAbiInfo::create_call(res.params, res.function_name); } // Check for event - let s = decode_unknown_function_response(abi_str.clone(), body.clone(), internal); + let s = decode_unknown_function_response(abi_str.clone(), body.clone(), internal, false); if let Ok(s) = s { - return MsgAbiInfo::create_event(s.params, s.function_name); + if gs.is_trace(5) { + println!("decode_unknown_function_response - {} {}", s.function_name, s.params); + } + if is_debot_call { + let mut abi_info = MsgAbiInfo::create_answer(s.params, s.function_name); + abi_info.set_debot_mode(); + return abi_info; + } else { + return MsgAbiInfo::create_event(s.params, s.function_name); + } } - return MsgAbiInfo::create_unknown(); + if gs.is_trace(1) { + println!("Unknown message! body = {:?}", body); + } + + MsgAbiInfo::create_unknown() } pub fn build_abi_body( @@ -150,7 +174,9 @@ pub fn build_abi_body( header: Option, internal: bool, pair: Option<&Keypair>, + address: Option, ) -> Result { + assert!(internal || address.is_some()); encode_function_call( abi_info.text().clone(), method.to_owned(), @@ -158,6 +184,7 @@ pub fn build_abi_body( params.to_owned(), internal, pair, + address ).map_err(|e| format!("cannot encode abi body: {:?}", e)) } @@ -165,7 +192,7 @@ pub fn set_public_key(state_init: &mut StateInit, pubkey: String) -> Result<(), let pubkey = hex::decode(pubkey) .map_err(|e| format!("cannot decode public key: {}", e))?; let pubkey = PublicKey::from_bytes(&pubkey).unwrap(); - let data = state_init.data.clone().unwrap().into(); + let data = SliceData::load_cell(state_init.data.clone().unwrap()).unwrap(); let new_data = ton_abi::Contract::insert_pubkey(data, pubkey.as_bytes()).unwrap(); state_init.set_data(new_data.into_cell()); Ok(()) diff --git a/linker_lib/src/actions.rs b/linker_lib/src/actions.rs index 8e92974..1db8dd4 100644 --- a/linker_lib/src/actions.rs +++ b/linker_lib/src/actions.rs @@ -1,12 +1,14 @@ /* - This file is part of TON OS. + This file is part of Ever OS. - TON OS is free software: you can redistribute it and/or modify + Ever OS is free software: you can redistribute it and/or modify it under the terms of the Apache License 2.0 (http://www.apache.org/licenses/) - Copyright 2019-2021 (c) TON LABS + Copyright 2019-2022 (c) TON LABS */ +use std::cmp::min; + use ton_types::{ Cell, }; @@ -15,6 +17,8 @@ use ton_block::{ Message as TonBlockMessage, MsgAddressInt, OutAction, + CurrencyCollection, + CommonMsgInfo, }; use crate::global_state::{ @@ -22,11 +26,12 @@ use crate::global_state::{ }; use crate::messages::{ - MsgInfo, + MsgInfo, MsgInfoJsonDebot, + AddressWrapper, }; use crate::util::{ - bigint_to_u64, get_msg_value, substitute_address, + grams_to_u64, get_msg_value, substitute_address, format3, }; use crate::call_contract::{ @@ -43,67 +48,30 @@ use crate::exec::{ #[derive(Default)] struct ActionsProcessor { - balance: u64, - code: Option, - reserved_balance: u64, - destroy: bool, - msg_value: u64, - verbose: bool, -} - -pub fn process_actions( - gs: &mut GlobalState, - mut contract_info: ContractInfo, - result: &ExecutionResult, - method: Option, - msg_value: Option, -) -> Result, String> { - - // let mut msg_value = msg_value.unwrap_or_default(); - - let address: &MsgAddressInt = contract_info.address(); - - let abi_info = contract_info.abi_info(); - // let mut balance = contract_info.balance(); + verbose: bool, + msg_value: u64, + gas_fee: Option, + balance: u64, + original_balance: u64, - let mut msgs = vec![]; - let mut state = ActionsProcessor::default(); - state.verbose = gs.trace; - state.balance = contract_info.balance(); - state.msg_value = msg_value.unwrap_or_default(); - - let info_ex = &result.info_ex; - - for act in &info_ex.out_actions { - if let Some(msg_info) = process_action( - gs, act, &address, &mut state, &method, - &abi_info, - )? { - msgs.push(msg_info); - } - } + code: Option, + reserved_balance: u64, + destroy: bool, +} - let address = address.clone(); - - if state.destroy { - gs.remove_contract(&address); - } else { +impl ActionsProcessor { - let mut state_init = info_ex.state_init.clone(); - if let Some(c) = state.code { - state_init.set_code(c); + fn create(gs: &GlobalState, original_balance: u64, balance: u64, msg_value: u64, gas_fee: Option) -> ActionsProcessor { + ActionsProcessor { + verbose: gs.is_trace(3), + msg_value, + gas_fee, + balance, + original_balance, + ..ActionsProcessor::default() } - - contract_info.set_balance(state.balance); - contract_info.set_state_init(state_init); - gs.set_contract(address, contract_info); } - Ok(msgs) -} - -impl ActionsProcessor { - fn decrease_balance(&mut self, value: u64) -> Result<(), String> { let error_str = "\n!!!!!!!!!!!! Message makes balance negative !!!!!!!!!!!!!\nBalance: ".to_string(); // TODO!: refactor, handle error @@ -119,16 +87,17 @@ impl ActionsProcessor { } fn process_send_msg(&mut self, mode: u8, out_msg: &TonBlockMessage) -> Result { - // TODO: why not to pass the value instead of `out_msg`? - if let Some(value) = get_msg_value(&out_msg) { + + if let Some(value) = get_msg_value(out_msg) { self.decrease_balance(value)?; } let mut additional_value = 0; - if mode == 64 { + if (mode & 64) != 0 { // send money back - self.decrease_balance(self.msg_value)?; - additional_value = self.msg_value; + let fee = self.gas_fee.unwrap_or_default(); + self.decrease_balance(self.msg_value - fee)?; + additional_value = self.msg_value - fee; self.msg_value = 0; } @@ -143,8 +112,93 @@ impl ActionsProcessor { // self-destroy self.destroy = true; } + Ok(additional_value) } + + fn reserve(&mut self, mode: u8, value: u64) { + + if self.verbose { + println!("Action(ReserveCurrency) - mode = {}, value = {}", mode, format3(value)); + } + if mode == 0 { + self.reserved_balance = value; + } else if mode == 2 { + self.reserved_balance = min(value, self.balance); + } else if mode == 4 { + // println!("orig_balance: {} vs {}", format3(orig_balance), format3(self.original_balance)); + self.reserved_balance = self.original_balance + value; + } else { + println!("OutAction::ReserveCurrency - Unsupported mode {}", mode); + } + if self.verbose { + println!("reserving balance {}", format3(self.reserved_balance)); + } + } +} + +pub fn process_actions( + gs: &mut GlobalState, + address: &MsgAddressInt, + mut contract_info: ContractInfo, + result: &ExecutionResult, + method: Option, + msg_value: Option, + is_debot_call: bool, + gas_fee: Option, +) -> Result, String> { + + let abi_info = contract_info.abi_info(); + + let orig_balance = gs.get_contract(address).unwrap().balance(); + + + let mut state = ActionsProcessor::create( + gs, orig_balance, contract_info.balance(), msg_value.unwrap_or_default(), gas_fee); + + let info_ex = &result.info_ex; + let parent_msg_id = result.info.inbound_msg_id; + + let mut msgs = vec![]; + + for act in &info_ex.out_actions { + if let Some(mut msg_info) = process_action( + gs, act, address, &mut state, &method, + abi_info, is_debot_call, + )? { + if let Some(parent_msg_id) = parent_msg_id { + msg_info.set_parent_id(parent_msg_id); + } + msgs.push(msg_info); + } + } + + if state.destroy { + gs.remove_contract(address); + } else { + + let mut state_init = info_ex.state_init.clone(); + + if let Some(c) = state.code { + state_init.set_code(c); + } + contract_info.set_state_init(state_init); + + contract_info.set_balance(state.balance); + gs.set_contract(address.clone(), contract_info); + + } + + Ok(msgs) +} + +fn set_int_msg_value(msg: &mut TonBlockMessage, additional_value: u64) { + let value = get_msg_value(msg); + if additional_value > 0 { + if let CommonMsgInfo::IntMsgInfo(hdr) = msg.header_mut() { + hdr.value = CurrencyCollection::with_grams(value.unwrap() + additional_value); + } + } } fn process_action( @@ -154,47 +208,58 @@ fn process_action( state: &mut ActionsProcessor, method: &Option, abi_info: &AbiInfo, + is_debot_call: bool, ) -> Result, String> { - // TODO!: refactor this function! Too many parameters // TODO: remove .clone() match action.clone() { OutAction::SendMsg{ mode, out_msg } => { - if gs.trace { - println!("Action(SendMsg):"); + if gs.is_trace(3) { + println!("Action(SendMsg): mode = {}", mode); } - let additional_value = state.process_send_msg(mode, &out_msg)?; - let out_msg = substitute_address(out_msg, &address); + let ignore_errors = (mode & 2) != 0; + let result = state.process_send_msg(mode, &out_msg); + + if result.is_err() && ignore_errors { + if gs.is_trace(1) { + println!("Ignoring failed SendMsg action."); + } + return Ok(None); + } + + let additional_value = result?; + // println!("{:?}", out_msg); + let mut out_msg = substitute_address(out_msg, address); + set_int_msg_value(&mut out_msg, additional_value); + let additional_value = 0; + + let is_debot_call2 = out_msg.src().is_none(); + + if !is_debot_call && get_msg_value(&out_msg) == Some(0) { + return Ok(None); + } // TODO: is this code needed here? Should it be moved? - let j = decode_message(&gs, abi_info, method.clone(), &out_msg, additional_value); - let msg_info2 = MsgInfo::create(out_msg, j); + let j = decode_message(gs, abi_info, method.clone(), &out_msg, additional_value, is_debot_call); + let mut msg_info2 = MsgInfo::create(out_msg, j); + + if is_debot_call2 { + msg_info2.json.debot_info = Some(MsgInfoJsonDebot{debot_addr: AddressWrapper::with_int(address.clone())}); + } + + // println!("!!!!! msg_info2.json.debot_info = {:?}", msg_info2.json.debot_info); return Ok(Some(msg_info2)); }, OutAction::SetCode { new_code } => { - if gs.trace { + if gs.is_trace(3) { println!("Action(SetCode)"); } state.code = Some(new_code); }, OutAction::ReserveCurrency { mode, value } => { - if gs.trace { - println!("Action(ReserveCurrency)"); - } - // TODO: support other modes when needed. Add more tests. Refactor balance logic - if mode == 0 { - // TODO: support other currencies - state.reserved_balance = value.grams.0 as u64; - if gs.trace { - println!("reserving balance {}", state.reserved_balance); - } - } else if mode == 4 { - let orig_balance = gs.get_contract(&address).unwrap().balance(); - state.reserved_balance = orig_balance + bigint_to_u64(&value.grams.value()); - } else { - println!("OutAction::ReserveCurrency - Unsupported mode {}", mode); - } + let value = grams_to_u64(&value.grams).unwrap(); + state.reserve(mode, value); }, OutAction::ChangeLibrary { .. } => { println!("Action(ChangeLibrary)"); diff --git a/linker_lib/src/call_contract.rs b/linker_lib/src/call_contract.rs index f26c7d9..c3f4e78 100644 --- a/linker_lib/src/call_contract.rs +++ b/linker_lib/src/call_contract.rs @@ -1,54 +1,33 @@ /* - This file is part of TON OS. + This file is part of Ever OS. - TON OS is free software: you can redistribute it and/or modify + Ever OS is free software: you can redistribute it and/or modify it under the terms of the Apache License 2.0 (http://www.apache.org/licenses/) - Copyright 2019-2021 (c) TON LABS + Copyright 2019-2023 (c) EverX */ use std::sync::{Arc, Mutex}; - -use serde::{ - Serialize, -}; +use std::cmp::min; +use serde::Serialize; use ton_block::{ - CurrencyCollection, Deserializable, - MsgAddressInt, - OutActions, - Serializable, StateInit, + ConfigParam8, CurrencyCollection, Deserializable, GlobalCapabilities, MsgAddressInt, + OutActions, Serializable, StateInit, }; - -use ton_types::{ - SliceData, BuilderData, Cell, +use ton_types::{Cell, HashmapE, SliceData}; +use ton_vm::{ + boolean, int, + error::tvm_exception, + executor::{gas::gas_state::Gas, Engine, EngineTraceInfo, EngineTraceInfoType}, + stack::{integer::IntegerData, savelist::SaveList, Stack, StackItem}, + SmartContractInfo, }; -use ton_vm::stack::{ - StackItem, Stack, savelist::SaveList, integer::IntegerData, -}; - -use ton_vm::error::tvm_exception; -use ton_vm::SmartContractInfo; - -use ton_vm::executor::{ - Engine, EngineTraceInfo, EngineTraceInfoType, gas::gas_state::Gas -}; - -use crate::global_state::{ - // GlobalState, - ContractInfo, -}; - -use crate::debug_info::{ - load_debug_info, ContractDebugInfo, TraceStepInfo, - get_function_name, -}; - -use crate::messages::{ - AddressWrapper, - MessageInfo2, -}; +use crate::global_state::ContractInfo; +use crate::debug_info::{get_function_name, load_debug_info, ContractDebugInfo, TraceStepInfo}; +use crate::messages::{AddressWrapper, CallContractMsgInfo}; +use crate::util::format3; //////////////////////////////////////////////////////////////////////////////////////////// @@ -70,6 +49,8 @@ pub struct ExecutionResultInfo { pub exit_code: i32, pub error_msg: Option, pub gas: i64, + pub accept_in_getter: bool, + pub stack: Vec, } #[derive(Clone, Debug)] @@ -79,87 +60,133 @@ pub struct ExecutionResultEx { } pub fn call_contract_ex( + _address: &MsgAddressInt, contract_info: &ContractInfo, - msg_info: &MessageInfo2, + msg_info: &CallContractMsgInfo, + global_gas_limit: u64, + trace_level: u64, debug: bool, - trace2: bool, + trace_tvm_1: bool, + trace_tvm_2: bool, config_params: Option, now: u64, lt: u64, ) -> ExecutionResult { - if debug { - println!("call_contract_ex"); - } - // TODO: Too long function let msg_value = msg_info.value(); - let ticktock = &msg_info.ticktock; - let addr = contract_info.address(); + let address = contract_info.address(); let state_init = contract_info.state_init(); let contract_balance = contract_info.balance(); let debug_info_filename = contract_info.debug_info_filename(); + if trace_level >= 5 { + println!("call_contract_ex: balance = {}", format3(contract_balance)); + } + // 0 - internal msg // -1 - external msg // -2 - tick-tock // -3 - Split Prepare Transaction // -4 - Merge Transaction - let func_selector = - if ticktock.is_some() { -2 } else { - if msg_value.is_some() { 0 } else { -1 } - }; - let value = msg_value.unwrap_or(0); - let (code, data) = load_code_and_data(&state_init); + let code = state_init.code.clone().unwrap_or_default(); + let data = state_init.data.clone().unwrap_or_default(); + let mut capabilities = GlobalCapabilities::CapInitCodeHash as u64 | GlobalCapabilities::CapMycode as u64; + if let Some(root) = config_params.clone() { + let config_params = HashmapE::with_hashmap(32, Some(root)); + let key = SliceData::load_builder(8u32.write_to_new_cell().unwrap()).unwrap(); + if let Ok(Some(value)) = config_params.get(key) { + if let Some(cell) = value.reference_opt(0) { + if let Ok(param) = ConfigParam8::construct_from_cell(cell) { + capabilities |= param.global_version.capabilities; + } + } + } + } let registers = initialize_registers( + code.clone(), data, - addr, + SliceData::load_cell(address.serialize().unwrap()).unwrap(), now, lt, - (contract_balance, CurrencyCollection::with_grams(contract_balance)), + CurrencyCollection::with_grams(contract_balance), + capabilities, config_params, ); let mut stack = Stack::new(); - if func_selector > -2 { // internal or external - let msg = msg_info.ton_msg().unwrap(); - let msg_cell = StackItem::Cell(msg.serialize().unwrap().into()); + if let Some(ticktock) = &msg_info.ticktock { + stack + .push(int!(contract_balance)) + // .push(StackItem::Integer(Arc::new(addr_int))) //contract address + .push(int!(0_i32)) // TODO: contract address + .push(int!(*ticktock)) //tick or tock + .push(int!(-2)); + } else if let Some(body) = msg_info.ton_msg_body() { + let msg_cell = StackItem::cell(Cell::default()); + stack + .push(int!(contract_balance)) + .push(int!(0)) //msg balance + .push(msg_cell) //msg + .push(StackItem::Slice(body.clone())) //msg.body + .push(boolean!(true)); //selector + } else if let Some(msg) = msg_info.ton_msg() { + let msg_cell = StackItem::Cell(msg.serialize().unwrap()); - let body: SliceData = match msg.body() { - Some(b) => b.into(), - None => BuilderData::new().into(), - }; + let body = msg.body().unwrap_or_default(); stack .push(int!(contract_balance)) .push(int!(value)) //msg balance .push(msg_cell) //msg .push(StackItem::Slice(body)) //msg.body - .push(int!(func_selector)); //selector + .push(boolean!(msg_value.is_none())); //selector + } else if let Some(id) = msg_info.id() { + stack.push(int!(id)); //selector } else { - stack - .push(int!(contract_balance)) - // .push(StackItem::Integer(Arc::new(addr_int))) //contract address - .push(int!(0)) // TODO: contract address - .push(int!(ticktock.unwrap())) //tick or tock - .push(int!(func_selector)); + unreachable!() } + let value_gas: i64 = value as i64 / 1000; + let balance_gas: i64 = contract_balance as i64 / 1000; + + let global_gas_limit: i64 = if global_gas_limit > 0 { global_gas_limit as i64 } else { 1_000_000 }; + let gas = if msg_info.is_external_call() { - Gas::test_with_credit(10_000) - } else { + let max_gas = min(global_gas_limit, balance_gas); + Gas::new(0, 10_000, max_gas, 10) + } else if msg_info.is_getter_call() { + Gas::new(0, 1_000_000_000, 1_000_000_000, 10) + } else if msg_info.is_offchain_ctor_call() || msg_info.is_debot_call() { Gas::test() + } else if msg_info.ticktock.is_some() { + // TODO: not sure if that is correct + Gas::test() + } else { + let max_gas = min(global_gas_limit, balance_gas); + // TODO: is first param correct here? + Gas::new(value_gas, 0, max_gas, 10) }; - let mut engine = Engine::new().setup(code, Some(registers), Some(stack), Some(gas)); + if trace_level >= 5 { + println!("call_contract_ex: value = {}, gas_credit = {}, gas_limit = {}, max = {}", + format3(value), + format3(gas.get_gas_credit()), + format3(gas.get_gas_limit()), + format3(gas.get_gas_limit_max()), + ); + } + + let mut engine = Engine::with_capabilities(capabilities) + .setup(SliceData::load_cell(code).unwrap(), Some(registers), Some(stack), Some(gas)); - let debug_info = if debug || trace2 { - load_debug_info(&state_init, debug_info_filename, debug) + let debug_info = if debug || trace_tvm_1 || trace_tvm_2 { + load_debug_info(debug_info_filename, debug) } else { None }; @@ -168,7 +195,7 @@ pub fn call_contract_ex( let trace1 = trace.clone(); engine.set_trace_callback(move |engine, info| { - trace_callback(engine, info, debug, trace2, true, &debug_info, &mut trace.clone().lock().unwrap()); + trace_callback(engine, info, trace_tvm_1, trace_tvm_2, true, debug_info.as_ref(), &mut trace.clone().lock().unwrap()); }); let mut error_msg = None; @@ -184,27 +211,33 @@ pub fn call_contract_ex( } _ => -1 } - Ok(code) => code as i32 + Ok(code) => code }; let trace: Vec = trace1.lock().unwrap().clone(); let gas_usage = engine.get_gas().get_gas_used(); - if debug { + if trace_level >= 10 || trace_tvm_1 { println!("TVM terminated with exit code {}", exit_code); println!("Gas used: {}", gas_usage); - println!(""); - println!("{}", engine.dump_stack("Post-execution stack state", false)); - println!("{}", engine.dump_ctrls(false)); + println!(); + if trace_level >= 15 { + println!("{}", engine.dump_stack("Post-execution stack state", false)); + println!("{}", engine.dump_ctrls(false)); + } } let gas_credit = engine.get_gas().get_gas_credit(); - if debug { + if trace_level >= 10 { println!("credit = {}", gas_credit); } + let accept_in_getter = msg_info.is_getter_call() && gas_credit == 0; + + let gas_credit = if msg_info.is_getter_call() { 0 } else { gas_credit }; + let mut state_init = state_init.clone(); if gas_credit == 0 { match engine.get_committed_state().get_root() { @@ -218,94 +251,88 @@ pub fn call_contract_ex( }; } + let stack = engine.stack().iter().map(|s| s.to_string()).collect(); + let info_msg = if is_success_exit_code(exit_code) && gas_credit > 0 { Some("no_accept".to_string()) } else { None }; - let actions = match engine.get_actions() { - StackItem::Cell(cell) => - OutActions::construct_from(&mut cell.into()).unwrap(), - _ => - OutActions::default(), + let out_actions = match engine.get_committed_state().get_actions() { + StackItem::Cell(cell) => OutActions::construct_from_cell(cell).unwrap(), + _ => OutActions::default(), }; let info_ex = ExecutionResultEx { - state_init: state_init, - out_actions: actions, + state_init, + out_actions, }; let info = ExecutionResultInfo { run_id: None, - address: AddressWrapper::with_int(addr.clone()), + address: AddressWrapper::with_int(address.clone()), inbound_msg_id: None, - exit_code: exit_code, - error_msg: error_msg, + stack, + exit_code, + error_msg, gas: gas_usage, + accept_in_getter, }; ExecutionResult { - info: info, - info_ex: info_ex, - info_msg: info_msg, + info, + info_ex, + info_msg, trace: Some(trace), } } -fn load_code_and_data(state_init: &StateInit) -> (SliceData, SliceData) { - let code: SliceData = state_init.code - .clone() - .unwrap_or(BuilderData::new().into()) - .into(); - let data = state_init.data - .clone() - .unwrap_or(BuilderData::new().into()) - .into(); - (code, data) -} - fn initialize_registers( - data: SliceData, - myself: &MsgAddressInt, + mycode: Cell, + data: Cell, + myself: SliceData, now: u64, lt: u64, - balance: (u64, CurrencyCollection), + balance: CurrencyCollection, + capabilities: u64, config_params: Option, ) -> SaveList { let mut ctrls = SaveList::new(); - let mut info = SmartContractInfo::with_myself(myself.serialize().unwrap().into()); - *info.balance_remaining_grams_mut() = balance.0 as u128; - *info.balance_remaining_other_mut() = balance.1.other_as_hashmap().clone(); - *info.unix_time_mut() = now as u32; - if let Some(config_params) = config_params { - info.set_config_params(config_params) - } - *info.block_lt_mut() = lt; - *info.trans_lt_mut() = lt; - ctrls.put(4, &mut StackItem::Cell(data.into_cell())).unwrap(); - ctrls.put(7, &mut info.into_temp_data()).unwrap(); + let info = SmartContractInfo { + unix_time: now as u32, + block_lt: lt, + trans_lt: lt, + config_params, + capabilities, + mycode, + myself, + balance, + ..Default::default() + }; + ctrls.put(4, &mut StackItem::cell(data)).unwrap(); + ctrls.put(7, &mut info.into_temp_data_item()).unwrap(); ctrls } fn trace_callback( _engine: &Engine, info: &EngineTraceInfo, - trace: bool, - trace2: bool, + trace_tvm_1: bool, + trace_tvm_2: bool, extended: bool, - debug_info: &Option, + debug_info: Option<&ContractDebugInfo>, result: &mut Vec, ) { - let fname = get_function_name(&debug_info, &info.cmd_code); + let fname = get_function_name(debug_info, info); - if trace2 { - let info2 = TraceStepInfo::from(&info, fname.clone()); + if trace_tvm_2 { + let info2 = TraceStepInfo::from(info, fname.clone()); result.push(info2); } - if trace { + if trace_tvm_1 { println!("{}: {}", info.step, info.cmd_str); if extended { @@ -320,7 +347,7 @@ fn trace_callback( ); if debug_info.is_some() { - let fname = fname.unwrap_or("n/a".to_string()); + let fname = fname.unwrap_or_else(|| "n/a".to_string()); println!("function: {}", fname); } @@ -332,7 +359,12 @@ fn trace_callback( } if info.info_type == EngineTraceInfoType::Dump { - println!("logstr: {}", info.cmd_str); + let s = &info.cmd_str; + if s.ends_with('\n') { + print!("logstr: {}", s); + } else { + println!("logstr: {}", s); + } } } diff --git a/linker_lib/src/debots.rs b/linker_lib/src/debots.rs new file mode 100644 index 0000000..c82a35c --- /dev/null +++ b/linker_lib/src/debots.rs @@ -0,0 +1,229 @@ +/* + This file is part of Ever OS. + + Ever OS is free software: you can redistribute it and/or modify + it under the terms of the Apache License 2.0 (http://www.apache.org/licenses/) + + Copyright 2019-2022 (c) TON LABS +*/ + +// use std::convert::TryFrom; +// use std::fmt::Display; +// use failure::format_err; + +// use ed25519_dalek::Keypair; + +use ton_block::{ + Message as TonBlockMessage, + MsgAddressInt, + // MsgAddressExt, + CurrencyCollection, MsgAddressExt, +}; + +use ton_types::{ + BuilderData, + // Cell, + IBitstring, + SliceData, +}; + +use ton_client::crypto::KeyPair; +use ton_client::debot::calltype::{ + prepare_ext_in_message as prepare_ext_in_message2, +}; + +use crate::global_state::{ + GlobalState, +}; + +use crate::exec::{ + decode_message, +}; + +use crate::messages::{ + MsgInfo, + // MsgAbiInfo, + // MsgInfoJson, +}; + +#[derive(Clone, Debug)] +pub struct DebotCallInfo { + pub answer_id: u32, + pub onerror_id: u32, + pub func_id: u32, + pub debot_addr: Option, + pub dst_addr: String, // why not MsgAddressInt? +} + +pub fn prepare_ext_in_message( + msg: TonBlockMessage, + now_ms: u64, + debot_keypair: Option, +) -> Result<(TonBlockMessage, DebotCallInfo), String> { + + // println!("!!!! prepare_ext_in_message: body = {:?}, sz = {}", msg.body().unwrap(), msg.body().unwrap().remaining_bits()); + + let (func_id, answer_id, onerror_id, dst_addr, message) = prepare_ext_in_message2(&msg, now_ms, debot_keypair)?; + + // println!("!!!! prepare_ext_in_message: func_id = {}, answer_id = {}", func_id, answer_id); + + let info = DebotCallInfo { + answer_id, + onerror_id, + func_id, + debot_addr: None, + dst_addr: format!("{}", dst_addr), + }; + + Ok((message, info)) + +} + +/* +fn msg_err(err: impl Display) -> ClientError { + format_err!("{}", err) +} +*/ + + +pub fn debot_translate_getter_answer_impl( + gs: &mut GlobalState, + msg_id: u32 +) -> Result { + + if gs.is_trace(1) { + println!("debot_translate_getter_answer_impl({})", msg_id); + } + + let msg_info: MsgInfo = (*gs.messages.get(msg_id)).clone(); + + let parent_msg_id = msg_info.parent_id().unwrap(); + + let msg_info_prev: MsgInfo = (*gs.messages.get(parent_msg_id)).clone(); + + // println!("!!! msg_info = {:?}", msg_info); + // println!("!!! msg_info_prev = {:?}", msg_info_prev); + + assert!(msg_info_prev.debot_call_info.is_some()); + assert!(!msg_info_prev.has_src()); + + let debot_call_info = msg_info_prev.debot_call_info.as_ref().unwrap(); + // println!("{:?}", debot_call_info); + + let debot_addr = debot_call_info.debot_addr.clone().unwrap(); + let contract_addr = msg_info_prev.dst(); + + // TODO: translate message here! + let answer_msg = build_answer_msg(msg_info.ton_msg().unwrap(), + debot_call_info.answer_id, debot_call_info.func_id, + contract_addr.clone(), debot_addr, gs.config.trace_level).unwrap(); + + // println!("answer_msg = {:?}", answer_msg); + + let debot_info = gs.get_contract(&contract_addr).unwrap(); + let debot_abi = debot_info.abi_info(); + + let j = decode_message(gs, debot_abi, None, &answer_msg, 0, false); + let mut msg_info = MsgInfo::create(answer_msg, j); + + msg_info.debot_call_info = Some(debot_call_info.clone()); + + let result = gs.messages.add(msg_info); + + Ok((*result).clone()) +} + +pub fn build_internal_message( + src: MsgAddressInt, + dst: MsgAddressInt, + body: SliceData, + value: CurrencyCollection, +) -> TonBlockMessage { + let mut msg = TonBlockMessage::with_int_header(ton_block::InternalMessageHeader::with_addresses( + src, + dst, + value, // Default::default(), + )); + msg.set_body(body); + msg +} + +pub fn build_external_message( + src: MsgAddressExt, + dst: MsgAddressInt, + body: SliceData, +) -> TonBlockMessage { + let h = ton_block::ExternalInboundMessageHeader::new(src, dst); + TonBlockMessage::with_ext_in_header_and_body(h, body) +} + +pub fn debot_build_on_success( + src: MsgAddressInt, + dst: MsgAddressInt, + answer_id: u32, +) -> TonBlockMessage { + let mut new_body = BuilderData::new(); + new_body.append_u32(answer_id).unwrap(); + build_internal_message( + src, + dst, + SliceData::load_builder(new_body).unwrap(), + Default::default() + ) +} + +pub fn debot_build_on_error( + src: MsgAddressInt, + dst: MsgAddressInt, + onerror_id: u32, + exitcode: u32, +) -> TonBlockMessage { + let mut new_body = BuilderData::new(); + new_body.append_u32(onerror_id).unwrap(); + new_body.append_u32(0).unwrap(); // SDK error + new_body.append_u32(exitcode).unwrap(); + build_internal_message( + src, + dst, + SliceData::load_builder(new_body).unwrap(), + Default::default() + ) +} + +fn build_answer_msg( + out_message: &TonBlockMessage, + answer_id: u32, + func_id: u32, + dest_addr: MsgAddressInt, + debot_addr: MsgAddressInt, + trace_level: u64, +) -> Option { + if trace_level >= 5 { + println!("!!! build_answer_msg: dest_addr = {}, debot_addr = {}", dest_addr, debot_addr); + } + if out_message.is_internal() { + return None; + } + let mut new_body = BuilderData::new(); + new_body.append_u32(answer_id).ok()?; + + if let Some(body_slice) = out_message.body().as_mut() { + let response_id = body_slice.get_next_u32().ok()?; + let request_id = response_id & !(1u32 << 31); + if func_id != request_id { + return None; + } + new_body + .append_builder(&BuilderData::from_slice(body_slice)) + .ok()?; + } + + Some(build_internal_message( + dest_addr, + debot_addr, + SliceData::load_builder(new_body).unwrap(), + Default::default() + )) +} + + diff --git a/linker_lib/src/debug_info.rs b/linker_lib/src/debug_info.rs index eb7472e..7bef9c8 100644 --- a/linker_lib/src/debug_info.rs +++ b/linker_lib/src/debug_info.rs @@ -1,43 +1,20 @@ /* - This file is part of TON OS. + This file is part of Ever OS. - TON OS is free software: you can redistribute it and/or modify + Ever OS is free software: you can redistribute it and/or modify it under the terms of the Apache License 2.0 (http://www.apache.org/licenses/) - Copyright 2019-2021 (c) TON LABS + Copyright 2019-2023 (c) EverX */ -use std::io::Write; -use std::collections::HashMap; -use ton_types::dictionary::HashmapE; use serde::{Deserialize, Serialize}; -use ton_block::{Serializable, StateInit}; - -use ton_types::{ - UInt256, Cell, SliceData, -}; - +use ton_labs_assembler::DbgInfo; use ton_vm::executor::{ EngineTraceInfo, }; - - pub struct ContractDebugInfo { - hash2function: HashMap, -} - -#[derive(Serialize, Deserialize, Debug)] -pub struct DebugInfoFunction { - pub id: i64, // actually either i32 or u32. - pub name: String -} - -#[derive(Serialize, Deserialize, Debug)] -pub struct DebugInfo { - pub internals: Vec, - pub publics: Vec, - pub privates: Vec, + hash2function: DbgInfo, } #[derive(Clone, Serialize, Deserialize, Debug)] @@ -49,46 +26,35 @@ pub struct TraceStepInfo { pub stack: Vec, } - -impl DebugInfo { - pub fn _new() -> Self { - DebugInfo { internals: vec![], publics: vec![], privates: vec![] } - } -} - impl ContractDebugInfo { - pub fn find_function(&self, cmd_code: &SliceData) -> Option<&String> { - self.hash2function.get(&cmd_code.cell().repr_hash()) + pub fn find_function(&self, info: &EngineTraceInfo) -> Result { + let cell_hash = info.cmd_code.cell().repr_hash(); + let offset = info.cmd_code.pos(); + match self.hash2function.get(&cell_hash) { + Some(offset_map) => match offset_map.get(&offset) { + Some(pos) => Ok(format!("{}:{}", pos.filename, pos.line)), + None => Err("-:0 (offset not found))") + }, + None => Err("-:0 (cell hash not found)") + } } } impl TraceStepInfo { #[allow(dead_code)] pub fn from(info: &EngineTraceInfo, fname: Option) -> TraceStepInfo { - let stack = info.stack.iter().map( - |x| format!("{}", x) - ).collect(); + let stack = info.stack.iter().map(|x| x.to_string()).collect(); TraceStepInfo { id: info.step, cmd: info.cmd_str.clone(), gas: info.gas_cmd, func: fname, - stack: stack, + stack, } } } -pub fn _save_debug_info( - info: DebugInfo, - filename: String -) { - let s = serde_json::to_string_pretty(&info).unwrap(); - let mut f = std::fs::File::create(filename).unwrap(); - write!(f, "{}", s).unwrap(); -} - pub fn load_debug_info( - state_init: &StateInit, filename: String, verbose: bool, ) -> Option { @@ -96,98 +62,20 @@ pub fn load_debug_info( if verbose { println!("---- load_debug_info ({})----", filename); } - - let mut hash2function = HashMap::new(); - - let debug_info_str = std::fs::read_to_string(filename); - if debug_info_str.is_err() { - return None; - } - let debug_info_json : DebugInfo = serde_json::from_str(&debug_info_str.unwrap()).unwrap(); - - let root_cell = state_init.code.as_ref().unwrap(); - let dict1 = HashmapE::with_hashmap(32, Some(root_cell.reference(0).unwrap())); - let dict2 = HashmapE::with_hashmap(32, Some(root_cell.reference(1).unwrap().reference(0).unwrap())); - - for func in debug_info_json.internals.iter() { - let id = &(func.id as i32); - let key = id.write_to_new_cell().unwrap().into_cell().unwrap().into(); - let val = dict1.get(key).unwrap(); - if val.is_some() { - let val = val.unwrap(); - let mut c = val.cell(); - let mut cc; - loop { - let hash = c.repr_hash(); - hash2function.insert(hash, func.name.clone()); - if c.references_count() == 0 { - break; - } - cc = c.reference(0).unwrap(); - c = &cc; - } - } - } - - for func in debug_info_json.publics.iter() { - let id = &(func.id as u32); - let key = id.write_to_new_cell().unwrap().into_cell().unwrap().into(); - let val = dict1.get(key).unwrap(); - if val.is_some() { - let val = val.unwrap(); - let mut c = val.cell(); - let mut cc; - loop { - let hash = c.repr_hash(); - hash2function.insert(hash, func.name.clone()); - if c.references_count() == 0 { - break; - } - cc = c.reference(0).unwrap(); - c = &cc; - } - } - } - - for func in debug_info_json.privates.iter() { - let id = &(func.id as u32); - let key = id.write_to_new_cell().unwrap().into_cell().unwrap().into(); - if let Some(val) = dict2.get(key).unwrap() { - set_function_hashes(&mut hash2function, &func.name, &val.cell()); + let hash2function = match std::fs::read_to_string(&filename) { + Ok(debug_info_str) => { + serde_json::from_str::(&debug_info_str) + .unwrap_or_else(|err| panic!("cannot parse {} - {}", filename, err)) } - } - - hash2function.insert(root_cell.repr_hash(), "selector".to_owned()); - if let Ok(selector2) = root_cell.reference(1) { - hash2function.insert(selector2.repr_hash(), "selector2".to_owned()); - } - - Some(ContractDebugInfo{hash2function: hash2function}) -} - -// TODO: make member -fn set_function_hashes( - mut hash2function: &mut HashMap, - fname: &String, - cell: &Cell, -) { - let hash = cell.repr_hash(); - hash2function.insert(hash, fname.clone()); - for i in 0..cell.references_count() { - set_function_hashes(&mut hash2function, fname, &cell.reference(i).unwrap()); - } + Err(_) => return None + }; + Some(ContractDebugInfo{hash2function}) } pub fn get_function_name( - debug_info: &Option, - cmd_code: &SliceData, + debug_info: Option<&ContractDebugInfo>, + info: &EngineTraceInfo, ) -> Option { - if let Some(debug_info) = debug_info { - debug_info - .find_function(&cmd_code) - .map(|fname| fname.clone()) - } else { - None - } + debug_info?.find_function(info).ok() } diff --git a/linker_lib/src/exec.rs b/linker_lib/src/exec.rs index 30d4131..5250a73 100644 --- a/linker_lib/src/exec.rs +++ b/linker_lib/src/exec.rs @@ -1,27 +1,26 @@ /* - This file is part of TON OS. + This file is part of Ever OS. - TON OS is free software: you can redistribute it and/or modify + Ever OS is free software: you can redistribute it and/or modify it under the terms of the Apache License 2.0 (http://www.apache.org/licenses/) - Copyright 2019-2021 (c) TON LABS + Copyright 2019-2023 (c) EverX */ -use ed25519_dalek::{ - Keypair, -}; +use ed25519_dalek::Keypair; + +use serde::Serialize; use ton_block::{ Message as TonBlockMessage, MsgAddressInt, StateInit, - GetRepresentationHash, + GetRepresentationHash, Deserializable, }; -use ton_types::{ - Cell, -}; +use ton_types::{Cell, SliceData}; +use crate::decode_cell; use crate::util::{ decode_address, load_from_file, get_msg_value, convert_address, @@ -44,7 +43,7 @@ use crate::call_contract::{ use crate::messages::{ MsgAbiInfo, - MsgInfo, MessageInfo2, + MsgInfo, CallContractMsgInfo, create_bounced_msg, create_inbound_msg, }; @@ -53,24 +52,30 @@ use crate::abi::{ build_abi_body, set_public_key, AbiInfo, }; +use crate::debots::{ + prepare_ext_in_message, debot_build_on_success, debot_build_on_error, +}; + use crate::debug_info::{ TraceStepInfo, }; -#[derive(Default)] +const XMODEM: crc::Crc = crc::Crc::::new(&crc::CRC_16_XMODEM); + +#[derive(Default, Serialize)] pub struct ExecutionResult2 { exit_code: i32, aborted: bool, + stack: Vec, out_actions: Vec, gas: i64, info: Option, pub trace: Option>, + debot_answer_msg: Option, + accept_in_getter: bool, } impl ExecutionResult2 { - pub fn unpack(self) -> (i32, Vec, i64, Option) { - (self.exit_code, self.out_actions, self.gas, self.info) - } fn with_actions(result: ExecutionResult, out_actions: Vec) -> ExecutionResult2 { ExecutionResult2 { exit_code: result.info.exit_code, @@ -78,14 +83,24 @@ impl ExecutionResult2 { gas: result.info.gas, info: result.info_msg, trace: result.trace, - out_actions: out_actions, + stack: result.info.stack, + out_actions, + debot_answer_msg: None, + accept_in_getter: result.info.accept_in_getter, } } fn with_aborted(reason: String) -> ExecutionResult2 { - let mut res = ExecutionResult2::default(); - res.aborted = true; - res.info = Some(reason); - res + ExecutionResult2 { + aborted: true, + info: Some(reason), + ..ExecutionResult2::default() + } + } +} + +impl std::fmt::Display for ExecutionResult2 { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(&serde_json::to_string(self).unwrap()) } } @@ -93,7 +108,7 @@ pub fn generate_contract_address( state_init: &StateInit, wc: i8, ) -> MsgAddressInt { - return convert_address(state_init.hash().unwrap(), wc); + convert_address(state_init.hash().unwrap(), wc) } pub fn deploy_contract_impl( @@ -110,7 +125,7 @@ pub fn deploy_contract_impl( let address = address.unwrap_or(address0); // println!("address = {:?}", address); - if gs.trace { + if gs.is_trace(1) { println!("deploy_contract_impl: {:?} {}", contract_name, address); } @@ -137,7 +152,10 @@ pub fn apply_constructor( abi_info: &AbiInfo, ctor_params : &str, private_key: Option, - trace: bool, + global_gas_limit: u64, + trace_level: u64, + debug: bool, + trace1: bool, trace2: bool, time_header: Option, now: u64, @@ -146,34 +164,39 @@ pub fn apply_constructor( ) -> Result { let keypair = decode_private_key(&private_key); + let address = Some("-1:0000000000000000000000000000000000000000000000000000000000000000".to_string()); - let body = build_abi_body( + let body = SliceData::load_builder(build_abi_body( abi_info, "constructor", ctor_params, time_header, false, // is_internal keypair.as_ref(), - )?; + address, + )?).unwrap(); - let addr = MsgAddressInt::default(); + let address = MsgAddressInt::default(); let contract_info = ContractInfo::create( - addr.clone(), + address.clone(), Some(abi_file.to_string()), state_init, abi_info.clone(), 0, // balance ); - let msg_info = MessageInfo2::with_offchain_ctor( - create_inbound_msg(addr.clone(), &body, now) + let msg_info = CallContractMsgInfo::with_offchain_ctor( + create_inbound_msg(address.clone(), body, now) ); let result = call_contract_ex( + &address, &contract_info, &msg_info, - trace, trace2, + global_gas_limit, + trace_level, + debug, trace1, trace2, None, now, lt, @@ -207,31 +230,54 @@ fn bounce_msg( let contract = gs.get_contract(&msg.src()); let mut msgs = vec![]; - if msg.bounce() && contract.is_some() { - msgs.push(create_bounced_msg2(&gs, &msg, &contract.unwrap().abi_info())); + if let (Some(contract), true) = (contract, msg.bounce()) { + msgs.push(create_bounced_msg2(gs, msg, contract.abi_info())); } else { - increase_dummy_balance(gs, msg.dst(), msg.value()); + increase_dummy_balance(gs, msg.dst(), msg.value().unwrap()); + } + ExecutionResult2 { + out_actions: gs.add_messages(msgs), + ..ExecutionResult2::default() } - let mut result = ExecutionResult2::default(); - result.out_actions = gs.add_messages(msgs); - result } fn dispatch_message_on_error( gs: &mut GlobalState, msg: &MsgInfo, - mut result: ExecutionResult2 + mut result: ExecutionResult2, + gas_fee: u64, ) -> ExecutionResult2 { + if gs.is_trace(5) { + println!("dispatch_message_on_error: msg.value = {:?}, gas_fee = {}", msg.value(), gas_fee); + } + let dst = msg.dst(); let mut contract = gs.get_contract(&dst).unwrap(); - let abi_info = contract.abi_info().clone(); // TODO!!: Arc? - contract.change_balance(-1, msg.value()); + let abi_info = contract.abi_info().clone(); // TODO: Arc? + if msg.value().is_none() { + // the dispatched message comes from Debot, but it should be handled before + println!("!!!!! dispatch_message_on_error: msg.value().is_none()"); + return result; + } + + let msg_value = msg.value().unwrap(); + + if msg_value <= gas_fee { + // no money to send error message, do nothing + if gs.is_trace(1) { + println!("dispatch_message_on_error: no money for reply on error"); + } + return result; + } + + contract.change_balance(-1, msg_value - gas_fee, gs.config.trace_level); gs.set_contract(dst, contract); if msg.bounce() { - let msg = create_bounced_msg2(&gs, &msg, &abi_info); + // TODO: account gas fee here + let msg = create_bounced_msg2(gs, msg, &abi_info); result.out_actions = gs.add_messages(vec![msg]); } @@ -240,67 +286,157 @@ fn dispatch_message_on_error( pub fn dispatch_message_impl( gs: &mut GlobalState, - msg_id: u32, // TODO!: pass MsgInfo instead? + msg_id: u32, // TODO: pass MsgInfo instead? ) -> ExecutionResult2 { - let msg_info = &*gs.messages.get(msg_id); - let ton_msg = &msg_info.ton_msg().unwrap(); + let mut msg_info: MsgInfo = (*gs.messages.get(msg_id)).clone(); + + let ton_msg = msg_info.ton_msg().unwrap().clone(); + + if gs.is_trace(1) { + println!("dispatch_message_impl: msg_id = {}", msg_id); + } + if gs.is_trace(5) { + println!("ton_msg = {:?}", ton_msg); + } + + let mut is_debot_call = false; + let mut debot_call_info = None; + + if ton_msg.ext_in_header().is_some() { // TODO: move this to process_actions? + /* + println!("================="); + println!("src2 = {}", msg_info.src2()); + println!("{:?}", ton_msg); + */ + let (new_msg, info) = + prepare_ext_in_message(ton_msg.clone(), gs.get_now_ms(), gs.debot_keypair.clone()).unwrap(); + msg_info.set_ton_msg(new_msg); + is_debot_call = true; + let mut info = info; + if msg_info.has_src() { + info.debot_addr = Some(msg_info.src()); + } else { + info.debot_addr = Some(msg_info.json.debot_info.as_ref().unwrap().debot_addr.to_int().unwrap()); + } + debot_call_info = Some(info); + + msg_info.debot_call_info = debot_call_info.clone(); + gs.messages.set_debot_call_info(msg_id, debot_call_info.clone().unwrap()); + } let address = msg_info.dst(); if let Some(state_init) = ton_msg.state_init() { if gs.address_exists(&address) { - return bounce_msg(gs, msg_info); + return bounce_msg(gs, &msg_info); } let wc = address.workchain_id() as i8; deploy_contract_impl(gs, None, state_init.clone(), None, AbiInfo::default(), wc, 0).unwrap(); } if !gs.address_exists(&address) { - return bounce_msg(gs, msg_info); + return bounce_msg(gs, &msg_info); } - let result = exec_contract_and_process_actions( + let mut result = exec_contract_and_process_actions( gs, - &MessageInfo2::with_info(&msg_info), + &CallContractMsgInfo::with_info(&msg_info), None, // method + is_debot_call, ); + + if is_debot_call && gs.is_trace(5) { + println!("!!!!!!!!!!!! debot_call_info = {:?}", debot_call_info); + } + + // println!("!!!!!! debot_call_info = {:?}", msg_info.debot_call_info.is_some()); if !is_success_exit_code(result.exit_code) { - dispatch_message_on_error(gs, msg_info, result) + if is_debot_call { + if gs.is_trace(5) { + println!("!!!!!!!!!!!! on_error = {:?}", result.exit_code); + } + let info = debot_call_info.unwrap(); + let src = decode_address(&info.dst_addr); + let dst = info.debot_addr.as_ref().unwrap(); + + let msg = debot_build_on_error(src, dst.clone(), info.onerror_id, result.exit_code as u32); + + let debot_abi = gs.get_contract(dst).unwrap().abi_info().clone(); + + let j = decode_message(gs, &debot_abi, None, &msg, 0, false); + let mut msg_info2 = MsgInfo::create(msg, j); + + msg_info2.debot_call_info = Some(info); + + let msg_info2 = gs.messages.add(msg_info2); + result.debot_answer_msg = Some(msg_info2.json_str()); + + result + } else { + let gas_fee = if gs.config.gas_fee { result.gas*1000 } else { 0 }; + dispatch_message_on_error(gs, &msg_info, result, gas_fee as u64) + } } else { + let out_actions = &result.out_actions; + if is_debot_call && out_actions.is_empty() { + let info = debot_call_info.unwrap(); + let answer_id = info.answer_id; + let src = decode_address(&info.dst_addr); + let dst = info.debot_addr.clone().unwrap(); + + let msg = debot_build_on_success(src, dst.clone(), answer_id); + + let debot_abi = gs.get_contract(&dst).unwrap().abi_info().clone(); + + let j = decode_message(gs, &debot_abi, None, &msg, 0, false); + let mut msg_info2 = MsgInfo::create(msg, j); + + msg_info2.debot_call_info = Some(info); + + let msg_info2 = gs.messages.add(msg_info2); + result.debot_answer_msg = Some(msg_info2.json_str()); + } result } } fn create_bounced_msg2(gs: &GlobalState, msg_info: &MsgInfo, abi_info: &AbiInfo) -> MsgInfo { - let msg2 = create_bounced_msg(&msg_info, gs.get_now()); - let j = decode_message(&gs, &abi_info, None, &msg2, 0); - MsgInfo::create(msg2.clone(), j) + let msg2 = create_bounced_msg(msg_info, gs.get_now()); + let j = decode_message(gs, abi_info, None, &msg2, 0, false); + MsgInfo::create(msg2, j) } pub fn exec_contract_and_process_actions( gs: &mut GlobalState, - msg_info: &MessageInfo2, + msg_info: &CallContractMsgInfo, method: Option, + is_debot_call: bool, ) -> ExecutionResult2 { // TODO: Too long function + if gs.is_trace(5) { + println!("exec_contract_and_process_actions: method={:?}", method); + } - gs.lt = gs.lt + 1; + gs.lt += 1; let address = msg_info.dst(); let mut contract_info = gs.get_contract(&address).unwrap(); if let Some(msg_value) = msg_info.value() { - contract_info.change_balance(1, msg_value); + contract_info.change_balance(1, msg_value, gs.config.trace_level); } let mut result = call_contract_ex( + &address, &contract_info, - &msg_info, - gs.trace, gs.trace_on, - make_config_params(&gs), + msg_info, + gs.config.global_gas_limit, + gs.config.trace_level, + gs.is_trace(5), gs.config.trace_tvm, gs.trace_on, + make_config_params(gs), gs.get_now(), gs.lt, ); @@ -308,18 +444,33 @@ pub fn exec_contract_and_process_actions( gs.last_error_msg = result.info.error_msg.clone(); result.info.inbound_msg_id = msg_info.id(); + gs.register_run_result(result.info.clone()); if result.info_msg == Some("no_accept".to_string()) { return ExecutionResult2::with_actions(result, vec![]) } + let gas_fee = if gs.config.gas_fee && !msg_info.is_getter_call() { + if gs.is_trace(5) { + println!("exec_contract_and_process_actions: charge for gas - {}", result.info.gas); + } + let fee = 1000*result.info.gas as u64; + contract_info.change_balance(-1, fee, gs.config.trace_level); + Some(fee) + } else { + None + }; + let msgs = process_actions( gs, + &address, contract_info, &result, method, msg_info.value(), + is_debot_call, + gas_fee, ); if let Err(reason) = msgs { @@ -343,40 +494,30 @@ pub fn encode_message_body_impl( None, true, None, - ); - - if body.is_err() { - return Err(body.err().unwrap()); - } - - let cell = body.unwrap().into_cell(); - - Ok(cell.unwrap()) + None, + )?; + Ok(body.into_cell().unwrap()) } pub fn call_contract_impl( gs: &mut GlobalState, - address_str: String, - method: String, + address_str: &str, + method: &str, is_getter: bool, is_debot: bool, - params: String, + params: &str, private_key: Option, ) -> Result { // TODO: Too long function - let addr = decode_address(&address_str); - - let contract_info = gs.get_contract(&addr); - - if contract_info.is_none() { - let err = format!("Account does not exist: {}", addr); - return Err(err); - } + let addr = decode_address(address_str); - let contract_info = contract_info.unwrap(); + let contract_info = match gs.get_contract(&addr) { + Some(contract_info) => contract_info, + None => return Err(format!("Account does not exist: {}", addr)) + }; - if gs.trace { - println!("encode_function_call(\"{}\",\"{}\")", method, params); + if gs.is_trace(1) { + println!("call_contract_impl: \"{}\" - \"{}\"", method, params); // println!("private_key {:?}", private_key); } @@ -384,62 +525,103 @@ pub fn call_contract_impl( let abi_info = contract_info.abi_info(); - let body = build_abi_body( + let body = SliceData::load_builder(build_abi_body( abi_info, - &method, - ¶ms, + method, + params, gs.make_time_header(), false, // internal keypair.as_ref(), - ); - - if body.is_err() { - return Err(body.err().unwrap()); - } + Some(address_str.to_string()), + )?).unwrap(); - let body = body.unwrap(); - - let msg = create_inbound_msg(addr.clone(), &body, gs.get_now()); + let msg = create_inbound_msg(addr, body, gs.get_now()); // TODO: move to function - let mut msg_abi = decode_message(&gs, &abi_info, Some(method.clone()), &msg, 0); + let mut msg_abi = decode_message(gs, abi_info, Some(method.to_string()), &msg, 0, false); msg_abi.fix_call(is_getter); let msg_info = MsgInfo::create(msg.clone(), msg_abi); - gs.messages.add(msg_info); + let msg_id = gs.messages.add(msg_info).id(); - let msg_info = MessageInfo2::with_getter(msg, is_getter, is_debot); + let mut msg_info = CallContractMsgInfo::with_getter(msg, is_getter, is_debot); + msg_info.set_id(msg_id); let result = exec_contract_and_process_actions( - gs, &msg_info, Some(method.clone()), + gs, &msg_info, Some(method.to_string()), false, ); Ok(result) } +pub fn run_get_contract_impl( + gs: &mut GlobalState, + address_str: &str, + method: &str, + _params: &str, +) -> Result { + let addr = decode_address(address_str); + + let contract_info = gs.get_contract(&addr) + .ok_or_else(|| format!("Account does not exist: {}", addr))?; + + let crc = XMODEM.checksum(method.as_bytes()); + let function_id = ((crc as u32) & 0xffff) | 0x10000; + + let msg_info = CallContractMsgInfo::with_get_id(function_id); + let result = call_contract_ex( + &address_str.parse().unwrap(), + &contract_info, + &msg_info, + gs.config.global_gas_limit, + gs.config.trace_level, + gs.is_trace(5), gs.config.trace_tvm, gs.trace_on, + make_config_params(gs), + gs.get_now(), + gs.lt, + ); + + // *error_msg = result.info.error_msg.clone(); + + if is_success_exit_code(result.info.exit_code) { + // TODO: check that no action is fired. Add a test + // TODO: remove constructor from dictionary of methods? + Ok(ExecutionResult2::with_actions(result, Vec::new())) + } else { + Err(format!("get failed. ec = {}", result.info.exit_code)) + } +} + +pub fn send_external_message_impl( + gs: &mut GlobalState, + _address_str: &str, + message: &str, +) -> ExecutionResult2 { + let message = TonBlockMessage::construct_from_cell(decode_cell(message)).unwrap(); + let msg_info = CallContractMsgInfo::with_ton_msg(message); + + exec_contract_and_process_actions(gs, &msg_info, None, false) +} + pub fn load_state_init( gs: &mut GlobalState, - contract_file: &String, - abi_file: &String, + contract_file: &str, + abi_file: &str, abi_info: &AbiInfo, ctor_params: &Option, initial_data: &Option, pubkey: &Option, private_key: &Option, - trace: bool, ) -> Result { - let mut state_init = load_from_file(&contract_file); + let mut state_init = load_from_file(contract_file)?; if let Some(pubkey) = pubkey { - let result = set_public_key(&mut state_init, pubkey.clone()); - if result.is_err() { - return Err(result.err().unwrap()); - } + set_public_key(&mut state_init, pubkey.clone())?; } if let Some(initial_data) = initial_data { let new_data = ton_abi::json_abi::update_contract_data( abi_info.text(), - &initial_data, - state_init.data.clone().unwrap_or_default().into(), + initial_data, + SliceData::load_cell(state_init.data.clone().unwrap_or_default()).unwrap(), ).map_err(|e| e.to_string())?; state_init.set_data(new_data.into_cell()); @@ -447,23 +629,22 @@ pub fn load_state_init( if let Some(ctor_params) = ctor_params { let time_header = gs.make_time_header(); - if gs.trace { + if gs.is_trace(3) { println!("apply_constructor: {}", ctor_params); } let mut error_msg = None; let result = apply_constructor( - state_init, &abi_file, &abi_info, &ctor_params, - private_key.clone(), - trace, gs.trace_on, - time_header, gs.get_now(), - gs.lt, - &mut error_msg, - ); + state_init, abi_file, abi_info, ctor_params, + private_key.clone(), + gs.config.global_gas_limit, + gs.config.trace_level, + gs.is_trace(5), gs.config.trace_tvm, gs.trace_on, + time_header, gs.get_now(), + gs.lt, + &mut error_msg, + ); gs.last_error_msg = error_msg; - if result.is_err() { - return Err(result.err().unwrap()); - } - state_init = result.unwrap(); + state_init = result?; } Ok(state_init) } @@ -481,11 +662,22 @@ pub fn decode_message( getter_name: Option, out_msg: &TonBlockMessage, additional_value: u64, + is_debot_call: bool, ) -> MsgAbiInfo { - let mut decoded_msg = decode_body(gs, abi_info, getter_name, out_msg); - if let Some(value) = get_msg_value(&out_msg) { + let mut abi_info = abi_info.clone(); + if let Some(dst) = out_msg.dst_ref() { + if let Some(contract_info) = gs.get_contract(dst) { + abi_info = contract_info.abi_info().clone(); + } + } + let mut decoded_msg = decode_body(gs, &abi_info, getter_name, out_msg, is_debot_call); + if let Some(value) = get_msg_value(out_msg) { decoded_msg.fix_value(value + additional_value); } decoded_msg.fix_timestamp(gs.get_now()); decoded_msg } + +#[cfg(test)] +#[path = "tests/test_exec.rs"] +mod tests; diff --git a/linker_lib/src/global_state.rs b/linker_lib/src/global_state.rs index 69b040d..9ac962b 100644 --- a/linker_lib/src/global_state.rs +++ b/linker_lib/src/global_state.rs @@ -1,26 +1,36 @@ /* - This file is part of TON OS. + This file is part of Ever OS. - TON OS is free software: you can redistribute it and/or modify + Ever OS is free software: you can redistribute it and/or modify it under the terms of the Apache License 2.0 (http://www.apache.org/licenses/) - Copyright 2019-2021 (c) TON LABS + Copyright 2019-2022 (c) TON LABS */ use std::sync::{Arc, Mutex}; use std::collections::HashMap; +use num_format::{Locale, ToFormattedString}; + +use serde::{ + Serialize, Deserialize +}; + +use pyo3::prelude::*; + +use ton_client::crypto::KeyPair; + use ton_block::{ MsgAddressInt, StateInit, }; use ton_types::{ BuilderData, Cell, HashmapE, IBitstring, - HashmapType, + HashmapType, SliceData, }; use crate::util::{ - get_now, + get_now, get_now_ms, }; use crate::call_contract::{ @@ -41,24 +51,43 @@ use crate::debug_info::{ //////////////////////////////////////////////////////////////////////////////////////////// +#[pyclass] +#[derive(Default, Clone, Serialize, Deserialize)] +pub struct GlobalConfig { + #[pyo3(get, set)] + pub trace_level: u64, + #[pyo3(get, set)] + pub trace_tvm: bool, + #[pyo3(get, set)] + pub gas_fee: bool, + #[pyo3(get, set)] + pub global_gas_limit: u64, + // pub trace_on: bool, + // pub config_params: HashMap, + // pub debot_keypair: Option, +} + +//////////////////////////////////////////////////////////////////////////////////////////// + #[derive(Default)] pub struct GlobalState { + pub config: GlobalConfig, contracts: HashMap, pub dummy_balances: HashMap, pub all_abis: AllAbis, pub messages: MessageStorage, - pub trace: bool, pub trace_on: bool, pub last_trace: Option>, pub last_error_msg: Option, pub config_params: HashMap, + pub debot_keypair: Option, now: Option, now2: u64, pub lt: u64, pub runs: Vec, } -lazy_static! { +lazy_static::lazy_static! { pub static ref GLOBAL_STATE: Mutex = Mutex::new(GlobalState::default()); } @@ -82,10 +111,19 @@ impl ContractInfo { ) -> ContractInfo { ContractInfo { addr: address, - name: contract_name.unwrap_or("n/a".to_string()), - state_init: state_init, - abi_info: abi_info, - balance: balance, + name: contract_name.unwrap_or_else(|| "n/a".to_string()), + state_init, + abi_info, + balance, + } + } + pub fn with_address(address: MsgAddressInt) -> ContractInfo { + ContractInfo { + addr: address, + name: "n/a".to_string(), + state_init: StateInit::default(), + abi_info: AbiInfo::default(), + balance: 0, } } pub fn address(&self) -> &MsgAddressInt { @@ -108,11 +146,19 @@ impl ContractInfo { pub fn set_balance(&mut self, balance: u64) { self.balance = balance; } - pub fn change_balance(&mut self, sign: i64, diff: u64) { + pub fn change_balance(&mut self, sign: i64, diff: u64, trace_level: u64) { + if trace_level >= 10 { + println!("!!!!! change_balance: {} {}", sign, diff.to_formatted_string(&Locale::en)); + println!("!!!!! before : {}", self.balance.to_formatted_string(&Locale::en)); + } self.balance = if sign < 0 { + assert!(diff <= self.balance); self.balance - diff } else { self.balance + diff + }; + if trace_level >= 10 { + println!("!!!!! after : {}", self.balance.to_formatted_string(&Locale::en)); } } pub fn state_init(&self) -> &StateInit { @@ -125,8 +171,12 @@ impl ContractInfo { impl GlobalState { + pub fn is_trace(&self, level: u64) -> bool { + level <= self.config.trace_level + } + pub fn set_contract(&mut self, address: MsgAddressInt, info: ContractInfo) { - assert!(address == *info.address()); + assert_eq!(&address, info.address()); self.all_abis.register_abi(info.abi_info().clone()); self.contracts.insert(address, info); @@ -135,11 +185,11 @@ impl GlobalState { self.contracts.remove(address); } pub fn address_exists(&self, address: &MsgAddressInt) -> bool { - self.contracts.contains_key(&address) + self.contracts.contains_key(address) } pub fn get_contract(&self, address: &MsgAddressInt) -> Option { - let state = self.contracts.get(&address); + let state = self.contracts.get(address); state.map(|info| (*info).clone()) } @@ -151,18 +201,22 @@ impl GlobalState { } pub fn get_now(&self) -> u64 { - self.now.unwrap_or(get_now()) + self.now.unwrap_or_else(get_now) } - pub fn make_time_header(&mut self) -> Option { + pub fn get_now_ms(&mut self) -> u64 { if self.now.is_none() { // Add sleep to avoid Replay Protection Error issue std::thread::sleep(std::time::Duration::from_millis(1)); } self.now.map(|v| { self.now2 += 1; - format!("{{\"time\": {}}}", v*1000 + self.now2) - }) + v*1000 + self.now2 + }).unwrap_or_else(get_now_ms) + } + + pub fn make_time_header(&mut self) -> Option { + Some(format!("{{\"time\": {}}}", self.get_now_ms())) } pub fn set_now(&mut self, now: u64) { @@ -187,10 +241,10 @@ pub fn make_config_params(gs: &GlobalState) -> Option { for (key, value) in gs.config_params.clone() { let mut b = BuilderData::new(); b.append_u32(key).unwrap(); - let key = b.into(); + let key = SliceData::load_builder(b).unwrap(); map.setref(key, &value).unwrap(); } - map.data().map(|v| v.clone()) + map.data().cloned() } fn messages_to_out_actions(msgs: Vec>) -> Vec { diff --git a/linker_lib/src/lib.rs b/linker_lib/src/lib.rs index d914708..4551535 100644 --- a/linker_lib/src/lib.rs +++ b/linker_lib/src/lib.rs @@ -1,10 +1,10 @@ /* - This file is part of TON OS. + This file is part of Ever OS. - TON OS is free software: you can redistribute it and/or modify + Ever OS is free software: you can redistribute it and/or modify it under the terms of the Apache License 2.0 (http://www.apache.org/licenses/) - Copyright 2019-2021 (c) TON LABS + Copyright 2019-2023 (c) EverX */ /* @@ -14,26 +14,38 @@ */ -extern crate base64; -extern crate ed25519_dalek; -extern crate hex; -#[macro_use] -extern crate lazy_static; -extern crate num; -extern crate rand; -// #[macro_use] -extern crate serde_json; - -extern crate ton_block; -extern crate ton_types; -#[macro_use] -extern crate ton_vm; -extern crate ton_abi; +use global_state::ContractInfo; +use serde_json::Value as JsonValue; + +use ed25519::Signature; +use ed25519_dalek::{ + Keypair, Signer +}; + +use rand::{rngs::{OsRng, StdRng}, SeedableRng}; + +use pyo3::prelude::{PyModule, PyResult, Python, pyfunction, pymodule}; +use pyo3::wrap_pyfunction; +use pyo3::exceptions::PyRuntimeError; + +use ton_block_json::{parse_config_with_mandatory_params, serialize_known_config_param, SerializationMode}; +use std::sync::Arc; + +use ton_block::{ + Serializable, CurrencyCollection, Deserializable, +}; + +use ton_types::{ + SliceData, Cell, + serialize_toc, + deserialize_tree_of_cells_inmem, +}; mod printer; mod util; mod abi; mod actions; +mod debots; mod debug_info; mod global_state; mod exec; @@ -41,16 +53,20 @@ mod call_contract; mod messages; use global_state::{ - GlobalState, GLOBAL_STATE, + GlobalState, GlobalConfig, GLOBAL_STATE, }; -use ton_block::Serializable; use util::{ decode_address, load_from_file, }; use messages::{ - MessageInfo2, + CallContractMsgInfo, + MsgInfo, +}; + +use debots::{ + build_internal_message, build_external_message, }; use exec::{ @@ -59,44 +75,35 @@ use exec::{ dispatch_message_impl, deploy_contract_impl, call_contract_impl, + run_get_contract_impl, load_state_init, encode_message_body_impl, + decode_message, send_external_message_impl, }; -use serde_json::Value as JsonValue; - -use ed25519_dalek::{ - Keypair, Signer, -}; - -use rand::rngs::OsRng; -use rand::rngs::StdRng; -use rand::SeedableRng; - -use pyo3::prelude::*; -use pyo3::wrap_pyfunction; -use pyo3::exceptions::PyRuntimeError; - -use std::io::Cursor; - -use ton_types::{ - SliceData, - serialize_toc, - cells_serialization::{deserialize_cells_tree}, - BagOfCells -}; - -use std::io::Write; +#[pyfunction] +fn trace_on() -> PyResult<()> { + GLOBAL_STATE.lock().unwrap().trace_on = true; + Ok(()) +} #[pyfunction] -fn set_trace(trace: bool) -> PyResult<()> { - GLOBAL_STATE.lock().unwrap().trace = trace; +fn set_debot_keypair(secret: Option, pubkey: Option) -> PyResult<()> { + let keypair = secret.map(|secret| ton_client::crypto::KeyPair::new(pubkey.unwrap(), secret)); + GLOBAL_STATE.lock().unwrap().debot_keypair = keypair; Ok(()) } #[pyfunction] -fn trace_on() -> PyResult<()> { - GLOBAL_STATE.lock().unwrap().trace_on = true; +fn get_global_config() -> PyResult { + let config = GLOBAL_STATE.lock().unwrap().config.clone(); + Ok(config) +} + +#[pyfunction] +fn set_global_config(cfg: GlobalConfig) -> PyResult<()> { + let mut gs = GLOBAL_STATE.lock().unwrap(); + gs.config = cfg; Ok(()) } @@ -110,10 +117,9 @@ fn gen_addr( wc: i8 ) -> PyResult { let mut gs = GLOBAL_STATE.lock().unwrap(); - let trace = gs.trace; + // let trace = gs.is_trace(1); - let abi_info = gs.all_abis.from_file(&abi_file) - .map_err(|e| PyRuntimeError::new_err(e))?; + let abi_info = gs.all_abis.read_from_file(&abi_file).map_err(PyRuntimeError::new_err)?; let state_init = load_state_init( &mut gs, @@ -124,8 +130,7 @@ fn gen_addr( &initial_data, &pubkey, &private_key, - trace, - ).map_err(|e| PyRuntimeError::new_err(e))?; + ).map_err(PyRuntimeError::new_err)?; let addr = generate_contract_address(&state_init, wc); let addr_str = format!("{}", addr); @@ -146,10 +151,9 @@ fn deploy_contract( balance: u64, ) -> PyResult { let mut gs = GLOBAL_STATE.lock().unwrap(); - let trace = gs.trace; + // let trace = gs.is_trace(1); - let abi_info = gs.all_abis.from_file(&abi_file) - .map_err(|e| PyRuntimeError::new_err(e))?; + let abi_info = gs.all_abis.read_from_file(&abi_file).map_err(PyRuntimeError::new_err)?; let state_init = load_state_init( &mut gs, @@ -160,10 +164,9 @@ fn deploy_contract( &initial_data, &pubkey, &private_key, - trace, - ).map_err(|e| PyRuntimeError::new_err(e))?; + ).map_err(PyRuntimeError::new_err)?; - let target_address = override_address.map(|addr| decode_address(&addr)); + let target_address = override_address.map(decode_address); deploy_contract_impl( &mut gs, Some(contract_file), @@ -172,7 +175,7 @@ fn deploy_contract( abi_info, wc, balance, - ).map_err(|err_str| PyRuntimeError::new_err(err_str)) + ).map_err(PyRuntimeError::new_err) } #[pyfunction] @@ -187,31 +190,46 @@ fn fetch_contract_state(address: String) -> PyResult<(Option, Option PyResult<()> { + let address = decode_address(&address); + let mut gs = GLOBAL_STATE.lock().unwrap(); + let mut contract = match gs.get_contract(&address) { + Some(contract) => contract, + None => ContractInfo::with_address(address.clone()) + }; + let mut state_init = contract.state_init().clone(); + if !code.is_empty() { + state_init.set_code(decode_cell(&code)); + } + if !data.is_empty() { + state_init.set_data(decode_cell(&data)); + } + contract.set_state_init(state_init); + gs.set_contract(address, contract); + + Ok(()) +} + #[pyfunction] fn save_tvc(address: String, filename: String) -> PyResult<()> { let address = decode_address(&address); let gs = GLOBAL_STATE.lock().unwrap(); let contract = gs.get_contract(&address).unwrap(); - let root = contract.state_init().write_to_new_cell() - .map_err(|e| format!("Serialization failed: {}", e)).unwrap() - .into(); - let mut buffer = vec![]; - BagOfCells::with_root(&root).write_to(&mut buffer, false) - .map_err(|e| format!("BOC failed: {}", e)).unwrap(); - - let mut file = std::fs::File::create(&filename).unwrap(); - file.write_all(&buffer).map_err(|e| format!("Write to file failed: {}", e)).unwrap(); - + contract + .state_init() + .write_to_file(&filename) + .unwrap_or_else(|e| panic!("Write to file {} failed: {}", filename, e)); Ok(()) } @@ -240,18 +258,17 @@ fn set_balance(address: String, balance: u64) -> PyResult<()> { } #[pyfunction] -fn dispatch_message(msg_id: u32) -> PyResult<(i32, Vec, i64, Option)> { +fn dispatch_message(msg_id: u32) -> PyResult { let mut gs = GLOBAL_STATE.lock().unwrap(); let result = dispatch_message_impl(&mut gs, msg_id); gs.last_trace = result.trace.clone(); - Ok(result.unpack()) + Ok(result.to_string()) } #[pyfunction] fn set_contract_abi(address_str: Option, abi_file: String) -> PyResult<()> { let mut gs = GLOBAL_STATE.lock().unwrap(); - let abi_info = gs.all_abis.from_file(&abi_file) - .map_err(|e| PyRuntimeError::new_err(e))?; + let abi_info = gs.all_abis.read_from_file(&abi_file).map_err(PyRuntimeError::new_err)?; if let Some(address_str) = address_str { let addr = decode_address(&address_str); let contract_info = gs.get_contract(&addr); @@ -270,22 +287,23 @@ fn set_contract_abi(address_str: Option, abi_file: String) -> PyResult<( fn call_ticktock( address_str: String, is_tock: bool, -) -> PyResult<(i32, Vec, i64, Option)> { +) -> PyResult { let address = decode_address(&address_str); let mut gs = GLOBAL_STATE.lock().unwrap(); // TODO: move to call_ticktock_impl() - let msg_info = MessageInfo2::with_ticktock(is_tock, address.clone()); + let msg_info = CallContractMsgInfo::with_ticktock(is_tock, address); let result = exec_contract_and_process_actions( &mut gs, &msg_info, None, // method + false, // is_debot_call ); // TODO: register in gs.messages? - Ok(result.unpack()) + Ok(result.to_string()) } #[pyfunction] @@ -300,23 +318,49 @@ fn log_str( } #[pyfunction] -fn call_contract( +fn call_contract( // TODO: is this message added to message store? address_str: String, method: String, is_getter: bool, is_debot: bool, params: String, private_key: Option, -) -> PyResult<(i32, Vec, i64, Option)> { +) -> PyResult { let mut gs = GLOBAL_STATE.lock().unwrap(); let result = - call_contract_impl(&mut gs, address_str, method, - is_getter, is_debot, params, private_key); + call_contract_impl(&mut gs, &address_str, &method, + is_getter, is_debot, ¶ms, private_key); if let Ok(ref result) = result { gs.last_trace = result.trace.clone(); } - let result = result.map_err(|e| PyRuntimeError::new_err(e))?; - Ok(result.unpack()) + let result = result.map_err(PyRuntimeError::new_err)?; + Ok(result.to_string()) +} + +#[pyfunction] +fn run_get( + address_str: String, + method: String, + params: String, +) -> PyResult { + let mut gs = GLOBAL_STATE.lock().unwrap(); + match run_get_contract_impl(&mut gs, &address_str, &method, ¶ms) { + Ok(result) => { + gs.last_trace = result.trace.clone(); + Ok(result.to_string()) + } + Err(e) => Err(PyRuntimeError::new_err(e)) + } +} + +#[pyfunction] +fn send_external_message( + address_str: String, + message: String, +) -> PyResult { + let mut gs = GLOBAL_STATE.lock().unwrap(); + let result = send_external_message_impl(&mut gs, &address_str, &message); + Ok(result.to_string()) } // --------------------------------------------------------------------------------------- @@ -340,12 +384,10 @@ fn get_now() -> PyResult { fn set_config_param(idx: u32, cell: String) -> PyResult<()> { let mut gs = GLOBAL_STATE.lock().unwrap(); - let cell = base64::decode(&cell).unwrap(); - let mut csor = Cursor::new(cell); - let cell = deserialize_cells_tree(&mut csor).unwrap().remove(0); + let cell = decode_cell(&cell); let is_empty = cell.bit_length() == 0; - if gs.trace { + if gs.is_trace(1) { println!("set_config_param {} is_empty={}", idx, is_empty); } if is_empty { @@ -357,6 +399,54 @@ fn set_config_param(idx: u32, cell: String) -> PyResult<()> { Ok(()) } +#[pyfunction] +fn parse_config_param(json: String) -> PyResult { + let config_json = serde_json::from_str::(&json) + .unwrap_or_else(|e| panic!("failed to parse {}", e)); + let config_json = config_json.as_object() + .unwrap_or_else(|| panic!("config param is not object")); + let (key, _value) = config_json.iter().next() + .unwrap_or_else(|| panic!("no p parameter")); + let index = key.strip_prefix('p') + .unwrap_or_else(|| panic!("parameter index must start with p")); + let index = index.parse::() + .unwrap_or_else(|err| panic!("param index wrong {}: {}", index, err)); + // let value = value.as_object() + // .unwrap_or_else(|| panic!("param is not object")); + let config_params = parse_config_with_mandatory_params(config_json, &[index]) + .unwrap_or_else(|err| panic!("cannot parse config param {}", err)); + let index = SliceData::load_builder(index.write_to_new_cell().unwrap()).unwrap(); + let cell = match config_params.config_params.get(index) { + Ok(Some(param)) => { + param.reference_opt(0) + .unwrap_or_else(|| panic!("param doesn't have reference")) + } + Ok(None) => panic!("no parameter after parsing"), + Err(err) => panic!("parsing parameter error {}", err), + }; + match ton_types::serialize_toc(&cell) { + Ok(bytes) => Ok(base64::encode(bytes)), + Err(err) => panic!("cannot create TOC {}", err) + } +} + +#[pyfunction] +fn print_config_param(index: u32, cell: String) -> PyResult { + // let bytes = base64::decode(¶m) + // .unwrap_or_else(|err| panic!("cannot parse base64 {}", err)); + // .unwrap_or_else(|err| panic!("cannot deserialize TOC {}", err)); + let cell = decode_cell(&cell); + if cell == Cell::default() { + return Ok("no parameter".to_string()) + } + let mut slice = SliceData::load_cell(cell).unwrap(); + match serialize_known_config_param(index, &mut slice, SerializationMode::Debug) { + Ok(Some(config_param)) => Ok(config_param.to_string()), + Ok(None) => Ok("None".to_string()), + Err(err) => Ok(err.to_string()) + } +} + #[pyfunction] fn reset_all() -> PyResult<()> { use std::ops::DerefMut; @@ -378,26 +468,41 @@ fn make_keypair(seed : Option) -> PyResult<(String, String)> { } }; let secret = keypair.to_bytes(); - let secret = hex::encode(secret.to_vec()); - let public = hex::encode(keypair.public.to_bytes()); + let secret = hex::encode(secret); + let public = hex::encode(keypair.public.as_bytes()); Ok((secret, public)) } #[pyfunction] fn sign_cell(cell: String, secret: String) -> PyResult { - let cell = base64::decode(&cell).unwrap(); - // TODO: util? - let mut csor = Cursor::new(cell); - let cell = deserialize_cells_tree(&mut csor).unwrap().remove(0); + let cell = decode_cell(&cell); + if cell.references_count() != 0 { + return Ok("use sign_cell_hash to sign cell with references".to_string()) + } + + let secret = hex::decode(secret).unwrap(); + let keypair = Keypair::from_bytes(&secret).expect("error: invalid key"); + + let signature = keypair.sign(cell.data()).to_bytes(); + + keypair.verify(cell.data(), &Signature::from_bytes(&signature).unwrap()).unwrap(); + + Ok(hex::encode(signature)) +} + +#[pyfunction] +fn sign_cell_hash(cell: String, secret: String) -> PyResult { + let cell = decode_cell(&cell); let secret = hex::decode(secret).unwrap(); let keypair = Keypair::from_bytes(&secret).expect("error: invalid key"); - let data = SliceData::from(cell).get_bytestring(0); - let signature = keypair.sign(&data).to_bytes(); - let signature = hex::encode(signature.to_vec()); + let hash = cell.repr_hash(); + let signature = keypair.sign(hash.as_slice()).to_bytes(); - Ok(signature) + keypair.verify(hash.as_slice(), &Signature::from_bytes(&signature).unwrap()).unwrap(); + + Ok(hex::encode(signature)) } #[pyfunction] @@ -428,33 +533,162 @@ fn get_last_error_msg() -> PyResult> { Ok(gs.last_error_msg.clone()) } +fn dump_cell_rec(cell: Cell, pfx: String) { + let slice = SliceData::load_cell(cell).unwrap(); + println!("{}> {:x}", pfx, slice); + let n = slice.remaining_references(); + let pfx = pfx + " "; + for i in 0..n { + dump_cell_rec(slice.reference(i).unwrap(), pfx.clone()); + } +} + +#[pyfunction] +fn dump_cell(cell: String) -> PyResult<()> { + let cell = decode_cell(&cell); + // println!("cell = {:?}", cell); + // println!("cell = {}", cell); + // println!("cell = {:x}", cell); + // let slice: SliceData = cell.clone().into(); + + // println!("slice = {:?}", slice); + // println!("slice = {}", slice); + // println!("slice = {:x}", slice); + + dump_cell_rec(cell, "".to_string()); + + Ok(()) +} + +#[pyfunction] +fn load_account_state(address: String, filename: String, abi_file: String) -> PyResult<()> { + let address = address.parse::() + .map_err(|_| PyRuntimeError::new_err(format!("Failed to parse address: {address}")))?; + let mut gs = GLOBAL_STATE.lock().unwrap(); + + let account = ton_block::Account::construct_from_file(&filename) + .map_err(|e| PyRuntimeError::new_err(e.to_string()))?; + + let balance = account.balance().map_or(0, |cc| cc.grams.as_u64().unwrap()); + let state_init = account.state_init().unwrap().to_owned(); + let abi_info = gs.all_abis.read_from_file(&abi_file).map_err(PyRuntimeError::new_err)?; + + let info = ContractInfo::create(address.clone(), None, state_init, abi_info, balance); + + gs.set_contract(address, info); + Ok(()) +} + +#[pyfunction] +fn load_state_cell(filename: String) -> PyResult { + let state_init = load_from_file(&filename).map_err(PyRuntimeError::new_err)?; + let bytes = state_init.write_to_bytes().unwrap(); + Ok(base64::encode(bytes)) +} + #[pyfunction] fn load_code_cell(filename: String) -> PyResult { - let state_init = load_from_file(&filename); - let code = state_init.code.unwrap(); - let bytes = serialize_toc(&code).unwrap(); - Ok(base64::encode(&bytes)) + let state_init = load_from_file(&filename).map_err(PyRuntimeError::new_err)?; + let bytes = serialize_toc(&state_init.code.unwrap()).unwrap(); + Ok(base64::encode(bytes)) } #[pyfunction] fn load_data_cell(filename: String) -> PyResult { // TODO: add tests for that - let state_init = load_from_file(&filename); - let data = state_init.data.unwrap(); - let bytes = serialize_toc(&data).unwrap(); - Ok(base64::encode(&bytes)) + match load_from_file(&filename) { + Ok(state_init) => { + let bytes = serialize_toc(&state_init.data.unwrap()).unwrap(); + Ok(base64::encode(bytes)) + } + Err(e) => Err(PyRuntimeError::new_err(e)) + } +} + +fn decode_cell(cell: &str) -> Cell { + let cell = Arc::new(base64::decode(cell).unwrap()); + deserialize_tree_of_cells_inmem(cell).unwrap() +} + +#[pyfunction] +fn get_compiler_version_from_cell(cell: String) -> PyResult> { + let cell = decode_cell(&cell); + let result = ton_client::boc::get_compiler_version_from_cell(cell).unwrap(); + Ok(result) +} + +#[pyfunction] +fn get_cell_repr_hash(cell: String) -> PyResult { + // TODO: make CellWrapper class interoperable with Python + let cell = decode_cell(&cell); + let hash = cell.repr_hash().as_hex_string(); + Ok(hash) } #[pyfunction] -fn encode_message_body(abi_file: String, method: String, params: String) -> PyResult { +fn get_msg_body(msg_id: u32) -> PyResult { + let gs = GLOBAL_STATE.lock().unwrap(); + let msg_info: MsgInfo = (*gs.messages.get(msg_id)).clone(); + let ton_msg = msg_info.ton_msg().unwrap().clone(); + let body = ton_msg.body().unwrap(); + let body_str = format!("{:?}", body); + Ok(body_str) +} + + +#[pyfunction] +fn encode_message_body(_adress: String, abi_file: String, method: String, params: String) -> PyResult { let mut gs = GLOBAL_STATE.lock().unwrap(); - let abi_info = gs.all_abis - .from_file(&abi_file) - .map_err(|e| PyRuntimeError::new_err(e))?; + let abi_info = gs.all_abis.read_from_file(&abi_file).map_err(PyRuntimeError::new_err)?; let cell = encode_message_body_impl(&abi_info, method, params); let result = serialize_toc(&cell.unwrap()).unwrap(); - Ok(base64::encode(&result)) + Ok(base64::encode(result)) +} + +#[pyfunction] +fn debot_translate_getter_answer(msg_id: u32) -> PyResult { + let mut gs = GLOBAL_STATE.lock().unwrap(); + let msg_info = debots::debot_translate_getter_answer_impl(&mut gs, msg_id) + .map_err(PyRuntimeError::new_err)?; + Ok(msg_info.json_str()) } + +#[pyfunction] +fn build_int_msg(src: String, dst: String, body: String, value: u64) -> PyResult { + let mut gs = GLOBAL_STATE.lock().unwrap(); + + let src = decode_address(&src); + let dst = decode_address(&dst); + + let contract = gs.get_contract(&dst).unwrap(); + + let body = decode_cell(&body); + let body = SliceData::load_cell(body).unwrap(); + + let msg = build_internal_message(src, dst, body, CurrencyCollection::with_grams(value)); + + let j = decode_message(&gs, contract.abi_info(), None, &msg, 0, false); + let msg_info = MsgInfo::create(msg, j); + let msg_info = gs.messages.add(msg_info); + + Ok(msg_info.json_str()) +} + +#[pyfunction] +fn build_ext_msg(src: String, dst: String, body: String) -> PyResult { + let src = src.parse().unwrap(); + let dst = decode_address(&dst); + let body = decode_cell(&body); + let body = SliceData::load_cell(body).unwrap(); + + let msg = build_external_message(src, dst, body); + + let bytes = msg.write_to_bytes().unwrap(); + + Ok(base64::encode(bytes)) +} + + ///////////////////////////////////////////////////////////////////////////////////// /// A Python module implemented in Rust. #[pymodule] @@ -463,33 +697,51 @@ fn linker_lib(_py: Python, m: &PyModule) -> PyResult<()> { m.add_wrapped(wrap_pyfunction!(deploy_contract))?; m.add_wrapped(wrap_pyfunction!(gen_addr))?; + m.add_wrapped(wrap_pyfunction!(run_get))?; m.add_wrapped(wrap_pyfunction!(call_contract))?; m.add_wrapped(wrap_pyfunction!(call_ticktock))?; + m.add_wrapped(wrap_pyfunction!(send_external_message))?; m.add_wrapped(wrap_pyfunction!(log_str))?; m.add_wrapped(wrap_pyfunction!(get_balance))?; m.add_wrapped(wrap_pyfunction!(set_balance))?; m.add_wrapped(wrap_pyfunction!(fetch_contract_state))?; + m.add_wrapped(wrap_pyfunction!(store_contract_state))?; m.add_wrapped(wrap_pyfunction!(dispatch_message))?; + m.add_wrapped(wrap_pyfunction!(debot_translate_getter_answer))?; + m.add_wrapped(wrap_pyfunction!(build_int_msg))?; + m.add_wrapped(wrap_pyfunction!(build_ext_msg))?; + + m.add_wrapped(wrap_pyfunction!(get_global_config))?; + m.add_wrapped(wrap_pyfunction!(set_global_config))?; m.add_wrapped(wrap_pyfunction!(set_now))?; m.add_wrapped(wrap_pyfunction!(get_now))?; - m.add_wrapped(wrap_pyfunction!(set_trace))?; m.add_wrapped(wrap_pyfunction!(trace_on))?; m.add_wrapped(wrap_pyfunction!(set_contract_abi))?; m.add_wrapped(wrap_pyfunction!(set_config_param))?; + m.add_wrapped(wrap_pyfunction!(parse_config_param))?; + m.add_wrapped(wrap_pyfunction!(print_config_param))?; m.add_wrapped(wrap_pyfunction!(make_keypair))?; m.add_wrapped(wrap_pyfunction!(sign_cell))?; + m.add_wrapped(wrap_pyfunction!(sign_cell_hash))?; + m.add_wrapped(wrap_pyfunction!(load_account_state))?; + m.add_wrapped(wrap_pyfunction!(load_state_cell))?; m.add_wrapped(wrap_pyfunction!(load_code_cell))?; m.add_wrapped(wrap_pyfunction!(load_data_cell))?; + m.add_wrapped(wrap_pyfunction!(get_compiler_version_from_cell))?; + m.add_wrapped(wrap_pyfunction!(get_cell_repr_hash))?; m.add_wrapped(wrap_pyfunction!(encode_message_body))?; + m.add_wrapped(wrap_pyfunction!(dump_cell))?; + m.add_wrapped(wrap_pyfunction!(get_msg_body))?; m.add_wrapped(wrap_pyfunction!(get_all_runs))?; m.add_wrapped(wrap_pyfunction!(get_all_messages))?; m.add_wrapped(wrap_pyfunction!(get_last_trace))?; m.add_wrapped(wrap_pyfunction!(get_last_error_msg))?; + m.add_wrapped(wrap_pyfunction!(set_debot_keypair))?; m.add_wrapped(wrap_pyfunction!(save_tvc))?; Ok(()) diff --git a/linker_lib/src/messages.rs b/linker_lib/src/messages.rs index ba1876a..7866041 100644 --- a/linker_lib/src/messages.rs +++ b/linker_lib/src/messages.rs @@ -1,10 +1,10 @@ /* - This file is part of TON OS. + This file is part of Ever OS. - TON OS is free software: you can redistribute it and/or modify + Ever OS is free software: you can redistribute it and/or modify it under the terms of the Apache License 2.0 (http://www.apache.org/licenses/) - Copyright 2019-2021 (c) TON LABS + Copyright 2019-2023 (c) EverX */ use std::sync::Arc; @@ -14,7 +14,6 @@ use serde::{ Serialize, Serializer }; - use ton_block::{ Message as TonBlockMessage, CommonMsgInfo, CurrencyCollection, @@ -22,13 +21,17 @@ use ton_block::{ }; use ton_types::{ - BuilderData, IBitstring, + BuilderData, IBitstring, SliceData, }; use crate::util::{ create_external_inbound_msg, create_internal_msg, }; +use crate::debots::{ + DebotCallInfo, +}; + /////////////////////////////////////////////////////////////////////////////////////// #[derive(Clone, Debug)] @@ -72,20 +75,22 @@ impl Serialize for AddressWrapper { #[derive(Clone, Debug, PartialEq)] pub enum MsgType { - MsgUnknown, - MsgEmpty, - MsgCall, - MsgCallGetter, - MsgExtCall, - MsgAnswer, - MsgEvent, - MsgLog, + Undefined, + Unknown, + Empty, + Call, + CallGetter, + ExtCall, + Answer, + Event, + Log, } // for sending to Python #[derive(Clone, Debug, Default, Serialize)] pub struct MsgInfoJson { id: Option, + pub parent_id: Option, msg_type: MsgType, src: Option, dst: Option, @@ -96,34 +101,44 @@ pub struct MsgInfoJson { bounce: Option, bounced: Option, log_str: Option, + pub debot_info: Option, +} + +// for sending to Python +#[derive(Clone, Debug, Serialize)] +pub struct MsgInfoJsonDebot { + pub debot_addr: AddressWrapper, } // from ABI #[derive(Default, Debug)] pub struct MsgAbiInfo { t: MsgType, - params: Option, - is_getter: Option, + params: Option, + is_getter: Option, name: Option, - value: Option, - timestamp: Option, + value: Option, + timestamp: Option, + is_debot: bool, } -// for storing +// for storing in all messages #[derive(Clone, Debug)] pub struct MsgInfo { ton_msg: Option, - json: MsgInfoJson, + pub json: MsgInfoJson, + pub debot_call_info: Option, } -// for call_context_ex() +// for call_contract_ex() #[derive(Default)] -pub struct MessageInfo2 { // TODO!!: rename? - id: Option, - ton_msg: Option, - dst: Option, - value: Option, - pub ticktock: Option, +pub struct CallContractMsgInfo { // TODO: move + id: Option, + ton_msg: Option, + ton_body: Option, + dst: Option, + value: Option, + pub ticktock: Option, is_getter_call: bool, is_offchain_ctor_call: bool, is_debot_call: bool, @@ -138,21 +153,22 @@ pub struct MessageStorage { impl Default for MsgType { fn default() -> Self { - MsgType::MsgUnknown + MsgType::Undefined } } impl MsgType { fn to_string(&self) -> &str { match self { - Self::MsgUnknown => "unknown", - Self::MsgEmpty => "empty", - Self::MsgCall => "call", - Self::MsgCallGetter => "call_getter", - Self::MsgExtCall => "external_call", - Self::MsgAnswer => "answer", - Self::MsgEvent => "event", - Self::MsgLog => "log", + Self::Undefined => "undefined", + Self::Unknown => "unknown", + Self::Empty => "empty", + Self::Call => "call", + Self::CallGetter => "call_getter", + Self::ExtCall => "external_call", + Self::Answer => "answer", + Self::Event => "event", + Self::Log => "log", } } } @@ -170,10 +186,10 @@ impl Serialize for MsgType { impl MsgInfoJson { fn with_decoded_info(ton_msg: &TonBlockMessage, msg: MsgAbiInfo) -> MsgInfoJson { - let (src, dst) = fetch_src_dst(&ton_msg); + let (src, dst) = fetch_src_dst(ton_msg); // TODO: move - // TODO!!: get_int_header() + // TODO: get_int_header() let (bounce, bounced) = match ton_msg.header() { CommonMsgInfo::IntMsgInfo(header) => (Some(header.bounce), Some(header.bounced)), @@ -182,46 +198,61 @@ impl MsgInfoJson { MsgInfoJson { id: None, + parent_id: None, msg_type: msg.msg_type(), - src: src.clone(), - dst: dst.clone(), + src, + dst, params: msg.params, name: msg.name, value: msg.value, timestamp: msg.timestamp, - bounce: bounce, - bounced: bounced, + bounce, + bounced, log_str: None, + debot_info: None, } } pub fn with_log_str(s: String, timestamp: u64) -> MsgInfoJson { - let mut msg_json = Self::default(); - msg_json.msg_type = MsgType::MsgLog; - msg_json.log_str = Some(s); - msg_json.timestamp = Some(timestamp); - msg_json + MsgInfoJson { + msg_type: MsgType::Log, + log_str: Some(s), + timestamp: Some(timestamp), + ..MsgInfoJson::default() + } } fn to_json(&self) -> JsonValue { - serde_json::to_value(&self).unwrap() + serde_json::to_value(self).unwrap() } } /////////////////////////////////////////////////////////////////////////////////////// -impl MessageInfo2 { +impl CallContractMsgInfo { // TODO: move to call_contract.rs pub fn id(&self) -> Option { - self.id.clone() + self.id + } + + pub fn set_id(&mut self, id: u32) { + self.id = Some(id); } pub fn ton_msg(&self) -> Option<&TonBlockMessage> { self.ton_msg.as_ref() } + pub fn ton_msg_body(&self) -> Option<&SliceData> { + self.ton_body.as_ref() + } + + // pub fn set_ton_msg(&mut self, ton_msg: TonBlockMessage) { + // self.ton_msg = Some(ton_msg); + // } + pub fn value(&self) -> Option { - self.value.clone() + self.value } pub fn dst(&self) -> MsgAddressInt { @@ -229,14 +260,10 @@ impl MessageInfo2 { } pub fn is_ext_msg(&self) -> bool { - match self.ton_msg() { - Some(msg) => { - match msg.header() { - CommonMsgInfo::ExtInMsgInfo(_header) => true, - _ => false - } - }, - None => false + if let Some(msg) = self.ton_msg() { + matches!(msg.header(), CommonMsgInfo::ExtInMsgInfo(_header)) + } else { + false } } @@ -244,41 +271,63 @@ impl MessageInfo2 { self.is_ext_msg() && !self.is_getter_call && !self.is_offchain_ctor_call && !self.is_debot_call } - pub fn with_info(msg: &MsgInfo) -> MessageInfo2 { - let mut info = Self::with_ton_msg(msg.ton_msg().unwrap().clone()); // TODO!: ton_msg to Arc - info.id = Some(msg.id()); - info.value = Some(msg.value()); - info + pub fn is_offchain_ctor_call(&self) -> bool { + self.is_offchain_ctor_call } - pub fn with_ticktock(is_tock: bool, address: MsgAddressInt) -> MessageInfo2 { - let mut msg_info = Self::default(); - msg_info.ticktock = Some(if is_tock { -1 } else { 0 }); - msg_info.dst = Some(address); - msg_info + pub fn is_getter_call(&self) -> bool { + self.is_getter_call } - fn with_ton_msg(msg: TonBlockMessage) -> MessageInfo2 { - let dst = msg.dst().clone().unwrap(); - let mut msg_info = Self::default(); - msg_info.dst = Some(dst); - msg_info.ton_msg = Some(msg); - msg_info + pub fn is_debot_call(&self) -> bool { + self.is_debot_call } - pub fn with_offchain_ctor(msg: TonBlockMessage) -> MessageInfo2 { + pub fn with_ton_msg(msg: TonBlockMessage) -> CallContractMsgInfo { + CallContractMsgInfo { + dst: msg.dst(), + ton_msg: Some(msg), + ..CallContractMsgInfo::default() + } + } + + pub fn with_info(msg: &MsgInfo) -> CallContractMsgInfo { + let mut info = Self::with_ton_msg(msg.ton_msg().unwrap().clone()); // TODO: ton_msg to Arc + info.id = Some(msg.id()); + info.value = msg.value(); + info.is_debot_call = msg.debot_call_info.is_some(); + info + } + + pub fn with_ticktock(is_tock: bool, address: MsgAddressInt) -> CallContractMsgInfo { + CallContractMsgInfo { + ticktock: Some(if is_tock { -1 } else { 0 }), + dst: Some(address), + ..CallContractMsgInfo::default() + } + } + + pub fn with_offchain_ctor(msg: TonBlockMessage) -> CallContractMsgInfo { let mut msg_info = Self::with_ton_msg(msg); msg_info.is_offchain_ctor_call = true; msg_info } - pub fn with_getter(msg: TonBlockMessage, is_getter: bool, is_debot: bool) -> MessageInfo2 { + pub fn with_getter(msg: TonBlockMessage, is_getter: bool, is_debot: bool) -> CallContractMsgInfo { let mut msg_info = Self::with_ton_msg(msg); msg_info.is_getter_call = is_getter; msg_info.is_debot_call = is_debot; msg_info } + pub fn with_get_id(id: u32) -> CallContractMsgInfo { + CallContractMsgInfo { + is_getter_call: true, + id: Some(id), + ..CallContractMsgInfo::default() + } + } + } /////////////////////////////////////////////////////////////////////////////////////// @@ -286,9 +335,10 @@ impl MessageInfo2 { impl MsgAbiInfo { fn with_type(t: MsgType) -> MsgAbiInfo { - let mut j = MsgAbiInfo::default(); - j.t = t; - j + MsgAbiInfo { + t, + ..MsgAbiInfo::default() + } } fn with_params(msg_type: MsgType, params: String, name: String) -> MsgAbiInfo { @@ -301,19 +351,19 @@ impl MsgAbiInfo { fn msg_type(&self) -> MsgType { match self.is_getter { Some(is_getter) => { - assert!(MsgType::MsgCall == self.t); - if is_getter { MsgType::MsgCallGetter } else { MsgType::MsgExtCall } + assert!(MsgType::Call == self.t); + if is_getter { MsgType::CallGetter } else { MsgType::ExtCall } }, None => self.t.clone() } } pub fn create_empty() -> MsgAbiInfo { - MsgAbiInfo::with_type(MsgType::MsgEmpty) + MsgAbiInfo::with_type(MsgType::Empty) } pub fn create_unknown() -> MsgAbiInfo { - MsgAbiInfo::with_type(MsgType::MsgUnknown) + MsgAbiInfo::with_type(MsgType::Unknown) } fn set_params(&mut self, s: String) { @@ -322,19 +372,19 @@ impl MsgAbiInfo { } pub fn create_answer(s: String, method: String) -> MsgAbiInfo { - Self::with_params(MsgType::MsgAnswer, s, method) + Self::with_params(MsgType::Answer, s, method) } pub fn create_call(s: String, method: String) -> MsgAbiInfo { - Self::with_params(MsgType::MsgCall, s, method) + Self::with_params(MsgType::Call, s, method) } pub fn create_event(s: String, event: String) -> MsgAbiInfo { - Self::with_params(MsgType::MsgEvent, s, event) + Self::with_params(MsgType::Event, s, event) } pub fn fix_call(&mut self, is_getter: bool) { - assert!(self.t == MsgType::MsgCall); + assert!(self.t == MsgType::Call); self.is_getter = Some(is_getter); } @@ -345,6 +395,10 @@ impl MsgAbiInfo { pub fn fix_timestamp(&mut self, timestamp: u64) { self.timestamp = Some(timestamp); } + + pub fn set_debot_mode(&mut self) { + self.is_debot = true; + } } @@ -354,27 +408,39 @@ impl MsgInfo { pub fn create(ton_msg: TonBlockMessage, msg: MsgAbiInfo) -> MsgInfo { let msg_json = MsgInfoJson::with_decoded_info(&ton_msg, msg); - MsgInfo { ton_msg: Some(ton_msg), json: msg_json } + MsgInfo { ton_msg: Some(ton_msg), json: msg_json, debot_call_info: None } } pub fn with_log_str(text: String, timestamp: u64) -> MsgInfo { let msg_json = MsgInfoJson::with_log_str(text, timestamp); - MsgInfo { ton_msg: None, json: msg_json } + MsgInfo { ton_msg: None, json: msg_json, debot_call_info: None } } pub fn id(&self) -> u32 { self.json.id.unwrap() } + pub fn parent_id(&self) -> Option { + self.json.parent_id + } + pub fn ton_msg(&self) -> Option<&TonBlockMessage> { self.ton_msg.as_ref() } - pub fn src(&self) -> MsgAddressInt { // TODO!: use AddressWrapper + pub fn set_ton_msg(&mut self, ton_msg: TonBlockMessage) { + self.ton_msg = Some(ton_msg); + } + + pub fn src(&self) -> MsgAddressInt { // TODO: use AddressWrapper self.json.src.as_ref().unwrap().to_int().unwrap() } - pub fn dst(&self) -> MsgAddressInt { // TODO!: use AddressWrapper + pub fn has_src(&self) -> bool { + self.json.src.as_ref().is_some() + } + + pub fn dst(&self) -> MsgAddressInt { // TODO: use AddressWrapper self.json.dst.as_ref().unwrap().to_int().unwrap() } @@ -395,8 +461,13 @@ impl MsgInfo { self.json.id = Some(id); } - pub fn value(&self) -> u64 { - self.json.value.unwrap() + pub fn set_parent_id(&mut self, parent_id: u32) { + assert!(self.json.parent_id.is_none()); + self.json.parent_id = Some(parent_id); + } + + pub fn value(&self) -> Option { + self.json.value } } @@ -454,7 +525,13 @@ impl MessageStorage { self.messages[id as usize].clone() } pub fn to_json(&self) -> JsonValue { - self.messages.iter().map(|msg| msg.json().clone()).collect() + self.messages.iter().map(|msg| msg.json()).collect() + } + pub fn set_debot_call_info(&mut self, id: u32, debot_call_info: DebotCallInfo) { + let mut msg_info = (*self.get(id)).clone(); + msg_info.debot_call_info = Some(debot_call_info); + let msg_info = Arc::new(msg_info); + self.messages[id as usize] = msg_info; } } @@ -463,7 +540,7 @@ impl MessageStorage { pub fn create_bounced_msg(msg: &MsgInfo, now: u64) -> TonBlockMessage { let ton_msg = msg.ton_msg().unwrap().clone(); - let msg_value = msg.value(); + let msg_value = msg.value().unwrap(); let bounce = msg.bounce(); assert!(bounce); @@ -474,7 +551,7 @@ pub fn create_bounced_msg(msg: &MsgInfo, now: u64) -> TonBlockMessage { // TODO: handle possible overflow here b.append_bytestring(&body).unwrap(); } - let body = b.into(); + let body = SliceData::load_builder(b).unwrap(); create_internal_msg( msg.dst(), @@ -489,15 +566,15 @@ pub fn create_bounced_msg(msg: &MsgInfo, now: u64) -> TonBlockMessage { pub fn create_inbound_msg( addr: MsgAddressInt, - body: &BuilderData, + body: SliceData, now: u64, ) -> TonBlockMessage { - create_inbound_msg_impl(-1, &body, addr, now).unwrap() + create_inbound_msg_impl(-1, body, addr, now).unwrap() } fn create_inbound_msg_impl( // TODO: this function is used in only one place selector: i32, - body: &BuilderData, + body: SliceData, dst: MsgAddressInt, now: u64 ) -> Option { @@ -515,7 +592,7 @@ fn create_inbound_msg_impl( // TODO: this function is used in only one p CurrencyCollection::with_grams(0), 1, now as u32, - Some(body.into()), + Some(body), bounced, )) }, @@ -528,14 +605,14 @@ fn create_inbound_msg_impl( // TODO: this function is used in only one p None => { // TODO: Use MsgAdressNone? MsgAddressExt::with_extern( - BuilderData::with_raw(vec![0x55; 8], 64).unwrap().into() + SliceData::from_raw(vec![0x55; 8], 64) ).unwrap() }, }; Some(create_external_inbound_msg( src, dst, - Some(body.into()), + Some(body), )) }, _ => None, diff --git a/linker_lib/src/printer.rs b/linker_lib/src/printer.rs index 8ac1bc3..084c3fa 100644 --- a/linker_lib/src/printer.rs +++ b/linker_lib/src/printer.rs @@ -1,7 +1,7 @@ /* - This file is part of TON OS. + This file is part of Ever OS. - TON OS is free software: you can redistribute it and/or modify + Ever OS is free software: you can redistribute it and/or modify it under the terms of the Apache License 2.0 (http://www.apache.org/licenses/) Copyright 2019-2021 (c) TON LABS @@ -14,8 +14,8 @@ use ton_types::Cell; #[allow(dead_code)] pub fn state_init_printer(state: &StateInit) -> String { format!("StateInit\n split_depth: {}\n special: {}\n data: {}\n code: {}\n lib: {}\n", - state.split_depth.as_ref().map(|x| format!("{:?}", x)).unwrap_or("None".to_string()), - state.special.as_ref().map(|x| format!("{:?}", x)).unwrap_or("None".to_string()), + state.split_depth.as_ref().map_or_else(|| "None".to_string(), |x| format!("{:?}", x)), + state.special.as_ref().map_or_else(|| "None".to_string(), |x| format!("{:?}", x)), tree_of_cells_into_base64(state.data.as_ref()), tree_of_cells_into_base64(state.code.as_ref()), tree_of_cells_into_base64(state.library.root()), @@ -37,22 +37,11 @@ fn tree_of_cells_into_base64(root_cell: Option<&Cell>) -> String { #[allow(dead_code)] pub fn msg_printer(msg: &Message) -> String { format!("message header\n{}init : {}\nbody : {}\nbody_hex: {}\nbody_base64: {}\n", - print_msg_header(&msg.header()), - msg.state_init().as_ref().map(|x| { - format!("{}", state_init_printer(x)) - }).unwrap_or("None".to_string()), - match msg.body() { - Some(slice) => format!("{:.2}", slice.into_cell()), - None => "None".to_string(), - }, - msg.body() - .map(|b| hex::encode(b.get_bytestring(0))) - .unwrap_or("None".to_string()), - tree_of_cells_into_base64( - msg.body() - .map(|slice| slice.into_cell()) - .as_ref(), - ), + print_msg_header(msg.header()), + msg.state_init().map_or_else(|| "None".to_string(), state_init_printer), + msg.body().map_or_else(|| "None".to_string(), |slice| format!("{:.2}", slice.into_cell())), + msg.body().map_or_else(|| "None".to_string(), |b| hex::encode(b.get_bytestring(0))), + tree_of_cells_into_base64(msg.body().map(|slice| slice.into_cell()).as_ref()), ) } diff --git a/linker_lib/src/tests/test_exec.rs b/linker_lib/src/tests/test_exec.rs new file mode 100644 index 0000000..f92a1bf --- /dev/null +++ b/linker_lib/src/tests/test_exec.rs @@ -0,0 +1,153 @@ +/* + This file is part of Ever OS. + + Ever OS is free software: you can redistribute it and/or modify + it under the terms of the Apache License 2.0 (http://www.apache.org/licenses/) + + Copyright 2019-2022 (c) TON LABS +*/ + +use std::sync::Arc; + +use crate::sign_cell_hash; + +use super::*; +use serde_json::Value; +use ton_block::{Deserializable, ExternalInboundMessageHeader, Serializable}; +use ton_types::{deserialize_tree_of_cells_inmem, serialize_toc}; + +const GRAM: u64 = 1_000_000_000; + +fn deploy_abi_config(gs: &mut GlobalState, address: &str) { + let address = decode_address(address); + let contract_file = "boc/Config.tvc".to_string(); + let abi_file = "boc/Config.abi.json".to_string(); + let abi_info = gs.all_abis.read_from_file(&abi_file).unwrap(); + let ctor_params = serde_json::json!({ + "elector_addr" : "0x3333333333333333333333333333333333333333333333333333333333333333", + "elect_for" : 65536, + "elect_begin_before" : 32768, + "elect_end_before" : 8192, + "stake_held" : 32768, + "max_validators" : 3, + "main_validators" : 100, + "min_validators" : 3, + "min_stake" : 10 * GRAM, + "max_stake" : 10000 * GRAM, + "min_total_stake" : 100 * GRAM, + "max_stake_factor" : 0x30000, + "utime_since" : 0, + "utime_until" : 86400, + }).to_string(); + + let state_init = load_state_init( + gs, + &contract_file, + &abi_file, + &abi_info, + &Some(ctor_params), + &None, + &None, + &None, + ).unwrap(); + let contract = ContractInfo::create(address.clone(), None, state_init, abi_info, 1 << 32); + gs.set_contract(address, contract); +} + +fn get_answer(result: ExecutionResult2) -> Value { + let answer: Value = result.out_actions[0].parse().unwrap(); + assert_eq!(answer["msg_type"], "answer"); + answer["params"]["value0"].clone() +} + +fn run_method(gs: &mut GlobalState, address_str: &str, method: &str, params: Value) -> Value { + let result = call_contract_impl( + gs, + address_str, + method, + true, + false, + ¶ms.to_string(), + None + ).unwrap(); + get_answer(result) +} + +#[test] +fn test_run_get_config() { + let mut gs = crate::global_state::GLOBAL_STATE.lock().unwrap(); + + let abi_config = "-1:7777777777777777777777777777777777777777777777777777777777777777"; + deploy_abi_config(&mut gs, abi_config); + + // gs.trace_on = true; + // gs.config.trace_tvm = true; + let address_str = "-1:5555555555555555555555555555555555555555555555555555555555555555"; + let address = decode_address(address_str); + + // private and public key + let secret = "e0989f07c5c61b85d5977a1d7b45d661a332408785a89ae5cce192af78fd7bca3f43df88f25976a25a8dd1355c46e751503556e966d94ce8172105abc0d68f47"; + let state_init = StateInit::construct_from_file("boc/Config.FunC.tvc").unwrap(); + let abi_file = "boc/Config.abi.json".to_string(); + let abi_info = gs.all_abis.read_from_file(&abi_file).unwrap(); + let contract = ContractInfo::create(address.clone(), None, state_init, abi_info, 1 << 32); + gs.set_contract(address, contract); + let result = run_get_contract_impl(&mut gs, address_str, "seqno", "").unwrap(); + assert_eq!(result.stack, vec!("18")); + + let msg_seqno: u32 = result.stack[0].parse().unwrap(); + + // prepare cell for siging + let method = "upgrade_code_sign_helper"; + let state_init = StateInit::construct_from_file("boc/Config.tvc").unwrap(); + let code = state_init.code.unwrap(); + let code = base64::encode(serialize_toc(&code).unwrap()); + let params = serde_json::json!({ + "msg_seqno": msg_seqno, + "valid_until": 2000000001, + "code": code, + }); + let answer = run_method(&mut gs, abi_config, method, params); + let cell = answer.as_str().unwrap().to_string(); + let signature = sign_cell_hash(cell, secret.to_string()).unwrap(); + assert_eq!(128, signature.len(), "{}", signature); + + // prepare cell with message body + let method = "upgrade_code_func_builder"; + let params = serde_json::json!({ + "signature": signature, + "msg_seqno": msg_seqno, + "valid_until": 2000000001, + "code": code, + }); + let answer = run_method(&mut gs, abi_config, method, params); + let body = answer.as_str().unwrap().to_string(); + let body = SliceData::load_cell(decode_cell(&body)).unwrap(); + + let src = "".parse().unwrap(); + let dst = address_str.parse().unwrap(); + let h = ExternalInboundMessageHeader::new(src, dst); + let message = TonBlockMessage::with_ext_in_header_and_body(h, body); + let message = message.write_to_bytes().unwrap(); + let message = base64::encode(message); + gs.config.trace_tvm = true; + // call external message to upgrade code + let _result = send_external_message_impl(&mut gs, address_str, &message); + // let cell = base64::decode(answer.as_str().unwrap()).unwrap(); + // let cell = deserialize_tree_of_cells_inmem(Arc::new(cell)).unwrap(); + // panic!("{}", result.to_string()) +} + +#[test] +fn test_call_contract_config() { + let mut gs = crate::global_state::GLOBAL_STATE.lock().unwrap(); + let address_str = "-1:5555555555555555555555555555555555555555555555555555555555555555"; + deploy_abi_config(&mut gs, address_str); + let method = "get_config_1"; + let result = call_contract_impl(&mut gs, address_str, method, true, false, "{}", None).unwrap(); + let answer = get_answer(result); + let bytes = base64::decode(answer.as_str().unwrap()).unwrap(); + let cell = deserialize_tree_of_cells_inmem(Arc::new(bytes)).unwrap(); + let addr = hex::encode(cell.data()); + assert_eq!("3333333333333333333333333333333333333333333333333333333333333333", &addr); +} diff --git a/linker_lib/src/util.rs b/linker_lib/src/util.rs index cd2906f..6e70619 100644 --- a/linker_lib/src/util.rs +++ b/linker_lib/src/util.rs @@ -1,42 +1,39 @@ /* - This file is part of TON OS. + This file is part of Ever OS. - TON OS is free software: you can redistribute it and/or modify + Ever OS is free software: you can redistribute it and/or modify it under the terms of the Apache License 2.0 (http://www.apache.org/licenses/) - Copyright 2019-2021 (c) TON LABS + Copyright 2019-2022 (c) TON LABS */ -use num::{BigInt}; -use crate::num::ToPrimitive; +use num_format::{Locale, ToFormattedString, ToFormattedStr}; -use std::io::Cursor; use std::time::SystemTime; -use std::str::FromStr; -use ton_types::{ - UInt256, SliceData, AccountId, - cells_serialization::{deserialize_cells_tree} -}; +use ton_types::{UInt256, SliceData, AccountId, Cell}; use ton_block::{ CommonMsgInfo, MsgAddressIntOrNone, CurrencyCollection, Deserializable, ExternalInboundMessageHeader, Grams, InternalMessageHeader, Message, MsgAddressExt, MsgAddressInt, - StateInit, UnixTime32, + StateInit, Account, }; -pub fn bigint_to_u64(n: &BigInt) -> u64 { - n.to_biguint().unwrap().to_u64().unwrap() +pub fn grams_to_u64(n: &Grams) -> Option { + let value = n.as_u128(); + if value <= u64::MAX as u128 { + Some(value as u64) + } else { + None + } } pub fn get_msg_value(msg: &Message) -> Option { - let grams = &msg.get_value()?.grams; - let grams = bigint_to_u64(&grams.value()); - Some(grams) + grams_to_u64(&msg.get_value()?.grams) } -fn get_src_addr_mut<'a>(msg: &'a mut Message) -> Option<&'a mut MsgAddressIntOrNone> { +fn get_src_addr_mut(msg: &mut Message) -> Option<&mut MsgAddressIntOrNone> { match msg.header_mut() { CommonMsgInfo::IntMsgInfo(hdr) => Some(&mut hdr.src), CommonMsgInfo::ExtOutMsgInfo(hdr) => Some(&mut hdr.src), @@ -45,7 +42,11 @@ fn get_src_addr_mut<'a>(msg: &'a mut Message) -> Option<&'a mut MsgAddressIntOrN } pub fn substitute_address(mut msg: Message, address: &MsgAddressInt) -> Message { - let src = get_src_addr_mut(&mut msg).unwrap(); + let src = get_src_addr_mut(&mut msg); + if src.is_none() { // possible for debots + return msg; + } + let src = src.unwrap(); if *src == MsgAddressIntOrNone::None { *src = MsgAddressIntOrNone::Some(address.clone()); } @@ -56,22 +57,26 @@ pub fn convert_address(address: UInt256, wc: i8) -> MsgAddressInt { MsgAddressInt::with_standart(None, wc, AccountId::from(address)).unwrap() } -pub fn load_from_file(contract_file: &str) -> StateInit { - let content = std::fs::read(contract_file) - .map_err(|e| format!("Cannot load {}: {}", contract_file, e)) - .unwrap(); // TODO!: return error - let mut csor = Cursor::new(content); - let cell = deserialize_cells_tree(&mut csor).unwrap().remove(0); - StateInit::construct_from(&mut cell.into()).unwrap() +pub fn load_from_file(contract_file: &str) -> Result { + let cell = Cell::read_from_file(contract_file); + if let Ok(state_init) = StateInit::construct_from_cell(cell.clone()) { + Ok(state_init) + } else if let Ok(account) = Account::construct_from_cell(cell) { + match account.state_init() { + Some(state_init) => Ok(state_init.clone()), + None => Err(format!("account is bad from {}", contract_file)) + } + } else { + Err(format!("bad file for state {}", contract_file)) + } } pub fn create_external_inbound_msg(src_addr: MsgAddressExt, dst_addr: MsgAddressInt, body: Option) -> Message { - let mut hdr = ExternalInboundMessageHeader::default(); - hdr.dst = dst_addr; - hdr.src = src_addr; - hdr.import_fee = Grams(0x1234u32.into()); // TODO: what's that? + let hdr = ExternalInboundMessageHeader::new(src_addr, dst_addr); let mut msg = Message::with_ext_in_header(hdr); - *msg.body_mut() = body; + if let Some(body) = body { + msg.set_body(body); + } msg } @@ -94,17 +99,26 @@ pub fn create_internal_msg( hdr.ihr_disabled = true; hdr.ihr_fee = Grams::from(0u64); hdr.created_lt = lt; - hdr.created_at = UnixTime32(at); + hdr.created_at = at.into(); let mut msg = Message::with_int_header(hdr); - *msg.body_mut() = body; + if let Some(body) = body { + msg.set_body(body); + } msg } pub fn get_now() -> u64 { - SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs() as u64 + SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs() +} + +pub fn get_now_ms() -> u64 { + SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_millis() as u64 } -pub fn decode_address(address: &String) -> MsgAddressInt { - MsgAddressInt::from_str(&address).unwrap() +pub fn decode_address(address: impl AsRef) -> MsgAddressInt { + address.as_ref().parse().unwrap() } +pub fn format3(value: T) -> String { + value.to_formatted_string(&Locale::en) +} diff --git a/tonos_ts4/BaseContract.py b/tonos_ts4/BaseContract.py index e79c2d5..daa0a22 100644 --- a/tonos_ts4/BaseContract.py +++ b/tonos_ts4/BaseContract.py @@ -1,21 +1,62 @@ -import os +""" + This file is part of Ever OS. + + Ever OS is free software: you can redistribute it and/or modify + it under the terms of the Apache License 2.0 (http://www.apache.org/licenses/) -from . import globals -from . import ts4 + Copyright 2019-2023 (c) EverX +""" -from .address import * -from .abi import * -from .global_functions import * +import os + +import globals + +from address import * +from abi import * +from core import * +from decoder import * +from dump import * +from exception import * +from global_functions import * + +def _build_params_dict(args, inputs): + if len(args) != len(inputs): + raise BaseException('Wrong parameters count: expected {}, got {}'.format(len(inputs), len(args))) + d = dict() + for i in range(len(args)): + t = AbiType(inputs[i]) + d[t.name] = args[i] + return d + +def _build_getter_wrapper(contract, method, inputs, mode): + def func0(*args): + return contract.call_getter(method, _build_params_dict(args, inputs)) + def func1(*args): + return contract.call_method(method, _build_params_dict(args, inputs)) + def func2(*args): + return contract.call_method_signed(method, _build_params_dict(args, inputs)) + + if mode == 0: return func0 + if mode == 1: return func1 + if mode == 2: return func2 + +class Getters: + def __init__(self, contract, mode): + assert isinstance(contract, BaseContract) + abi = contract.abi + for rec in abi.json['functions']: + method = rec['name'] + setattr(self, method, _build_getter_wrapper(contract, method, rec['inputs'], mode)) class BaseContract: """The :class:`BaseContract ` object, which is responsible for deploying contracts and interaction with deployed contracts. """ def __init__(self, - name, - ctor_params, - wc = 0, + name: str, + ctor_params = None, initial_data = None, + wc = 0, address = None, override_address = None, pubkey = None, @@ -42,9 +83,9 @@ def __init__(self, :param num balance: Desired contract balance :param str nickname: Nickname of the contract used in verbose output """ + if name.startswith('debots:'): + name = os.path.join(os.path.dirname(__file__), name.replace('debots:', 'debots/')) self.name_ = name - full_name = os.path.join(globals.G_TESTS_PATH, name) - just_deployed = False p_n = '' if nickname == None else f'({nickname})' if override_address is not None: Address.ensure_address(override_address) @@ -57,43 +98,70 @@ def __init__(self, balance = either_or(balance, globals.G_DEFAULT_BALANCE) # Load ABI - self.abi = Abi(name) + exception = None + try: + self.abi = Abi(name) + except FileNotFoundError as err: + exception = FileNotFoundError(str(err)) + raise exception if address is None: if globals.G_VERBOSE: - print(blue(f'Deploying {full_name} {p_n}')) + print(blue(f'Deploying {name} {p_n}')) - if ctor_params is not None: - ctor_params = ts4.check_method_params(self.abi, 'constructor', ctor_params) + exception = None + try: + if ctor_params is not None: + ctor_params = check_method_params(self.abi, 'constructor', ctor_params) + if initial_data is not None: + initial_data = check_method_params(self.abi, '.data', initial_data) + except Exception as err: + exception = translate_exception(err) + raise exception - if initial_data is not None: - initial_data = ts4.check_method_params(self.abi, '.data', initial_data) if pubkey is not None: - assert pubkey[0:2] == '0x' + # assert pubkey[0:2] == '0x' pubkey = pubkey.replace('0x', '') try: - address = globals.core.deploy_contract( - full_name + '.tvc', - full_name + '.abi.json', - ts4.json_dumps(ctor_params) if ctor_params is not None else None, - ts4.json_dumps(initial_data) if initial_data is not None else None, - pubkey, - private_key, - wc, - override_address, - balance, - ) - except: - err_msg = globals.core.get_last_error_msg() - if err_msg is not None: - ts4.verbose_(err_msg) - raise + address = deploy_contract_ext(self, ctor_params, initial_data, pubkey, None, wc, override_address, balance) + except RuntimeError as err: + tvm_err_msg = globals.core.get_last_error_msg() + if tvm_err_msg is not None: + verbose_(tvm_err_msg) + exception = BaseException(err) + raise exception address = Address(address) just_deployed = True + else: + assert isinstance(address, Address) + globals.core.load_account_state(address.str(), make_path(name, '.boc'), make_path(name.split('.', 1)[0], '.abi.json')) + just_deployed = False self._init2(name, address, just_deployed = just_deployed) if nickname is not None: - ts4.register_nickname(self.address, nickname) + register_nickname(self.address, nickname) + + if globals.G_GENERATE_GETTERS: + self._generate_wrappers() + + @property + def abi_path(self): + """Returns path to contract ABI file. + + :return: Path to ABI file + :rtype: str + """ + return self.abi.path_ + + @property + def tvc_path(self) -> str: + """Returns path to contract TVC file. + + :return: Path to TVC file + :rtype: str + """ + + return make_path(self.name_, '.tvc') @property def abi_json(self): @@ -102,13 +170,13 @@ def abi_json(self): def _init2(self, name, address, nickname = None, just_deployed = False): Address.ensure_address(address) self.addr_ = address - if not just_deployed: - if globals.G_VERBOSE: - print(blue('Creating wrapper for ' + name)) - globals.core.set_contract_abi(self.address.str(), self.abi.path_) + # if not just_deployed: + # if globals.G_VERBOSE: + # print(blue('Creating wrapper for ' + name)) + # globals.core.set_contract_abi(self.address.str(), self.abi.path_) if globals.G_ABI_FIXER is not None: - ts4.fix_abi(self.name_, self.abi_json, globals.G_ABI_FIXER) + fix_abi(self.name_, self.abi_json, globals.G_ABI_FIXER) @property def balance(self): @@ -117,10 +185,10 @@ def balance(self): :return: Account balance :rtype: num """ - return ts4.get_balance(self.address) + return get_balance(self.address) @property - def address(self): + def address(self) -> Address: """Returns address of a given contract. :return: Address of contract @@ -129,7 +197,7 @@ def address(self): return self.addr_ @property - def addr(self): + def addr(self) -> Address: """Returns address of a given contract. Shorter version of `address`. :return: Address of contract @@ -137,12 +205,35 @@ def addr(self): """ return self.addr_ + @property + def hex_addr(self) -> str: + """Returns hexadecimal representation of address of a given contract without workchain + + :return: Address of contract + :rtype: str + """ + return '0x' + self.addr.str().split(':')[1] + def ensure_balance(self, v, dismiss = False): # TODO: is this method needed here? - ts4.ensure_balance(v, self.balance, dismiss) + ensure_balance(v, self.balance, dismiss) - def call_getter_raw(self, method, params = dict(), expect_ec = 0): - """Calls a given getter and returns an answer in raw JSON format. + def prepare_getter_params(self, method: str, params = dict(), expect_ec = 0): + params = check_method_params(self.abi, method, params) + + if globals.G_VERBOSE and globals.G_SHOW_GETTERS: + print(green(' getter') + grey(': -> ') + bright_cyan(format_addr(self.addr))) + print(cyan(grey(' method: ') + bright_cyan('{}'.format(method)))) + if params != dict(): + print(cyan(grey(' params: ') + bright_cyan(Params.stringify(params)))) + + assert isinstance(method, str) + assert isinstance(params, dict) + assert isinstance(expect_ec, int) + return params + + def run_get(self, method: str, params = dict(), expect_ec = 0, decoder = None) -> dict: + """Calls a given getter for funcC contract and returns an answer in raw JSON format. :param str method: Name of a getter :param dict params: A dictionary with getter parameters @@ -151,31 +242,50 @@ def call_getter_raw(self, method, params = dict(), expect_ec = 0): :return: Message parameters :rtype: JSON """ + params = self.prepare_getter_params(method, params, expect_ec) - params = ts4.check_method_params(self.abi, method, params) + result = run_get(self.addr, method, params) + values = self.analyze_getter_answer(result, expect_ec, method) + assert isinstance(values, dict) - if globals.G_VERBOSE and globals.G_SHOW_GETTERS: - print(green(' getter') + grey(': ') + bright_cyan(format_addr(self.addr)), end='') - print(cyan(grey('\n method: ') + bright_cyan('{}'.format(method)))) + if expect_ec > 0: + # TODO: ensure values is empty? + return - assert isinstance(method, str) - assert isinstance(params, dict) - assert isinstance(expect_ec, int) + if decoder is None: + decoder = Decoder.defaults() + decoder.fill_nones(decoder) + + # print('values =', values) + answer = decode_contract_answer(self.abi, values, method, None, decoder) + return answer + # return make_params(answer) if decode else answer + + def call_getter_raw(self, method, params = dict(), expect_ec = 0) -> dict: + """Calls a given getter and returns an answer in raw JSON format. + + :param str method: Name of a getter + :param dict params: A dictionary with getter parameters + :param num expect_ec: Expected exit code. Use non-zero value + if you expect a getter to raise an exception + :return: Message parameters + :rtype: JSON + """ + params = self.prepare_getter_params(method, params, expect_ec) + + result = call_contract_ext(self.addr, method, params, is_getter = True) + values = self.analyze_getter_answer(result, expect_ec, method) + assert isinstance(values, dict) + return values - result = globals.core.call_contract( - self.addr.str(), - method, - True, # is_getter - False, # is_debot - ts4.json_dumps(params), - None, # private_key - ) + def analyze_getter_answer(self, result: ExecutionResult, expect_ec = 0, method = None): + if result.data['accept_in_getter'] and globals.G_WARN_ON_ACCEPT_IN_GETTER: + print(yellow('WARNING! Accept in getter!')) - result = ExecutionResult(result) assert eq(None, result.error) - # print(actions) + # print(result.actions) - ts4.check_exitcode(expect_ec, result.exit_code) + check_exitcode([expect_ec], result.exit_code) if expect_ec != 0: return @@ -184,14 +294,16 @@ def call_getter_raw(self, method, params = dict(), expect_ec = 0): for msg in actions: if not msg.is_answer(): - raise Exception("Unexpected message type '{}' in getter output".format(msg.type)) + raise BaseException("Unexpected message type '{}' in getter output".format(msg.type)) + + if len(result.actions) == 0: + raise BaseException("Getter '{}' returns no answer".format(method)) assert eq(1, len(result.actions)), 'len(actions) == 1' - msg = Msg(json.loads(result.actions[0])) - assert msg.is_answer(method) + msg = actions[0] if globals.G_VERBOSE and globals.G_SHOW_GETTERS: - print(f"{grey(' result: ')} {cyan(Params.stringify(msg.params))}\n") + print(f"{grey(' result:')} {cyan(Params.stringify(msg.params))}\n") return msg.params @@ -217,25 +329,34 @@ def call_getter(self, :return: A returned value in decoded form (exact type depends on the type of getter) :rtype: type """ - values = self.call_getter_raw(method, params, expect_ec) + exception = None + try: + values = self.call_getter_raw(method, params, expect_ec) + except Exception as err: + exception = translate_exception(err) + raise exception if expect_ec > 0: # TODO: ensure values is empty? return + if decoder is None: + decoder = Decoder.defaults() + decoder.fill_nones(decoder) + if decode_ints is not None: - deprecated_msg = "Parameter is deprecated. Use `decoder = ts4.Decoder(ints = {})` instead.".format(decode_ints) - assert False, red(deprecated_msg) + decoder.ints = decode_ints + # deprecated_msg = "Parameter is deprecated. Use `decoder = Decoder(ints = {})` instead.".format(decode_ints) + # assert False, red(deprecated_msg) if decode_tuples is not None: - deprecated_msg = "Parameter is deprecated. Use `decoder = ts4.Decoder(tuples = {})` instead.".format(decode_tuples) + deprecated_msg = "Parameter is deprecated. Use `decoder = Decoder(tuples = {})` instead.".format(decode_tuples) assert False, red(deprecated_msg) if dont_decode_fields is not None: - deprecated_msg = "Parameter is deprecated. Use `decoder = ts4.Decoder(skip_fields = ...)` instead." + deprecated_msg = "Parameter is deprecated. Use `decoder = Decoder(skip_fields = ...)` instead." assert False, red(deprecated_msg) - decoder = either_or(decoder, ts4.decoder).fill_nones(ts4.decoder) # print('values =', values) answer = decode_contract_answer(self.abi, values, method, key, decoder) @@ -256,7 +377,7 @@ def decode_event(self, event_msg): assert event_def is not None, red('Cannot find event: {}'.format(event_name)) - # decoder = either_or(decoder, ts4.decoder).fill_nones(ts4.decoder) + # decoder = either_or(decoder, decoder).fill_nones(decoder) return decode_event_inputs(event_def, values) @@ -277,56 +398,86 @@ def call_method(self, method, params = dict(), private_key = None, expect_ec = 0 """ # TODO: check param types. In particular, that `private_key` looks correct. # Or introduce special type for keys... + assert isinstance(params, dict) if globals.G_VERBOSE: - print(blue('> ext_in_msg') + grey(': '), end='') - print(cyan(' '), grey('->'), bright_cyan(format_addr(self.addr))) - print(cyan(grey(' method: ') + bright_cyan('{}'.format(method)) - + grey('\n params: ') + cyan('{}'.format(Params.stringify(prettify_dict(params))))) + '\n') + print_ext_in_msg(self.addr, method, params) - params = ts4.check_method_params(self.abi, method, params) + exception = None + try: + params = check_method_params(self.abi, method, params) + except Exception as err: + exception = translate_exception(err) + raise exception try: - result = globals.core.call_contract( - self.addr.str(), - method, - False, # is_getter - is_debot, - ts4.json_dumps(params), - private_key, - ) - result = ExecutionResult(result) - except: + result = call_contract_ext(self.addr, method, params, is_debot = is_debot, private_key = private_key) + except RuntimeError as err: if globals.G_VERBOSE: - print("Exception when calling '{}' with params {}".format(method, ts4.json_dumps(params))) - raise + print(err.__repr__()) + print("Exception when calling '{}' with params {}".format(method, json_dumps(params))) + exception = BaseException(str(err)) + raise exception + + return self.analyze_call_result(result, expect_ec, method) + + def send_external_raw(self, message: Cell, expect_ec = 0): + assert isinstance(message, Cell) + + try: + result = globals.core.send_external_message(self.addr.str(), message.raw_) + except RuntimeError as err: + if globals.G_VERBOSE: + print(err.__repr__()) + print("Exception when sending raw external message", message) + raise BaseException(str(err)) + + self.analyze_call_result(ExecutionResult(result), expect_ec) + + def analyze_call_result(self, result: ExecutionResult, expect_ec = 0, method = None): + if isinstance(expect_ec, int): + expect_ec = [expect_ec] + + globals.G_LAST_GAS_USED = result.gas_used + + globals.time_shift() if result.error == 'no_accept': severity = 'ERROR' if globals.G_STOP_ON_NO_ACCEPT else 'WARNING' err_msg = '{}! No ACCEPT in the contract method `{}`'.format(severity, method) - assert not globals.G_STOP_ON_NO_ACCEPT, red(err_msg) + if globals.G_STOP_ON_NO_ACCEPT: + raise BaseException(red(err_msg)) verbose_(err_msg) elif result.error == 'no_account': severity = 'ERROR' if globals.G_STOP_ON_NO_ACCOUNT else 'WARNING' err_msg = '{}! Account doesn\'t exist: `{}`'.format(severity, self.addr.str()) - assert not globals.G_STOP_ON_NO_ACCOUNT, err_msg + if globals.G_STOP_ON_NO_ACCOUNT: + raise BaseException(red(err_msg)) verbose_(err_msg) elif result.error == 'no_funds': severity = 'ERROR' if globals.G_STOP_ON_NO_FUNDS else 'WARNING' err_msg = '{}! Not enough funds on: `{}`'.format(severity, self.addr.str()) - assert not globals.G_STOP_ON_NO_FUNDS, err_msg + if globals.G_STOP_ON_NO_FUNDS: + raise BaseException(red(err_msg)) verbose_(err_msg) else: - _gas, answer = ts4.process_actions(result, expect_ec) + try: + _gas, answer = process_actions(result, expect_ec) + except Exception as err: + exception = translate_exception(err) + raise exception if answer is not None: assert answer.is_answer(method) key = None - decoded_answer = decode_contract_answer(self.abi, answer.params, method, key, ts4.decoder) - if globals.G_AUTODISPATCH: - ts4.dispatch_messages() - if answer is not None: + decoded_answer = decode_contract_answer(self.abi, answer.params, method, key, decoder) + if globals.G_AUTODISPATCH: + try: + dispatch_messages() + except Exception as err: + exception = translate_exception(err) + raise exception return decoded_answer - + return None def call_method_signed(self, method, params = dict(), expect_ec = 0): """Calls a given method using contract's private key. @@ -348,10 +499,9 @@ def ticktock(self, is_tock): :rtype: num """ if globals.G_VERBOSE: - print('ticktock {}'.format(format_addr(self.address))) - result = globals.core.call_ticktock(self.address.str(), is_tock) - result = ExecutionResult(result) - gas, answer = ts4.process_actions(result) + print_tick_tock(self.address, is_tock) + result = call_ticktock_ext(self.address, is_tock) + gas, answer = process_actions(result) assert answer is None return gas @@ -367,13 +517,24 @@ def keypair(self): """ return (self.private_key_, self.public_key_) + def _generate_wrappers(self): + self.g = Getters(self, 0) # getter + self.m = Getters(self, 1) # method + self.ms = Getters(self, 2) # signed method + # assert False + def _make_tuple_result(abi, method, values, decoder): + assert isinstance(abi, Abi) + assert isinstance(decoder, Decoder) types = abi.find_getter_output_types(method) res_dict = {} res_arr = [] for type in types: - value = ts4.decode_json_value(values[type.name], type, decoder) + if type.name in decoder.skip_fields: + value = values[type.name] + else: + value = decode_json_value(values[type.name], type, decoder) res_dict[type.name] = value res_arr.append(value) if decoder.tuples is True: @@ -382,11 +543,11 @@ def _make_tuple_result(abi, method, values, decoder): return res_dict def decode_contract_answer( - abi, - values, + abi: Abi, + values: dict, method, key, - params, + decoder, ): keys = list(values.keys()) @@ -394,7 +555,7 @@ def decode_contract_answer( key = keys[0] if key is None: - return _make_tuple_result(abi, method, values, params) + return _make_tuple_result(abi, method, values, decoder) assert key is not None assert key in values, red("No '{}' in {}".format(key, values)) @@ -402,5 +563,146 @@ def decode_contract_answer( value = values[key] abi_type = abi.find_getter_output_type(method, key) - return ts4.decode_json_value(value, abi_type, params) + return decode_json_value(value, abi_type, decoder) + +def process_actions(result: ExecutionResult, expect_ec = [0]): + # print('process_actions: expect_ec = {}'.format(expect_ec)) + assert isinstance(expect_ec, list) + assert isinstance(result, ExecutionResult) + ec = result.exit_code + + if globals.G_VERBOSE: + if ec != 0: + print(grey(' exit_code: ') + yellow(ec) + '\n') + + if ec not in expect_ec: + verbose_(globals.core.get_last_error_msg()) + + check_exitcode(expect_ec, ec) + + if result.error is not None: + raise BaseException("Transaction aborted: {}".format(result.error)) + + answer = None + + for j in result.actions: + # print(j) + msg = Msg(json.loads(j)) + # if globals.G_VERBOSE: + # print('process msg:', msg) + # print('process msg:', msg, msg.is_event()) + if msg.is_event(): + if globals.G_VERBOSE or globals.G_SHOW_EVENTS: + # TODO: move this printing code to a separate function and file + xtra = '' + params = msg.params + if msg.is_event('DebugEvent'): + xtra = ' ={}'.format(decode_int(params['x'])) + elif msg.is_event('LogEvent'): + params['comment'] = bytearray.fromhex(params['comment']).decode() + print(bright_blue('< event') + grey(': '), end='') + print(cyan(' '), grey('<-'), bright_cyan(format_addr(msg.src))) + print(cyan(grey(' name: ') + cyan('{}'.format(bright_cyan(msg.event))))) + print(grey(' params: ') + cyan(Params.stringify(params)), cyan(xtra), '\n') + globals.EVENTS.append(msg) + else: + # not event + if msg.is_unknown(): + #print(msg) + if globals.G_VERBOSE: + body_str = ellipsis(globals.core.get_msg_body(msg.id), 64) + print(yellow('WARNING! Unknown message!' + 'body = {}'.format(body_str))) + elif msg.is_bounced(): + pass + elif msg.is_answer(): + # We expect only one answer + assert answer is None + answer = msg + continue + else: + assert msg.is_call() or msg.is_empty(), red('Unexpected type: {}'.format(msg.type)) + globals.QUEUE.append(msg) + return (result.gas_used, answer) + +def dispatch_messages(callback = None, limit = None, expect_ec = [0]): + """Dispatches all messages in the queue one by one until the queue becomes empty. + + :param callback: Callback to be called for each processed message. + If callback returns False then the given message is skipped. + :param num limit: Limit the number of processed messages by a given value. + :param num expect_ec: List of expected exit codes + :return: False if queue was empty, True otherwise + :rtype: bool + """ + count = 0 + while len(globals.QUEUE) > 0: + count = count + 1 + msg = peek_msg() + if callback is not None and callback(msg, False) == False: + pop_msg() + continue + dispatch_one_message(expect_ec) + if callback is not None: + callback(msg, True) + if limit is not None: + if count >= limit: + break + return count > 0 + +def dispatch_one_message(expect_ec = 0, src = None, dst = None): + """Takes first unprocessed message from the queue and dispatches it. + Use `expect_ec` parameter if you expect non-zero exit code. + + :param num expect_ec: Expected exit code + :return: The amount of gas spent on the execution of the transaction + :rtype: num + """ + if isinstance(expect_ec, int): + expect_ec = [expect_ec] + assert isinstance(expect_ec, list) + msg = pop_msg() + globals.ALL_MESSAGES.append(msg) + dump1 = globals.G_VERBOSE or globals.G_DUMP_MESSAGES + dump2 = globals.G_MSG_FILTER is not None and globals.G_MSG_FILTER(msg.data) + if dump1 or dump2: + print_int_msg(msg) + if msg.dst.is_none(): + # TODO: a getter's reply. Add a test for that + return + if src is not None: + assert eq(src.addr, msg.src) + + if dst is not None: + assert eq(dst.addr, msg.dst) + + # dump_struct(msg.data) + + # CONFIRM_INPUT_ADDR = Address('-31:16653eaf34c921467120f2685d425ff963db5cbb5aa676a62a2e33bfc3f6828a') + # if msg.dst == CONFIRM_INPUT_ADDR: + # verbose_('!!!!!!!!!!!!') + + result = dispatch_message_ext(msg.id) + + globals.G_LAST_GAS_USED = result.gas_used + + # print('actions =', result.actions) + exception = None + try: + gas, answer = process_actions(result, expect_ec) + except Exception as err: + exception = translate_exception(err) + raise exception + if result.debot_answer_msg is not None: + answer_msg = Msg(json.loads(result.debot_answer_msg)) + # verbose_(answer_msg) + globals.QUEUE.append(answer_msg) + if answer is not None: + # verbose_('debot_answer = {}'.format(answer)) + translated_msg = globals.core.debot_translate_getter_answer(answer.id) + # verbose_('translated_msg = {}'.format(translated_msg)) + translated_msg = Msg(json.loads(translated_msg)) + # verbose_(translated_msg) + globals.QUEUE.append(translated_msg) + + return gas diff --git a/tonos_ts4/__init__.py b/tonos_ts4/__init__.py index e69de29..36fcabe 100644 --- a/tonos_ts4/__init__.py +++ b/tonos_ts4/__init__.py @@ -0,0 +1,9 @@ +""" + This file is part of Ever OS. + + Ever OS is free software: you can redistribute it and/or modify + it under the terms of the Apache License 2.0 (http://www.apache.org/licenses/) + + Copyright 2019-2023 (c) EverX +""" + diff --git a/tonos_ts4/abi.py b/tonos_ts4/abi.py index 2caf864..c8f5e98 100644 --- a/tonos_ts4/abi.py +++ b/tonos_ts4/abi.py @@ -1,17 +1,22 @@ -# TODO: add header +""" + This file is part of Ever OS. -import copy + Ever OS is free software: you can redistribute it and/or modify + it under the terms of the Apache License 2.0 (http://www.apache.org/licenses/) -from .util import * -from .address import * + Copyright 2019-2023 (c) EverX +""" -from . import ts4 +import copy +import re +from address import * +from util import * class Abi: - def __init__(self, contract_name): - self.contract_name_ = contract_name - self.path_ = ts4.make_path(contract_name, '.abi.json') + def __init__(self, contract_name: str): + self.contract_name_ = contract_name.split('.')[0] + self.path_ = make_path(self.contract_name_, '.abi.json') with open(self.path_, 'rb') as fp: self.json = json.load(fp) @@ -112,7 +117,7 @@ def decode_event_inputs(event_def, values): name = type.name value = values[name] if not type.dont_decode: - value = ts4.decode_json_value(value, type, ts4.decoder) + value = decode_json_value(value, type, decoder) res[name] = value return Params(res) @@ -121,35 +126,35 @@ def decode_event_inputs(event_def, values): def check_method_params(abi, method, params): assert isinstance(abi, Abi) - # ts4.verbose('check_method_params {}'.format(params)) + # verbose('check_method_params {} {}'.format(method, params)) if method == '.data': inputs = abi.json['data'] else: func = abi.find_abi_method(method) if func is None: - raise Exception("Unknown method name '{}'".format(method)) + raise BaseException("Unknown method name '{}'".format(method)) inputs = func['inputs'] res = {} for param in inputs: pname = param['name'] if pname not in params: - # ts4.verbose('Raising exception') + # verbose('Raising exception') if globals.G_VERBOSE: print('params =', params) - raise Exception("Parameter '{}' is missing when calling method '{}'".format(pname, method)) - # ts4.dump_struct(param) - # ts4.dump_struct(params[pname]) + raise BaseException("Parameter '{}' is missing when calling method '{}'".format(pname, method)) + # dump_struct(param) + # dump_struct(params[pname]) res[pname] = check_param_names_rec(params[pname], AbiType(param)) return res def _raise_type_mismatch(expected_type, value, abi_type): msg = 'Expected {}, got {}'.format(expected_type, value.__repr__()) - if ts4.globals.G_CHECK_ABI_TYPES: - if ts4.globals.G_VERBOSE: - ts4.verbose_('Expected type: {}'.format(abi_type)) - raise Exception(msg) + if globals.G_CHECK_ABI_TYPES: + if globals.G_VERBOSE: + verbose_('Expected type: {}'.format(abi_type)) + raise BaseException(msg) else: - ts4.verbose_(msg) + verbose_(msg) def create_AbiType(type_str, abi_type): assert isinstance(abi_type, AbiType) @@ -167,6 +172,9 @@ def check_param_names_rec(value, abi_type): return value if abi_type.is_array(): + if not isinstance(value, list): + _raise_type_mismatch('list', value, abi_type) + type2 = abi_type.remove_array() value2 = [] for v in value: @@ -174,7 +182,7 @@ def check_param_names_rec(value, abi_type): value2.append(v2) return value2 - # print(ts4.red(value.__str__()), ts4.yellow(value.__repr__())) + # print(red(value.__str__()), yellow(value.__repr__())) if type == 'bool': if not isinstance(value, bool): @@ -200,18 +208,19 @@ def check_param_names_rec(value, abi_type): if type == 'bytes': if isinstance(value, str): - return Bytes(str2bytes(value)) + return Bytes(value) if isinstance(value, Bytes): return value - _raise_type_mismatch('string', value, abi_type) + _raise_type_mismatch('bytes', value, abi_type) if type == 'tuple': - assert isinstance(value, dict) + if not isinstance(value, dict): + _raise_type_mismatch('dict', value, abi_type) res = {} for c in abi_type.components: field = c.name if not field in value: - raise Exception("Field '{}' is missing in structure '{}'".format(field, abi_type.name)) + raise BaseException("Field '{}' is missing in structure '{}'".format(field, abi_type.name)) res[field] = check_param_names_rec(value[field], c) return res @@ -233,6 +242,6 @@ def check_param_names_rec(value, abi_type): return res print(type, value) - ts4.verbose_("Unsupported type to encode '{}'".format(type)) + verbose_("Unsupported type to encode '{}'".format(type)) return value diff --git a/tonos_ts4/address.py b/tonos_ts4/address.py index 08c0a08..297917b 100644 --- a/tonos_ts4/address.py +++ b/tonos_ts4/address.py @@ -1,8 +1,15 @@ -import json +""" + This file is part of Ever OS. + + Ever OS is free software: you can redistribute it and/or modify + it under the terms of the Apache License 2.0 (http://www.apache.org/licenses/) -from . import globals -from . import ts4 -from .util import * + Copyright 2019-2023 (c) EverX +""" + +import json +import globals +from util import * class Address: """The :class:`Address
` object, which contains an @@ -95,6 +102,7 @@ def ensure_address(addr): class Bytes(): """The :class:`Bytes ` object, which represents bytes type. + if string is symbols - need to call str2bytes to get hexadecimal representation """ def __init__(self, value): """Constructs :class:`Bytes ` object. @@ -135,7 +143,13 @@ def __str__(self): return self.__repr__() def __repr__(self): - return "Cell('{}')".format(self.raw_) + return "Cell('{}')".format(self.short_raw()) + + def short_raw(self): + t = self.raw_ + if (len(t) > 16): + t = t[:13] + '...' + return t def __eq__(self, other): if isinstance(other, Cell): @@ -289,7 +303,7 @@ def dump_data(self): dump_struct(self.data) def __str__(self): - return ts4.dump_struct_str(self.data) + return dump_struct_str(self.data) class Params: @@ -307,18 +321,23 @@ def transform(self, params): value = params[key] if isinstance(value, dict): value = Params(value) - if isinstance(value, Bytes) and ts4.decoder.strings is True: + if isinstance(value, Bytes) and decoder.strings is True: value = str(value) if isinstance(value, list): value = [self.tr(x) for x in value] setattr(self, key, value) @staticmethod - def stringify(d): + def stringify(d, max_str_len = 67): arr = [] for k, v in d.items(): if isinstance(v, dict): arr.append(f'{(k)}: {{{Params.stringify(v)}}}') + elif isinstance(v, str): + v = v if len(v) <= max_str_len else v[:max_str_len] + '...' + arr.append(f'{(k)}: {(v)}') + elif isinstance(v, Bytes): + arr.append(f'{(k)}: {(v)!r}') else: arr.append(f'{(k)}: {(v)}') @@ -345,14 +364,6 @@ def make_params(data): return data -class ExecutionResult: - def __init__(self, result): - (ec, actions, gas, err) = result - self.exit_code = ec - self.actions = actions - self.gas_used = gas - self.error = err - def prettify_dict(d, max_str_len = 67): nd = {} for k, v in d.items(): @@ -361,7 +372,9 @@ def prettify_dict(d, max_str_len = 67): elif isinstance(v, str): nd[k] = v if len(v) <= max_str_len else v[:max_str_len] + '...' elif isinstance(v, Address): - nd[k] = ts4.format_addr(v, compact = False) + nd[k] = format_addr(v, compact = False) + elif isinstance(v, Cell): + nd[k] = v.short_raw() else: nd[k] = v diff --git a/tonos_ts4/core.py b/tonos_ts4/core.py new file mode 100644 index 0000000..cd0504f --- /dev/null +++ b/tonos_ts4/core.py @@ -0,0 +1,246 @@ +""" + This file is part of Ever OS. + + Ever OS is free software: you can redistribute it and/or modify + it under the terms of the Apache License 2.0 (http://www.apache.org/licenses/) + + Copyright 2019-2023 (c) EverX +""" + +import json + +import globals +from address import * +from abi import * +from dump import * +from exception import check_exitcode +from global_functions import * +from util import * + +########################################################################### +## Public functions + +def enable_fees(value): + """Enables gas consumtion accounting in the balance. + + :param bool value: `True` to enable gas accounting + """ + assert isinstance(value, bool) + cfg = globals.core.get_global_config() + cfg.gas_fee = value + globals.core.set_global_config(cfg) + +def set_balance(target, value): + """Sets balance for a given account. + + :param Address target: Target address + :param num value: Desired balance + """ + ensure_address(target) + globals.core.set_balance(target.str(), int(value)) + +def set_trace_level(level): + """Sets the trace level for `core`. + + :param num value: desired trace level. Set `0` to disable trace logging + """ + cfg = globals.core.get_global_config() + cfg.trace_level = level + globals.core.set_global_config(cfg) + +def set_trace_tvm(value: bool): + """Enables TVM tracing. + + :param bool value: `True` to enable TVM tracing + """ + cfg = globals.core.get_global_config() + cfg.trace_tvm = value + globals.core.set_global_config(cfg) + +def set_global_gas_limit(value): + """Sets global gas limit. + + :param num value: Desired global gas limit + """ + cfg = globals.core.get_global_config() + cfg.global_gas_limit = value + globals.core.set_global_config(cfg) + +def get_cell_repr_hash(cell): + """Calculates hash of a given `Cell`. + + :param Cell cell: Cell to be hashed + :return: Hexadecimal representation of the hash of the given cell + :rtype: str + """ + assert isinstance(cell, Cell) + return '0x' + globals.core.get_cell_repr_hash(cell.raw_) + +########################################################################### +## Internal functions + +class ExecutionResult: + def __init__(self, result): + assert isinstance(result, str) + result = json.loads(result) + # dump_struct(result) + self.data = result + self.exit_code = result['exit_code'] + self.actions = result['out_actions'] + self.gas_used = result['gas'] + self.error = result['info'] + self.debot_answer_msg = result['debot_answer_msg'] + +def dispatch_message_ext(msg_id): + result = globals.core.dispatch_message(msg_id) + return ExecutionResult(result) + +# func contract getter +def run_get(addr, method, params): + assert isinstance(addr, Address) + assert isinstance(params, dict) + result = globals.core.run_get( + addr.str(), method, json_dumps(params), + ) + result = json.loads(result) + if len(result["out_actions"]) == 0: + stack = result.get("stack") + assert isinstance(stack, list) + params = dict() + for i in range(len(stack)): + params["value%d" % i] = stack[i] + + result["out_actions"] = [json_dumps(dict( + id = 1, + parent_id = 0, + msg_type = "answer", + src = addr, + dst = "", + name = method, + params = params, + timestamp = globals.time_get(), + log_str = "" + # value = None, + ))] + result = json_dumps(result) + return ExecutionResult(result) + +def call_contract_ext(addr, method, params, is_getter = False, is_debot = False, private_key = None) -> ExecutionResult: + assert isinstance(addr, Address) + assert isinstance(params, dict) + result = globals.core.call_contract( + addr.str(), method, is_getter, is_debot, json_dumps(params), private_key, + ) + return ExecutionResult(result) + +def call_getter(addr, method, params, is_debot = False) -> str: + assert isinstance(addr, Address) + assert isinstance(params, dict) + return globals.core.call_getter(addr.str(), method, json_dumps(params), is_debot) + +def call_ticktock_ext(addr, is_tock): + result = globals.core.call_ticktock(addr.str(), is_tock) + return ExecutionResult(result) + +def deploy_contract_ext(contract, ctor_params, initial_data, pubkey, private_key, wc, override_address, balance): + address = globals.core.deploy_contract( + contract.tvc_path, + contract.abi_path, + json_dumps(ctor_params) if ctor_params is not None else None, + json_dumps(initial_data) if initial_data is not None else None, + pubkey, + private_key, + wc, + override_address, + balance, + ) + return address + +def parse_config_param(p: dict): + assert isinstance(p, dict) + cell = globals.core.parse_config_param(json_dumps(p)) + try: + result = ExecutionResult(cell) + except: + return Cell(cell) + check_exitcode([0], result.exit_code) + return None + +def gen_addr(name, initial_data = None, keypair = None, wc = 0): + """Generates contract addresss. + + :param str name: Name used to load contract's bytecode and ABI + :param dict initial_data: Initial data for the contract (static members) + :param keypair: Keypair containing private and public keys + :param num wc: workchain_id to deploy contract to + :return: Expected contract address + :rtype: Address + """ + if keypair is not None: + # TODO: copy-paste below! + (private_key, pubkey) = keypair + if pubkey is not None: + assert pubkey[0:2] == '0x' + pubkey = pubkey.replace('0x', '') + else: + (private_key, pubkey) = (None, None) + + abi = Abi(name) + + if initial_data is not None: + initial_data = check_method_params(abi, '.data', initial_data) + + result = globals.core.gen_addr( + make_path(name, '.tvc'), + abi.path_, + json_dumps(initial_data) if initial_data is not None else None, + pubkey, + private_key, + wc + ) + return Address(result) + +def encode_message_body(address: Address, abi_name: str, method, params): + """Encode given message body. + + :param str abi_name: The contract name the ABI of which should be used for encoding + :param str method: A name of the encoded method + :param dict params: A dictionary with parameters for the encoded method + :return: Cell object containing encoded message + :rtype: Cell + """ + assert isinstance(abi_name, str) + assert isinstance(address, Address) + abi_file = abi_name + encoded = globals.core.encode_message_body( + address.str(), + abi_file, + method, + json_dumps(params) + ) + return Cell(encoded) + +def build_int_msg(src, dst, abi_file, method, params, value): + """Creates an internal message representing the given call. + + :param Address src: Source address + :param Address dst: Destination address + :param Address abi_file: The name of the destination contract whose ABI should be used for encoding message + :param str method: A name of the encoded method + :param dict params: A dictionary with parameters for the encoded method + :param num value: Desired message value + :return: Msg object containing encoded message + :rtype: Msg + """ + assert isinstance(src, Address) + assert isinstance(dst, Address) + + msg_body = encode_message_body(src, abi_file, method, params) + + # src = zero_addr(-1) + msg = globals.core.build_int_msg(src.str(), dst.str(), msg_body.raw_, value) + # verbose_(msg) + msg = Msg(json.loads(msg)) + verbose_(msg) + globals.QUEUE.append(msg) + return msg diff --git a/tonos_ts4/debotlib.py b/tonos_ts4/debotlib.py new file mode 100644 index 0000000..25d9573 --- /dev/null +++ b/tonos_ts4/debotlib.py @@ -0,0 +1,201 @@ +""" + This file is part of Ever OS. + + Ever OS is free software: you can redistribute it and/or modify + it under the terms of the Apache License 2.0 (http://www.apache.org/licenses/) + + Copyright 2019-2023 (c) EverX +""" + +import ts4 +from ts4 import eq + +def load_debot(filename, nickname): + global g_debot + g_debot = ts4.BaseContract(filename, {}, nickname = nickname) + g_debot.call_method('start', {}, is_debot = True) + return g_debot + +class Terminal(ts4.BaseContract): + def __init__(self): + addr = ts4.Address('-31:8796536366ee21852db56dccb60bc564598b618c865fc50c8b1ab740bba128e3') + super(Terminal, self).__init__('debots:TerminalImpl', {}, nickname = 'term', override_address = addr) + + def expect_print(self, txt): + msg = ts4.pop_event() + assert msg.is_event('Print', src = self.addr) + e = self.decode_event(msg) + ts4.verbose_(e.message) + if txt is not None: + assert eq(txt, e.message) + + def expect_input(self, txt): + msg = ts4.pop_event() + # print(msg) + assert msg.is_event('Input', src = self.addr) + e = self.decode_event(msg) + assert eq(txt, e.prompt) + return e.answerId + + def expect_input2(self, txt, answer): + print('{}: {}'.format(ts4.red(txt), ts4.yellow(answer))) + answerId = self.expect_input(txt) + self.send_input(answerId, answer) + + def send_input(self, answerId, txt): + self.call_method('call_input', dict( + addr = g_debot.addr, + answerId = answerId, + txt = txt, + )) + + +class ConfirmInput(ts4.BaseContract): + def __init__(self): + addr = ts4.Address('-31:16653eaf34c921467120f2685d425ff963db5cbb5aa676a62a2e33bfc3f6828a') + super(ConfirmInput, self).__init__('debots:ConfirmInputImpl', {}, nickname = 'confirmInput', override_address = addr) + + def expect(self, prompt, answer): + assert eq(prompt, self.call_getter('m_prompt')) + self.call_method('reply', dict(answer = answer)) + +class AddressInput(ts4.BaseContract): + def __init__(self): + addr = ts4.Address('-31:d7ed1bd8e6230871116f4522e58df0a93c5520c56f4ade23ef3d8919a984653b') + super(AddressInput, self).__init__('debots:AddressInputImpl', {}, nickname = 'addrInp', override_address = addr) + + def expect_get(self, prompt): + msg = ts4.pop_event() + # print(msg) + assert msg.is_event('Get', src = self.addr) + e = self.decode_event(msg) + ts4.verbose_(e.prompt) + assert eq(prompt, e.prompt) + return e.answerId + + def reply_get(self, answerId, addr): + self.call_method('reply_get', dict( + debot_addr = g_debot.addr, + answerId = answerId, + answer_addr = addr, + )) + + +class DebotMenu(ts4.BaseContract): + def __init__(self): + addr = ts4.Address('-31:ac1a4d3ecea232e49783df4a23a81823cdca3205dc58cd20c4db259c25605b48') + super(DebotMenu, self).__init__('debots:MenuImpl', {}, nickname = 'menu', override_address = addr) + + def expect_menu(self, title): + self.title = self.call_getter('m_title') + assert eq(title, self.title) + self.description = self.call_getter('m_description') + self.items = self.call_getter('m_items', decode = True) + + print('MENU:', ts4.white(self.title)) + print('MENU:', ts4.white(self.description)) + for item in self.items: + print('MENU:', ts4.green(item.__raw__)) + + def select(self, title): + print('Selecting:', ts4.yellow(title)) + for index, item in enumerate(self.items): + if item.title == title: + self.call_method('reply_select', dict(index = index)) + return + assert False, "No '{}' in menu".format(title) + + +class DebotContext(): + def __init__(self): + self.term = Terminal() + self.addrInp = AddressInput() + self.menu = DebotMenu() + self.confirmInput = ConfirmInput() + + self.auto_dispatch = False + self._prints = [] + self.expect_ec = [0] + + def load_debot(self, filename, nickname): + debot = load_debot(filename, nickname) + if self.auto_dispatch: + self.dispatch_messages() + return debot + + def dispatch_messages(self): + while True: + msg = ts4.peek_msg() + # print(msg) + if msg is None: + break + if msg.is_call(): + # verbose_('call') + ts4.dispatch_one_message(expect_ec = self.expect_ec) + else: + break + while True: + event = ts4.peek_event() + if event is None: + break + if event.is_event('Print', src = self.term.addr): + ts4.pop_event() + e = self.term.decode_event(event) + ts4.verbose_(e.message) + self._prints.append(e.message) + else: + break + + def set_keypair(self, keys): + secret = keys[0][:64] + pubkey = keys[1].replace('0x', '') + ts4.core.set_debot_keypair(secret, pubkey) + + def expect_menu(self, title, select): + self.expect_print(None) + if self.auto_dispatch: + self.dispatch_messages() + self.menu.expect_menu(title) + self.menu.select(select) + if self.auto_dispatch: + self.dispatch_messages() + + def expect_address_input(self, title, reply): + self.expect_print(None) + if self.auto_dispatch: + self.dispatch_messages() + answerId = self.addrInp.expect_get(title) + self.addrInp.reply_get(answerId, reply) + if self.auto_dispatch: + self.dispatch_messages() + + def expect_input(self, txt, answer): + self.expect_print(None) + if self.auto_dispatch: + self.dispatch_messages() + self.term.expect_input2(txt, answer) + if self.auto_dispatch: + self.dispatch_messages() + + def expect_confirmInput(self, txt, answer): + print('{}: {}'.format(ts4.red(txt), ts4.yellow('YES' if answer else 'NO'))) + self.expect_print(None) + if self.auto_dispatch: + self.dispatch_messages() + self.confirmInput.expect(txt, answer) + if self.auto_dispatch: + self.dispatch_messages() + + def expect_print(self, msg): + # print('expect: {}, size = {}'.format(ts4.yellow(msg), len(self._prints))) + while len(self._prints) > 0: + t = self._prints.pop(0) + # print('print:', ts4.yellow(t)) + if msg is not None: + if msg in t: + return + if msg is None: + return + assert False, "Expected print not found! "+ ts4.red("{}".format(msg)) + + diff --git a/tonos_ts4/decoder.py b/tonos_ts4/decoder.py index cdb9bbf..7ba787e 100644 --- a/tonos_ts4/decoder.py +++ b/tonos_ts4/decoder.py @@ -1,6 +1,16 @@ -from .util import * -from .address import * -from .abi import * +""" + This file is part of Ever OS. + + Ever OS is free software: you can redistribute it and/or modify + it under the terms of the Apache License 2.0 (http://www.apache.org/licenses/) + + Copyright 2019-2023 (c) EverX +""" + +from address import * +from abi import * +from global_functions import verbose_ +from util import * class Decoder: """The :class:`Decoder ` object, which contains decoder settings. @@ -23,6 +33,7 @@ def __init__(self, :param bool tuples: When getter returns tuple whether to return it as tuple or if no return as a map/dict :param list skip_fields: The list of the field names to be skipped during decoding stage """ + assert isinstance(skip_fields, list) self.ints = ints self.strings = strings self.tuples = tuples @@ -74,6 +85,7 @@ def decode_json_value(value, abi_type, decoder): res = {} for c in abi_type.components: field = c.name + # print(red(field), value[field]) if c.dont_decode or field in decoder.skip_fields: res[field] = value[field] else: @@ -103,5 +115,5 @@ def decode_json_value(value, abi_type, decoder): print(type, value) - ts4.verbose_("Unsupported type '{}'".format(type)) + verbose_("Unsupported type '{}'".format(type)) return value diff --git a/tonos_ts4/dump.py b/tonos_ts4/dump.py index 496a7be..8c82f9c 100644 --- a/tonos_ts4/dump.py +++ b/tonos_ts4/dump.py @@ -1,8 +1,20 @@ -from .util import * -from .address import * -from .abi import * -from .global_functions import * +""" + This file is part of Ever OS. + Ever OS is free software: you can redistribute it and/or modify + it under the terms of the Apache License 2.0 (http://www.apache.org/licenses/) + + Copyright 2019-2023 (c) EverX +""" + +import os +import json + +from address import * +from abi import * +from globals import EVER +from global_functions import * +from util import * def dump_struct(struct, compact = False): if compact: @@ -38,9 +50,9 @@ def transform_value(v): return v return transform_structure(v, transform_value) -def json_dumps(j): +def json_dumps(j: dict, indent = None) -> str: j = _fix_large_ints(j) - return json.dumps(j) #, cls = JsonEncoder) + return json.dumps(j, indent = indent) #, cls = JsonEncoder) ######################################################################################################### @@ -48,7 +60,7 @@ def json_dumps(j): def dump_all_messages(): prev_time = 0 - for msg in ALL_MESSAGES: + for msg in globals.ALL_MESSAGES: cur_time = msg['timestamp'] if cur_time == prev_time: print('---------------') @@ -56,12 +68,22 @@ def dump_all_messages(): print('--------------- {} ------------ ------------ ------------' .format(colorize(BColors.BOLD, str(cur_time)))) prev_time = cur_time - dump_message(msg) + print_int_msg(msg) +def dump_last_message(): + assert ne(0, len(globals.QUEUE)) + msg = globals.QUEUE[-1] + print_int_msg(msg) -def dump_message(msg: Msg): +def print_ext_in_msg(addr, method, params): + print(blue('> ext_in_msg') + grey(': '), end='') + print(cyan(' '), grey('->'), bright_cyan(format_addr(addr))) + print(cyan(grey(' method: ') + bright_cyan('{}'.format(method)) + + grey('\n params: ') + cyan('{}'.format(Params.stringify(prettify_dict(params))))) + '\n') + +def print_int_msg(msg: Msg): assert isinstance(msg, Msg) - value = msg.value / GRAM if msg.value is not None else 'n/a' + value = str(msg.value / EVER) if msg.value is not None else 'n/a' #print(msg) msg_type = '' @@ -69,11 +91,12 @@ def dump_message(msg: Msg): if msg.is_type('call', 'empty', 'bounced'): # ttt = "{}".format(msg) if msg.is_call(): + # print(msg) ttt = bright_cyan(msg.method) + grey('\n params: ') + cyan(Params.stringify(msg.params) + '\n') - ttt = grey(' method:') + ttt + ttt = grey(' method: ') + ttt elif msg.is_bounced(): msg_type = yellow(' ') - elif msg.is_type('empty') and value > 0: + elif msg.is_type('empty') and value != "0": msg_type = cyan(' ') else: msg_type = cyan(' ') @@ -81,25 +104,49 @@ def dump_message(msg: Msg): elif msg.is_unknown(): #print(msg) ttt = "> " + yellow('') #TODO to highlight the print + ttt = ttt + ' body = {}'.format(ellipsis(globals.core.get_msg_body(msg.id), 64)) #print("> " + ttt) else: assert msg.is_answer() ttt = "> " + green('{}'.format(msg.data)) #print("> " + green(ttt)) + if msg.value is None: + msg_value_is_correct = True + msg_value = 'None' + else: + msg_value_is_correct = msg.value < 2**63 + msg_value = '{:,}'.format(msg.value) + msg_value = cyan(msg_value) if msg_value_is_correct else red(msg_value) + print(blue('> int_msg' + msg_type) + grey(': '), end='') - print(bright_cyan(format_addr(msg.src)), grey('->'), bright_cyan(format_addr(msg.dst)), end='') - print(grey(', value:'), cyan(msg.value)) + + src = format_addr_colored(msg.src, BColors.BRIGHT_CYAN, BColors.RESET) + dst = format_addr_colored(msg.dst, BColors.BRIGHT_CYAN, BColors.RESET) + + print(src, grey('->'), dst, end='') + print(grey(', value:'), msg_value) if ttt != '': print(ttt) + assert msg_value_is_correct + +def print_tick_tock(addr, is_tock): + print(blue('> tick tock') + grey(': '), end='') + print(cyan(' '), grey('->'), bright_cyan(format_addr(addr))) ######################################################################################################### -def dump_js_data(): +def dump_js_data(path = '.'): + fn = os.path.join(path, 'msg_data.js') all_runs = get_all_runs() msgs = get_all_messages() - with open('msg_data.js', 'w') as f: + def truncate_long_strings(msg): + if msg['params'] is not None: + msg['params'] = prettify_dict(msg['params']) + return msg + msgs = [truncate_long_strings(msg) for msg in msgs] + with open(fn, 'w') as f: print('var allMessages = ' + dump_struct_str(msgs) + ';', file = f) print('var nicknames = ' + dump_struct_str(globals.NICKNAMES) + ';', file = f) print('var allRuns = ' + dump_struct_str(all_runs) + ';', file = f) diff --git a/tonos_ts4/exception.py b/tonos_ts4/exception.py new file mode 100644 index 0000000..cc9d18e --- /dev/null +++ b/tonos_ts4/exception.py @@ -0,0 +1,111 @@ +""" + This file is part of Ever OS. + + Ever OS is free software: you can redistribute it and/or modify + it under the terms of the Apache License 2.0 (http://www.apache.org/licenses/) + + Copyright 2019-2023 (c) EverX +""" + +import os +import base64 +import hashlib + +from abi import * +from address import * +from decoder import Decoder +from globals import EVER, GRAM, EMPTY_CELL +from util import * +from global_functions import ensure_balance, verbose_ + + +######################################################################################################### + +# TODO: add docs? +class BalanceWatcher: + def __init__(self, contract): + self.contract_ = contract + self.balance_ = contract.balance + self.epsilon_ = 2 + + def ensure_change(self, expected_diff, epsilon = None): + cur_balance = self.contract_.balance + prev_balance = self.balance_ + ensure_balance(prev_balance + expected_diff, cur_balance, epsilon = either_or(epsilon, self.epsilon_)) + self.balance_ = cur_balance + + +######################################################################################################### + +class BaseException(Exception): + def __init__(self, msg): + super(Exception, self).__init__(msg) + def clone(self): + return BaseException(str(self)) + +class UnexpectedExitCodeException(BaseException): + def __init__(self, expected, real): + self.expected = expected + self.real = real + super(BaseException, self).__init__( + 'Unexpected exit code: {}, expected {}'.format(real, expected)) + def clone(self): + return UnexpectedExitCodeException(self.expected, self.real) + +def translate_exception(exception: Exception) -> Exception: + # print('translate_exception:', exception) + if globals.G_SHOW_FULL_STACKTRACE: + return exception + verbose_(exception) + if isinstance(exception, BaseException): + return exception.clone() + return exception + +# TODO: Global decoding params. Add documentation +decoder = Decoder.defaults() + +G_SOLC_ERRORS = { + 40: 'External inbound message has an invalid signature.', + 50: 'Array index is out of range.', + 51: 'Contract\'s constructor has already been called.', + 52: 'Replay protection exception.', +# 53: 'See [\.unpack()](#addressunpack). + 54: 'pop() called for an empty array.', +# 55: 'See [tvm.insertPubkey()](#tvminsertpubkey). + 57: 'External inbound message is expired.', + 58: 'External inbound message has no signature but has public key.', + 60: 'Inbound message has wrong function id.', + 61: 'Deploying `StateInit` has no public key in `data` field.', +# 62: 'Reserved for internal usage. +# 63: 'See [\.get()](#optionaltypeget). + 64: 'tvm.buildExtMSg() called with wrong parameters.', + 65: 'Call of the unassigned variable of function type.', + 66: 'Convert an integer to a string with width less than number length.', +# 67: 'See [gasToValue](#gastovalue) and [valueToGas](#valuetogas). + 68: 'There is no config parameter 20 or 21.', + 69: 'Zero to the power of zero calculation.', + 70: 'string method substr was called with substr longer than the whole string.', + 71: 'Function marked by `externalMsg` was called by internal message.', + 72: 'Function marked by `internalMsg` was called by external message.', + 73: 'The value can\'t be converted to enum type.', + 74: 'Await answer message has wrong source address.', + 75: 'Await answer message has wrong function id.', + 76: 'Public function was called before constructor.', +} + +def check_exitcode(expected_ec, real_ec): + expected_ec = either_or(globals.G_OVERRIDE_EXPECT_EC, expected_ec) + assert isinstance(expected_ec, list) + if real_ec not in expected_ec: + xtra = None + if real_ec in G_SOLC_ERRORS: + xtra = G_SOLC_ERRORS[real_ec] + if xtra is not None: + xtra = ': ' + xtra + else: + xtra = '' + last_error = globals.core.get_last_error_msg() + if last_error is not None: + verbose_('{}{}'.format(last_error, xtra)) + if globals.G_STOP_AT_CRASH: + raise UnexpectedExitCodeException(expected_ec, real_ec) diff --git a/tonos_ts4/global_functions.py b/tonos_ts4/global_functions.py index 3bab167..eb225a0 100644 --- a/tonos_ts4/global_functions.py +++ b/tonos_ts4/global_functions.py @@ -1,40 +1,48 @@ +""" + This file is part of Ever OS. + + Ever OS is free software: you can redistribute it and/or modify + it under the terms of the Apache License 2.0 (http://www.apache.org/licenses/) + + Copyright 2019-2023 (c) EverX +""" + import os import base64 +import ed25519 import hashlib -from . import globals as g -from .globals import GRAM, EMPTY_CELL -from .util import * -from .address import * -from .abi import * -from . import ts4 +from abi import * +from address import * +from globals import EVER +from util import * -def version(): +def version() -> str: """Returns current version of TestSuite4. :return: Current version :rtype: str """ - return g.G_VERSION + return globals.version def reset_all(): - """Resets entire TS4 state. Useful when starting new testset. + """Resets entire TS state. Useful when starting new testset. """ - g.core.reset_all() - g.QUEUE = [] - g.EVENTS = [] - g.ALL_MESSAGES = [] - g.NICKNAMES = dict() + globals.core.reset_all() + globals.QUEUE = [] + globals.EVENTS = [] + globals.ALL_MESSAGES = [] + globals.NICKNAMES = dict() def set_tests_path(path): """Sets the directory where the system will look for compiled contracts. :param str path: The path to contract artifacts """ - g.G_TESTS_PATH = path + globals.G_TESTS_PATH = path -def init(path, verbose = False, time = None): +def init(path, verbose = False, time = None, show_getters = False): """Initializes the library. :param str path: Directory where the artifacts of the used contracts are located @@ -50,8 +58,9 @@ def init(path, verbose = False, time = None): ) set_tests_path(path) set_verbose(verbose) + globals.G_SHOW_GETTERS = show_getters if time is not None: - g.core.set_now(time) + globals.core.set_now(time) def set_verbose(verbose = True): """Sets verbosity mode. When verbosity is enabled all the messages @@ -59,7 +68,7 @@ def set_verbose(verbose = True): :param bool verbose: Toggle to print additional execution info """ - g.G_VERBOSE = verbose + globals.G_VERBOSE = verbose def set_stop_at_crash(do_stop): """Sets `G_STOP_AT_CRASH` global flag. @@ -69,7 +78,7 @@ def set_stop_at_crash(do_stop): :param bool do_stop: Toggle for crash stop mode """ - g.G_STOP_AT_CRASH = do_stop + globals.G_STOP_AT_CRASH = do_stop def verbose_(msg): """Helper function to show text colored red in console. Useful when debugging. @@ -85,67 +94,65 @@ def verbose(msg, show_always = False, color_red = False): :param bool show_always: When enabled forces to show message even when verbose mode is off :param bool color_red: Emphasize the message in color """ - if g.G_VERBOSE or show_always: + if globals.G_VERBOSE or show_always: if color_red: msg = red(str(msg)) print(msg) -def pop_msg(): - """Removes first message from the unprocessed messages g.QUEUE and returns it. +def pop_msg() -> Msg: + """Removes first message from the unprocessed messages globals.QUEUE and returns it. :return: Object :rtype: Msg """ - assert len(g.QUEUE) > 0 - return g.QUEUE.pop(0) + assert ne(0, len(globals.QUEUE), msg = "message queue is empty") + return globals.QUEUE.pop(0) def peek_msg(): - """Returns first message from the unprocessed messages g.QUEUE and leaves the g.QUEUE unchanged. + """Returns first message from the unprocessed messages globals.QUEUE and leaves the globals.QUEUE unchanged. :return: Object :rtype: Msg """ - assert len(g.QUEUE) > 0 - return g.QUEUE[0] + return globals.QUEUE[0] if len(globals.QUEUE) != 0 else None def pop_event(): - """Removes first event from the unprocessed events g.QUEUE and returns it. + """Removes first event from the unprocessed events globals.QUEUE and returns it. :return: Object :rtype: Msg """ - assert len(g.EVENTS) > 0 - return g.EVENTS.pop(0) + assert ne(0, len(globals.EVENTS), msg = "event queue is empty") + return globals.EVENTS.pop(0) def peek_event(): - """Returns first event from the unprocessed events g.QUEUE and leaves the g.QUEUE unchanged. + """Returns first event from the unprocessed events globals.QUEUE and leaves the globals.QUEUE unchanged. :return: Object :rtype: Msg """ - assert len(g.EVENTS) > 0 - return g.EVENTS[0] + return globals.EVENTS[0] if len(globals.EVENTS) > 0 else None def queue_length(): - """Returns the size of the unprocessed messages g.QUEUE. + """Returns the size of the unprocessed messages globals.QUEUE. - :return: g.QUEUE length + :return: globals.QUEUE length :rtype: num """ - return len(g.QUEUE) + return len(globals.QUEUE) def ensure_queue_empty(): - """Checks if the unprocessed messages g.QUEUE is empty + """Checks if the unprocessed messages globals.QUEUE is empty and raises an error if it is not. Useful for debugging. """ - assert eq(0, len(g.QUEUE), msg = ('ensure_queue_empty() -')) + assert eq(0, len(globals.QUEUE), msg = 'message queue is not empty') def dump_queue(): - """Dumps messages g.QUEUE to the console. + """Dumps messages globals.QUEUE to the console. """ - print(white("g.QUEUE:")) # revise print - for i in range(len(g.QUEUE)): - print(" {}: {}".format(i, g.QUEUE[i])) + print(white("globals.QUEUE:")) # revise print + for i in range(len(globals.QUEUE)): + print(" {}: {}".format(i, globals.QUEUE[i])) def set_msg_filter(filter): if filter is True: filter = lambda msg: True @@ -191,41 +198,20 @@ def format_addr(addr, compact = True): s = 'Addr({})'.format(s) return s -def gen_addr(name, initial_data = None, keypair = None, wc = 0): - """Generates contract addresss. - - :param str name: Name used to load contract's bytecode and ABI - :param dict initial_data: Initial data for the contract (static members) - :param keypair: Keypair containing private and public keys - :param num wc: workchain_id to deploy contract to - :return: Expected contract address - :rtype: Address - """ - if keypair is not None: - # TODO: copy-paste below! - (private_key, pubkey) = keypair - if pubkey is not None: - assert pubkey[0:2] == '0x' - pubkey = pubkey.replace('0x', '') +def format_addr_colored(addr, color1, color2): + Address.ensure_address(addr) + if addr.is_none(): + return colorize(color1, 'addr_none') + addr = addr.str() + s = addr[:10] + if addr in globals.NICKNAMES: + s = colorize(color1, globals.NICKNAMES[addr]) + colorize(color2, ' ({})'.format(s)) + # s = "{} ({})".format(globals.NICKNAMES[addr], s) else: - (private_key, pubkey) = (None, None) - - abi = Abi(name) - - if initial_data is not None: - initial_data = ts4.check_method_params(abi, '.data', initial_data) - - result = ts4.core.gen_addr( - make_path(name, '.tvc'), - abi.path_, - ts4.json_dumps(initial_data) if initial_data is not None else None, - pubkey, - private_key, - wc - ) - return Address(result) + s = colorize(color1, '{}'.format(s)) + return s -def make_keypair(seed = None): +def make_keypair(seed = None) -> tuple[str, str]: """Generates random keypair. :param str seed: Seed to be used to generate keys. Useful when constant keypair is needed @@ -236,9 +222,10 @@ def make_keypair(seed = None): hash = hashlib.sha256(seed.encode('utf-8')) seed = decode_int('0x' + hash.hexdigest()) seed = seed % (2**64) - (secret_key, public_key) = globals.core.make_keypair(seed) - public_key = '0x' + public_key - return (secret_key, public_key) + (private_key, public_key) = ed25519.create_keypair() + private_key = private_key.to_ascii(encoding='hex').decode() + public_key = public_key.to_ascii(encoding='hex').decode() + return (private_key + public_key, public_key) def save_keypair(keypair, filename): """Saves keypair to file. @@ -268,27 +255,19 @@ def load_keypair(filename): secret = j['secret'] return (secret, '0x' + public) -def make_path(name, ext): - fn = os.path.join(globals.G_TESTS_PATH, name) - if not fn.endswith('.boc'): - if not fn.endswith(ext): - fn += ext - return fn - -# TODO: Shouldn't this function return Cell? -def load_tvc(fn): +def load_tvc(fn, extension = '.tvc') -> Cell: """Loads a compiled contract image (`.tvc`) with a given name. :param str fn: The file name :return: Cell object loaded from a given file :rtype: Cell """ - fn = make_path(fn, '.tvc') + fn = make_path(fn, extension) with open(fn, 'rb') as fp: str = base64.b64encode(fp.read(1_000_000)).decode('utf-8') return Cell(str) -def load_code_cell(fn): +def load_code_cell(fn) -> Cell: """Loads contract code cell from a compiled contract image with a given name. :param str fn: The file name @@ -298,7 +277,7 @@ def load_code_cell(fn): fn = make_path(fn, '.tvc') return Cell(globals.core.load_code_cell(fn)) -def load_data_cell(fn): +def load_data_cell(fn) -> Cell: """Loads contract data cell from a compiled contract image with a given name. :param str fn: The file name @@ -308,15 +287,20 @@ def load_data_cell(fn): fn = make_path(fn, '.tvc') return Cell(globals.core.load_data_cell(fn)) +def evers(n): + if n is None: return None + return '{:.3f}'.format(n / EVER).replace('.000', '') + +# deprecated def grams(n): - return '{:.3f}'.format(n / GRAM).replace('.000', '') + return evers(n) def ensure_balance(expected, got, dismiss = False, epsilon = 0, msg = None): """Checks the contract balance for exact match. In case of mismatch prints the difference in a convenient form. :param num expected: Expected balance value - :param num got: Сurrent balance value + :param num got: Current balance value :param bool dismiss: When False don't stop the execution in case of mismatch :param num epsilon: Allowed difference between requested and actual balances :param str msg: Optional message to print in case of mismatch @@ -327,7 +311,7 @@ def ensure_balance(expected, got, dismiss = False, epsilon = 0, msg = None): diff = got - int(expected) if abs(diff) <= epsilon: return - xtra = ", diff = {}g ({})".format(grams(diff), diff) + xtra = ", diff = {}g ({})".format(evers(diff), diff) assert eq(int(expected), got, xtra = xtra, dismiss = dismiss, msg = msg) def register_abi(contract_name): @@ -341,7 +325,7 @@ def register_abi(contract_name): print(blue("Loading ABI " + fn)) globals.core.set_contract_abi(None, fn) -def sign_cell(cell, private_key): +def sign_cell(cell: Cell, private_key: str) -> str: """Signs cell with a given key and returns signature. :param Cell value: Cell to be signed @@ -352,27 +336,30 @@ def sign_cell(cell, private_key): assert isinstance(cell, Cell) assert isinstance(private_key, str) assert eq(128, len(private_key)) - # TODO: check that it is hexadecimal number return globals.core.sign_cell(cell.raw_, private_key) -def encode_message_body(abi_name, method, params): - """Encode given message body. +def sign_cell_hash(cell: Cell, private_key: str) -> str: + """Signs cell's repr_hash with a given key and returns signature. - :param str abi_name: The contract name the ABI of which should be used for encoding - :param str method: A name of the encoded method - :param dict params: A dictionary with parameters for the encoded method - :return: Cell object containing encoded message - :rtype: Cell + :param Cell value: Cell to be signed + :param str private_key: Hexadecimal representation of 1024-bits long private key + :return: Hexadecimal string representing resulting signature + :rtype: str """ - abi_file = make_path(abi_name, '.abi.json') - encoded = globals.core.encode_message_body( - abi_file, - method, - ts4.json_dumps(params) - ) - return Cell(encoded) + assert isinstance(cell, Cell) + assert isinstance(private_key, str) + assert eq(128, len(private_key)) + return globals.core.sign_cell_hash(cell.raw_, private_key) + +def last_gas(): + """Returns the gas used in the last contract execution. + + :return: gas used in the last contract execution + :rtype: num + """ + return globals.G_LAST_GAS_USED -def set_config_param(index, value): +def set_config_param(index: int, value: Cell): """Sets global config parameter. :param num index: Parameter index @@ -404,7 +391,7 @@ def set_contract_abi(contract, new_abi_name): :param BaseContract contract: An instance of the contract where the ABI will be set :param str new_abi_name: Name of the file containing the ABI """ - assert isinstance(contract, ts4.BaseContract) + assert isinstance(contract, BaseContract) contract.abi = Abi(new_abi_name) globals.core.set_contract_abi(contract.addr.str(), contract.abi.path_) diff --git a/tonos_ts4/globals.py b/tonos_ts4/globals.py index 755cadd..2fcb1cb 100644 --- a/tonos_ts4/globals.py +++ b/tonos_ts4/globals.py @@ -1,47 +1,83 @@ -import os -import sys -import importlib +""" + This file is part of Ever OS. -G_VERSION = '0.4.1' + Ever OS is free software: you can redistribute it and/or modify + it under the terms of the Apache License 2.0 (http://www.apache.org/licenses/) + + Copyright 2019-2023 (c) EverX +""" QUEUE = [] EVENTS = [] ALL_MESSAGES = [] NICKNAMES = dict() -GRAM = 1_000_000_000 +GRAM = 1_000_000_000 # deprecated +EVER = 1_000_000_000 EMPTY_CELL = 'te6ccgEBAQEAAgAAAA==' -G_DEFAULT_BALANCE = 100*GRAM +G_DEFAULT_BALANCE = 100*EVER G_TESTS_PATH = 'contracts/' -G_VERBOSE = False -G_DUMP_MESSAGES = False -G_STOP_AT_CRASH = True -G_SHOW_EVENTS = False -G_SHOW_GETTERS = False -G_MSG_FILTER = None -G_WARN_ON_UNEXPECTED_ANSWERS = False -G_STOP_ON_NO_ACCEPT = True -G_STOP_ON_NO_ACCOUNT = True -G_STOP_ON_NO_FUNDS = True -G_CHECK_ABI_TYPES = True -G_AUTODISPATCH = False +G_VERBOSE = False +G_AUTODISPATCH = False +G_DUMP_MESSAGES = False + +G_SHOW_EVENTS = False +G_SHOW_GETTERS = False +G_SHOW_FULL_STACKTRACE = False + +G_STOP_AT_CRASH = True +G_STOP_ON_NO_ACCEPT = True +G_STOP_ON_NO_ACCOUNT = True +G_STOP_ON_NO_FUNDS = True + +G_WARN_ON_UNEXPECTED_ANSWERS = False +G_WARN_ON_ACCEPT_IN_GETTER = True + +G_CHECK_ABI_TYPES = True +G_GENERATE_GETTERS = True + +G_LAST_GAS_USED = 0 + +G_MSG_FILTER = None +G_ABI_FIXER = None +G_OVERRIDE_EXPECT_EC = None +G_SET_CONFIG_PARAMS = True + +core = None +version = None -G_ABI_FIXER = None +def set_core(_core, _version): + global core + core = _core + global version + version = _version +def time_set(t): + global now + assert now <= t + now = t + assert core is not None + core.set_now(now) -PACKAGE_DIR = os.path.basename(os.path.dirname(__file__)) -CORE = '.' + sys.platform + '.linker_lib' +def time_get(): + global now + return now -try: - core = importlib.import_module(CORE, PACKAGE_DIR) -except ImportError as err: - print('Error: {}'.format(err)) - exit() -except: - print('Unsupported platform:', sys.platform) - exit() +def time_shift(delta = 1): + global now + now += delta + assert core is not None + core.set_now(now) +def reset(): + global now + now = 0 + assert core is not None + core.reset_all() +def status(message): + global now + print('{0:>7} {1}'.format(now, message)) diff --git a/tonos_ts4/testlib.py b/tonos_ts4/testlib.py new file mode 100644 index 0000000..3f7fd9a --- /dev/null +++ b/tonos_ts4/testlib.py @@ -0,0 +1,31 @@ +""" + This file is part of Ever OS. + + Ever OS is free software: you can redistribute it and/or modify + it under the terms of the Apache License 2.0 (http://www.apache.org/licenses/) + + Copyright 2019-2023 (c) EverX +""" + +import ts4 +from tonos_ts4.ts4 import verbose_, eq, leq, GRAM, BaseContract +# eq = ts4.eq +# GRAM = ts4.GRAM +# BaseContract = ts4.BaseContract + +def events_queue_size(): + return len(ts4.globals.EVENTS) + +def reset_all(): + ts4.ensure_queue_empty() + assert eq(0, events_queue_size()) + ts4.reset_all() + +def start_test(tag : str): + tag = ' {} '.format(tag) + N = 80 + while len(tag) < N: + if len(tag) < N: tag = '*' + tag + if len(tag) < N: tag = tag + '*' + print(ts4.colorize(ts4.BColors.HEADER, tag)) + reset_all() diff --git a/tonos_ts4/ts4.py b/tonos_ts4/ts4.py index f7ff795..d0dce9a 100644 --- a/tonos_ts4/ts4.py +++ b/tonos_ts4/ts4.py @@ -1,10 +1,10 @@ """ - This file is part of TON OS. + This file is part of Ever OS. - TON OS is free software: you can redistribute it and/or modify + Ever OS is free software: you can redistribute it and/or modify it under the terms of the Apache License 2.0 (http://www.apache.org/licenses/) - Copyright 2019-2021 (c) TON LABS + Copyright 2019-2023 (c) EverX """ import sys @@ -15,144 +15,29 @@ import re import copy import os.path +import importlib from glob import glob -from .util import * -from .address import * -from .abi import * -from .decoder import * -from .dump import * -from .global_functions import * - -from .globals import core -from .BaseContract import BaseContract, decode_contract_answer - -__version__ = version() - -# TODO: Global decoding params. Add documentation -decoder = Decoder.defaults() - -def check_exitcode(expected_ec, real_ec): - if expected_ec != real_ec: - xtra = None - if real_ec == 51: xtra = 'Calling of contract\'s constructor that has already been called.' - if real_ec == 52: xtra = 'Replay protection exception.' - if real_ec == 60: xtra = 'Inbound message has wrong function id.' - if real_ec == 76: xtra = 'Public function was called before constructor.' - # TODO: add more codes here... - - if xtra is not None: - xtra = ': ' + xtra - verbose_('{}{}'.format(globals.core.get_last_error_msg(), xtra)) - assert eq(expected_ec, real_ec, dismiss = not globals.G_STOP_AT_CRASH) - -def process_actions(result: ExecutionResult, expect_ec = 0): - assert isinstance(result, ExecutionResult) - ec = result.exit_code - - if globals.G_VERBOSE: - if ec != 0: - print(grey(' exit_code: ') + yellow(ec) + '\n') - - if expect_ec != ec: - verbose_(globals.core.get_last_error_msg()) - - check_exitcode(expect_ec, ec) - - if result.error is not None: - raise Exception("Transaction aborted: {}".format(result.error)) - - answer = None - - for j in result.actions: - msg = Msg(json.loads(j)) - # if globals.G_VERBOSE: - # print('process msg:', msg) - if msg.is_event(): - if globals.G_VERBOSE or globals.G_SHOW_EVENTS: - # TODO: move this printing code to a separate function and file - xtra = '' - params = msg.params - if msg.is_event('DebugEvent'): - xtra = ' ={}'.format(decode_int(params['x'])) - elif msg.is_event('LogEvent'): - params['comment'] = bytearray.fromhex(params['comment']).decode() - print(bright_blue('< event') + grey(': '), end='') - print(cyan(' '), grey('<-'), bright_cyan(format_addr(msg.src))) - print(cyan(grey(' name: ') + cyan('{}'.format(bright_cyan(msg.event))))) - print(grey(' params: ') + cyan(Params.stringify(params)), cyan(xtra), '\n') - globals.EVENTS.append(msg) - else: - # not event - if msg.is_unknown(): - #print(msg) - if globals.G_VERBOSE: - print(yellow('WARNING! Unknown message!')) #TODO to highlight the print - elif msg.is_bounced(): - pass - elif msg.is_answer(): - # We expect only one answer - assert answer is None - answer = msg - continue - else: - assert msg.is_call() or msg.is_empty(), red('Unexpected type: {}'.format(msg.type)) - globals.QUEUE.append(msg) - return (result.gas_used, answer) - -def dispatch_messages(callback = None): - """Dispatches all messages in the queue one by one until the queue becomes empty. - - :param callback: Callback to be called for each processed message. - If callback returns False then the given message is skipped. - """ - while len(globals.QUEUE) > 0: - if callback is not None and callback(peek_msg()) == False: - pop_msg() - continue - dispatch_one_message() - -def dispatch_one_message(expect_ec = 0): - """Takes first unprocessed message from the queue and dispatches it. - Use `expect_ec` parameter if you expect non-zero exit code. - - :param num expect_ec: Expected exit code - :return: The amount of gas spent on the execution of the transaction - :rtype: num - """ - msg = pop_msg() - globals.ALL_MESSAGES.append(msg) - # if is_method_call(msg, 'onRoundComplete'): - # dump_message(msg) - dump1 = globals.G_VERBOSE or globals.G_DUMP_MESSAGES - dump2 = globals.G_MSG_FILTER is not None and globals.G_MSG_FILTER(msg.data) - if dump1 or dump2: - dump_message(msg) - if msg.dst.is_none(): - # TODO: a getter's reply. Add a test for that - return - result = globals.core.dispatch_message(msg.id) - result = ExecutionResult(result) - gas, answer = process_actions(result, expect_ec) - assert answer is None - return gas - - -######################################################################################################### - -# TODO: add docs? -class BalanceWatcher: - def __init__(self, contract): - self.contract_ = contract - self.balance_ = contract.balance - self.epsilon_ = 2 - - def ensure_change(self, expected_diff): - cur_balance = self.contract_.balance - prev_balance = self.balance_ - ensure_balance(prev_balance + expected_diff, cur_balance, epsilon = self.epsilon_) - self.balance_ = cur_balance - - -######################################################################################################### - +from address import * +from abi import * +from core import * +from decoder import * +from dump import * +from global_functions import * +from util import * + +def load_linker_lib(): + CORE = sys.platform + '.linker_lib' + + try: + core = importlib.import_module(CORE) + except ImportError as err: + print('Import module error: {}'.format(err)) + exit() + except: + print('Unsupported platform:', sys.platform) + exit() + return core + +core = load_linker_lib() +globals.set_core(core, '0.5.0a3') diff --git a/tonos_ts4/ts_cli.py b/tonos_ts4/ts_cli.py new file mode 100644 index 0000000..f6fa70e --- /dev/null +++ b/tonos_ts4/ts_cli.py @@ -0,0 +1,594 @@ +""" + This file is part of Ever OS. + + Ever OS is free software: you can redistribute it and/or modify + it under the terms of the Apache License 2.0 (http://www.apache.org/licenses/) + + Copyright 2019-2023 (c) EverX +""" + +import base64 +import json +import os +import os.path +import shutil +import subprocess +import time + +from abi import * +from address import * +from core import * +from decoder import * +from dump import * +from globals import EMPTY_CELL, core +from global_functions import * +from util import * + +class GlobalConfig: + def __init__(self): + self.trace_tvm = None + +class ContractDescription: + def __init__ (self, tvc_path, abi_path, boc_path, dbg_path, key_path): + self.tvc_path = tvc_path + self.abi_path = abi_path + self.boc_path = boc_path + self.dbg_path = dbg_path + self.key_path = key_path + +class Core: + def __init__(self) -> None: + self.temp_path = "" + self.contracts = dict() + self.messages = dict() + self.last_error = None + self.config_path = None + self.config_key = "18ba321b20fd6df7e317623b7109bc0c30717d783a2ad54407dc116ff614cfcfd189e68c5465891838ef026302f97e28127a8bf72f6bf494991fe8c12e466180" + self.global_config = GlobalConfig() + + def __new__(cls): + if not hasattr(cls, 'instance'): + cls.instance = super(Core, cls).__new__(cls) + else: + 3/0 + return cls.instance + + def reset_all(self): + # TODO: need to clean path after finish tests on clean previous on reset_all + self.temp_path = "debug/" + str(int(time.time())) + os.makedirs(self.make_temp_path("keys"), exist_ok=True) + os.makedirs(self.make_temp_path("msgs"), exist_ok=True) + os.makedirs(self.make_temp_path("bocs"), exist_ok=True) + os.makedirs(self.make_temp_path("trns"), exist_ok=True) + self.contracts = dict() + self.messages = dict() + self.last_error = None + self.config_path = make_path("config", ".boc") + assert os.path.exists(self.config_path) + self.global_config = GlobalConfig() + + def set_now(self, _now): + pass + + def get_global_config(self) -> GlobalConfig: + return self.global_config + + def set_global_config(self, config: GlobalConfig): + self.global_config = config + + def make_temp_path(self, name, extension = "") -> str: + return make_path(self.temp_path + "/" + name, extension) + + def set_config(self, config): + # we no need this method because we are using real config contract + print(yellow('we no need new config'), config) + + def set_config_param(self, index, value): + print(yellow('we no need new config'), index) + # assert isinstance(self.config_path, str) + # assert os.path.exists(self.config_path) + # abi_path = make_path('config', ".abi.json") + # assert os.path.exists(abi_path) + # cli_params = ["debug", "call", "--abi", abi_path, "--update"] + # self.add_now_param(cli_params, 1) + # self.add_config_param(cli_params, "--config") + # self.add_temp_keys_param(cli_params, self.config_key) + # method = "set_config_param" + # cli_params += ["--boc", "--addr", self.config_path, "--method", method] + # self.add_method_params(cli_params, value) + # result = self.run_tonos_cli(cli_params) + # return self.parse_execution_result("bc_config", result, method) + + def add_config_param(self, cli_params, name = "--bc_config"): + if self.config_path is not None and os.path.exists(self.config_path): + cli_params += [name, self.config_path] + + def add_method_params(self, cli_params, params: str, name = None): + if params is not None and params != "{}": + assert isinstance(params, str) + params_path = self.make_temp_path("params", ".json") + with open(params_path, 'wt') as fp: + fp.write(params) + # if ctor_name is not None: + # cli_params += ["--method", ctor_name] + if name is not None: + cli_params += [name] + cli_params += [params_path] + + def add_keys_param(self, cli_params, key_path = None): + if key_path is not None: + cli_params += ["--keys", key_path] + + def add_temp_keys_param(self, cli_params, private_key): + if private_key is None: + return + assert eq(128, len(private_key)) + keys = json_dumps(dict( + secret = private_key[:64], + public = private_key[64:] + )) + assert isinstance(keys, str) + key_path = self.store_keys("sign", keys) + self.add_keys_param(cli_params, key_path) + # cli_params += ["--keys", key_path] + + def add_now_param(self, cli_params, shift = 0): + cli_params += ["--now", str(globals.now * 1000 + shift)] + + def add_output_param(self, cli_params, dbg_path): + if self.global_config.trace_tvm is None: + output_path = "nul" + else: + if self.global_config.trace_tvm: + cli_params += ["--full_trace"] + output_path = make_path("trace", ".log") + if dbg_path is not None: + cli_params += ["--dbg_info", dbg_path] + + cli_params += ["--output", output_path] + + def generate_address(self, name: str, wc = -1) -> str: + abi_path = make_path(name, ".abi.json") + tvc_path = make_path(name, ".tvc") + cli_params = ["genaddr", "--abi", abi_path, "--wc", str(wc), tvc_path] + return self.run_tonos_cli(cli_params)["raw_address"] + + def store_keys(self, name: str, keys: str) -> str: + key_path = self.make_temp_path("keys/" + name, '.json') + with open(key_path, "wt") as f: + f.write(keys) + return key_path + + def deploy_contract(self, tvc_name: str, abi_name, ctor_params: str, initial_data, pubkey, private_key, wc, override_address, balance) -> str: + (_, name) = os.path.split(tvc_name) + (name, _) = os.path.splitext(name) + # TODO: problems with WC + assert eq(-1, wc) + # TODO: we never use private key + assert private_key is None + assert initial_data is None + + address = override_address if override_address is not None else self.generate_address(name, wc) + assert self.contracts.get(address) is None + + abi_path = shutil.copy(make_path(name, '.abi.json'), self.make_temp_path("bocs/" + name, '.abi.json')) + boc_path = self.make_temp_path("bocs/" + name, '.boc') + dbg_path = shutil.copy(make_path(name, '.debug.json'), self.make_temp_path("bocs/" + name, '.debug.json')) + tvc_path = shutil.copy(make_path(name, '.tvc'), self.make_temp_path("bocs/" + name, '.tvc')) + + if pubkey is not None: + keys = json_dumps(dict( + secret = private_key[:64] if private_key is not None else '0' * 64, + public = pubkey, + )) + key_path = self.store_keys(address.replace(':', '-'), keys) + else: + key_path = None + + cli_params = ["test", "deploy", tvc_path, "--abi", abi_path, "--address", address] + cli_params += ["--initial_balance", str(balance)] + self.add_now_param(cli_params) + self.add_keys_param(cli_params, key_path) + self.add_output_param(cli_params, dbg_path) + self.add_config_param(cli_params) + self.add_method_params(cli_params, ctor_params, "--params") + result = self.run_tonos_cli(cli_params) + + # analyze result + self.parse_execution_result(address, result, None, abi_path) + assert os.path.exists(boc_path) + if pubkey is not None: + boc_path = shutil.move(boc_path, boc_path.removesuffix(".boc") + "-" + pubkey[:8] + ".boc") + if name.startswith('Config'): + self.config_path = boc_path + # TODO: we don't story keys if no private + # TODO: don't store tvc_path + self.contracts[address] = ContractDescription(tvc_path, abi_path, boc_path, dbg_path, key_path) + return address + + def load_account_state(self, address: str, boc_path: str, abi_path: str): + assert os.path.exists(boc_path) + _, name = os.path.split(boc_path) + boc_path = shutil.copy(boc_path, self.make_temp_path("bocs/" + name)) + name = name.split('.', 1)[0] + abi_path = make_path(name, '.abi.json') + dbg_path = make_path(name, '.debug.json') + if os.path.exists(dbg_path): + dbg_path = shutil.copy(dbg_path, self.make_temp_path("bocs/" + name, '.debug.json')) + else: + dbg_path = None + if os.path.exists(abi_path): + abi_path = shutil.copy(abi_path, self.make_temp_path("bocs/" + name, '.abi.json')) + else: + abi_path = None + self.contracts[address] = ContractDescription(None, abi_path, boc_path, dbg_path, None) + + def call_contract(self, address: str, method: str, is_getter, is_debot, params: str, private_key: str = None) -> str: + if is_getter: + assert private_key is None + return self.call_getter(address, method, params, is_debot) + descr = self.get_contract(address) + cli_params = ["debug", "call", "--abi", descr.abi_path, "--update"] + self.add_now_param(cli_params, 1) + self.add_output_param(cli_params, descr.dbg_path) + self.add_config_param(cli_params, "--config") + self.add_temp_keys_param(cli_params, private_key) + cli_params += ["--boc", "--addr", descr.boc_path, "--method", method] + self.add_method_params(cli_params, params) + result = self.run_tonos_cli(cli_params) + return self.parse_execution_result(address, result, method) + + def call_getter(self, address: str, method: str, params: str, is_debot = False) -> str: + descr = self.get_contract(address) + if 0: # self.global_config.trace_tvm is not None: + # it does not give answers + cli_params = ["debug", "run", "--boc", "--abi", descr.abi_path] + self.add_config_param(cli_params, "--config") + self.add_now_param(cli_params) + self.add_output_param(cli_params, descr.dbg_path) + cli_params += ["--addr", descr.boc_path, "--method", method, params] + result = self.run_tonos_cli(cli_params) + return self.parse_execution_result(address, result, method) + else: + # it does not give debug + cli_params = ["runx", "--boc", "--abi", descr.abi_path] + self.add_config_param(cli_params, "--bc_config") + cli_params += ["--addr", descr.boc_path, "--method", method, params] + + result = self.run_tonos_cli(cli_params) + error = result.get("Error") + if error is not None: + return self.parse_error(error) + msg = json_dumps(dict( + id = "getter", + name = method, + params = result, + src = address, + dst = None, + msg_type = "answer", + timestamp = globals.now, + log_str = None, + )) + result = json_dumps(dict( + exit_code = 0, + out_actions = [msg], + gas = 0, + info = None, + debot_answer_msg = None, + accept_in_getter = False, + )) + return result + + def call_ticktock(self, address: str, is_tock = False) -> str: + descr = self.get_contract(address) + cli_params = ["test", "ticktock", descr.boc_path] + if is_tock: + cli_params = ["--tock"] + self.add_now_param(cli_params) + self.add_config_param(cli_params) + self.add_output_param(cli_params, descr.dbg_path) + result = self.run_tonos_cli(cli_params) + return self.parse_execution_result(address, result) + + def encode_message_body(self, address: str, abi_path: str, method: str, params: str) -> str: + assert isinstance(address, str) + descr = self.get_contract(address) + cli_params = ["body", "--abi", descr.abi_path, method, params] + result = self.run_tonos_cli(cli_params) + body = result.get("Message") + if body is not None: + return body + return self.parse_execution_result(address, result) + + def send_external_message(self, address: str, msg_base64: str): + descr = self.get_contract(address) + msg_path = self.make_temp_path("msgs/temp" +str(globals.now), '.boc') + with open(msg_path, 'wb') as fp: + fp.write(base64.b64decode(msg_base64)) + + cli_params = ["debug", "message", msg_path, "--boc", "--addr", descr.boc_path, "--update"] + self.add_config_param(cli_params, "--config") + self.add_output_param(cli_params, descr.dbg_path) + self.add_now_param(cli_params) + + result = self.run_tonos_cli(cli_params) + return self.parse_execution_result(address, result) + + def dispatch_message(self, id: str) -> str: + assert eq(64, len(id)) + (msg_path, address) = self.messages[id] + return self.process_message(msg_path, address) + + def process_message(self, msg_path, address) -> str: + descr = self.get_contract(address) + cli_params = ["debug", "message", "--update"] + self.add_config_param(cli_params, "--config") + self.add_output_param(cli_params, descr.dbg_path) + self.add_now_param(cli_params, 1) + cli_params += ["--boc", "--addr", descr.boc_path, msg_path] + result = self.run_tonos_cli(cli_params) + return self.parse_execution_result(address, result) + + def parse_error(self, error) -> str: + if isinstance(error, str): + exit_code = -1 + self.last_error = error + else: + code = error.get("code") + if code == 414: + exit_code = error["data"]["exit_code"] + # TODO: self.last_error = error["message"] + else: + exit_code = error["exit_code"] + self.last_error = error.get("message") + + result = dict( + exit_code = exit_code, + out_actions = [], + gas = [], + debot_answer_msg = None, + accept_in_getter = False, + info = None, + ) + return json_dumps(result) + + def parse_execution_result(self, address: str, result: dict, name: str = None, abi_path: str = None) -> str: + error = result.get("Error") + if error is not None: + return self.parse_error(error) + + out_actions = [] + messages = result.get("messages") + tr = result.get("transaction") + if tr is not None: + self.write_transaction(tr) + if int(tr["total_fees"]) != 0: + print(yellow("fees!!!"), tr["total_fees"], tr["id"]) + raise + if messages is not None: + for description in messages: + msg = self.parse_message(address, description) + out_actions.append(msg) + description = result.get("description") + if description is not None: + exit_code = description["exit_code"] + gas = description["gas_usage"] + in_msg = description.get("in_msg") + if in_msg is not None: + result = self.decode_message(in_msg, abi_path) + print(result) + total_fees = description["total_fees"] + if total_fees is not None and int(total_fees) != 0: + print(yellow("fees!!!"), total_fees) + else: + exit_code = 0 + gas = 0 + result = dict( + exit_code = exit_code, + out_actions = out_actions, + gas = gas, + info = None, + debot_answer_msg = None, + accept_in_getter = False, + ) + return json_dumps(result) + + def parse_message(self, address: str, description: dict) -> str: + header = description["Header"] + src = header["source"] + assert eq(address, src) + dst = header["destination"] + abi_path = self.get_contract(dst).abi_path + msg_base64 = description["Message_base64"] + id = description["id"] + msg_path = self.make_temp_path("msgs/" + id, '.boc') + self.messages[id] = (msg_path, dst) + with open(msg_path, 'wb') as fp: + fp.write(base64.b64decode(msg_base64)) + result = self.decode_message(msg_path, abi_path) + body = result.get("BodyCall", None) + if body is not None and isinstance(body, dict): + msg_type = "call" + (name, params) = body.popitem() + else: + msg_type = "empty" + name = None + params = dict() + + return json_dumps(dict( + id = id, + value = int(header["value"]), + bounced = header["bounced"], + src = src, + dst = dst, + timestamp = header["created_at"], + log_str = None, + msg_type = msg_type, + name = name, + params = params + )) + + def write_transaction(self, tr: dict): + if self.global_config.trace_tvm: + id = tr["id"][:8] + trns_path = self.make_temp_path("trns/" + id, ".json") + with open(trns_path, "w") as f: + f.write(json_dumps(tr, 2)) + + def get_msg_body(self, id: str) -> str: + (msg_path, address) = self.messages[id] + with open(msg_path, 'rb') as fp: + return base64.b64encode(fp.read(1_000_000)).decode('utf-8') + + def get_last_error_msg(self): + return self.last_error + + def sign_cell_hash(self, cell: str, private_key: str) -> str: + return self.sign_cell(cell, private_key) + + def sign_cell(self, cell: str, private_key: str) -> str: + assert isinstance(cell, str) + assert isinstance(private_key, str) + cli_params = ["test", "sign", "--cell", cell] + self.add_temp_keys_param(cli_params, private_key) + result = self.run_tonos_cli(cli_params) + signature = result.get("Signature") + if signature is not None: + return base64_to_hex(signature) + return self.parse_execution_result(None, result) + + def get_contract(self, address: str) -> ContractDescription: + assert isinstance(address, str) + result = self.contracts.get(address) + if result is None: + print(red("BAD address"), address) + exit(1) + return result + + def get_balance(self, address: str) -> int: + boc_path = self.get_contract(address).boc_path + cli_params = ["decode", "account", "boc", boc_path] + result = self.run_tonos_cli(cli_params) + balance = result.get("balance") + if balance is not None: + return int(balance) + balance = self.parse_execution_result(address, result) + return int(balance) + + def load_code_cell(self, path: str) -> str: + assert os.path.exists(path) + cli_params = ["decode", "stateinit", path] + if path.endswith(".boc"): + cli_params += ["--boc"] + else: + cli_params += ["--tvc"] + result = self.run_tonos_cli(cli_params) + code = result.get("code") + if code is not None: + return code + return self.parse_execution_result(None, result) + + def run_get(self, address: str, method, params): + boc_path = self.get_contract(address).boc_path + cli_params = ["runget", "--boc", boc_path, method] + if params != "{}": + cli_params += [params] + result = self.run_tonos_cli(cli_params) + value = result.get("value0") + if value is not None: + # return [value] + msg = json_dumps(dict( + id = "getter", + name = method, + params = result, + src = address, + dst = None, + msg_type = "answer", + timestamp = globals.now, + log_str = None, + )) + result = json_dumps(dict( + exit_code = 0, + out_actions = [msg], + gas = 0, + info = None, + debot_answer_msg = None, + accept_in_getter = False, + stack = value + )) + return result + return self.parse_execution_result(None, result) + + def decode_message(self, message: str, abi_path: str) -> dict: + cli_params = ["decode", "msg", "--abi", abi_path, message] + return self.run_tonos_cli(cli_params) + + def parse_config_param(self, p: str) -> str: + cli_params = ["test", "config", "--encode"] + self.add_method_params(cli_params, p) + result = self.run_tonos_cli(cli_params) + cell = result.get("Cell") + if cell is not None: + return cell + return self.parse_execution_result("bc_config", result) + + def dump_config_param(self, index: int): + 1/0 + # self.print_config_param + + def print_config_param(self, index: int, cell: str): + if cell == EMPTY_CELL: + return 'no parameter set' + param_path = self.make_temp_path("bocs/params", ".bin") + with open(param_path, "wb") as f: + f.write(base64.b64decode(cell)) + cli_params = ["test", "config", "--decode", param_path, "--index", str(index)] + result = self.run_tonos_cli(cli_params) + p = result.get('p' + str(index)) + if p is not None: + return p + return self.parse_execution_result(None, result) + + # # TODO: in future get directly from config just by index + # assert isinstance(cell, str) + # print(yellow("print_config_param"), index, cell) + # # TODO: get config param + + def run_tonos_cli(self, cli_params: list[str], help = False, is_json = True, print_cmdline = False, print_output = False) -> dict: + tonos_cli = 'c:\\work/ton-node/target/release/tonos-cli' + try: + cmdline = subprocess.list2cmdline(cli_params) + if print_cmdline or self.global_config.trace_tvm is not None: + print(time.ctime(), green("cmdline"), cmdline) + except: + print(red("BAD list"), cli_params) + exit(1) + + if help: + list = [tonos_cli, "--help"] + cli_params + print(subprocess.getoutput(list)) + exit(1) + if not is_json: + list = [tonos_cli] + cli_params + print(subprocess.getoutput(list)) + exit(1) + + list = [tonos_cli, "-j"] + cli_params + result = subprocess.getoutput(list) + if print_output or self.global_config.trace_tvm is not None: + print(time.ctime(), green("result"), result) + if result == '': + return dict() + try: + answer = json.loads(result) + except: + print(red("BAD JSON result"), result) + exit(1) + # if answer.get("Error") is not None: + # print(red("BAD answer"), result) + # exit(1) + self.global_config.trace_tvm = None + return answer + +core = Core() +globals.set_core(core, '0.1.0a1') diff --git a/tonos_ts4/util.py b/tonos_ts4/util.py index be0bbce..0f7e1cf 100644 --- a/tonos_ts4/util.py +++ b/tonos_ts4/util.py @@ -1,7 +1,19 @@ +""" + This file is part of Ever OS. + + Ever OS is free software: you can redistribute it and/or modify + it under the terms of the Apache License 2.0 (http://www.apache.org/licenses/) + + Copyright 2019-2023 (c) EverX +""" + +import base64 +import os import sys -import re import binascii +from address import * + class BColors: HEADER = '\033[95m' OKBLUE = '\033[94m' @@ -45,13 +57,16 @@ def transform_structure(value, callback): return [transform_structure(x, callback) for x in value] return callback(value) -def decode_int(v): +def decode_int(v: str) -> int: """Decodes integer value from hex string. Helper function useful when decoding data from contracts. :param str v: Hexadecimal string :return: Decoded number :rtype: num """ + if isinstance(v, int): + return v + assert isinstance(v, str) if v[0:2] == '0x': return int(v.replace('0x', ''), 16) else: @@ -109,7 +124,89 @@ def eq(v1, v2, dismiss = False, msg = None, xtra = ''): print(msg + red('exp: {}, got: {}.'.format(v1, v2)) + xtra) return True if dismiss else False +def ne(v1, v2, dismiss = False, msg = None, xtra = ''): + """Helper function to check that two values are not equal. + Prints the message in case of mismatch, and optionally stops tests execution. + + :param Any v1: Expected value + :param Any v2: Actual value + :param bool dismiss: When False stops the entire execution in case of match. + When True only error message is shown + :param str msg: Optional additional message to be printed in case of match + :param str xtra: Another optional additional message to be printed + :return: Result of check + :rtype: bool + """ + if v1 != v2: + return True + else: + msg = '' if msg is None else msg + ' ' + v1 = v1.__repr__() + v2 = v2.__repr__() + print(msg + red('unexp: {}.'.format(v1)) + xtra) + return True if dismiss else False + +def leq(v1, v2): + """Helper function to check that one value is less or equal than enother. + Prints the message in case of mismatch. + + :param Any v1: First value + :param Any v2: Second value + :return: Result of check + :rtype: bool + """ + if v1 <= v2: + return True + print(red('expected {} <= {}'.format(v1, v2))) + return False + def either_or(value, default): return value if value is not None else default +def ellipsis(s, max_len): + if len(s) > max_len: + s = s[:max_len-3] + '...' + return s + +def check_err(v1: str, v2: str, dismiss = False, msg = None, xtra = ''): + """Helper function to check that two values are not equal. + Prints the message in case of mismatch, and optionally stops tests execution. + + :param Any v1: Expected value + :param Any v2: Actual value + :param bool dismiss: When False stops the entire execution in case of match. + When True only error message is shown + :param str msg: Optional additional message to be printed in case of match + :param str xtra: Another optional additional message to be printed + :return: Result of check + :rtype: bool + """ + if v2.startswith(v1): + return True + else: + msg = '' if msg is None else msg + ' ' + v1 = v1.__repr__() + v2 = v2.__repr__() + print(msg + red('exp: {}, got: {}.'.format(v1, v2)) + xtra) + return True if dismiss else False +def base64_to_hex(b64: str) -> str: + # decode base64 to bytes + bytes = base64.b64decode(b64) + + # convert bytes to hex string + return binascii.hexlify(bytes).decode('utf-8') + +def make_path(name, ext = "") -> str: + fn = os.path.join(globals.G_TESTS_PATH, name) + if ext != "": + assert ext.find('.') != -1 + if ext == '.abi.json' and os.path.exists(fn + '.abi'): + ext = '.abi' + elif ext == ".abi" and not os.path.exists(fn + '.abi'): + assert os.path.exists(fn + '.abi.json') + ext = '.abi.json' + if not fn.endswith('.boc'): + if not fn.endswith(ext): + fn += ext + return fn