diff --git a/modules/apps/08-callbacks/ibc_middleware.go b/modules/apps/08-callbacks/ibc_middleware.go new file mode 100644 index 00000000000..d23474fa944 --- /dev/null +++ b/modules/apps/08-callbacks/ibc_middleware.go @@ -0,0 +1,221 @@ +package callbacks + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + capabilitytypes "github.com/cosmos/cosmos-sdk/x/capability/types" + + clienttypes "github.com/cosmos/ibc-go/v7/modules/core/02-client/types" + channeltypes "github.com/cosmos/ibc-go/v7/modules/core/04-channel/types" + porttypes "github.com/cosmos/ibc-go/v7/modules/core/05-port/types" + "github.com/cosmos/ibc-go/v7/modules/core/exported" +) + +type Example struct { +} + +var _ porttypes.Middleware = &IBCMiddleware[Example]{} + +// todo: concrete type +type CallbackPacketData any + +type IBCMiddleware[T CallbackPacketData] struct { + next porttypes.Middleware + decoder Decoder[T] + executor Executor[T] + limits GasLimits +} + +// Decoder unpacks a raw ibc packet to the custom type +type Decoder[T CallbackPacketData] interface { + // Decode packet to custom type. An empty result skips callback execution, error aborts process + Decode(packet channeltypes.Packet) (*T, error) +} + +// Executor executes the callback +type Executor[T CallbackPacketData] interface { + OnRecvPacket(ctx sdk.Context, obj T, relayer sdk.AccAddress) error + OnAcknowledgementPacket(ctx sdk.Context, obj T, acknowledgement []byte, relayer sdk.AccAddress) error + OnTimeoutPacket(ctx sdk.Context, obj T, relayer sdk.AccAddress) error +} +type GasLimits struct { + OnRecvPacketLimit *sdk.Gas + OnAcknowledgementPacketLimit *sdk.Gas + OnTimeoutPacketLimit *sdk.Gas +} + +// NewIBCMiddleware constructor +func NewIBCMiddleware[T CallbackPacketData](next porttypes.Middleware, decoder Decoder[T], executor Executor[T], limits GasLimits) IBCMiddleware[T] { + return IBCMiddleware[T]{ + next: next, + decoder: decoder, + executor: executor, + limits: limits, + } +} + +// OnRecvPacket implements the IBCMiddleware interface. +// If fees are not enabled, this callback will default to the ibc-core packet callback +func (im IBCMiddleware[T]) OnRecvPacket( + ctx sdk.Context, + packet channeltypes.Packet, + relayer sdk.AccAddress, +) exported.Acknowledgement { + + ack := im.next.OnRecvPacket(ctx, packet, relayer) + + // todo: limit gas + p, err := im.decoder.Decode(packet) + if err != nil { + // see comments in https://github.com/cosmos/ibc-go/blob/main/docs/architecture/adr-008-app-caller-cbs/adr-008-app-caller-cbs.md + // todo: filter out events: https://github.com/cosmos/ibc-go/issues/3358 + // todo: set event with raw error for debug as defined in ErrorAcknowledgement? + return channeltypes.NewErrorAcknowledgement(err) + } + if p != nil { + if err := im.executor.OnRecvPacket(ctx, *p, relayer); err != nil { + // see comments in https://github.com/cosmos/ibc-go/blob/main/docs/architecture/adr-008-app-caller-cbs/adr-008-app-caller-cbs.md + // todo: filter out events: https://github.com/cosmos/ibc-go/issues/3358 + // todo: set event with raw error for debug as defined in ErrorAcknowledgement? + return channeltypes.NewErrorAcknowledgement(err) + } + } + return ack +} + +// OnAcknowledgementPacket implements the IBCMiddleware interface +// If fees are not enabled, this callback will default to the ibc-core packet callback +func (im IBCMiddleware[T]) OnAcknowledgementPacket( + ctx sdk.Context, + packet channeltypes.Packet, + acknowledgement []byte, + relayer sdk.AccAddress, +) error { + // call underlying callback + if err := im.next.OnAcknowledgementPacket(ctx, packet, acknowledgement, relayer); err != nil { + return err + } + // todo: limit gas + p, err := im.decoder.Decode(packet) + if err != nil { + return err + } + if p != nil { + return im.executor.OnAcknowledgementPacket(ctx, *p, acknowledgement, relayer) + } + return nil +} + +// OnTimeoutPacket implements the IBCMiddleware interface +// If fees are not enabled, this callback will default to the ibc-core packet callback +func (im IBCMiddleware[T]) OnTimeoutPacket( + ctx sdk.Context, + packet channeltypes.Packet, + relayer sdk.AccAddress, +) error { + if err := im.next.OnTimeoutPacket(ctx, packet, relayer); err != nil { + return err + } + // todo: limit gas + p, err := im.decoder.Decode(packet) + if err != nil { + return err + } + if p != nil { + return im.executor.OnTimeoutPacket(ctx, *p, relayer) + } + return nil +} + +// OnChanOpenInit implements the IBCMiddleware interface +func (im IBCMiddleware[T]) OnChanOpenInit( + ctx sdk.Context, + order channeltypes.Order, + connectionHops []string, + portID string, + channelID string, + chanCap *capabilitytypes.Capability, + counterparty channeltypes.Counterparty, + version string, +) (string, error) { + return im.OnChanOpenInit(ctx, order, connectionHops, portID, channelID, chanCap, counterparty, version) +} + +// OnChanOpenTry implements the IBCMiddleware interface +func (im IBCMiddleware[T]) OnChanOpenTry( + ctx sdk.Context, + order channeltypes.Order, + connectionHops []string, + portID, + channelID string, + chanCap *capabilitytypes.Capability, + counterparty channeltypes.Counterparty, + counterpartyVersion string, +) (string, error) { + return im.OnChanOpenTry(ctx, order, connectionHops, portID, channelID, chanCap, counterparty, counterpartyVersion) +} + +// OnChanOpenAck implements the IBCMiddleware interface +func (im IBCMiddleware[T]) OnChanOpenAck( + ctx sdk.Context, + portID, + channelID string, + counterpartyChannelID string, + counterpartyVersion string, +) error { + return im.next.OnChanOpenAck(ctx, portID, channelID, counterpartyChannelID, counterpartyVersion) +} + +// OnChanOpenConfirm implements the IBCMiddleware interface +func (im IBCMiddleware[T]) OnChanOpenConfirm( + ctx sdk.Context, + portID, + channelID string, +) error { + return im.next.OnChanOpenConfirm(ctx, portID, channelID) +} + +// OnChanCloseInit implements the IBCMiddleware interface +func (im IBCMiddleware[T]) OnChanCloseInit( + ctx sdk.Context, + portID, + channelID string, +) error { + return im.next.OnChanCloseInit(ctx, portID, channelID) +} + +// OnChanCloseConfirm implements the IBCMiddleware interface +func (im IBCMiddleware[T]) OnChanCloseConfirm( + ctx sdk.Context, + portID, + channelID string, +) error { + return im.next.OnChanCloseConfirm(ctx, portID, channelID) +} + +// SendPacket implements the ICS4 Wrapper interface +func (im IBCMiddleware[T]) SendPacket( + ctx sdk.Context, + chanCap *capabilitytypes.Capability, + sourcePort string, + sourceChannel string, + timeoutHeight clienttypes.Height, + timeoutTimestamp uint64, + data []byte, +) (uint64, error) { + return im.next.SendPacket(ctx, chanCap, sourcePort, sourceChannel, timeoutHeight, timeoutTimestamp, data) +} + +// WriteAcknowledgement implements the ICS4 Wrapper interface +func (im IBCMiddleware[T]) WriteAcknowledgement( + ctx sdk.Context, + chanCap *capabilitytypes.Capability, + packet exported.PacketI, + ack exported.Acknowledgement, +) error { + return im.next.WriteAcknowledgement(ctx, chanCap, packet, ack) +} + +// GetAppVersion returns the application version of the underlying application +func (im IBCMiddleware[T]) GetAppVersion(ctx sdk.Context, portID, channelID string) (string, bool) { + return "ics-8.0-alex", true +} diff --git a/modules/apps/08-callbacks/ics20_example_cb.go b/modules/apps/08-callbacks/ics20_example_cb.go new file mode 100644 index 00000000000..fb68134e696 --- /dev/null +++ b/modules/apps/08-callbacks/ics20_example_cb.go @@ -0,0 +1,58 @@ +package callbacks + +import ( + "encoding/json" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/ibc-go/v7/modules/apps/transfer/types" + channeltypes "github.com/cosmos/ibc-go/v7/modules/core/04-channel/types" +) + +// MyICS20Payload my random payload example +type MyICS20Payload struct { + SrcCallbackAddress string `json:"src_callback_address"` + DstCallbackAddress string `json:"dst_callback_address"` +} + +var _ Decoder[MyICS20Payload] = &MyICS20Decoder{} + +type MyICS20Decoder struct { //nolint:gofumpt +} + +func (m MyICS20Decoder) Decode(packet channeltypes.Packet) (*MyICS20Payload, error) { + var data types.FungibleTokenPacketData + if err := types.ModuleCdc.UnmarshalJSON(packet.GetData(), &data); err != nil { + return nil, err + } + // do we need a sanity check that type was unpacked proper? like denom not empty + + if data.Memo == "" { + return nil, nil + } + // unmarshal json to type, I prefer using concrete type here: + var r MyICS20Payload + if err := json.Unmarshal([]byte(data.Memo), &r); err != nil { + return nil, err + } + // todo: check that contains valid data + return &r, nil +} + +var _ Executor[MyICS20Payload] = &MyICS20Callbacks{} + +type MyICS20Callbacks struct { //nolint:gofumpt +} + +func (m MyICS20Callbacks) OnRecvPacket(ctx sdk.Context, obj MyICS20Payload, relayer sdk.AccAddress) error { + ctx.Logger().Info("OnRecvPacket executed") + return nil +} + +func (m MyICS20Callbacks) OnAcknowledgementPacket(ctx sdk.Context, obj MyICS20Payload, acknowledgement []byte, relayer sdk.AccAddress) error { + ctx.Logger().Info("OnAcknowledgementPacket executed") + return nil +} + +func (m MyICS20Callbacks) OnTimeoutPacket(ctx sdk.Context, obj MyICS20Payload, relayer sdk.AccAddress) error { + ctx.Logger().Info("OnTimeoutPacket executed") + return nil +} diff --git a/testing/simapp/app.go b/testing/simapp/app.go index 6d454eefcb8..8aac5f60571 100644 --- a/testing/simapp/app.go +++ b/testing/simapp/app.go @@ -3,6 +3,7 @@ package simapp import ( "encoding/json" "fmt" + callbacks "github.com/cosmos/ibc-go/v7/modules/apps/08-callbacks" "io" "net/http" "os" @@ -471,7 +472,12 @@ func NewSimApp( // create IBC module from bottom to top of stack var transferStack porttypes.IBCModule transferStack = transfer.NewIBCModule(app.TransferKeeper) - transferStack = ibcfee.NewIBCMiddleware(transferStack, app.IBCFeeKeeper) + transferStack = callbacks.NewIBCMiddleware[callbacks.MyICS20Payload]( + ibcfee.NewIBCMiddleware(transferStack, app.IBCFeeKeeper), + callbacks.MyICS20Decoder{}, + callbacks.MyICS20Callbacks{}, + callbacks.GasLimits{}, + ) // Add transfer stack to IBC Router ibcRouter.AddRoute(ibctransfertypes.ModuleName, transferStack)