diff --git a/.github/workflows/plotter-test.yml b/.github/workflows/plotter-test.yml new file mode 100644 index 0000000000..d509759003 --- /dev/null +++ b/.github/workflows/plotter-test.yml @@ -0,0 +1,30 @@ +name: CI + +on: + push: + branches: + - master + pull_request: + +permissions: + checks: write + pull-requests: write + +jobs: + template_test_current: + name: Plotter tests + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Install rust + uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + toolchain: stable + target: wasm32-unknown-unknown + + - name: Run plotter tests + run: | + cd tools/plotter + cargo test diff --git a/contracts/feature-tests/basic-features/sc-config.toml b/contracts/feature-tests/basic-features/sc-config.toml index 42c4880f67..cdba7ce722 100644 --- a/contracts/feature-tests/basic-features/sc-config.toml +++ b/contracts/feature-tests/basic-features/sc-config.toml @@ -21,4 +21,10 @@ kill_legacy_callback = true [[proxy]] path = "src/basic_features_proxy.rs" add-unlabelled = false -add-endpoints = ["init", "store_bytes", "load_bytes", "returns_egld_decimal"] +add-endpoints = [ + "init", + "store_bytes", + "load_bytes", + "returns_egld_decimal", + "echo_managed_option", +] diff --git a/contracts/feature-tests/basic-features/src/basic_features_proxy.rs b/contracts/feature-tests/basic-features/src/basic_features_proxy.rs index 469b2d3323..78e267f6bc 100644 --- a/contracts/feature-tests/basic-features/src/basic_features_proxy.rs +++ b/contracts/feature-tests/basic-features/src/basic_features_proxy.rs @@ -62,6 +62,20 @@ where To: TxTo, Gas: TxGas, { + /// This tests how is generated type name in proxy + pub fn echo_managed_option< + Arg0: ProxyArg>>, + >( + self, + mo: Arg0, + ) -> TxTypedCall>> { + self.wrapped_tx + .payment(NotPayable) + .raw_call("echo_managed_option") + .argument(&mo) + .original_result() + } + pub fn load_bytes( self, ) -> TxTypedCall> { diff --git a/contracts/feature-tests/basic-features/src/echo_managed.rs b/contracts/feature-tests/basic-features/src/echo_managed.rs index 62ee33a7a7..17084a26c8 100644 --- a/contracts/feature-tests/basic-features/src/echo_managed.rs +++ b/contracts/feature-tests/basic-features/src/echo_managed.rs @@ -1,4 +1,4 @@ -multiversx_sc::imports!(); +use multiversx_sc::imports::*; /// Test endpoint argument and result serialization. #[multiversx_sc::module] @@ -23,6 +23,12 @@ pub trait EchoManagedTypes { ma } + /// This tests how is generated type name in proxy + #[endpoint] + fn echo_managed_option(&self, mo: ManagedOption) -> ManagedOption { + mo + } + /// This tests that nested serialization of big ints within unmanaged types works. #[endpoint] fn echo_big_int_managed_vec(&self, x: ManagedVec) -> ManagedVec { diff --git a/contracts/feature-tests/basic-features/tests/basic_features_managed_option_test.rs b/contracts/feature-tests/basic-features/tests/basic_features_managed_option_test.rs new file mode 100644 index 0000000000..38d5730c03 --- /dev/null +++ b/contracts/feature-tests/basic-features/tests/basic_features_managed_option_test.rs @@ -0,0 +1,41 @@ +use imports::{MxscPath, ReturnsResult, TestAddress, TestSCAddress}; +use multiversx_sc::types::{BigUint, ManagedOption}; +use multiversx_sc_scenario::{api::StaticApi, imports, ScenarioTxRun, ScenarioWorld}; + +const OWNER_ADDRESS: TestAddress = TestAddress::new("owner"); +const BASIC_FEATURES_ADDRESS: TestSCAddress = TestSCAddress::new("basic-features"); +const BASIC_FEATURES_PATH: MxscPath = MxscPath::new("output/basic-features.mxsc.json"); + +fn world() -> ScenarioWorld { + let mut blockchain = ScenarioWorld::new(); + + blockchain.register_contract(BASIC_FEATURES_PATH, basic_features::ContractBuilder); + + blockchain.account(OWNER_ADDRESS).nonce(1); + blockchain + .account(BASIC_FEATURES_ADDRESS) + .nonce(1) + .code(BASIC_FEATURES_PATH); + + blockchain +} + +#[test] +fn managed_option_test() { + let mut world = world(); + + let type_number: BigUint = BigUint::zero(); + let expected_type_managed_option: ManagedOption> = + ManagedOption::some(type_number); + + let output = world + .tx() + .from(OWNER_ADDRESS) + .to(BASIC_FEATURES_ADDRESS) + .typed(basic_features::basic_features_proxy::BasicFeaturesProxy) + .echo_managed_option(expected_type_managed_option.clone()) + .returns(ReturnsResult) + .run(); + + assert_eq!(output, expected_type_managed_option); +} diff --git a/contracts/feature-tests/basic-features/wasm/src/lib.rs b/contracts/feature-tests/basic-features/wasm/src/lib.rs index 7b988da222..b4501a4450 100644 --- a/contracts/feature-tests/basic-features/wasm/src/lib.rs +++ b/contracts/feature-tests/basic-features/wasm/src/lib.rs @@ -169,6 +169,7 @@ multiversx_sc_wasm_adapter::endpoints! { echo_big_int => echo_big_int echo_managed_buffer => echo_managed_buffer echo_managed_address => echo_managed_address + echo_managed_option => echo_managed_option echo_big_int_managed_vec => echo_big_int_managed_vec echo_big_int_tuple => echo_big_int_tuple echo_big_int_option => echo_big_int_option diff --git a/contracts/feature-tests/composability/tests/promises_feature_blackbox_test.rs b/contracts/feature-tests/composability/tests/promises_feature_blackbox_test.rs index 4fd4f4d1cc..2eb20508f8 100644 --- a/contracts/feature-tests/composability/tests/promises_feature_blackbox_test.rs +++ b/contracts/feature-tests/composability/tests/promises_feature_blackbox_test.rs @@ -85,3 +85,44 @@ fn test_multi_call_back_transfers() { .check_account(PROMISES_FEATURE_ADDRESS) .esdt_balance(TOKEN_ID_EXPR, token_amount); } + +#[test] +fn test_back_transfers_logs() { + let mut state = PromisesFeaturesTestState::new(); + let token_amount = BigUint::from(1000u64); + + let logs = state + .world + .tx() + .from(USER_ADDRESS) + .to(PROMISES_FEATURE_ADDRESS) + .typed(promises_feature_proxy::PromisesFeaturesProxy) + .forward_sync_retrieve_funds_bt(VAULT_ADDRESS, TOKEN_ID, 0u64, &token_amount) + .returns(ReturnsLogs) + .run(); + + assert!(!logs.is_empty() && !logs[0].topics.is_empty()); + assert_eq!(logs[0].address, PROMISES_FEATURE_ADDRESS); + assert_eq!(logs[0].endpoint, "transferValueOnly"); +} + +#[test] +fn test_multi_call_back_transfers_logs() { + let mut state = PromisesFeaturesTestState::new(); + let token_amount = BigUint::from(1000u64); + let half_token_amount = token_amount.clone() / 2u64; + + let logs = state + .world + .tx() + .from(USER_ADDRESS) + .to(PROMISES_FEATURE_ADDRESS) + .typed(promises_feature_proxy::PromisesFeaturesProxy) + .forward_sync_retrieve_funds_bt_twice(VAULT_ADDRESS, TOKEN_ID, 0u64, &half_token_amount) + .returns(ReturnsLogs) + .run(); + + assert!(!logs.is_empty() && !logs[0].topics.is_empty()); + assert_eq!(logs[0].address, PROMISES_FEATURE_ADDRESS); + assert_eq!(logs[0].endpoint, "transferValueOnly"); +} diff --git a/framework/scenario/src/facade/result_handlers.rs b/framework/scenario/src/facade/result_handlers.rs index 1893647b45..33b5b1a495 100644 --- a/framework/scenario/src/facade/result_handlers.rs +++ b/framework/scenario/src/facade/result_handlers.rs @@ -2,6 +2,7 @@ mod expect_error; mod expect_message; mod expect_status; mod expect_value; +mod returns_logs; mod returns_message; mod returns_new_bech32_address; mod returns_new_token_identifier; @@ -12,6 +13,7 @@ pub use expect_error::ExpectError; pub use expect_message::ExpectMessage; pub use expect_status::ExpectStatus; pub use expect_value::ExpectValue; +pub use returns_logs::ReturnsLogs; pub use returns_message::ReturnsMessage; pub use returns_new_bech32_address::ReturnsNewBech32Address; pub use returns_new_token_identifier::ReturnsNewTokenIdentifier; diff --git a/framework/scenario/src/facade/result_handlers/returns_logs.rs b/framework/scenario/src/facade/result_handlers/returns_logs.rs new file mode 100644 index 0000000000..7aa4292fa2 --- /dev/null +++ b/framework/scenario/src/facade/result_handlers/returns_logs.rs @@ -0,0 +1,24 @@ +use multiversx_sc::types::RHListItemExec; + +use crate::{ + multiversx_sc::types::{RHListItem, TxEnv}, + scenario_model::{Log, TxResponse}, +}; + +pub struct ReturnsLogs; + +impl RHListItem for ReturnsLogs +where + Env: TxEnv, +{ + type Returns = Vec; +} + +impl RHListItemExec for ReturnsLogs +where + Env: TxEnv, +{ + fn item_process_result(self, raw_result: &TxResponse) -> Self::Returns { + raw_result.logs.clone() + } +} diff --git a/framework/scenario/src/scenario/model/transaction/log.rs b/framework/scenario/src/scenario/model/transaction/log.rs index 27ebccfd08..803c521bbf 100644 --- a/framework/scenario/src/scenario/model/transaction/log.rs +++ b/framework/scenario/src/scenario/model/transaction/log.rs @@ -1,9 +1,9 @@ -use crate::scenario_model::BytesValue; +use multiversx_sc::types::Address; #[derive(Debug, Clone)] pub struct Log { - pub address: BytesValue, - pub endpoint: BytesValue, - pub topics: Vec, - pub data: BytesValue, + pub address: Address, + pub endpoint: String, + pub topics: Vec>, + pub data: Vec>, } diff --git a/framework/scenario/src/scenario/model/transaction/tx_response.rs b/framework/scenario/src/scenario/model/transaction/tx_response.rs index fd9721721f..8a7a5c4bf9 100644 --- a/framework/scenario/src/scenario/model/transaction/tx_response.rs +++ b/framework/scenario/src/scenario/model/transaction/tx_response.rs @@ -31,6 +31,16 @@ impl TxResponse { status: tx_result.result_status, message: tx_result.result_message, }, + logs: tx_result + .result_logs + .iter() + .map(|tx_log| Log { + address: Address::from_slice(tx_log.address.as_bytes()), + endpoint: tx_log.endpoint.to_string(), + topics: tx_log.topics.clone(), + data: tx_log.data.clone(), + }) + .collect(), ..Default::default() } } diff --git a/framework/snippets/src/network_response.rs b/framework/snippets/src/network_response.rs index 4d828e12c7..0140dd839e 100644 --- a/framework/snippets/src/network_response.rs +++ b/framework/snippets/src/network_response.rs @@ -1,7 +1,7 @@ use multiversx_sc_scenario::{ imports::{Address, ESDTSystemSCAddress}, multiversx_chain_vm::crypto_functions::keccak256, - scenario_model::{TxResponse, TxResponseStatus}, + scenario_model::{Log, TxResponse, TxResponseStatus}, }; use multiversx_sdk::{ data::transaction::{ApiSmartContractResult, Events, TransactionOnNetwork}, @@ -44,6 +44,7 @@ fn process_success(tx: &TransactionOnNetwork) -> TxResponse { out: process_out(tx), new_deployed_address: process_new_deployed_address(tx), new_issued_token_identifier: process_new_issued_token_identifier(tx), + logs: process_logs(tx), ..Default::default() } } @@ -58,18 +59,55 @@ fn process_out(tx: &TransactionOnNetwork) -> Vec> { } } +fn process_logs(tx: &TransactionOnNetwork) -> Vec { + if let Some(api_logs) = &tx.logs { + return api_logs + .events + .iter() + .map(|event| Log { + address: Address::from_slice(&event.address.to_bytes()), + endpoint: event.identifier.clone(), + topics: extract_topics(event), + data: extract_data(event), + }) + .collect::>(); + } + + Vec::new() +} + +fn extract_data(event: &Events) -> Vec> { + let mut out: Vec> = Vec::new(); + event + .data + .for_each(|data_field| out.push(data_field.clone().into_bytes())); + out +} + +fn extract_topics(event: &Events) -> Vec> { + event + .topics + .clone() + .unwrap_or_default() + .into_iter() + .map(|s| s.into_bytes()) + .collect() +} + fn process_out_from_log(tx: &TransactionOnNetwork) -> Option>> { if let Some(logs) = &tx.logs { logs.events.iter().rev().find_map(|event| { if event.identifier == "writeLog" { - if let Some(data) = &event.data { - let decoded_data = String::from_utf8(base64_decode(data)).unwrap(); + let mut out = Vec::new(); + event.data.for_each(|data_member| { + let decoded_data = String::from_utf8(base64_decode(data_member)).unwrap(); if decoded_data.starts_with('@') { - let out = decode_scr_data_or_panic(decoded_data.as_str()); - return Some(out); + let out_content = decode_scr_data_or_panic(decoded_data.as_str()); + out.extend(out_content); } - } + }); + return Some(out); } None diff --git a/sdk/core/src/data/transaction.rs b/sdk/core/src/data/transaction.rs index 561647776d..664ea3a08e 100644 --- a/sdk/core/src/data/transaction.rs +++ b/sdk/core/src/data/transaction.rs @@ -92,7 +92,27 @@ pub struct Events { pub address: Address, pub identifier: String, pub topics: Option>, - pub data: Option, + #[serde(default)] + pub data: LogData, +} + +#[derive(Default, Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] +#[serde(untagged)] +pub enum LogData { + #[default] + Empty, + String(String), + Vec(Vec), +} + +impl LogData { + pub fn for_each(&self, mut f: F) { + match self { + LogData::Empty => {}, + LogData::String(s) => f(s), + LogData::Vec(v) => v.iter().for_each(f), + } + } } // ApiLogs represents logs with changed fields' types in order to make it friendly for API's json @@ -211,3 +231,62 @@ pub struct SendTransactionsResponse { pub code: String, pub data: Option, } + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn parse_event_log_0() { + let data = r#" +{ + "address": "erd1qqqqqqqqqqqqqpgq0628nau8zydgwu96fn8ksqklzhrggkcfq33sm4vmwv", + "identifier": "completedTxEvent", + "topics": [], + "data": null, + "additionalData": null +} + "#; + + let event_log = serde_json::from_str::(data).unwrap(); + assert_eq!(event_log.data, LogData::Empty); + } + + #[test] + fn parse_event_log_1() { + let data = r#" +{ + "address": "erd1qqqqqqqqqqqqqpgq0628nau8zydgwu96fn8ksqklzhrggkcfq33sm4vmwv", + "identifier": "completedTxEvent", + "topics": [], + "data": "data-string", + "additionalData": null +} + "#; + + let event_log = serde_json::from_str::(data).unwrap(); + assert_eq!(event_log.data, LogData::String("data-string".to_owned())); + } + + #[test] + fn parse_event_log_2() { + let data = r#" +{ + "address": "erd1qqqqqqqqqqqqqpgq0628nau8zydgwu96fn8ksqklzhrggkcfq33sm4vmwv", + "identifier": "completedTxEvent", + "topics": [], + "data": [ + "data1", + "data2" + ], + "additionalData": null +} + "#; + + let event_log = serde_json::from_str::(data).unwrap(); + assert_eq!( + event_log.data, + LogData::Vec(vec!["data1".to_owned(), "data2".to_owned()]) + ); + } +} diff --git a/tools/plotter/start-server.sh b/tools/plotter/start-server.sh index 09641b1a03..3a5698f05e 100755 --- a/tools/plotter/start-server.sh +++ b/tools/plotter/start-server.sh @@ -1,8 +1,6 @@ #!/bin/bash set -e -mkdir -p www/pkg - ./build-wasm.sh cd www diff --git a/tools/plotter/www/bootstrap.js b/tools/plotter/www/bootstrap.js index bea8b6b67a..2520df8952 100644 --- a/tools/plotter/www/bootstrap.js +++ b/tools/plotter/www/bootstrap.js @@ -4,14 +4,14 @@ async function init() { if (typeof process == "object") { // We run in the npm/webpack environment. const [{Chart}, {main, setup}] = await Promise.all([ - import("wasm-demo"), + import("sc-plotter-wasm"), import("./index.js"), ]); setup(Chart); main(); } else { const [{Chart, default: init}, {main, setup}] = await Promise.all([ - import("../pkg/wasm_demo.js"), + import("../pkg/sc_plotter_wasm.js"), import("./index.js"), ]); await init(); diff --git a/tools/plotter/www/package.json b/tools/plotter/www/package.json index 32a99f8aff..87f6fd3412 100644 --- a/tools/plotter/www/package.json +++ b/tools/plotter/www/package.json @@ -25,7 +25,7 @@ }, "homepage": "https://github.com/38/plotters", "dependencies": { - "wasm-demo": "file:../pkg" + "sc-plotter-wasm": "file:../pkg" }, "devDependencies": { "webpack": "^4.43.0",