Skip to content

Commit

Permalink
feat: add ibc transfer command (#166)
Browse files Browse the repository at this point in the history
* add ibctransfer command
  • Loading branch information
quasystaty1 authored Oct 9, 2024
1 parent ed9bb95 commit c0247ed
Show file tree
Hide file tree
Showing 4 changed files with 237 additions and 1 deletion.
95 changes: 95 additions & 0 deletions modules/cli/cmd/sequencer/ibctransfer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package sequencer

import (
"github.com/astriaorg/astria-cli-go/modules/cli/cmd"
"github.com/astriaorg/astria-cli-go/modules/cli/internal/sequencer"
"github.com/astriaorg/astria-cli-go/modules/cli/internal/ui"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)

var ibctransferCmd = &cobra.Command{
Use: "ibctransfer [amount] [to] [src-channel] [--keyfile | --keyring-address | --privkey]",
Short: "Ibc Transfer tokens from a sequencer account to another chain account.",
Args: cobra.ExactArgs(3),
Run: ibctransferCmdHandler,
}

func init() {
SequencerCmd.AddCommand(ibctransferCmd)

flagHandler := cmd.CreateCliFlagHandler(ibctransferCmd, cmd.EnvPrefix)
flagHandler.BindBoolFlag("json", false, "Output in JSON format.")
flagHandler.BindStringPFlag("sequencer-url", "u", DefaultSequencerURL, "The URL of the sequencer.")
flagHandler.BindStringPFlag("sequencer-chain-id", "c", DefaultSequencerChainID, "The chain ID of the sequencer.")
flagHandler.BindStringFlag("keyfile", "", "Path to secure keyfile for sender.")
flagHandler.BindStringFlag("keyring-address", "", "The address of the sender. Requires private key be stored in keyring.")
flagHandler.BindStringFlag("privkey", "", "The private key of the sender.")
flagHandler.BindStringFlag("asset", DefaultAsset, "The asset to be transferred.")
flagHandler.BindStringFlag("fee-asset", DefaultFeeAsset, "The asset used for paying fees.")
flagHandler.BindStringFlag("network", DefaultTargetNetwork, "Configure the values to target a specific network.")
flagHandler.BindBoolFlag("async", false, "If true, the function will return immediately. If false, the function will wait for the transaction to be seen on the network.")

ibctransferCmd.MarkFlagsOneRequired("keyfile", "keyring-address", "privkey")
ibctransferCmd.MarkFlagsMutuallyExclusive("keyfile", "keyring-address", "privkey")
}

func ibctransferCmdHandler(c *cobra.Command, args []string) {
flagHandler := cmd.CreateCliFlagHandlerWithUseConfigFlag(c, cmd.EnvPrefix, "network")
networkConfig := GetNetworkConfigFromFlags(flagHandler)
flagHandler.SetConfig(networkConfig)

sequencerURL := flagHandler.GetValue("sequencer-url")
sequencerURL = AddPortToURL(sequencerURL)
asset := flagHandler.GetValue("asset")
feeAsset := flagHandler.GetValue("fee-asset")
sequencerChainID := flagHandler.GetValue("sequencer-chain-id")
sourceChannelID := args[2]
destinationChainAddress := args[1]
returnAddress := "astria12n3yqgdt92kmgmrwj6vzu7lvvsq7wn4yh94403"
returnAddr := AddressFromText(returnAddress)
printJSON := flagHandler.GetValue("json") == "true"

priv, err := GetPrivateKeyFromFlags(c)
if err != nil {
log.WithError(err).Error("Could not get private key from flags")
panic(err)
}
from, err := PrivateKeyFromText(priv)
if err != nil {
log.WithError(err).Error("Error decoding private key")
panic(err)
}
amount, err := convertToUint128(args[0])
if err != nil {
log.WithError(err).Error("Error converting amount to Uint128 proto")
panic(err)
}

isAsync := flagHandler.GetValue("async") == "true"

opts := sequencer.IbcTransferOpts{
IsAsync: isAsync,
AddressPrefix: DefaultAddressPrefix,
SequencerURL: sequencerURL,
FromKey: from,
DestinationChainAddressAddress: destinationChainAddress,
ReturnAddress: returnAddr,
Amount: amount,
Asset: asset,
FeeAsset: feeAsset,
SequencerChainID: sequencerChainID,
SourceChannelID: sourceChannelID,
}
tx, err := sequencer.IbcTransfer(opts)
if err != nil {
log.WithError(err).Error("Error transferring tokens")
panic(err)
}

printer := ui.ResultsPrinter{
Data: tx,
PrintJSON: printJSON,
}
printer.Render()
}
6 changes: 6 additions & 0 deletions modules/cli/internal/sequencer/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package sequencer

import (
"crypto/sha256"
"time"

primproto "buf.build/gen/go/astria/primitives/protocolbuffers/go/astria/primitive/v1"
)
Expand All @@ -13,3 +14,8 @@ func rollupIdFromText(rollup string) *primproto.RollupId {
Inner: hash[:],
}
}

