Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: implement ics-721-nft-transfer #1510

2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ module github.com/cosmos/ibc-go/v3

replace github.com/gogo/protobuf => github.com/regen-network/protobuf v1.3.3-alpha.regen.1

replace github.com/zondax/hid => github.com/zondax/hid v0.9.0

require (
cosmossdk.io/math v1.0.0-beta.2
github.com/armon/go-metrics v0.3.11
Expand Down
43 changes: 43 additions & 0 deletions modules/apps/nft-transfer/client/cli/cli.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package cli

import (
"github.com/spf13/cobra"

"github.com/cosmos/cosmos-sdk/client"
)

// GetQueryCmd returns the query commands for IBC connections
func GetQueryCmd() *cobra.Command {
queryCmd := &cobra.Command{
Use: "nft-transfer",
Short: "IBC non-fungible token transfer query subcommands",
DisableFlagParsing: true,
SuggestionsMinimumDistance: 2,
}

queryCmd.AddCommand(
GetCmdQueryClassTrace(),
GetCmdQueryClassTraces(),
GetCmdQueryEscrowAddress(),
GetCmdQueryClassHash(),
)

return queryCmd
}

// NewTxCmd returns the transaction commands for IBC non-fungible token transfer
func NewTxCmd() *cobra.Command {
txCmd := &cobra.Command{
Use: "nft-transfer",
Short: "IBC non-fungible token transfer transaction subcommands",
DisableFlagParsing: true,
SuggestionsMinimumDistance: 2,
RunE: client.ValidateCmd,
}

txCmd.AddCommand(
NewTransferTxCmd(),
)

return txCmd
}
140 changes: 140 additions & 0 deletions modules/apps/nft-transfer/client/cli/query.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
package cli

import (
"fmt"

"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/client/flags"
"github.com/cosmos/cosmos-sdk/version"
"github.com/spf13/cobra"

"github.com/cosmos/ibc-go/v3/modules/apps/nft-transfer/types"
)

// GetCmdQueryClassTrace defines the command to query a a class trace from a given trace hash or ibc class.
func GetCmdQueryClassTrace() *cobra.Command {
cmd := &cobra.Command{
Use: "class-trace [hash/class]",
Short: "Query the class trace info from a given trace hash or ibc class",
Long: "Query the class trace info from a given trace hash or ibc class",
Example: fmt.Sprintf("%s query nft-transfer class-trace 27A6394C3F9FF9C9DCF5DFFADF9BB5FE9A37C7E92B006199894CF1824DF9AC7C", version.AppName),
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
clientCtx, err := client.GetClientQueryContext(cmd)
if err != nil {
return err
}
queryClient := types.NewQueryClient(clientCtx)

req := &types.QueryClassTraceRequest{
Hash: args[0],
}

res, err := queryClient.ClassTrace(cmd.Context(), req)
if err != nil {
return err
}

return clientCtx.PrintProto(res)
},
}

flags.AddQueryFlagsToCmd(cmd)
return cmd
}

// GetCmdQueryClassTraces defines the command to query all the class trace infos
// that this chain mantains.
func GetCmdQueryClassTraces() *cobra.Command {
cmd := &cobra.Command{
Use: "class-traces",
Short: "Query the trace info for all the class",
Long: "Query the trace info for all the class",
Example: fmt.Sprintf("%s query nft-transfer class-traces", version.AppName),
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, _ []string) error {
clientCtx, err := client.GetClientQueryContext(cmd)
if err != nil {
return err
}
queryClient := types.NewQueryClient(clientCtx)

pageReq, err := client.ReadPageRequest(cmd.Flags())
if err != nil {
return err
}

req := &types.QueryClassTracesRequest{
Pagination: pageReq,
}

res, err := queryClient.ClassTraces(cmd.Context(), req)
if err != nil {
return err
}

return clientCtx.PrintProto(res)
},
}
flags.AddQueryFlagsToCmd(cmd)
flags.AddPaginationFlagsToCmd(cmd, "class trace")

return cmd
}

// GetCmdQueryEscrowAddress returns the command handler for nft-transfer escrow-address querying.
func GetCmdQueryEscrowAddress() *cobra.Command {
cmd := &cobra.Command{
Use: "escrow-address",
Short: "Get the escrow address for a channel",
Long: "Get the escrow address for a channel",
Args: cobra.ExactArgs(2),
Example: fmt.Sprintf("%s query nft-transfer escrow-address [port] [channel-id]", version.AppName),
RunE: func(cmd *cobra.Command, args []string) error {
clientCtx, err := client.GetClientQueryContext(cmd)
if err != nil {
return err
}
port := args[0]
channel := args[1]
addr := types.GetEscrowAddress(port, channel)
return clientCtx.PrintString(fmt.Sprintf("%s\n", addr.String()))
},
}

flags.AddQueryFlagsToCmd(cmd)

return cmd
}

