diff --git a/chain/vm/src/vm_err_msg.rs b/chain/vm/src/vm_err_msg.rs index 339e926f4f..4356223a5e 100644 --- a/chain/vm/src/vm_err_msg.rs +++ b/chain/vm/src/vm_err_msg.rs @@ -14,3 +14,5 @@ pub const ERROR_SIGNALLED_BY_SMARTCONTRACT: &str = "error signalled by smartcont pub const ERROR_NO_CALLBACK_CLOSURE: &str = "no callback for closure, cannot call callback directly"; + +pub const PROMISES_TOKENIZE_FAILED: &str = "tokenize failed"; diff --git a/chain/vm/src/vm_hooks/vh_handler/vh_send.rs b/chain/vm/src/vm_hooks/vh_handler/vh_send.rs index 67d590f772..df8e24448a 100644 --- a/chain/vm/src/vm_hooks/vh_handler/vh_send.rs +++ b/chain/vm/src/vm_hooks/vh_handler/vh_send.rs @@ -5,6 +5,7 @@ use crate::{ }, tx_mock::{AsyncCallTxData, Promise, TxFunctionName, TxTokenTransfer}, types::{top_encode_big_uint, top_encode_u64, RawHandle, VMAddress, VMCodeMetadata}, + vm_err_msg, vm_hooks::VMHooksHandlerSource, }; use num_traits::Zero; @@ -215,6 +216,11 @@ pub trait VMHooksSend: VMHooksHandlerSource { let endpoint_name = self .m_types_lock() .mb_to_function_name(endpoint_name_handle); + if endpoint_name.is_empty() { + // immitating the behavior of the VM + // TODO: lift limitation from the VM, then also remove this condition here + self.vm_error(vm_err_msg::PROMISES_TOKENIZE_FAILED); + } let arg_buffer = self.m_types_lock().mb_get_vec_of_bytes(arg_buffer_handle); let tx_hash = self.tx_hash(); let callback_closure_data = self.m_types_lock().mb_get(callback_closure_handle).to_vec(); diff --git a/contracts/feature-tests/composability/promises-features/src/common.rs b/contracts/feature-tests/composability/promises-features/src/common.rs index 46208c937a..1b985a0dc9 100644 --- a/contracts/feature-tests/composability/promises-features/src/common.rs +++ b/contracts/feature-tests/composability/promises-features/src/common.rs @@ -21,6 +21,9 @@ pub trait CommonModule { #[indexed] payment: &BigUint, ); + #[event("callback_result")] + fn callback_result(&self, #[indexed] result: MultiValueEncoded); + #[view] #[storage_mapper("callback_data")] fn callback_data(&self) -> VecMapper>; diff --git a/contracts/feature-tests/composability/promises-features/src/fwd_call_promises.rs b/contracts/feature-tests/composability/promises-features/src/fwd_call_promises.rs index 92a2c8e0e1..04fe76e64e 100644 --- a/contracts/feature-tests/composability/promises-features/src/fwd_call_promises.rs +++ b/contracts/feature-tests/composability/promises-features/src/fwd_call_promises.rs @@ -55,4 +55,50 @@ pub trait CallPromisesModule: common::CommonModule { args: ManagedVec::new(), }); } + + #[endpoint] + #[payable("*")] + fn forward_payment_callback(&self, to: ManagedAddress) { + let payment = self.call_value().any_payment(); + let gas_limit = self.blockchain().get_gas_left() / 2; + + self.tx() + .to(&to) + .gas(gas_limit) + .payment(payment) + .callback(self.callbacks().transfer_callback()) + .register_promise(); + } + + #[promises_callback] + fn transfer_callback(&self, #[call_result] result: MultiValueEncoded) { + self.callback_result(result); + + let call_value = self.call_value().any_payment(); + match call_value { + EgldOrMultiEsdtPayment::Egld(egld) => { + self.retrieve_funds_callback_event(&EgldOrEsdtTokenIdentifier::egld(), 0, &egld); + let _ = self.callback_data().push(&CallbackData { + callback_name: ManagedBuffer::from(b"transfer_callback"), + token_identifier: EgldOrEsdtTokenIdentifier::egld(), + token_nonce: 0, + token_amount: egld, + args: ManagedVec::new(), + }); + }, + EgldOrMultiEsdtPayment::MultiEsdt(multi_esdt) => { + for esdt in multi_esdt.into_iter() { + let token_identifier = EgldOrEsdtTokenIdentifier::esdt(esdt.token_identifier); + self.retrieve_funds_callback_event(&token_identifier, 0, &esdt.amount); + let _ = self.callback_data().push(&CallbackData { + callback_name: ManagedBuffer::from(b"transfer_callback"), + token_identifier, + token_nonce: 0, + token_amount: esdt.amount, + args: ManagedVec::new(), + }); + } + }, + } + } } diff --git a/contracts/feature-tests/composability/promises-features/src/promises_feature_proxy.rs b/contracts/feature-tests/composability/promises-features/src/promises_feature_proxy.rs index d5fb14bf19..7c5a740b0f 100644 --- a/contracts/feature-tests/composability/promises-features/src/promises_feature_proxy.rs +++ b/contracts/feature-tests/composability/promises-features/src/promises_feature_proxy.rs @@ -127,6 +127,18 @@ where .original_result() } + pub fn forward_payment_callback< + Arg0: ProxyArg>, + >( + self, + to: Arg0, + ) -> TxTypedCall { + self.wrapped_tx + .raw_call("forward_payment_callback") + .argument(&to) + .original_result() + } + pub fn promise_raw_single_token< Arg0: ProxyArg>, Arg1: ProxyArg>, diff --git a/contracts/feature-tests/composability/promises-features/wasm/src/lib.rs b/contracts/feature-tests/composability/promises-features/wasm/src/lib.rs index 1dc649df0d..895ab88636 100644 --- a/contracts/feature-tests/composability/promises-features/wasm/src/lib.rs +++ b/contracts/feature-tests/composability/promises-features/wasm/src/lib.rs @@ -5,10 +5,10 @@ //////////////////////////////////////////////////// // Init: 1 -// Endpoints: 10 +// Endpoints: 11 // Async Callback (empty): 1 -// Promise callbacks: 3 -// Total number of exported functions: 15 +// Promise callbacks: 4 +// Total number of exported functions: 17 #![no_std] @@ -24,12 +24,14 @@ multiversx_sc_wasm_adapter::endpoints! { clear_callback_data => clear_callback_data forward_promise_accept_funds => forward_promise_accept_funds forward_promise_retrieve_funds => forward_promise_retrieve_funds + forward_payment_callback => forward_payment_callback promise_raw_single_token => promise_raw_single_token promise_raw_multi_transfer => promise_raw_multi_transfer forward_sync_retrieve_funds_bt => forward_sync_retrieve_funds_bt forward_sync_retrieve_funds_bt_twice => forward_sync_retrieve_funds_bt_twice forward_promise_retrieve_funds_back_transfers => forward_promise_retrieve_funds_back_transfers retrieve_funds_callback => retrieve_funds_callback + transfer_callback => transfer_callback the_one_callback => the_one_callback retrieve_funds_back_transfers_callback => retrieve_funds_back_transfers_callback ) diff --git a/contracts/feature-tests/composability/scenarios/promises_call_transfer_callback_egld.scen.json b/contracts/feature-tests/composability/scenarios/promises_call_transfer_callback_egld.scen.json new file mode 100644 index 0000000000..8d5a3f5a95 --- /dev/null +++ b/contracts/feature-tests/composability/scenarios/promises_call_transfer_callback_egld.scen.json @@ -0,0 +1,45 @@ +{ + "steps": [ + { + "step": "setState", + "accounts": { + "address:a_user": { + "nonce": "0", + "balance": "1000" + }, + "sc:vault": { + "nonce": "0", + "balance": "0", + "code": "mxsc:../vault/output/vault.mxsc.json" + }, + "sc:forwarder": { + "nonce": "0", + "balance": "0", + "code": "mxsc:../promises-features/output/promises-features.mxsc.json" + } + } + }, + { + "step": "scCall", + "id": "1", + "tx": { + "from": "address:a_user", + "to": "sc:forwarder", + "egldValue": "1000", + "function": "forward_payment_callback", + "arguments": [ + "sc:vault" + ], + "gasLimit": "60,000,000", + "gasPrice": "0" + }, + "expect": { + "out": [], + "status": "10", + "message": "str:tokenize failed", + "gas": "*", + "refund": "*" + } + } + ] +} diff --git a/contracts/feature-tests/composability/scenarios/promises_call_transfer_callback_esdt.scen.json b/contracts/feature-tests/composability/scenarios/promises_call_transfer_callback_esdt.scen.json new file mode 100644 index 0000000000..50cc085548 --- /dev/null +++ b/contracts/feature-tests/composability/scenarios/promises_call_transfer_callback_esdt.scen.json @@ -0,0 +1,145 @@ +{ + "steps": [ + { + "step": "setState", + "accounts": { + "address:a_user": { + "nonce": "0", + "balance": "0", + "esdt": { + "str:FWD-TOKEN": "1000" + } + }, + "sc:vault": { + "nonce": "0", + "balance": "0", + "code": "mxsc:../vault/output/vault.mxsc.json" + }, + "sc:forwarder": { + "nonce": "0", + "balance": "0", + "code": "mxsc:../promises-features/output/promises-features.mxsc.json" + } + } + }, + { + "step": "scCall", + "id": "1", + "tx": { + "from": "address:a_user", + "to": "sc:forwarder", + "esdtValue": [ + { + "tokenIdentifier": "str:FWD-TOKEN", + "value": "1000" + } + ], + "function": "forward_payment_callback", + "arguments": [ + "sc:vault" + ], + "gasLimit": "60,000,000", + "gasPrice": "0" + }, + "expect": { + "out": [], + "status": "0", + "logs": [ + { + "address": "sc:forwarder", + "endpoint": "str:ESDTTransfer", + "topics": [ + "str:FWD-TOKEN", + "", + "1000", + "sc:vault" + ], + "data": [ + "str:AsyncCall", + "str:ESDTTransfer", + "str:FWD-TOKEN", + "1000" + ] + }, + { + "address": "sc:vault", + "endpoint": "str:transferValueOnly", + "topics": [ + "", + "sc:forwarder" + ], + "data": [ + "str:AsyncCallback", + "str:transfer_callback", + "0x00" + ] + }, + { + "address": "sc:forwarder", + "endpoint": "str:transfer_callback", + "topics": [ + "str:callback_result", + "0x00" + ], + "data": [ + "" + ] + }, + { + "address": "sc:forwarder", + "endpoint": "str:transfer_callback", + "topics": [ + "str:retrieve_funds_callback", + "str:EGLD", + "", + "" + ], + "data": [ + "" + ] + } + ], + "gas": "*", + "refund": "*" + } + }, + { + "step": "checkState", + "accounts": { + "address:a_user": { + "nonce": "*", + "balance": "0", + "storage": {}, + "code": "" + }, + "sc:vault": { + "nonce": "0", + "balance": "0", + "esdt": { + "str:FWD-TOKEN": "1000" + }, + "storage": { + "str:call_counts|nested:str:accept_funds": "0" + }, + "code": "mxsc:../vault/output/vault.mxsc.json" + }, + "sc:forwarder": { + "nonce": "0", + "balance": "0", + "storage": { + "str:callback_data.len": "1", + "str:callback_data.item|u32:1": [ + "nested:str:transfer_callback", + "nested:str:EGLD", + "u64:0", + "u32:0", + "u32:0" + ] + + }, + "code": "mxsc:../promises-features/output/promises-features.mxsc.json" + } + } + } + ] +} diff --git a/contracts/feature-tests/composability/tests/composability_scenario_go_test.rs b/contracts/feature-tests/composability/tests/composability_scenario_go_test.rs index 3dfb9fdc2d..e6b5144c19 100644 --- a/contracts/feature-tests/composability/tests/composability_scenario_go_test.rs +++ b/contracts/feature-tests/composability/tests/composability_scenario_go_test.rs @@ -437,6 +437,16 @@ fn promises_call_callback_directly_go() { world().run("scenarios/promises_call_callback_directly.scen.json"); } +#[test] +fn promises_call_transfer_callback_egld_go() { + world().run("scenarios/promises_call_transfer_callback_egld.scen.json"); +} + +#[test] +fn promises_call_transfer_callback_esdt_go() { + world().run("scenarios/promises_call_transfer_callback_esdt.scen.json"); +} + #[test] #[ignore = "TODO"] fn promises_multi_transfer_go() { diff --git a/contracts/feature-tests/composability/tests/composability_scenario_rs_test.rs b/contracts/feature-tests/composability/tests/composability_scenario_rs_test.rs index dce2f9031a..771e60eddb 100644 --- a/contracts/feature-tests/composability/tests/composability_scenario_rs_test.rs +++ b/contracts/feature-tests/composability/tests/composability_scenario_rs_test.rs @@ -488,6 +488,16 @@ fn promises_call_callback_directly_rs() { world().run("scenarios/promises_call_callback_directly.scen.json"); } +#[test] +fn promises_call_transfer_callback_egld_rs() { + world().run("scenarios/promises_call_transfer_callback_egld.scen.json"); +} + +#[test] +fn promises_call_transfer_callback_esdt_rs() { + world().run("scenarios/promises_call_transfer_callback_esdt.scen.json"); +} + #[test] fn promises_multi_transfer_rs() { world().run("scenarios/promises_multi_transfer.scen.json"); diff --git a/framework/base/src/types/interaction/tx_exec/tx_exec_async_promises.rs b/framework/base/src/types/interaction/tx_exec/tx_exec_async_promises.rs index 38e38fcbcd..43f28cd6d8 100644 --- a/framework/base/src/types/interaction/tx_exec/tx_exec_async_promises.rs +++ b/framework/base/src/types/interaction/tx_exec/tx_exec_async_promises.rs @@ -188,26 +188,26 @@ where } } -impl Tx, (), To, Payment, (), FunctionCall, Callback> +impl + Tx, (), To, Payment, ExplicitGas, (), Callback> where Api: CallTypeApi, To: TxToSpecified>, Payment: TxPayment>, + GasValue: TxGasValue>, Callback: TxPromisesCallback, { - /// ## Incorrect call - /// - /// Must set **gas** in order to call `register_promise`. + /// Launches a transaction as an asynchronous promise (async v2 mechanism), + /// but without calling any function on the destination. /// - /// ## Safety - /// - /// This version of the method must never be called. It is only here to provide a more readable error. - pub unsafe fn register_promise(self) { - ErrorHelper::::signal_error_with_message("register_promise requires explicit gas"); + /// Such calls are useful for appending callbacks to simple transfers, + /// mitigating edge cases such as non-payable SCs and frozen assets. + pub fn register_promise(self) { + self.raw_call("").register_promise(); } } -impl Tx, (), To, Payment, (), (), Callback> +impl Tx, (), To, Payment, (), FunctionCall, Callback> where Api: CallTypeApi, To: TxToSpecified>, @@ -216,36 +216,34 @@ where { /// ## Incorrect call /// - /// Must set **gas** and **function call** in order to call `register_promise`. + /// Must set **gas** in order to call `register_promise`. /// /// ## Safety /// /// This version of the method must never be called. It is only here to provide a more readable error. pub unsafe fn register_promise(self) { - ErrorHelper::::signal_error_with_message( - "register_promise requires explicit gas and function call", - ); + ErrorHelper::::signal_error_with_message("register_promise requires explicit gas"); } } -impl - Tx, (), To, Payment, ExplicitGas, (), Callback> +impl Tx, (), To, Payment, (), (), Callback> where Api: CallTypeApi, To: TxToSpecified>, Payment: TxPayment>, - GasValue: TxGasValue>, Callback: TxPromisesCallback, { /// ## Incorrect call /// - /// Must set **function call** in order to call `register_promise`. + /// Must set **gas** in order to call `register_promise`, even when no SC endpoint is called. /// /// ## Safety /// /// This version of the method must never be called. It is only here to provide a more readable error. pub unsafe fn register_promise(self) { - ErrorHelper::::signal_error_with_message("register_promise requires function call"); + ErrorHelper::::signal_error_with_message( + "register_promise requires explicit gas (even when no SC endpoint is called)", + ); } }