From df18b1286d4f74522c365d84f7ec5c8c5a556ceb Mon Sep 17 00:00:00 2001 From: Aditya Date: Wed, 29 Mar 2023 11:54:49 +0200 Subject: [PATCH] ADR 8 Interface and Packet Data Implementations (#3287) Conflicts: --- .../27-interchain-accounts/types/packet.go | 75 ++++++ .../types/packet_test.go | 122 +++++++++ modules/apps/transfer/types/msgs_test.go | 43 ++-- modules/apps/transfer/types/packet.go | 111 ++++++++ modules/apps/transfer/types/packet_test.go | 237 +++++++++++++++++- modules/core/exported/channel.go | 20 -- modules/core/exported/packet.go | 48 ++++ 7 files changed, 603 insertions(+), 53 deletions(-) create mode 100644 modules/core/exported/packet.go diff --git a/modules/apps/27-interchain-accounts/types/packet.go b/modules/apps/27-interchain-accounts/types/packet.go index f484f7b76cf..5cc9a64333f 100644 --- a/modules/apps/27-interchain-accounts/types/packet.go +++ b/modules/apps/27-interchain-accounts/types/packet.go @@ -1,11 +1,13 @@ package types import ( + "encoding/json" "time" codectypes "github.com/cosmos/cosmos-sdk/codec/types" sdk "github.com/cosmos/cosmos-sdk/types" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/cosmos/ibc-go/v6/modules/core/exported" ) // MaxMemoCharLength defines the maximum length for the InterchainAccountPacketData memo field @@ -24,6 +26,8 @@ var ( DefaultRelativePacketTimeoutTimestamp = uint64((time.Duration(10) * time.Minute).Nanoseconds()) ) +var _ exported.CallbackPacketData = (*InterchainAccountPacketData)(nil) + // ValidateBasic performs basic validation of the interchain account packet data. // The memo may be empty. func (iapd InterchainAccountPacketData) ValidateBasic() error { @@ -47,6 +51,77 @@ func (iapd InterchainAccountPacketData) GetBytes() []byte { return sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(&iapd)) } +/* + +ADR-8 CallbackPacketData implementation + +InterchainAccountPacketData implements CallbackPacketData interface. This will allow middlewares targeting specific VMs +to retrieve the desired callback address for the ICA packet on the source chain. Destination callback addresses are not +supported for ICS 27. + +The Memo is used to set the desired callback addresses. + +The Memo format is defined like so: + +```json +{ + // ... other memo fields we don't care about + "callbacks": { + "src_callback_address": {contractAddrOnSourceChain}, + + // optional fields + "src_callback_msg": {jsonObjectForSourceChainCallback}, + } +} +``` + +*/ + +// GetSourceCallbackAddress returns the source callback address provided in the packet data memo. +// If no callback address is specified, an empty string is returned. +// +// The memo is expected to specify the callback address in the following format: +// { "callbacks": { "src_callback_address": {contractAddrOnSourceChain}} +// +// ADR-8 middleware should callback on the returned address if it is a PacketActor +// (i.e. smart contract that accepts IBC callbacks). +func (iapd InterchainAccountPacketData) GetSourceCallbackAddress() string { + if len(iapd.Memo) == 0 { + return "" + } + + jsonObject := make(map[string]interface{}) + err := json.Unmarshal([]byte(iapd.Memo), &jsonObject) + if err != nil { + return "" + } + + callbackData, ok := jsonObject["callbacks"].(map[string]interface{}) + if !ok { + return "" + } + + callbackAddr, ok := callbackData["src_callback_address"].(string) + if !ok { + return "" + } + + return callbackAddr +} + +// GetDestCallbackAddress returns an empty string. Destination callback addresses +// are not supported for ICS 27. This feature is natively supported by +// interchain accounts host submodule transaction execution. +func (iapd InterchainAccountPacketData) GetDestCallbackAddress() string { + return "" +} + +// UserDefinedGasLimit returns 0 (no-op). The gas limit of the executing +// transaction will be used. +func (iapd InterchainAccountPacketData) UserDefinedGasLimit() uint64 { + return 0 +} + // GetBytes returns the JSON marshalled interchain account CosmosTx. func (ct CosmosTx) GetBytes() []byte { return sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(&ct)) diff --git a/modules/apps/27-interchain-accounts/types/packet_test.go b/modules/apps/27-interchain-accounts/types/packet_test.go index 244d7782357..661679f28f6 100644 --- a/modules/apps/27-interchain-accounts/types/packet_test.go +++ b/modules/apps/27-interchain-accounts/types/packet_test.go @@ -1,6 +1,8 @@ package types_test import ( + "fmt" + "github.com/cosmos/ibc-go/v6/modules/apps/27-interchain-accounts/types" ) @@ -82,3 +84,123 @@ func (suite *TypesTestSuite) TestValidateBasic() { }) } } + +func (suite *TypesTestSuite) TestGetSourceCallbackAddress() { + const expSrcCbAddr = "srcCbAddr" + + testCases := []struct { + name string + packetData types.InterchainAccountPacketData + expPass bool + }{ + { + "memo is empty", + types.InterchainAccountPacketData{ + Type: types.EXECUTE_TX, + Data: []byte("data"), + Memo: "", + }, + false, + }, + { + "memo is not json string", + types.InterchainAccountPacketData{ + Type: types.EXECUTE_TX, + Data: []byte("data"), + Memo: "memo", + }, + false, + }, + { + "memo does not have callbacks in json struct", + types.InterchainAccountPacketData{ + Type: types.EXECUTE_TX, + Data: []byte("data"), + Memo: `{"Key": 10}`, + }, + false, + }, + { + "memo has callbacks in json struct but does not have src_callback_address key", + types.InterchainAccountPacketData{ + Type: types.EXECUTE_TX, + Data: []byte("data"), + Memo: `{"callbacks": {"Key": 10}}`, + }, + false, + }, + { + "memo has callbacks in json struct but does not have string value for src_callback_address key", + types.InterchainAccountPacketData{ + Type: types.EXECUTE_TX, + Data: []byte("data"), + Memo: `{"callbacks": {"src_callback_address": 10}}`, + }, + false, + }, + { + "memo has callbacks in json struct and properly formatted src_callback_address", + types.InterchainAccountPacketData{ + Type: types.EXECUTE_TX, + Data: []byte("data"), + Memo: fmt.Sprintf(`{"callbacks": {"src_callback_address": "%s"}}`, expSrcCbAddr), + }, + true, + }, + } + + for _, tc := range testCases { + tc := tc + suite.Run(tc.name, func() { + srcCbAddr := tc.packetData.GetSourceCallbackAddress() + + if tc.expPass { + suite.Require().Equal(expSrcCbAddr, srcCbAddr) + } else { + suite.Require().Equal("", srcCbAddr) + } + }) + } +} + +func (suite *TypesTestSuite) TestGetDestCallbackAddress() { + testCases := []struct { + name string + packetData types.InterchainAccountPacketData + }{ + { + "memo is empty", + types.InterchainAccountPacketData{ + Type: types.EXECUTE_TX, + Data: []byte("data"), + Memo: "", + }, + }, + { + "memo has dest callback address specified in json struct", + types.InterchainAccountPacketData{ + Type: types.EXECUTE_TX, + Data: []byte("data"), + Memo: `{"callbacks": {"dest_callback_address": "testAddress"}}`, + }, + }, + } + + for _, tc := range testCases { + tc := tc + suite.Run(tc.name, func() { + destCbAddr := tc.packetData.GetDestCallbackAddress() + suite.Require().Equal("", destCbAddr) + }) + } +} + +func (suite *TypesTestSuite) TestUserDefinedGasLimit() { + packetData := types.InterchainAccountPacketData{ + Type: types.EXECUTE_TX, + Data: []byte("data"), + Memo: `{"callbacks": {"user_defined_gas_limit": 100}}`, + } + + suite.Require().Equal(uint64(0), packetData.UserDefinedGasLimit(), "user defined gas limit does not return 0") +} diff --git a/modules/apps/transfer/types/msgs_test.go b/modules/apps/transfer/types/msgs_test.go index 4268a282ef4..fa63fb352d2 100644 --- a/modules/apps/transfer/types/msgs_test.go +++ b/modules/apps/transfer/types/msgs_test.go @@ -1,4 +1,4 @@ -package types +package types_test import ( "fmt" @@ -8,6 +8,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/stretchr/testify/require" + "github.com/cosmos/ibc-go/v6/modules/apps/transfer/types" clienttypes "github.com/cosmos/ibc-go/v6/modules/core/02-client/types" ) @@ -41,20 +42,20 @@ var ( // TestMsgTransferRoute tests Route for MsgTransfer func TestMsgTransferRoute(t *testing.T) { - msg := NewMsgTransfer(validPort, validChannel, coin, addr1, addr2, timeoutHeight, 0, "") + msg := types.NewMsgTransfer(validPort, validChannel, coin, addr1, addr2, timeoutHeight, 0, "") - require.Equal(t, RouterKey, msg.Route()) + require.Equal(t, types.RouterKey, msg.Route()) } // TestMsgTransferType tests Type for MsgTransfer func TestMsgTransferType(t *testing.T) { - msg := NewMsgTransfer(validPort, validChannel, coin, addr1, addr2, timeoutHeight, 0, "") + msg := types.NewMsgTransfer(validPort, validChannel, coin, addr1, addr2, timeoutHeight, 0, "") require.Equal(t, "transfer", msg.Type()) } func TestMsgTransferGetSignBytes(t *testing.T) { - msg := NewMsgTransfer(validPort, validChannel, coin, addr1, addr2, timeoutHeight, 0, "") + msg := types.NewMsgTransfer(validPort, validChannel, coin, addr1, addr2, timeoutHeight, 0, "") expected := fmt.Sprintf(`{"type":"cosmos-sdk/MsgTransfer","value":{"receiver":"%s","sender":"%s","source_channel":"testchannel","source_port":"testportid","timeout_height":{"revision_height":"10"},"token":{"amount":"100","denom":"atom"}}}`, addr2, addr1) require.NotPanics(t, func() { res := msg.GetSignBytes() @@ -66,23 +67,23 @@ func TestMsgTransferGetSignBytes(t *testing.T) { func TestMsgTransferValidation(t *testing.T) { testCases := []struct { name string - msg *MsgTransfer + msg *types.MsgTransfer expPass bool }{ - {"valid msg with base denom", NewMsgTransfer(validPort, validChannel, coin, addr1, addr2, timeoutHeight, 0, ""), true}, - {"valid msg with trace hash", NewMsgTransfer(validPort, validChannel, ibcCoin, addr1, addr2, timeoutHeight, 0, ""), true}, - {"invalid ibc denom", NewMsgTransfer(validPort, validChannel, invalidIBCCoin, addr1, addr2, timeoutHeight, 0, ""), false}, - {"too short port id", NewMsgTransfer(invalidShortPort, validChannel, coin, addr1, addr2, timeoutHeight, 0, ""), false}, - {"too long port id", NewMsgTransfer(invalidLongPort, validChannel, coin, addr1, addr2, timeoutHeight, 0, ""), false}, - {"port id contains non-alpha", NewMsgTransfer(invalidPort, validChannel, coin, addr1, addr2, timeoutHeight, 0, ""), false}, - {"too short channel id", NewMsgTransfer(validPort, invalidShortChannel, coin, addr1, addr2, timeoutHeight, 0, ""), false}, - {"too long channel id", NewMsgTransfer(validPort, invalidLongChannel, coin, addr1, addr2, timeoutHeight, 0, ""), false}, - {"channel id contains non-alpha", NewMsgTransfer(validPort, invalidChannel, coin, addr1, addr2, timeoutHeight, 0, ""), false}, - {"invalid denom", NewMsgTransfer(validPort, validChannel, invalidDenomCoin, addr1, addr2, timeoutHeight, 0, ""), false}, - {"zero coin", NewMsgTransfer(validPort, validChannel, zeroCoin, addr1, addr2, timeoutHeight, 0, ""), false}, - {"missing sender address", NewMsgTransfer(validPort, validChannel, coin, emptyAddr, addr2, timeoutHeight, 0, ""), false}, - {"missing recipient address", NewMsgTransfer(validPort, validChannel, coin, addr1, "", timeoutHeight, 0, ""), false}, - {"empty coin", NewMsgTransfer(validPort, validChannel, sdk.Coin{}, addr1, addr2, timeoutHeight, 0, ""), false}, + {"valid msg with base denom", types.NewMsgTransfer(validPort, validChannel, coin, addr1, addr2, timeoutHeight, 0, ""), true}, + {"valid msg with trace hash", types.NewMsgTransfer(validPort, validChannel, ibcCoin, addr1, addr2, timeoutHeight, 0, ""), true}, + {"invalid ibc denom", types.NewMsgTransfer(validPort, validChannel, invalidIBCCoin, addr1, addr2, timeoutHeight, 0, ""), false}, + {"too short port id", types.NewMsgTransfer(invalidShortPort, validChannel, coin, addr1, addr2, timeoutHeight, 0, ""), false}, + {"too long port id", types.NewMsgTransfer(invalidLongPort, validChannel, coin, addr1, addr2, timeoutHeight, 0, ""), false}, + {"port id contains non-alpha", types.NewMsgTransfer(invalidPort, validChannel, coin, addr1, addr2, timeoutHeight, 0, ""), false}, + {"too short channel id", types.NewMsgTransfer(validPort, invalidShortChannel, coin, addr1, addr2, timeoutHeight, 0, ""), false}, + {"too long channel id", types.NewMsgTransfer(validPort, invalidLongChannel, coin, addr1, addr2, timeoutHeight, 0, ""), false}, + {"channel id contains non-alpha", types.NewMsgTransfer(validPort, invalidChannel, coin, addr1, addr2, timeoutHeight, 0, ""), false}, + {"invalid denom", types.NewMsgTransfer(validPort, validChannel, invalidDenomCoin, addr1, addr2, timeoutHeight, 0, ""), false}, + {"zero coin", types.NewMsgTransfer(validPort, validChannel, zeroCoin, addr1, addr2, timeoutHeight, 0, ""), false}, + {"missing sender address", types.NewMsgTransfer(validPort, validChannel, coin, emptyAddr, addr2, timeoutHeight, 0, ""), false}, + {"missing recipient address", types.NewMsgTransfer(validPort, validChannel, coin, addr1, "", timeoutHeight, 0, ""), false}, + {"empty coin", types.NewMsgTransfer(validPort, validChannel, sdk.Coin{}, addr1, addr2, timeoutHeight, 0, ""), false}, } for i, tc := range testCases { @@ -99,7 +100,7 @@ func TestMsgTransferValidation(t *testing.T) { func TestMsgTransferGetSigners(t *testing.T) { addr := sdk.AccAddress(secp256k1.GenPrivKey().PubKey().Address()) - msg := NewMsgTransfer(validPort, validChannel, coin, addr.String(), addr2, timeoutHeight, 0, "") + msg := types.NewMsgTransfer(validPort, validChannel, coin, addr.String(), addr2, timeoutHeight, 0, "") res := msg.GetSigners() require.Equal(t, []sdk.AccAddress{addr}, res) diff --git a/modules/apps/transfer/types/packet.go b/modules/apps/transfer/types/packet.go index aed80c6043b..12243f26fac 100644 --- a/modules/apps/transfer/types/packet.go +++ b/modules/apps/transfer/types/packet.go @@ -1,11 +1,14 @@ package types import ( + "encoding/json" "strings" "time" sdk "github.com/cosmos/cosmos-sdk/types" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + + "github.com/cosmos/ibc-go/v6/modules/core/exported" ) var ( @@ -21,6 +24,8 @@ var ( DefaultRelativePacketTimeoutTimestamp = uint64((time.Duration(10) * time.Minute).Nanoseconds()) ) +var _ exported.CallbackPacketData = (*FungibleTokenPacketData)(nil) + // NewFungibleTokenPacketData contructs a new FungibleTokenPacketData instance func NewFungibleTokenPacketData( denom string, amount string, @@ -60,3 +65,109 @@ func (ftpd FungibleTokenPacketData) ValidateBasic() error { func (ftpd FungibleTokenPacketData) GetBytes() []byte { return sdk.MustSortJSON(mustProtoMarshalJSON(&ftpd)) } + +/* + +ADR-8 CallbackPacketData implementation + +FungibleTokenPacketData implements CallbackPacketData interface. This will allow middlewares targeting specific VMs +to retrieve the desired callback addresses for the ICS20 packet on the source and destination chains. + +The Memo is used to ensure that the callback is desired by the user. This allows a user to send an ICS20 packet +to a contract with ADR-8 enabled without automatically triggering the callback logic which may lead to unexpected +behaviour. + +The Memo format is defined like so: + +```json +{ + // ... other memo fields we don't care about + "callbacks": { + "src_callback_address": {contractAddrOnSourceChain}, + "dest_callback_address": {contractAddrOnDestChain}, + + // optional fields + "src_callback_msg": {jsonObjectForSourceChainCallback}, + "dest_callback_msg": {jsonObjectForDestChainCallback}, + } +} +``` + +For transfer, we will enforce that the src_callback_address is the same as sender and dest_callback_address is the same as receiver. +However, we may remove this restriction at a later date if it proves useful. + +*/ + +// GetSourceCallbackAddress returns the sender address if it is also specified in +// the packet data memo. The desired callback address must be confirmed in the +// memo under the "callbacks" key. This ensures that the callback is explicitly +// desired by the user and not called automatically. If no callback address is +// specified, an empty string is returned. +// +// The memo is expected to contain the source callback address in the following format: +// { "callbacks": { "src_callback_address": {contractAddrOnSourceChain}} +// +// ADR-8 middleware should callback on the returned address if it is a PacketActor +// (i.e. smart contract that accepts IBC callbacks). +func (ftpd FungibleTokenPacketData) GetSourceCallbackAddress() string { + if len(ftpd.Memo) == 0 { + return "" + } + + jsonObject := make(map[string]interface{}) + err := json.Unmarshal([]byte(ftpd.Memo), &jsonObject) + if err != nil { + return "" + } + + callbackData, ok := jsonObject["callbacks"].(map[string]interface{}) + if !ok { + return "" + } + + if callbackData["src_callback_address"] == ftpd.Sender { + return ftpd.Sender + } + + return "" +} + +// GetDestCallbackAddress returns the receiving address if it is also specified in +// the packet data memo. The desired callback address must be confirmed in the +// memo under the "callbacks" key. This ensures that the callback is explicitly +// desired by the user and not called automatically. If no callback address is +// specified, an empty string is returned. +// +// The memo is expected to contain the destination callback address in the following format: +// { "callbacks": { "dest_callback_address": {contractAddrOnDestChain}} +// +// ADR-8 middleware should callback on the returned address if it is a PacketActor +// (i.e. smart contract that accepts IBC callbacks). +func (ftpd FungibleTokenPacketData) GetDestCallbackAddress() string { + if len(ftpd.Memo) == 0 { + return "" + } + + jsonObject := make(map[string]interface{}) + err := json.Unmarshal([]byte(ftpd.Memo), &jsonObject) + if err != nil { + return "" + } + + callbackData, ok := jsonObject["callbacks"].(map[string]interface{}) + if !ok { + return "" + } + + if callbackData["dest_callback_address"] == ftpd.Receiver { + return ftpd.Receiver + } + + return "" +} + +// UserDefinedGasLimit returns 0 (no-op). The gas limit of the executing +// transaction will be used. +func (ftpd FungibleTokenPacketData) UserDefinedGasLimit() uint64 { + return 0 +} diff --git a/modules/apps/transfer/types/packet_test.go b/modules/apps/transfer/types/packet_test.go index cef36ab2211..dff6f1c0935 100644 --- a/modules/apps/transfer/types/packet_test.go +++ b/modules/apps/transfer/types/packet_test.go @@ -1,9 +1,12 @@ -package types +package types_test import ( + "fmt" "testing" "github.com/stretchr/testify/require" + + "github.com/cosmos/ibc-go/v6/modules/apps/transfer/types" ) const ( @@ -17,19 +20,19 @@ const ( func TestFungibleTokenPacketDataValidateBasic(t *testing.T) { testCases := []struct { name string - packetData FungibleTokenPacketData + packetData types.FungibleTokenPacketData expPass bool }{ - {"valid packet", NewFungibleTokenPacketData(denom, amount, addr1, addr2, ""), true}, - {"valid packet with memo", NewFungibleTokenPacketData(denom, amount, addr1, addr2, "memo"), true}, - {"valid packet with large amount", NewFungibleTokenPacketData(denom, largeAmount, addr1, addr2, ""), true}, - {"invalid denom", NewFungibleTokenPacketData("", amount, addr1, addr2, ""), false}, - {"invalid empty amount", NewFungibleTokenPacketData(denom, "", addr1, addr2, ""), false}, - {"invalid zero amount", NewFungibleTokenPacketData(denom, "0", addr1, addr2, ""), false}, - {"invalid negative amount", NewFungibleTokenPacketData(denom, "-1", addr1, addr2, ""), false}, - {"invalid large amount", NewFungibleTokenPacketData(denom, invalidLargeAmount, addr1, addr2, ""), false}, - {"missing sender address", NewFungibleTokenPacketData(denom, amount, emptyAddr, addr2, ""), false}, - {"missing recipient address", NewFungibleTokenPacketData(denom, amount, addr1, emptyAddr, ""), false}, + {"valid packet", types.NewFungibleTokenPacketData(denom, amount, addr1, addr2, ""), true}, + {"valid packet with memo", types.NewFungibleTokenPacketData(denom, amount, addr1, addr2, "memo"), true}, + {"valid packet with large amount", types.NewFungibleTokenPacketData(denom, largeAmount, addr1, addr2, ""), true}, + {"invalid denom", types.NewFungibleTokenPacketData("", amount, addr1, addr2, ""), false}, + {"invalid empty amount", types.NewFungibleTokenPacketData(denom, "", addr1, addr2, ""), false}, + {"invalid zero amount", types.NewFungibleTokenPacketData(denom, "0", addr1, addr2, ""), false}, + {"invalid negative amount", types.NewFungibleTokenPacketData(denom, "-1", addr1, addr2, ""), false}, + {"invalid large amount", types.NewFungibleTokenPacketData(denom, invalidLargeAmount, addr1, addr2, ""), false}, + {"missing sender address", types.NewFungibleTokenPacketData(denom, amount, emptyAddr, addr2, ""), false}, + {"missing recipient address", types.NewFungibleTokenPacketData(denom, amount, addr1, emptyAddr, ""), false}, } for i, tc := range testCases { @@ -41,3 +44,213 @@ func TestFungibleTokenPacketDataValidateBasic(t *testing.T) { } } } + +func (suite *TypesTestSuite) TestGetSourceCallbackAddress() { + testCases := []struct { + name string + packetData types.FungibleTokenPacketData + expPass bool + }{ + { + "memo is empty", + types.FungibleTokenPacketData{ + Denom: denom, + Amount: amount, + Sender: addr1, + Receiver: addr2, + Memo: "", + }, + false, + }, + { + "memo is not json string", + types.FungibleTokenPacketData{ + Denom: denom, + Amount: amount, + Sender: addr1, + Receiver: addr2, + Memo: "memo", + }, + false, + }, + { + "memo does not have callbacks in json struct", + types.FungibleTokenPacketData{ + Denom: denom, + Amount: amount, + Sender: addr1, + Receiver: addr2, + Memo: `{"Key": 10}`, + }, + false, + }, + { + "memo has callbacks in json struct but does not have src_callback_address key", + types.FungibleTokenPacketData{ + Denom: denom, + Amount: amount, + Sender: addr1, + Receiver: addr2, + Memo: `{"callbacks": {"Key": 10}}`, + }, + false, + }, + { + "memo has callbacks in json struct but does not have string value for src_callback_address key", + types.FungibleTokenPacketData{ + Denom: denom, + Amount: amount, + Sender: addr1, + Receiver: addr2, + Memo: `{"callbacks": {"src_callback_address": 10}}`, + }, + false, + }, + { + "memo has callbacks in json struct and properly formatted src_callback_address which does not match packet sender", + types.FungibleTokenPacketData{ + Denom: denom, + Amount: amount, + Sender: addr1, + Receiver: addr2, + Memo: `{"callbacks": {"src_callback_address": "testAddress"}}`, + }, + false, + }, + { + "valid src_callback_address specified in memo that matches sender", + types.FungibleTokenPacketData{ + Denom: denom, + Amount: amount, + Sender: addr1, + Receiver: addr2, + Memo: fmt.Sprintf(`{"callbacks": {"src_callback_address": "%s"}}`, addr1), + }, + true, + }, + } + + for _, tc := range testCases { + tc := tc + suite.Run(tc.name, func() { + srcCbAddr := tc.packetData.GetSourceCallbackAddress() + + if tc.expPass { + suite.Require().Equal(addr1, srcCbAddr) + } else { + suite.Require().Equal("", srcCbAddr) + } + }) + } +} + +func (suite *TypesTestSuite) TestGetDestCallbackAddress() { + testCases := []struct { + name string + packetData types.FungibleTokenPacketData + expPass bool + }{ + { + "memo is empty", + types.FungibleTokenPacketData{ + Denom: denom, + Amount: amount, + Sender: addr1, + Receiver: addr2, + Memo: "", + }, + false, + }, + { + "memo is not json string", + types.FungibleTokenPacketData{ + Denom: denom, + Amount: amount, + Sender: addr1, + Receiver: addr2, + Memo: "memo", + }, + false, + }, + { + "memo does not have callbacks in json struct", + types.FungibleTokenPacketData{ + Denom: denom, + Amount: amount, + Sender: addr1, + Receiver: addr2, + Memo: `{"Key": 10}`, + }, + false, + }, + { + "memo has callbacks in json struct but does not have dest_callback_address key", + types.FungibleTokenPacketData{ + Denom: denom, + Amount: amount, + Sender: addr1, + Receiver: addr2, + Memo: `{"callbacks": {"Key": 10}}`, + }, + false, + }, + { + "memo has callbacks in json struct but does not have string value for dest_callback_address key", + types.FungibleTokenPacketData{ + Denom: denom, + Amount: amount, + Sender: addr1, + Receiver: addr2, + Memo: `{"callbacks": {"dest_callback_address": 10}}`, + }, + false, + }, + { + "memo has callbacks in json struct and properly formatted dest_callback_address which does not match packet sender", + types.FungibleTokenPacketData{ + Denom: denom, + Amount: amount, + Sender: addr1, + Receiver: addr2, + Memo: `{"callbacks": {"dest_callback_address": "testAddress"}}`, + }, + false, + }, + { + "valid dest_callback_address specified in memo that matches sender", + types.FungibleTokenPacketData{ + Denom: denom, + Amount: amount, + Sender: addr1, + Receiver: addr2, + Memo: fmt.Sprintf(`{"callbacks": {"dest_callback_address": "%s"}}`, addr2), + }, + true, + }, + } + + for _, tc := range testCases { + tc := tc + suite.Run(tc.name, func() { + destCbAddr := tc.packetData.GetDestCallbackAddress() + + if tc.expPass { + suite.Require().Equal(addr2, destCbAddr) + } else { + suite.Require().Equal("", destCbAddr) + } + }) + } +} + +func (suite *TypesTestSuite) TestUserDefinedGasLimit() { + packetData := types.FungibleTokenPacketData{ + Denom: denom, + Amount: amount, + Sender: addr1, + Receiver: addr2, + Memo: `{"callbacks": {"user_defined_gas_limit": 100}}`, + } + + suite.Require().Equal(uint64(0), packetData.UserDefinedGasLimit(), "user defined gas limit does not return 0") +} diff --git a/modules/core/exported/channel.go b/modules/core/exported/channel.go index f6393393513..a6ec511880b 100644 --- a/modules/core/exported/channel.go +++ b/modules/core/exported/channel.go @@ -17,23 +17,3 @@ type CounterpartyChannelI interface { GetChannelID() string ValidateBasic() error } - -// PacketI defines the standard interface for IBC packets -type PacketI interface { - GetSequence() uint64 - GetTimeoutHeight() Height - GetTimeoutTimestamp() uint64 - GetSourcePort() string - GetSourceChannel() string - GetDestPort() string - GetDestChannel() string - GetData() []byte - ValidateBasic() error -} - -// Acknowledgement defines the interface used to return -// acknowledgements in the OnRecvPacket callback. -type Acknowledgement interface { - Success() bool - Acknowledgement() []byte -} diff --git a/modules/core/exported/packet.go b/modules/core/exported/packet.go new file mode 100644 index 00000000000..39c9dc3fdb6 --- /dev/null +++ b/modules/core/exported/packet.go @@ -0,0 +1,48 @@ +package exported + +// PacketI defines the standard interface for IBC packets +type PacketI interface { + GetSequence() uint64 + GetTimeoutHeight() Height + GetTimeoutTimestamp() uint64 + GetSourcePort() string + GetSourceChannel() string + GetDestPort() string + GetDestChannel() string + GetData() []byte + ValidateBasic() error +} + +// Acknowledgement defines the interface used to return +// acknowledgements in the OnRecvPacket callback. +type Acknowledgement interface { + Success() bool + Acknowledgement() []byte +} + +// CallbackPacketData defines the interface used by ADR 008 implementations +// to obtain callback addresses associated with a specific packet data type. +// This is an optional interface which indicates support for ADR 8 implementations. +// See https://github.com/cosmos/ibc-go/tree/main/docs/architecture/adr-008-app-caller-cbs +// for more information. +type CallbackPacketData interface { + // GetSourceCallbackAddress should return the callback address of a packet data on the source chain. + // This may or may not be the sender of the packet. If no source callback address exists for the packet, + // an empty string may be returned. + GetSourceCallbackAddress() string + + // GetDestCallbackAddress should return the callback address of a packet data on the destination chain. + // This may or may not be the receiver of the packet. If no dest callback address exists for the packet, + // an empty string may be returned. + GetDestCallbackAddress() string + + // UserDefinedGasLimit allows the sender of the packet to define inside the packet data + // a gas limit for how much the ADR-8 callbacks can consume. If defined, this will be passed + // in as the gas limit so that the callback is guaranteed to complete within a specific limit. + // On recvPacket, a gas-overflow will just fail the transaction allowing it to timeout on the sender side. + // On ackPacket and timeoutPacket, a gas-overflow will reject state changes made during callback but still + // commit the transaction. This ensures the packet lifecycle can always complete. + // If the packet data returns 0, the remaining gas limit will be passed in (modulo any chain-defined limit) + // Otherwise, we will set the gas limit passed into the callback to the `min(ctx.GasLimit, UserDefinedGasLimit())` + UserDefinedGasLimit() uint64 +}