// GetCmdQueryClassHash defines the command to query a class hash from a given trace.
func GetCmdQueryClassHash() *cobra.Command {
cmd := &cobra.Command{
Use: "class-hash [trace]",
Short: "Query the class hash info from a given class trace",
Long: "Query the class hash info from a given class trace",
Example: fmt.Sprintf("%s query nft-transfer class-hash transfer/channel-0/class-id", version.AppName),
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
clientCtx, err := client.GetClientQueryContext(cmd)
if err != nil {
return err
}
queryClient := types.NewQueryClient(clientCtx)

req := &types.QueryClassHashRequest{
Trace: args[0],
}

res, err := queryClient.ClassHash(cmd.Context(), req)
if err != nil {
return err
}

return clientCtx.PrintProto(res)
},
}

flags.AddQueryFlagsToCmd(cmd)
return cmd
}
121 changes: 121 additions & 0 deletions modules/apps/nft-transfer/client/cli/tx.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
package cli

import (
"errors"
"fmt"
"strings"
"time"

"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/client/flags"
"github.com/cosmos/cosmos-sdk/client/tx"
"github.com/cosmos/cosmos-sdk/version"
"github.com/spf13/cobra"

"github.com/cosmos/ibc-go/v3/modules/apps/nft-transfer/types"
clienttypes "github.com/cosmos/ibc-go/v3/modules/core/02-client/types"
channelutils "github.com/cosmos/ibc-go/v3/modules/core/04-channel/client/utils"
)

const (
flagPacketTimeoutHeight = "packet-timeout-height"
flagPacketTimeoutTimestamp = "packet-timeout-timestamp"
flagAbsoluteTimeouts = "absolute-timeouts"
)

// NewTransferTxCmd returns the command to create a NewMsgTransfer transaction
func NewTransferTxCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "transfer [src-port] [src-channel] [receiver] [classID] [tokenIDs]",
Short: "Transfer a non-fungible token through IBC",
Long: strings.TrimSpace(`Transfer a non-fungible token through IBC. Timeouts can be specified
as absolute or relative using the "absolute-timeouts" flag. Timeout height can be set by passing in the height string
in the form {revision}-{height} using the "packet-timeout-height" flag. Relative timeout height is added to the block
height queried from the latest consensus state corresponding to the counterparty channel. Relative timeout timestamp
is added to the greater value of the local clock time and the block timestamp queried from the latest consensus state
corresponding to the counterparty channel. Any timeout set to 0 is disabled.`),
Example: fmt.Sprintf("%s tx nft-transfer transfer [src-port] [src-channel] [receiver] [classID] [tokenIDs]", version.AppName),
Args: cobra.ExactArgs(5),
RunE: func(cmd *cobra.Command, args []string) error {
clientCtx, err := client.GetClientTxContext(cmd)
if err != nil {
return err
}
sender := clientCtx.GetFromAddress().String()
srcPort := args[0]
srcChannel := args[1]
receiver := args[2]
classID := args[3]
tokenIDs := strings.Split(args[4], ",")

if len(tokenIDs) == 0 {
return errors.New("tokenIDs cannot be empty")
}

timeoutHeightStr, err := cmd.Flags().GetString(flagPacketTimeoutHeight)
if err != nil {
return err
}
timeoutHeight, err := clienttypes.ParseHeight(timeoutHeightStr)
if err != nil {
return err
}

timeoutTimestamp, err := cmd.Flags().GetUint64(flagPacketTimeoutTimestamp)
if err != nil {
return err
}

absoluteTimeouts, err := cmd.Flags().GetBool(flagAbsoluteTimeouts)
if err != nil {
return err
}

// if the timeouts are not absolute, retrieve latest block height and block timestamp
// for the consensus state connected to the destination port/channel
if !absoluteTimeouts {
consensusState, height, _, err := channelutils.QueryLatestConsensusState(clientCtx, srcPort, srcChannel)
if err != nil {
return err
}

if !timeoutHeight.IsZero() {
absoluteHeight := height
absoluteHeight.RevisionNumber += timeoutHeight.RevisionNumber
absoluteHeight.RevisionHeight += timeoutHeight.RevisionHeight
timeoutHeight = absoluteHeight
}

if timeoutTimestamp != 0 {
// use local clock time as reference time if it is later than the
// consensus state timestamp of the counter party chain, otherwise
// still use consensus state timestamp as reference
now := time.Now().UnixNano()
consensusStateTimestamp := consensusState.GetTimestamp()
if now > 0 {
now := uint64(now)
if now > consensusStateTimestamp {
timeoutTimestamp = now + timeoutTimestamp
} else {
timeoutTimestamp = consensusStateTimestamp + timeoutTimestamp
}
} else {
return errors.New("local clock time is not greater than Jan 1st, 1970 12:00 AM")
}
}
}

msg := types.NewMsgTransfer(
srcPort, srcChannel, classID, tokenIDs, sender, receiver, timeoutHeight, timeoutTimestamp,
)
return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg)
},
}

cmd.Flags().String(flagPacketTimeoutHeight, types.DefaultRelativePacketTimeoutHeight, "Packet timeout block height. The timeout is disabled when set to 0-0.")
cmd.Flags().Uint64(flagPacketTimeoutTimestamp, types.DefaultRelativePacketTimeoutTimestamp, "Packet timeout timestamp in nanoseconds from now. Default is 10 minutes. The timeout is disabled when set to 0.")
cmd.Flags().Bool(flagAbsoluteTimeouts, false, "Timeout flags are used as absolute timeouts.")
flags.AddTxFlagsToCmd(cmd)

return cmd
}
Loading