wasmd/tests/e2e/ibc_callbacks_test.go

222 lines
8.1 KiB
Go

package e2e_test
import (
"encoding/json"
"fmt"
"testing"
"time"
wasmvmtypes "github.com/CosmWasm/wasmvm/v3/types"
ibctransfertypes "github.com/cosmos/ibc-go/v10/modules/apps/transfer/types"
channeltypes "github.com/cosmos/ibc-go/v10/modules/core/04-channel/types"
ibctesting "github.com/cosmos/ibc-go/v10/testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
sdkmath "cosmossdk.io/math"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/CosmWasm/wasmd/tests/e2e"
wasmibctesting "github.com/CosmWasm/wasmd/tests/wasmibctesting"
"github.com/CosmWasm/wasmd/x/wasm/types"
)
func TestIBCCallbacks(t *testing.T) {
// scenario:
// given two chains
// with an ics-20 channel established
// and an ibc-callbacks contract deployed on chain A and B each
// when the contract on A sends an IBCMsg::Transfer to the contract on B
// then the contract on B should receive a destination chain callback
// and the contract on A should receive a source chain callback with the result (ack or timeout)
coord := wasmibctesting.NewCoordinator(t, 2)
chainA := wasmibctesting.NewWasmTestChain(coord.GetChain(ibctesting.GetChainID(1)))
chainB := wasmibctesting.NewWasmTestChain(coord.GetChain(ibctesting.GetChainID(2)))
actorChainA := sdk.AccAddress(chainA.SenderPrivKey.PubKey().Address())
oneToken := sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdkmath.NewInt(1)))
path := wasmibctesting.NewWasmPath(chainA, chainB)
path.EndpointA.ChannelConfig = &ibctesting.ChannelConfig{
PortID: ibctransfertypes.PortID,
Version: ibctransfertypes.V1,
Order: channeltypes.UNORDERED,
}
path.EndpointB.ChannelConfig = &ibctesting.ChannelConfig{
PortID: ibctransfertypes.PortID,
Version: ibctransfertypes.V1,
Order: channeltypes.UNORDERED,
}
// with an ics-20 transfer channel setup between both chains
coord.Setup(&path.Path)
// with an ibc-callbacks contract deployed on chain A
codeIDonA := chainA.StoreCodeFile("./testdata/ibc_callbacks.wasm").CodeID
// and on chain B
codeIDonB := chainB.StoreCodeFile("./testdata/ibc_callbacks.wasm").CodeID
type TransferExecMsg struct {
ToAddress string `json:"to_address"`
ChannelID string `json:"channel_id"`
TimeoutSeconds uint32 `json:"timeout_seconds"`
}
// ExecuteMsg is the ibc-callbacks contract's execute msg
type ExecuteMsg struct {
Transfer *TransferExecMsg `json:"transfer"`
}
type QueryMsg struct {
CallbackStats struct{} `json:"callback_stats"`
}
type QueryResp struct {
IBCAckCallbacks []wasmvmtypes.IBCPacketAckMsg `json:"ibc_ack_callbacks"`
IBCTimeoutCallbacks []wasmvmtypes.IBCPacketTimeoutMsg `json:"ibc_timeout_callbacks"`
IBCDestinationCallbacks []wasmvmtypes.IBCDestinationCallbackMsg `json:"ibc_destination_callbacks"`
}
specs := map[string]struct {
contractMsg ExecuteMsg
// expAck is true if the packet is relayed, false if it times out
expAck bool
}{
"success": {
contractMsg: ExecuteMsg{
Transfer: &TransferExecMsg{
ChannelID: path.EndpointA.ChannelID,
TimeoutSeconds: 100,
},
},
expAck: true,
},
"timeout": {
contractMsg: ExecuteMsg{
Transfer: &TransferExecMsg{
ChannelID: path.EndpointA.ChannelID,
TimeoutSeconds: 1,
},
},
expAck: false,
},
}
for name, spec := range specs {
t.Run(name, func(t *testing.T) {
contractAddrA := chainA.InstantiateContract(codeIDonA, []byte(`{}`))
require.NotEmpty(t, contractAddrA)
contractAddrB := chainB.InstantiateContract(codeIDonB, []byte(`{}`))
require.NotEmpty(t, contractAddrB)
if spec.contractMsg.Transfer != nil && spec.contractMsg.Transfer.ToAddress == "" {
spec.contractMsg.Transfer.ToAddress = contractAddrB.String()
}
contractMsgBz, err := json.Marshal(spec.contractMsg)
require.NoError(t, err)
// when the contract on chain A sends an IBCMsg::Transfer to the contract on chain B
execMsg := types.MsgExecuteContract{
Sender: actorChainA.String(),
Contract: contractAddrA.String(),
Msg: contractMsgBz,
Funds: oneToken,
}
_, err = chainA.SendMsgs(&execMsg)
require.NoError(t, err)
if spec.expAck {
// and the packet is relayed
wasmibctesting.RelayAndAckPendingPackets(path)
// then the contract on chain B should receive a receive callback
var response QueryResp
chainB.SmartQuery(contractAddrB.String(), QueryMsg{CallbackStats: struct{}{}}, &response)
assert.Empty(t, response.IBCAckCallbacks)
assert.Empty(t, response.IBCTimeoutCallbacks)
assert.Len(t, response.IBCDestinationCallbacks, 1)
// and the receive callback should contain the ack
assert.Equal(t, []byte("{\"result\":\"AQ==\"}"), response.IBCDestinationCallbacks[0].Ack.Data)
// and the contract on chain A should receive a callback with the ack
chainA.SmartQuery(contractAddrA.String(), QueryMsg{CallbackStats: struct{}{}}, &response)
assert.Len(t, response.IBCAckCallbacks, 1)
assert.Empty(t, response.IBCTimeoutCallbacks)
assert.Empty(t, response.IBCDestinationCallbacks)
// and the ack result should be the ics20 success ack
assert.Equal(t, []byte(`{"result":"AQ=="}`), response.IBCAckCallbacks[0].Acknowledgement.Data)
} else {
// and the packet times out
require.NoError(t, wasmibctesting.TimeoutPendingPackets(coord, path))
// then the contract on chain B should not receive anything
var response QueryResp
chainB.SmartQuery(contractAddrB.String(), QueryMsg{CallbackStats: struct{}{}}, &response)
assert.Empty(t, response.IBCAckCallbacks)
assert.Empty(t, response.IBCTimeoutCallbacks)
assert.Empty(t, response.IBCDestinationCallbacks)
// and the contract on chain A should receive a callback with the timeout result
chainA.SmartQuery(contractAddrA.String(), QueryMsg{CallbackStats: struct{}{}}, &response)
assert.Empty(t, response.IBCAckCallbacks)
assert.Len(t, response.IBCTimeoutCallbacks, 1)
assert.Empty(t, response.IBCDestinationCallbacks)
}
})
}
}
func TestIBCCallbacksWithoutEntrypoints(t *testing.T) {
// scenario:
// given two chains
// with an ics-20 channel established
// and a reflect contract deployed on chain A and B each
// when the contract on A sends an IBCMsg::Transfer to the contract on B
// then the VM should try to call the callback on B and fail gracefully
// and should try to call the callback on A and fail gracefully
coord := wasmibctesting.NewCoordinator(t, 2)
chainA := wasmibctesting.NewWasmTestChain(coord.GetChain(ibctesting.GetChainID(1)))
chainB := wasmibctesting.NewWasmTestChain(coord.GetChain(ibctesting.GetChainID(2)))
oneToken := sdk.NewCoin(sdk.DefaultBondDenom, sdkmath.NewInt(1))
path := wasmibctesting.NewWasmPath(chainA, chainB)
path.EndpointA.ChannelConfig = &ibctesting.ChannelConfig{
PortID: ibctransfertypes.PortID,
Version: ibctransfertypes.V1,
Order: channeltypes.UNORDERED,
}
path.EndpointB.ChannelConfig = &ibctesting.ChannelConfig{
PortID: ibctransfertypes.PortID,
Version: ibctransfertypes.V1,
Order: channeltypes.UNORDERED,
}
// with an ics-20 transfer channel setup between both chains
coord.Setup(&path.Path)
// with a reflect contract deployed on chain A and B
contractAddrA := e2e.InstantiateReflectContract(t, chainA)
chainA.Fund(contractAddrA, oneToken.Amount)
contractAddrB := e2e.InstantiateReflectContract(t, chainA)
// when the contract on A sends an IBCMsg::Transfer to the contract on B
memo := fmt.Sprintf(`{"src_callback":{"address":"%v"},"dest_callback":{"address":"%v"}}`, contractAddrA.String(), contractAddrB.String())
e2e.MustExecViaReflectContract(t, chainA, contractAddrA, wasmvmtypes.CosmosMsg{
IBC: &wasmvmtypes.IBCMsg{
Transfer: &wasmvmtypes.TransferMsg{
ToAddress: contractAddrB.String(),
ChannelID: path.EndpointA.ChannelID,
Amount: wasmvmtypes.NewCoin(oneToken.Amount.Uint64(), oneToken.Denom),
Timeout: wasmvmtypes.IBCTimeout{
Timestamp: uint64(chainA.LatestCommittedHeader.GetTime().Add(time.Second * 100).UnixNano()),
},
Memo: memo,
},
},
})
// and the packet is relayed without problems
wasmibctesting.RelayAndAckPendingPackets(path)
assert.Empty(t, *chainA.PendingSendPackets)
}