Some checks failed
Docs Deploy / build_and_deploy (push) Has been cancelled
Generate Docs / cli (push) Has been cancelled
Generate Config Doc / cli (push) Has been cancelled
Go formatting / go-formatting (push) Has been cancelled
Check links / markdown-link-check (push) Has been cancelled
Integration / pre-test (push) Has been cancelled
Integration / test on (push) Has been cancelled
Integration / status (push) Has been cancelled
Lint / Lint Go code (push) Has been cancelled
Test / test (ubuntu-latest) (push) Has been cancelled
439 lines
14 KiB
Go
439 lines
14 KiB
Go
package ibc
|
|
|
|
import (
|
|
"embed"
|
|
"fmt"
|
|
"io/fs"
|
|
"path/filepath"
|
|
|
|
"github.com/emicklei/proto"
|
|
"github.com/gobuffalo/genny/v2"
|
|
"github.com/gobuffalo/plush/v4"
|
|
|
|
"git.cw.tr/mukan-network/mukan-ignite/ignite/pkg/errors"
|
|
"git.cw.tr/mukan-network/mukan-ignite/ignite/pkg/multiformatname"
|
|
"git.cw.tr/mukan-network/mukan-ignite/ignite/pkg/protoanalysis/protoutil"
|
|
"git.cw.tr/mukan-network/mukan-ignite/ignite/pkg/xast"
|
|
"git.cw.tr/mukan-network/mukan-ignite/ignite/pkg/xgenny"
|
|
"git.cw.tr/mukan-network/mukan-ignite/ignite/pkg/xstrings"
|
|
"git.cw.tr/mukan-network/mukan-ignite/ignite/templates/field"
|
|
"git.cw.tr/mukan-network/mukan-ignite/ignite/templates/field/plushhelpers"
|
|
"git.cw.tr/mukan-network/mukan-ignite/ignite/templates/typed"
|
|
)
|
|
|
|
var (
|
|
//go:embed files/packet/component/* files/packet/component/**/*
|
|
fsPacketComponent embed.FS
|
|
|
|
//go:embed files/packet/messages/* files/packet/messages/**/*
|
|
fsPacketMessages embed.FS
|
|
)
|
|
|
|
// PacketOptions are options to scaffold a packet in a IBC module.
|
|
type PacketOptions struct {
|
|
AppName string
|
|
ProtoDir string
|
|
ProtoVer string
|
|
ModuleName string
|
|
ModulePath string
|
|
PacketName multiformatname.Name
|
|
MsgSigner multiformatname.Name
|
|
Fields field.Fields
|
|
AckFields field.Fields
|
|
NoMessage bool
|
|
}
|
|
|
|
// ProtoFile returns the path to the proto folder.
|
|
func (opts *PacketOptions) ProtoFile(fname string) string {
|
|
return filepath.Join(opts.ProtoDir, opts.AppName, opts.ModuleName, opts.ProtoVer, fname)
|
|
}
|
|
|
|
// NewPacket returns the generator to scaffold a packet in an IBC module.
|
|
func NewPacket(opts *PacketOptions) (*genny.Generator, error) {
|
|
subPacketComponent, err := fs.Sub(fsPacketComponent, "files/packet/component")
|
|
if err != nil {
|
|
return nil, errors.Errorf("fail to generate sub: %w", err)
|
|
}
|
|
subPacketMessages, err := fs.Sub(fsPacketMessages, "files/packet/messages")
|
|
if err != nil {
|
|
return nil, errors.Errorf("fail to generate sub: %w", err)
|
|
}
|
|
|
|
// Add the component
|
|
g := genny.New()
|
|
g.RunFn(moduleModify(opts))
|
|
g.RunFn(protoModify(opts))
|
|
g.RunFn(eventModify(opts))
|
|
if err := g.OnlyFS(subPacketComponent, nil, nil); err != nil {
|
|
return g, err
|
|
}
|
|
|
|
// Add the send message
|
|
if !opts.NoMessage {
|
|
g.RunFn(protoTxModify(opts))
|
|
g.RunFn(clientCliTxModify(opts))
|
|
g.RunFn(codecModify(opts))
|
|
if err := g.OnlyFS(subPacketMessages, nil, nil); err != nil {
|
|
return g, err
|
|
}
|
|
}
|
|
|
|
ctx := plush.NewContext()
|
|
ctx.Set("moduleName", opts.ModuleName)
|
|
ctx.Set("ModulePath", opts.ModulePath)
|
|
ctx.Set("appName", opts.AppName)
|
|
ctx.Set("protoVer", opts.ProtoVer)
|
|
ctx.Set("packetName", opts.PacketName)
|
|
ctx.Set("MsgSigner", opts.MsgSigner)
|
|
ctx.Set("fields", opts.Fields)
|
|
ctx.Set("ackFields", opts.AckFields)
|
|
|
|
plushhelpers.ExtendPlushContext(ctx)
|
|
g.Transformer(xgenny.Transformer(ctx))
|
|
g.Transformer(genny.Replace("{{protoDir}}", opts.ProtoDir))
|
|
g.Transformer(genny.Replace("{{appName}}", opts.AppName))
|
|
g.Transformer(genny.Replace("{{moduleName}}", opts.ModuleName))
|
|
g.Transformer(genny.Replace("{{protoVer}}", opts.ProtoVer))
|
|
g.Transformer(genny.Replace("{{packetName}}", opts.PacketName.Snake))
|
|
|
|
return g, nil
|
|
}
|
|
|
|
func moduleModify(opts *PacketOptions) genny.RunFn {
|
|
return func(r *genny.Runner) error {
|
|
path := filepath.Join("x", opts.ModuleName, "module/module_ibc.go")
|
|
f, err := r.Disk.Find(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Recv packet dispatch
|
|
templateRecv := `packetAck, err := im.keeper.OnRecv%[1]vPacket(ctx, modulePacket, *packet.%[1]vPacket)
|
|
if err != nil {
|
|
ack = channeltypes.NewErrorAcknowledgement(err)
|
|
} else {
|
|
// Encode packet acknowledgment
|
|
packetAckBytes, err := im.cdc.MarshalJSON(&packetAck)
|
|
if err != nil {
|
|
return channeltypes.NewErrorAcknowledgement(errorsmod.Wrap(sdkerrors.ErrJSONMarshal, err.Error()))
|
|
}
|
|
ack = channeltypes.NewResultAcknowledgement(packetAckBytes)
|
|
}
|
|
|
|
|
|
sdkCtx := sdk.UnwrapSDKContext(ctx)
|
|
sdkCtx.EventManager().EmitEvent(
|
|
sdk.NewEvent(
|
|
types.EventType%[1]vPacket,
|
|
sdk.NewAttribute(sdk.AttributeKeyModule, types.ModuleName),
|
|
sdk.NewAttribute(types.AttributeKeyAckSuccess, fmt.Sprintf("%%t", err != nil)),
|
|
),
|
|
)`
|
|
replacementRecv := fmt.Sprintf(
|
|
templateRecv,
|
|
opts.PacketName.UpperCamel,
|
|
)
|
|
content, err := xast.ModifyFunction(
|
|
f.String(),
|
|
"OnRecvPacket",
|
|
xast.AppendSwitchCase(
|
|
"packet := modulePacketData.Packet.(type)",
|
|
fmt.Sprintf("*types.%[1]vPacketData_%[2]vPacket", xstrings.Title(opts.ModuleName), opts.PacketName.UpperCamel),
|
|
replacementRecv,
|
|
),
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Ack packet dispatch
|
|
templateAck := `err := im.keeper.OnAcknowledgement%[1]vPacket(ctx, modulePacket, *packet.%[1]vPacket, ack)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
eventType = types.EventType%[1]vPacket`
|
|
replacementAck := fmt.Sprintf(
|
|
templateAck,
|
|
opts.PacketName.UpperCamel,
|
|
)
|
|
content, err = xast.ModifyFunction(
|
|
content,
|
|
"OnAcknowledgementPacket",
|
|
xast.AppendSwitchCase(
|
|
"packet := modulePacketData.Packet.(type)",
|
|
fmt.Sprintf("*types.%[1]vPacketData_%[2]vPacket", xstrings.Title(opts.ModuleName), opts.PacketName.UpperCamel),
|
|
replacementAck,
|
|
),
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Timeout packet dispatch
|
|
templateTimeout := `err := im.keeper.OnTimeout%[1]vPacket(ctx, modulePacket, *packet.%[1]vPacket)
|
|
if err != nil {
|
|
return err
|
|
}`
|
|
replacementTimeout := fmt.Sprintf(
|
|
templateTimeout,
|
|
opts.PacketName.UpperCamel,
|
|
)
|
|
content, err = xast.ModifyFunction(
|
|
content,
|
|
"OnTimeoutPacket",
|
|
xast.AppendSwitchCase(
|
|
"packet := modulePacketData.Packet.(type)",
|
|
fmt.Sprintf("*types.%[1]vPacketData_%[2]vPacket", xstrings.Title(opts.ModuleName), opts.PacketName.UpperCamel),
|
|
replacementTimeout,
|
|
),
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
newFile := genny.NewFileS(path, content)
|
|
return r.File(newFile)
|
|
}
|
|
}
|
|
|
|
// Modifies packet.proto to add a field on the oneof element of the message created and
|
|
// add a couple of messages.
|
|
//
|
|
// What it depends on:
|
|
// - Existence of a Oneof field named 'packet'.
|
|
func protoModify(opts *PacketOptions) genny.RunFn {
|
|
return func(r *genny.Runner) error {
|
|
path := opts.ProtoFile("packet.proto")
|
|
f, err := r.Disk.Find(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
protoFile, err := protoutil.ParseProtoFile(f)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
name := fmt.Sprintf("%sPacketData", xstrings.Title(opts.ModuleName))
|
|
message, err := protoutil.GetMessageByName(protoFile, name)
|
|
if err != nil {
|
|
return errors.Errorf("failed while looking up '%s' message in %s: %w", name, path, err)
|
|
}
|
|
// Use a directly Apply call here, modifying oneofs isn't common enough to warrant a separate function.
|
|
var packet *proto.Oneof
|
|
protoutil.Apply(message, nil, func(c *protoutil.Cursor) bool {
|
|
if o, ok := c.Node().(*proto.Oneof); ok {
|
|
if o.Name == "packet" {
|
|
packet = o
|
|
return false
|
|
}
|
|
}
|
|
// continue traversing.
|
|
return true
|
|
})
|
|
if packet == nil {
|
|
return errors.Errorf("could not find 'oneof packet' in message '%s' of file %s", name, path)
|
|
}
|
|
// Count fields of oneof:
|
|
maximum := 1
|
|
protoutil.Apply(packet, nil, func(c *protoutil.Cursor) bool {
|
|
if o, ok := c.Node().(*proto.OneOfField); ok {
|
|
if o.Sequence > maximum {
|
|
maximum = o.Sequence
|
|
}
|
|
}
|
|
return true
|
|
})
|
|
// Add it to Oneof.
|
|
typenamePascal, typenameSnake := opts.PacketName.PascalCase, opts.PacketName.Snake
|
|
packetField := protoutil.NewOneofField(typenameSnake+"_packet", typenamePascal+"PacketData", maximum+1)
|
|
protoutil.Append(packet, packetField)
|
|
|
|
// Add the message definition for packet and acknowledgment
|
|
var packetFields []*proto.NormalField
|
|
for i, f := range opts.Fields {
|
|
packetFields = append(packetFields, f.ToProtoField(i+1))
|
|
}
|
|
packetData := protoutil.NewMessage(typenamePascal+"PacketData", protoutil.WithFields(packetFields...))
|
|
protoutil.AttachComment(packetData, typenamePascal+"PacketData defines a struct for the packet payload")
|
|
var ackFields []*proto.NormalField
|
|
for i, f := range opts.AckFields {
|
|
ackFields = append(ackFields, f.ToProtoField(i+1))
|
|
}
|
|
packetAck := protoutil.NewMessage(typenamePascal+"PacketAck", protoutil.WithFields(ackFields...))
|
|
protoutil.AttachComment(packetAck, typenamePascal+"PacketAck defines a struct for the packet acknowledgment")
|
|
protoutil.Append(protoFile, packetData, packetAck)
|
|
|
|
// Add any custom imports.
|
|
var protoImports []*proto.Import
|
|
for _, imp := range append(opts.Fields.ProtoImports(), opts.AckFields.ProtoImports()...) {
|
|
protoImports = append(protoImports, protoutil.NewImport(imp))
|
|
}
|
|
for _, f := range append(opts.Fields.Custom(), opts.AckFields.Custom()...) {
|
|
protoPath := fmt.Sprintf("%[1]v/%[2]v/%[3]v/%[4]v.proto", opts.AppName, opts.ModuleName, opts.ProtoVer, f)
|
|
protoImports = append(protoImports, protoutil.NewImport(protoPath))
|
|
}
|
|
if err := protoutil.AddImports(protoFile, true, protoImports...); err != nil {
|
|
return errors.Errorf("failed while adding imports to %s: %w", path, err)
|
|
}
|
|
|
|
newFile := genny.NewFileS(path, protoutil.Print(protoFile))
|
|
return r.File(newFile)
|
|
}
|
|
}
|
|
|
|
func eventModify(opts *PacketOptions) genny.RunFn {
|
|
return func(r *genny.Runner) error {
|
|
path := filepath.Join("x", opts.ModuleName, "types/events_ibc.go")
|
|
f, err := r.Disk.Find(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Keeper declaration
|
|
content, err := xast.InsertGlobal(
|
|
f.String(),
|
|
xast.GlobalTypeConst,
|
|
xast.WithGlobal(
|
|
fmt.Sprintf("EventType%[1]vPacket", opts.PacketName.UpperCamel),
|
|
"",
|
|
fmt.Sprintf(`"%[1]v_packet"`, opts.PacketName.LowerCamel),
|
|
),
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
newFile := genny.NewFileS(path, content)
|
|
return r.File(newFile)
|
|
}
|
|
}
|
|
|
|
// Modifies tx.proto to add a new RPC and the required messages.
|
|
//
|
|
// What it depends on:
|
|
// - Existence of a service named 'Msg'. The other elements don't depend on already existing
|
|
// elements in the file.
|
|
func protoTxModify(opts *PacketOptions) genny.RunFn {
|
|
return func(r *genny.Runner) error {
|
|
path := opts.ProtoFile("tx.proto")
|
|
f, err := r.Disk.Find(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
protoFile, err := protoutil.ParseProtoFile(f)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Add RPC to service Msg.
|
|
serviceMsg, err := protoutil.GetServiceByName(protoFile, "Msg")
|
|
if err != nil {
|
|
return errors.Errorf("failed while looking up service 'Msg' in %s: %w", path, err)
|
|
}
|
|
typenamePascal := opts.PacketName.PascalCase
|
|
send := protoutil.NewRPC(
|
|
fmt.Sprintf("Send%s", typenamePascal),
|
|
fmt.Sprintf("MsgSend%s", typenamePascal),
|
|
fmt.Sprintf("MsgSend%sResponse", typenamePascal),
|
|
)
|
|
protoutil.Append(serviceMsg, send)
|
|
|
|
// Create fields for MsgSend.
|
|
var sendFields []*proto.NormalField
|
|
for i, field := range opts.Fields {
|
|
sendFields = append(sendFields, field.ToProtoField(i+5))
|
|
}
|
|
|
|
// set address options on signer field
|
|
signerField := protoutil.NewField(opts.MsgSigner.Snake, "string", 1)
|
|
signerField.Options = append(signerField.Options, protoutil.NewOption("cosmos_proto.scalar", "cosmos.AddressString", protoutil.Custom()))
|
|
|
|
sendFields = append(sendFields,
|
|
signerField,
|
|
protoutil.NewField("port", "string", 2),
|
|
protoutil.NewField("channelID", "string", 3),
|
|
protoutil.NewField("timeoutTimestamp", "uint64", 4),
|
|
)
|
|
creatorOpt := protoutil.NewOption(typed.MsgSignerOption, opts.MsgSigner.Snake)
|
|
|
|
// Create MsgSend, MsgSendResponse and add to file.
|
|
msgSend := protoutil.NewMessage(
|
|
"MsgSend"+typenamePascal,
|
|
protoutil.WithFields(sendFields...),
|
|
protoutil.WithMessageOptions(creatorOpt),
|
|
)
|
|
msgSendResponse := protoutil.NewMessage("MsgSend" + typenamePascal + "Response")
|
|
protoutil.Append(protoFile, msgSend, msgSendResponse)
|
|
|
|
// Ensure custom types are imported
|
|
var protoImports []*proto.Import
|
|
for _, imp := range opts.Fields.ProtoImports() {
|
|
protoImports = append(protoImports, protoutil.NewImport(imp))
|
|
}
|
|
for _, f := range opts.Fields.Custom() {
|
|
protopath := fmt.Sprintf("%[1]v/%[2]v/%[3]v/%[4]v.proto", opts.AppName, opts.ModuleName, opts.ProtoVer, f)
|
|
protoImports = append(protoImports, protoutil.NewImport(protopath))
|
|
}
|
|
if err := protoutil.AddImports(protoFile, true, protoImports...); err != nil {
|
|
return errors.Errorf("error while processing %s: %w", path, err)
|
|
}
|
|
|
|
newFile := genny.NewFileS(path, protoutil.Print(protoFile))
|
|
return r.File(newFile)
|
|
}
|
|
}
|
|
|
|
// clientCliTxModify does not use AutoCLI here, because it as a better UX as it is.
|
|
func clientCliTxModify(opts *PacketOptions) genny.RunFn {
|
|
return func(r *genny.Runner) error {
|
|
filePath := filepath.Join("x", opts.ModuleName, "client/cli/tx.go")
|
|
f, err := r.Disk.Find(filePath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
replacement := fmt.Sprintf("cmd.AddCommand(CmdSend%[1]v())", opts.PacketName.UpperCamel)
|
|
content, err := xast.ModifyFunction(
|
|
f.String(),
|
|
"GetTxCmd",
|
|
xast.AppendFuncCode(replacement),
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
newFile := genny.NewFileS(filePath, content)
|
|
return r.File(newFile)
|
|
}
|
|
}
|
|
|
|
func codecModify(opts *PacketOptions) genny.RunFn {
|
|
return func(r *genny.Runner) error {
|
|
path := filepath.Join("x", opts.ModuleName, "types/codec.go")
|
|
f, err := r.Disk.Find(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Set import if not set yet
|
|
content, err := xast.AppendImports(f.String(), xast.WithNamedImport("sdk", "github.com/cosmos/cosmos-sdk/types"))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Register the module packet interface
|
|
templateInterface := `registrar.RegisterImplementations((*sdk.Msg)(nil),
|
|
&MsgSend%[1]v{},
|
|
)`
|
|
replacementInterface := fmt.Sprintf(templateInterface, opts.PacketName.PascalCase)
|
|
content, err = xast.ModifyFunction(
|
|
content,
|
|
"RegisterInterfaces",
|
|
xast.AppendFuncAtLine(replacementInterface, 0),
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
newFile := genny.NewFileS(path, content)
|
|
return r.File(newFile)
|
|
}
|
|
}
|