Repository: CosmWasm/cw-multi-test
Pull request #223 (2024-10-14 14:25:59)

Fixed payload for submessages


Patch:
diff --git a/src/contracts.rs b/src/contracts.rs
index 4ac1605..1f12360 100644
--- a/src/contracts.rs
+++ b/src/contracts.rs
@@ -446,7 +446,7 @@ where
 {
     SubMsg {
         id: msg.id,
-        payload: Binary::default(),
+        payload: msg.payload,
         msg: match msg.msg {
             CosmosMsg::Wasm(wasm) => CosmosMsg::Wasm(wasm),
             CosmosMsg::Bank(bank) => CosmosMsg::Bank(bank),
@@ -459,7 +459,7 @@ where
             CosmosMsg::Ibc(ibc) => CosmosMsg::Ibc(ibc),
             #[cfg(feature = "cosmwasm_2_0")]
             CosmosMsg::Any(any) => CosmosMsg::Any(any),
-            _ => panic!("unknown message variant {:?}", msg),
+            other => panic!("unknown message variant {:?}", other),
         },
         gas_limit: msg.gas_limit,
         reply_on: msg.reply_on,


Test patch:
diff --git a/tests/mod.rs b/tests/mod.rs
index c7e7a91..6975e12 100644
--- a/tests/mod.rs
+++ b/tests/mod.rs
@@ -7,6 +7,7 @@ mod test_attributes;
 mod test_bank;
 mod test_contract_storage;
 mod test_module;
+mod test_payload;
 mod test_prefixed_storage;
 #[cfg(feature = "staking")]
 mod test_staking;
diff --git a/tests/test_payload/mod.rs b/tests/test_payload/mod.rs
new file mode 100644
index 0000000..7a5b840
--- /dev/null
+++ b/tests/test_payload/mod.rs
@@ -0,0 +1,143 @@
+mod test_submessage_payload;
+
+mod test_contracts {
+    pub mod payloader {
+        use cosmwasm_schema::cw_serde;
+        use cosmwasm_std::{
+            from_json, to_json_binary, BankMsg, Binary, Coin, Deps, DepsMut, Empty, Env,
+            MessageInfo, Reply, ReplyOn, Response, StdResult, SubMsg, Uint128,
+        };
+
+        #[cw_serde]
+        pub enum ExecuteMessage {
+            Send(String, u128, String),
+            SendMulti(String, u128, String, u128, String),
+            Burn(u128, String),
+            BurnNoPayload(u128, String),
+            Nop,
+        }
+
+        #[cw_serde]
+        pub struct Payload {
+            pub id: u64,
+            pub action: String,
+        }
+
+        pub fn instantiate(
+            _deps: DepsMut,
+            _env: Env,
+            _info: MessageInfo,
+            _msg: Empty,
+        ) -> StdResult<Response> {
+            Ok(Response::default())
+        }
+
+        pub fn execute(
+            _deps: DepsMut,
+            _env: Env,
+            _info: MessageInfo,
+            msg: ExecuteMessage,
+        ) -> StdResult<Response> {
+            let mut response = Response::new().set_data(to_json_binary(&Payload {
+                id: 0,
+                action: "EXECUTE".to_string(),
+            })?);
+            match msg {
+                ExecuteMessage::Send(addr, amount, denom) => {
+                    let msg_send = BankMsg::Send {
+                        to_address: addr.clone(),
+                        amount: vec![Coin::new(Uint128::new(amount), denom.clone())],
+                    };
+                    response = response.add_submessage(SubMsg {
+                        id: 1,
+                        payload: to_json_binary(&Payload {
+                            id: 0,
+                            action: "SEND".to_string(),
+                        })?,
+                        msg: msg_send.into(),
+                        gas_limit: None,
+                        reply_on: ReplyOn::Always,
+                    });
+                }
+                ExecuteMessage::SendMulti(addr1, amount1, addr2, amount2, denom) => {
+                    let msg_send = BankMsg::Send {
+                        to_address: addr1.clone(),
+                        amount: vec![Coin::new(Uint128::new(amount1), denom.clone())],
+                    };
+                    response = response.add_submessage(SubMsg {
+                        id: 2,
+                        payload: to_json_binary(&Payload {
+                            id: 0,
+                            action: "SEND".to_string(),
+                        })?,
+                        msg: msg_send.into(),
+                        gas_limit: None,
+                        reply_on: ReplyOn::Always,
+                    });
+                    let msg_send = BankMsg::Send {
+                        to_address: addr2.clone(),
+                        amount: vec![Coin::new(Uint128::new(amount2), denom.clone())],
+                    };
+                    response = response.add_submessage(SubMsg {
+                        id: 3,
+                        payload: to_json_binary(&Payload {
+                            id: 0,
+                            action: "SEND".to_string(),
+                        })?,
+                        msg: msg_send.into(),
+                        gas_limit: None,
+                        reply_on: ReplyOn::Always,
+                    });
+                }
+                ExecuteMessage::Burn(amount, denom) => {
+                    let msg_send = BankMsg::Burn {
+                        amount: vec![Coin::new(Uint128::new(amount), denom.clone())],
+                    };
+                    response = response.add_submessage(SubMsg {
+                        id: 4,
+                        payload: to_json_binary(&Payload {
+                            id: 0,
+                            action: "BURN".to_string(),
+                        })?,
+                        msg: msg_send.into(),
+                        gas_limit: None,
+                        reply_on: ReplyOn::Always,
+                    });
+                }
+                ExecuteMessage::BurnNoPayload(amount, denom) => {
+                    let msg_send = BankMsg::Burn {
+                        amount: vec![Coin::new(Uint128::new(amount), denom.clone())],
+                    };
+                    response = response.add_submessage(SubMsg {
+                        id: 5,
+                        payload: Binary::default(),
+                        msg: msg_send.into(),
+                        gas_limit: None,
+                        reply_on: ReplyOn::Always,
+                    });
+                }
+                ExecuteMessage::Nop => {}
+            }
+            Ok(response)
+        }
+
+        pub fn query(_deps: Deps, _env: Env, _msg: Empty) -> StdResult<Binary> {
+            Ok(Binary::default())
+        }
+
+        pub fn reply(_deps: DepsMut, _env: Env, msg: Reply) -> StdResult<Response> {
+            #[allow(deprecated)]
+            let Reply { id, payload, .. } = msg;
+            let payload = if let Ok(mut payload) = from_json::<Payload>(payload.clone()) {
+                payload.id = id;
+                payload
+            } else {
+                Payload {
+                    id,
+                    action: "EMPTY".to_string(),
+                }
+            };
+            Ok(Response::new().set_data(to_json_binary(&payload)?))
+        }
+    }
+}
diff --git a/tests/test_payload/test_submessage_payload.rs b/tests/test_payload/test_submessage_payload.rs
new file mode 100644
index 0000000..e344d3c
--- /dev/null
+++ b/tests/test_payload/test_submessage_payload.rs
@@ -0,0 +1,243 @@
+use super::*;
+use crate::test_payload::test_contracts::payloader::Payload;
+use cosmwasm_std::{from_json, Addr, Coin, Empty, Uint128};
+use cw_multi_test::{App, Contract, ContractWrapper, Executor, IntoAddr};
+
+const DENOM: &str = "pao";
+
+pub fn payloader_contract() -> Box<dyn Contract<Empty>> {
+    Box::new(
+        ContractWrapper::new_with_empty(
+            test_contracts::payloader::execute,
+            test_contracts::payloader::instantiate,
+            test_contracts::payloader::query,
+        )
+        .with_reply(test_contracts::payloader::reply),
+    )
+}
+
+fn coins(amount: u128) -> Vec<Coin> {
+    vec![Coin {
+        denom: DENOM.to_string(),
+        amount: Uint128::new(amount),
+    }]
+}
+
+fn assert_balance(app: &App, amount: u128, addr: &Addr) {
+    let coins = app.wrap().query_all_balances(addr).unwrap();
+    assert_eq!(1, coins.len());
+    assert_eq!(amount, coins[0].amount.u128());
+    assert_eq!(DENOM, coins[0].denom);
+}
+
+#[test]
+fn submessage_payload_should_work() {
+    // Prepare addresses for Alice, Bob and Cecil accounts.
+    let alice_addr = "alice".into_addr();
+    let bob_addr = "bob".into_addr();
+    let cecil_addr = "cecil".into_addr();
+
+    // Initialize the chain with initial balances for Alice, Bob and Cecil.
+    let mut app = App::new(|router, _api, storage| {
+        router
+            .bank
+            .init_balance(storage, &alice_addr, coins(100))
+            .unwrap();
+        router
+            .bank
+            .init_balance(storage, &bob_addr, coins(200))
+            .unwrap();
+        router
+            .bank
+            .init_balance(storage, &cecil_addr, coins(300))
+            .unwrap();
+    });
+
+    // Check the balance for Alice.
+    assert_balance(&app, 100, &alice_addr);
+
+    // Check the balance for Bob.
+    assert_balance(&app, 200, &bob_addr);
+
+    // Check the balance for Cecil.
+    assert_balance(&app, 300, &cecil_addr);
+
+    // Alice stores the code of payloader contract on chain.
+    let code_id = app.store_code_with_creator(alice_addr.clone(), payloader_contract());
+
+    // Alice instantiates the contract.
+    let contract_addr = app
+        .instantiate_contract(
+            code_id,
+            alice_addr.clone(),
+            &Empty {},
+            &coins(90),
+            "payloader",
+            None,
+        )
+        .unwrap();
+
+    // Now, Alice should have only 10pao, because 90pao was sent to the instantiated contract.
+    assert_balance(&app, 10, &alice_addr);
+
+    // Now the contract should have 90pao.
+    assert_balance(&app, 90, &contract_addr);
+
+    //---------------------------------------------------------------------------------------------
+    // Alice sends 10pao to Bob using payloader contract.
+    //
+    // 1. Alice executes ExecuteMessage::Send message variant on payloader contract.
+    // 2. Payloader contract returns single submessage that is the Bank::Send variant
+    //    with the address of Bob.
+    // 3. Chain processes the bank message (sends 10pao to Bob) and returns to payloader contract
+    //    with the response executing the reply entrypoint.
+    // 4. The response from processing the execute and reply is the data returned from reply.
+    //    Because there was only one submessage, the result is the data from reply processed
+    //    for this single submessage.
+    //---------------------------------------------------------------------------------------------
+
+    // Alice sends 10pao to Bob using payloader contract.
+    let msg = test_contracts::payloader::ExecuteMessage::Send(
+        bob_addr.to_string(),
+        10,
+        DENOM.to_string(),
+    );
+    let response = app
+        .execute_contract(alice_addr.clone(), contract_addr.clone(), &msg, &[])
+        .unwrap();
+    let payload = from_json::<Payload>(response.data.unwrap()).unwrap();
+    assert_eq!(1, payload.id);
+    assert_eq!("SEND", payload.action);
+
+    // Now, Bob should have 10pao more, because Alice sent him 10pao.
+    assert_balance(&app, 210, &bob_addr);
+
+    // No changes for Cecil.
+    assert_balance(&app, 300, &cecil_addr);
+
+    // No changes for Alice.
+    assert_balance(&app, 10, &alice_addr);
+
+    // Now the contract should have 80pao.
+    assert_balance(&app, 80, &contract_addr);
+
+    //---------------------------------------------------------------------------------------------
+    // Alice sends 1pao to Bob and 2pao to Cecil using payloader contract.
+    //
+    // 1. Alice executes ExecuteMessage::SendMulti message variant on payloader contract.
+    // 2. Payloader contract returns two submessages that are the Bank::Send variants
+    //    one with the address of Bob and second with the address of Cecil.
+    // 3. Chain processes both bank messages (sends 1pao to Bob nad 2pao to Cecil)
+    //    and returns to payloader contract with the responses executing the reply
+    //    entrypoint twice.
+    // 4. The response from processing the execute and two replies is the data returned
+    //    from processing the LAST REPLY.
+    //---------------------------------------------------------------------------------------------
+
+    // Alice sends 1pao to Bob and 2pao to Cecil using payloader contract.
+    let msg = test_contracts::payloader::ExecuteMessage::SendMulti(
+        bob_addr.to_string(),
+        1,
+        cecil_addr.to_string(),
+        2,
+        DENOM.to_string(),
+    );
+    let response = app
+        .execute_contract(alice_addr.clone(), contract_addr.clone(), &msg, &[])
+        .unwrap();
+    let payload = from_json::<Payload>(response.data.unwrap()).unwrap();
+    assert_eq!(3, payload.id);
+    assert_eq!("SEND", payload.action);
+
+    // Now, Bob should have 1pao more, because Alice sent him 10pao.
+    assert_balance(&app, 211, &bob_addr);
+
+    // Now, Cecil should have 2pao more, because Alice sent him 10pao.
+    assert_balance(&app, 302, &cecil_addr);
+
+    // No changes for Alice.
+    assert_balance(&app, 10, &alice_addr);
+
+    // Now the contract should have 77pao.
+    assert_balance(&app, 77, &contract_addr);
+
+    //---------------------------------------------------------------------------------------------
+    // Alice burns 10pao in her contract.
+    //
+    // 1. Alice executes ExecuteMessage::Burn message variant on payloader contract.
+    // 2. Payloader contract returns two submessage that is the Bank::Burn variant.
+    // 3. Chain processes both bank message (burns 10pao for the contract)
+    //    and returns to payloader contract with the response executing the reply entrypoint.
+    // 4. The response is from reply entrypoint.
+    //---------------------------------------------------------------------------------------------
+
+    let msg = test_contracts::payloader::ExecuteMessage::Burn(10, DENOM.to_string());
+    let response = app
+        .execute_contract(alice_addr.clone(), contract_addr.clone(), &msg, &[])
+        .unwrap();
+    let payload = from_json::<Payload>(response.data.unwrap()).unwrap();
+    assert_eq!(4, payload.id);
+    assert_eq!("BURN", payload.action);
+
+    // No changes for Bob.
+    assert_balance(&app, 211, &bob_addr);
+
+    // No changes for Cecil.
+    assert_balance(&app, 302, &cecil_addr);
+
+    // No changes for Alice.
+    assert_balance(&app, 10, &alice_addr);
+
+    // Now the contract should have 67pao.
+    assert_balance(&app, 67, &contract_addr);
+
+    //---------------------------------------------------------------------------------------------
+    // Alice burns 10pao in her contract.
+    // This time without filling the payload field in the submessage.
+    //---------------------------------------------------------------------------------------------
+
+    let msg = test_contracts::payloader::ExecuteMessage::BurnNoPayload(10, DENOM.to_string());
+    let response = app
+        .execute_contract(alice_addr.clone(), contract_addr.clone(), &msg, &[])
+        .unwrap();
+    let payload = from_json::<Payload>(response.data.unwrap()).unwrap();
+    assert_eq!(5, payload.id);
+    assert_eq!("EMPTY", payload.action);
+
+    // No changes for Bob.
+    assert_balance(&app, 211, &bob_addr);
+
+    // No changes for Cecil.
+    assert_balance(&app, 302, &cecil_addr);
+
+    // No changes for Alice.
+    assert_balance(&app, 10, &alice_addr);
+
+    // Now the contract should have 57pao.
+    assert_balance(&app, 57, &contract_addr);
+
+    //---------------------------------------------------------------------------------------------
+    // Alice executes a variant without reply.
+    // The response is from the execute entrypoint.
+    //---------------------------------------------------------------------------------------------
+
+    let msg = test_contracts::payloader::ExecuteMessage::Nop;
+    let response = app
+        .execute_contract(alice_addr.clone(), contract_addr.clone(), &msg, &[])
+        .unwrap();
+    let payload = from_json::<Payload>(response.data.unwrap()).unwrap();
+    assert_eq!(0, payload.id);
+    assert_eq!("EXECUTE", payload.action);
+
+    // No changes for Bob.
+    assert_balance(&app, 211, &bob_addr);
+
+    // No changes for Cecil.
+    assert_balance(&app, 302, &cecil_addr);
+
+    // No changes for Alice.
+    assert_balance(&app, 10, &alice_addr);
+
+    // Now changes for the contract.
+    assert_balance(&app, 57, &contract_addr);
+}