// nowPlusFiveMinutes returns the current time plus five minutes in nanoseconds.
func nowPlusFiveMinutes() uint64 {
return uint64(time.Now().UnixNano() + 5*60*1e9)
}
84 changes: 83 additions & 1 deletion modules/cli/internal/sequencer/sequencer.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@ import (
"crypto/ed25519"
"encoding/hex"
"fmt"
"math"
"time"

txproto "buf.build/gen/go/astria/protocol-apis/protocolbuffers/go/astria/protocol/transactions/v1alpha1"
"github.com/astriaorg/astria-cli-go/modules/bech32m"
"github.com/astriaorg/astria-cli-go/modules/go-sequencer-client/client"

log "github.com/sirupsen/logrus"
)

Expand Down Expand Up @@ -231,6 +231,88 @@ func Transfer(opts TransferOpts) (*TransferResponse, error) {
return tr, nil
}

// IbcTransfer performs an ICS20 withdrawal from the sequencer to a recipient on another chain.
func IbcTransfer(opts IbcTransferOpts) (*IbcTransferResponse, error) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()

// client
log.Debug("Creating CometBFT client with url: ", opts.SequencerURL)
c, err := client.NewClient(opts.SequencerURL)
if err != nil {
log.WithError(err).Error("Error creating sequencer client")
return &IbcTransferResponse{}, err
}

signer := client.NewSigner(opts.FromKey)
fromAddr := signer.Address()
addr, err := bech32m.EncodeFromBytes(opts.AddressPrefix, fromAddr)
if err != nil {
log.WithError(err).Error("Failed to encode address")
return nil, err
}
nonce, err := c.GetNonce(ctx, addr.String())
if err != nil {
log.WithError(err).Error("Error getting nonce")
return &IbcTransferResponse{}, err
}
log.Debugf("Nonce: %v", nonce)

tx := &txproto.UnsignedTransaction{
Params: &txproto.TransactionParams{
ChainId: opts.SequencerChainID,
Nonce: nonce,
},
Actions: []*txproto.Action{
{
Value: &txproto.Action_Ics20Withdrawal{
Ics20Withdrawal: &txproto.Ics20Withdrawal{
Amount: opts.Amount,
Denom: opts.Asset,
DestinationChainAddress: opts.DestinationChainAddressAddress,
ReturnAddress: opts.ReturnAddress,
TimeoutHeight: &txproto.IbcHeight{
RevisionNumber: math.MaxUint64,
RevisionHeight: math.MaxUint64,
},
TimeoutTime: nowPlusFiveMinutes(),
SourceChannel: opts.SourceChannelID,
FeeAsset: opts.FeeAsset,
},
},
},
},
}

// sign transaction
signed, err := signer.SignTransaction(tx)
if err != nil {
log.WithError(err).Error("Error signing transaction")
return &IbcTransferResponse{}, err
}

// broadcast tx
resp, err := c.BroadcastTx(ctx, signed, opts.IsAsync)
if err != nil {
log.WithError(err).Error("Error broadcasting transaction")
return &IbcTransferResponse{}, err
}
log.Debugf("Broadcast response: %v", resp)
// response
hash := hex.EncodeToString(resp.Hash)
amount := fmt.Sprint(client.ProtoU128ToBigInt(opts.Amount))
tr := &IbcTransferResponse{
From: addr.String(),
To: opts.DestinationChainAddressAddress,
Nonce: nonce,
Amount: amount,
TxHash: hash,
}

log.Debugf("Transfer hash: %v", hash)
return tr, nil
}

func InitBridgeAccount(opts InitBridgeOpts) (*InitBridgeResponse, error) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
Expand Down
53 changes: 53 additions & 0 deletions modules/cli/internal/sequencer/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,59 @@ func (tr *TransferResponse) TableRows() [][]string {
}
}

type IbcTransferOpts struct {
// Choose to wait for the transaction to be included in a block.
IsAsync bool
// AddressPrefix is the prefix that will be used when generating the address
// from the FromKey private key.
AddressPrefix string
// SequencerURL is the URL of the sequencer
SequencerURL string
// FromKey is the private key of the sender
FromKey ed25519.PrivateKey
// ToAddress is the address of the receiver
DestinationChainAddressAddress string
// ReturnAddress is the address to return funds to in case of a failed ibc transfer
ReturnAddress *primproto.Address
// Amount is the amount to be transferred. Using string type to support huge numbers
Amount *primproto.Uint128
// Asset is the name of the asset to lock
Asset string
// FeeAsset is the name of the asset to use for the transaction fee
FeeAsset string
// SequencerChainID is the chain ID of the sequencer
SequencerChainID string
// SourceChannelID is the channel ID of the source chain
SourceChannelID string
}

type IbcTransferResponse struct {
// From is the address of the sender
From string `json:"from"`
// To is the address of the receiver
To string `json:"to"`
// Amount is the amount transferred
Amount string `json:"amount"`
// Nonce is the nonce of the transaction
Nonce uint32 `json:"nonce"`
// TxHash is the hash of the transaction
TxHash string `json:"txHash"`
}

func (tr *IbcTransferResponse) JSON() ([]byte, error) {
return json.MarshalIndent(tr, "", " ")
}

func (tr *IbcTransferResponse) TableHeader() []string {
return []string{"From", "To", "Amount", "Nonce", "TxHash"}
}

func (tr *IbcTransferResponse) TableRows() [][]string {
return [][]string{
{tr.From, tr.To, tr.Amount, strconv.Itoa(int(tr.Nonce)), tr.TxHash},
}
}

type FeeAssetOpts struct {
// Choose to wait for the transaction to be included in a block.
IsAsync bool
Expand Down

0 comments on commit c0247ed

Please sign in to comment.