From d2170e977ba7c4af1f3b1302129346c8d553b695 Mon Sep 17 00:00:00 2001 From: felipemadero Date: Tue, 22 Oct 2024 10:28:14 -0300 Subject: [PATCH] Key transfer P to P, C to P, C to C, EVM to EVM (#2157) * needs pretty much more work * working C -> P * add support to specifying key for devnets * fix * added C -> C * partial work * subnet-evm to subnet-evm * fix endpoints (again) * finding my blockchain * improvements in endpoint and sidecar network queries * support also p/x in chain flags * starts to make sense * refactor into function * only remaining: P/X related transfers * added P->P * refactored P->P * P->C working * feature complete * nit * lint * e2e * continue key list operation even when a blockchain url is not up * show url on err message * lint * address PR comments * address PR comments --- cmd/blockchaincmd/describe.go | 5 +- cmd/contractcmd/deploy_erc20.go | 6 +- .../tokentransferrercmd/deploy.go | 19 +- cmd/keycmd/list.go | 42 +- cmd/keycmd/transfer.go | 1269 ++++++++++------- cmd/teleportercmd/deploy.go | 5 +- cmd/teleportercmd/relayercmd/configList.go | 3 +- pkg/application/app.go | 36 +- pkg/contract/chain.go | 211 ++- pkg/models/network.go | 5 + pkg/networkoptions/network_options.go | 32 +- pkg/prompts/prompts.go | 24 +- pkg/prompts/validations.go | 2 + tests/e2e/commands/key.go | 5 +- 14 files changed, 1026 insertions(+), 638 deletions(-) diff --git a/cmd/blockchaincmd/describe.go b/cmd/blockchaincmd/describe.go index 83ac2a5b8..a8d3c93a0 100644 --- a/cmd/blockchaincmd/describe.go +++ b/cmd/blockchaincmd/describe.go @@ -15,7 +15,6 @@ import ( "github.com/ava-labs/avalanche-cli/pkg/key" "github.com/ava-labs/avalanche-cli/pkg/localnet" "github.com/ava-labs/avalanche-cli/pkg/models" - "github.com/ava-labs/avalanche-cli/pkg/networkoptions" "github.com/ava-labs/avalanche-cli/pkg/subnet" "github.com/ava-labs/avalanche-cli/pkg/txutils" "github.com/ava-labs/avalanche-cli/pkg/utils" @@ -114,7 +113,7 @@ func PrintSubnetInfo(blockchainName string, onlyLocalnetInfo bool) error { localChainID := "" for net, data := range sc.Networks { - network, err := networkoptions.GetNetworkFromSidecarNetworkName(app, net) + network, err := app.GetNetworkFromSidecarNetworkName(net) if err != nil { return err } @@ -173,7 +172,7 @@ func PrintSubnetInfo(blockchainName string, onlyLocalnetInfo bool) error { t.SetTitle("Teleporter") hasTeleporterInfo := false for net, data := range sc.Networks { - network, err := networkoptions.GetNetworkFromSidecarNetworkName(app, net) + network, err := app.GetNetworkFromSidecarNetworkName(net) if err != nil { return err } diff --git a/cmd/contractcmd/deploy_erc20.go b/cmd/contractcmd/deploy_erc20.go index eacce6040..704cab7c7 100644 --- a/cmd/contractcmd/deploy_erc20.go +++ b/cmd/contractcmd/deploy_erc20.go @@ -46,7 +46,9 @@ func newDeployERC20Cmd() *cobra.Command { } networkoptions.AddNetworkFlagsToCmd(cmd, &deployERC20Flags.Network, true, deployERC20SupportedNetworkOptions) deployERC20Flags.PrivateKeyFlags.AddToCmd(cmd, "as contract deployer") - deployERC20Flags.chainFlags.AddToCmd(cmd, "deploy the ERC20 contract", true) + // enabling blockchain names, C-Chain and blockchain IDs + deployERC20Flags.chainFlags.SetEnabled(true, true, false, false, true) + deployERC20Flags.chainFlags.AddToCmd(cmd, "deploy the ERC20 contract into %s") cmd.Flags().StringVar(&deployERC20Flags.symbol, "symbol", "", "set the token symbol") cmd.Flags().Uint64Var(&deployERC20Flags.supply, "supply", 0, "set the token supply") cmd.Flags().StringVar(&deployERC20Flags.funded, "funded", "", "set the funded address") @@ -76,9 +78,7 @@ func deployERC20(_ *cobra.Command, _ []string) error { app, network, prompt, - false, "", - true, &deployERC20Flags.chainFlags, ); cancel || err != nil { return err diff --git a/cmd/interchaincmd/tokentransferrercmd/deploy.go b/cmd/interchaincmd/tokentransferrercmd/deploy.go index cded2f221..b3fca4989 100644 --- a/cmd/interchaincmd/tokentransferrercmd/deploy.go +++ b/cmd/interchaincmd/tokentransferrercmd/deploy.go @@ -75,14 +75,18 @@ func NewDeployCmd() *cobra.Command { "home-blockchain", "c-chain-home", "", + "", + "", ) - deployFlags.homeFlags.chainFlags.AddToCmd(cmd, "set the Transferrer's Home Chain", false) + deployFlags.homeFlags.chainFlags.AddToCmd(cmd, "set the Transferrer's Home Chain into %s") deployFlags.remoteFlags.chainFlags.SetFlagNames( "remote-blockchain", "c-chain-remote", "", + "", + "", ) - deployFlags.remoteFlags.chainFlags.AddToCmd(cmd, "set the Transferrer's Remote Chain", false) + deployFlags.remoteFlags.chainFlags.AddToCmd(cmd, "set the Transferrer's Remote Chain into %s") cmd.Flags().BoolVar(&deployFlags.homeFlags.native, "deploy-native-home", false, "deploy a Transferrer Home for the Chain's Native Token") cmd.Flags().StringVar(&deployFlags.homeFlags.erc20Address, "deploy-erc20-home", "", "deploy a Transferrer Home for the given Chain's ERC20 Token") cmd.Flags().StringVar(&deployFlags.homeFlags.homeAddress, "use-home", "", "use the given Transferrer's Home Address") @@ -177,7 +181,7 @@ func CallDeploy(_ []string, flags DeployFlags) error { // Home Chain Prompts if !flags.homeFlags.chainFlags.Defined() { prompt := "Where is the Token origin?" - if cancel, err := contract.PromptChain(app, network, prompt, false, "", false, &flags.homeFlags.chainFlags); err != nil { + if cancel, err := contract.PromptChain(app, network, prompt, "", &flags.homeFlags.chainFlags); err != nil { return err } else if cancel { return nil @@ -368,13 +372,18 @@ func CallDeploy(_ []string, flags DeployFlags) error { if flags.remoteFlags.native { prompt = "Where should the token be available as a Native Token?" } + flags.remoteFlags.chainFlags.SetEnabled( + true, + !flags.homeFlags.chainFlags.CChain, + false, + false, + false, + ) if cancel, err := contract.PromptChain( app, network, prompt, - flags.homeFlags.chainFlags.CChain, flags.homeFlags.chainFlags.BlockchainName, - false, &flags.remoteFlags.chainFlags, ); err != nil { return err diff --git a/cmd/keycmd/list.go b/cmd/keycmd/list.go index 7170e7f2d..f5f4499d2 100644 --- a/cmd/keycmd/list.go +++ b/cmd/keycmd/list.go @@ -13,6 +13,7 @@ import ( "github.com/ava-labs/avalanche-cli/pkg/models" "github.com/ava-labs/avalanche-cli/pkg/networkoptions" "github.com/ava-labs/avalanche-cli/pkg/utils" + "github.com/ava-labs/avalanche-cli/pkg/ux" "github.com/ava-labs/avalanchego/ids" ledger "github.com/ava-labs/avalanchego/utils/crypto/ledger" "github.com/ava-labs/avalanchego/utils/formatting/address" @@ -143,12 +144,13 @@ keys or for the ledger addresses associated to certain indices.`, } type Clients struct { - x map[models.Network]avm.Client - p map[models.Network]platformvm.Client - c map[models.Network]ethclient.Client - cGeth map[models.Network]*goethereumethclient.Client - evm map[models.Network]map[string]ethclient.Client - evmGeth map[models.Network]map[string]*goethereumethclient.Client + x map[models.Network]avm.Client + p map[models.Network]platformvm.Client + c map[models.Network]ethclient.Client + cGeth map[models.Network]*goethereumethclient.Client + evm map[models.Network]map[string]ethclient.Client + evmGeth map[models.Network]map[string]*goethereumethclient.Client + blockchainRPC map[models.Network]map[string]string } func getClients(networks []models.Network, pchain bool, cchain bool, xchain bool, subnets []string) ( @@ -162,6 +164,7 @@ func getClients(networks []models.Network, pchain bool, cchain bool, xchain bool cGethClients := map[models.Network]*goethereumethclient.Client{} evmClients := map[models.Network]map[string]ethclient.Client{} evmGethClients := map[models.Network]map[string]*goethereumethclient.Client{} + blockchainRPCs := map[models.Network]map[string]string{} for _, network := range networks { if pchain { pClients[network] = platformvm.NewClient(network.Endpoint) @@ -207,7 +210,12 @@ func getClients(networks []models.Network, pchain bool, cchain bool, xchain bool false, ) if err == nil { - _, b := evmClients[network] + _, b := blockchainRPCs[network] + if !b { + blockchainRPCs[network] = map[string]string{} + } + blockchainRPCs[network][subnetName] = endpoint + _, b = evmClients[network] if !b { evmClients[network] = map[string]ethclient.Client{} } @@ -231,12 +239,13 @@ func getClients(networks []models.Network, pchain bool, cchain bool, xchain bool } } return &Clients{ - p: pClients, - x: xClients, - c: cClients, - evm: evmClients, - cGeth: cGethClients, - evmGeth: evmGethClients, + p: pClients, + x: xClients, + c: cClients, + evm: evmClients, + cGeth: cGethClients, + evmGeth: evmGethClients, + blockchainRPC: blockchainRPCs, }, nil } @@ -376,7 +385,12 @@ func getStoredKeyInfo( keyName, ) if err != nil { - return nil, err + ux.Logger.RedXToUser( + "failure obtaining info for blockchain %s on url %s", + subnetName, + clients.blockchainRPC[network][subnetName], + ) + continue } addrInfos = append(addrInfos, addrInfo...) } diff --git a/cmd/keycmd/transfer.go b/cmd/keycmd/transfer.go index f28c596a4..28d0bca95 100644 --- a/cmd/keycmd/transfer.go +++ b/cmd/keycmd/transfer.go @@ -6,24 +6,24 @@ import ( "context" "fmt" "math/big" - "strings" "time" "github.com/ava-labs/avalanche-cli/pkg/cobrautils" "github.com/ava-labs/avalanche-cli/pkg/contract" + clievm "github.com/ava-labs/avalanche-cli/pkg/evm" "github.com/ava-labs/avalanche-cli/pkg/ictt" "github.com/ava-labs/avalanche-cli/pkg/key" + "github.com/ava-labs/avalanche-cli/pkg/models" "github.com/ava-labs/avalanche-cli/pkg/networkoptions" "github.com/ava-labs/avalanche-cli/pkg/prompts" - "github.com/ava-labs/avalanche-cli/pkg/subnet" "github.com/ava-labs/avalanche-cli/pkg/utils" "github.com/ava-labs/avalanche-cli/pkg/ux" + "github.com/ava-labs/avalanche-cli/pkg/vm" "github.com/ava-labs/avalanchego/ids" avagoconstants "github.com/ava-labs/avalanchego/utils/constants" "github.com/ava-labs/avalanchego/utils/crypto/keychain" ledger "github.com/ava-labs/avalanchego/utils/crypto/ledger" "github.com/ava-labs/avalanchego/utils/formatting/address" - "github.com/ava-labs/avalanchego/utils/logging" "github.com/ava-labs/avalanchego/utils/units" avmtxs "github.com/ava-labs/avalanchego/vms/avm/txs" "github.com/ava-labs/avalanchego/vms/components/avax" @@ -32,20 +32,17 @@ import ( "github.com/ava-labs/avalanchego/vms/secp256k1fx" "github.com/ava-labs/avalanchego/wallet/subnet/primary" "github.com/ava-labs/avalanchego/wallet/subnet/primary/common" + "github.com/ava-labs/coreth/plugin/evm" goethereumcommon "github.com/ethereum/go-ethereum/common" "github.com/spf13/cobra" ) const ( - cChain = "c-chain" - sendFlag = "send" - receiveFlag = "receive" - keyNameFlag = "key" - ledgerIndexFlag = "ledger" - destinationAddrFlag = "destination-addr" - amountFlag = "amount" - wrongLedgerIndexVal = 32768 - receiveRecoveryStepFlag = "receive-recovery-step" + keyNameFlag = "key" + ledgerIndexFlag = "ledger" + amountFlag = "amount" + destinationAddrFlag = "destination-addr" + wrongLedgerIndexVal = 32768 ) var ( @@ -55,22 +52,19 @@ var ( networkoptions.Devnet, networkoptions.Local, } - send bool - receive bool - keyName string - ledgerIndex uint32 - force bool - destinationAddrStr string - amountFlt float64 - receiveRecoveryStep uint64 - PToX bool - PToP bool + keyName string + ledgerIndex uint32 + destinationAddrStr string + amountFlt float64 // token transferrer experimental originSubnet string destinationSubnet string originTransferrerAddress string destinationTransferrerAddress string destinationKeyName string + // + senderChainFlags contract.ChainSpec + receiverChainFlags contract.ChainSpec ) func newTransferCmd() *cobra.Command { @@ -82,38 +76,6 @@ func newTransferCmd() *cobra.Command { Args: cobrautils.ExactArgs(0), } networkoptions.AddNetworkFlagsToCmd(cmd, &globalNetworkFlags, false, transferSupportedNetworkOptions) - cmd.Flags().BoolVar( - &PToX, - "fund-x-chain", - false, - "fund X-Chain account on destination", - ) - cmd.Flags().BoolVar( - &PToP, - "fund-p-chain", - false, - "fund P-Chain account on destination", - ) - cmd.Flags().BoolVar( - &force, - forceFlag, - false, - "avoid transfer confirmation", - ) - cmd.Flags().BoolVarP( - &send, - sendFlag, - "s", - false, - "send the transfer", - ) - cmd.Flags().BoolVarP( - &receive, - receiveFlag, - "g", - false, - "receive the transfer", - ) cmd.Flags().StringVarP( &keyName, keyNameFlag, @@ -128,13 +90,6 @@ func newTransferCmd() *cobra.Command { wrongLedgerIndexVal, "ledger index associated to the sender or receiver address", ) - cmd.Flags().Uint64VarP( - &receiveRecoveryStep, - receiveRecoveryStepFlag, - "r", - 0, - "receive step to use for multiple step transaction recovery", - ) cmd.Flags().StringVarP( &destinationAddrStr, destinationAddrFlag, @@ -179,14 +134,26 @@ func newTransferCmd() *cobra.Command { "", "token transferrer address at the destination subnet (token transferrer experimental)", ) + senderChainFlags.SetFlagNames( + "sender-blockchain", + "c-chain-sender", + "p-chain-sender", + "x-chain-sender", + "sender-blockchain-id", + ) + senderChainFlags.AddToCmd(cmd, "send from %s") + receiverChainFlags.SetFlagNames( + "receiver-blockchain", + "c-chain-receiver", + "p-chain-receiver", + "x-chain-receiver", + "receiver-blockchain-id", + ) + receiverChainFlags.AddToCmd(cmd, "receive at %s") return cmd } func transferF(*cobra.Command, []string) error { - if send && receive { - return fmt.Errorf("only one of %s, %s flags should be selected", sendFlag, receiveFlag) - } - if keyName != "" && ledgerIndex != wrongLedgerIndexVal { return fmt.Errorf("only one between a keyname or a ledger index must be given") } @@ -204,232 +171,69 @@ func transferF(*cobra.Command, []string) error { return err } - subnetNames, err := app.GetBlockchainNamesOnNetwork(network) - if err != nil { - return err - } - - if originSubnet == "" && !PToX && !PToP { + if !senderChainFlags.Defined() { prompt := "Where are the funds to transfer?" - cancel, pChainChoosen, _, cChainChoosen, subnetName, _, err := prompts.PromptChain( - app.Prompt, + if cancel, err := contract.PromptChain( + app, + network, prompt, - subnetNames, - false, - true, - false, "", - false, - ) - if err != nil { + &senderChainFlags, + ); err != nil { return err - } - switch { - case cancel: + } else if cancel { return nil - case pChainChoosen: - option, err := app.Prompt.CaptureList( - "Destination Chain", - []string{"P-Chain", "X-Chain"}, - ) - if err != nil { - return err - } - if option == "P-Chain" { - PToP = true - } else { - PToX = true - } - case cChainChoosen: - originSubnet = cChain - default: - originSubnet = subnetName } } - // token transferrer experimental - if originSubnet != "" { - if destinationSubnet == "" { - prompt := "Where are the funds going to?" - avoidSubnet := originSubnet - if originSubnet == cChain { - avoidSubnet = "" - } - cancel, _, _, cChainChoosen, subnetName, _, err := prompts.PromptChain( - app.Prompt, - prompt, - subnetNames, - true, - true, - originSubnet == cChain, - avoidSubnet, - false, - ) - if err != nil { - return err - } - switch { - case cancel: - return nil - case cChainChoosen: - destinationSubnet = cChain - default: - destinationSubnet = subnetName - } - } - originURL := network.CChainEndpoint() - if strings.ToLower(originSubnet) != cChain { - originURL, _, err = contract.GetBlockchainEndpoints( - app, - network, - contract.ChainSpec{ - BlockchainName: originSubnet, - }, - true, - false, - ) - if err != nil { - return err - } - } - var destinationBlockchainID ids.ID - if strings.ToLower(destinationSubnet) == cChain { - destinationBlockchainID, err = utils.GetChainID(network.Endpoint, "C") - if err != nil { - return err - } - } else { - sc, err := app.LoadSidecar(destinationSubnet) - if err != nil { - return err - } - blockchainID := sc.Networks[network.Name()].BlockchainID - if blockchainID == ids.Empty { - return fmt.Errorf("subnet %s is not deployed to %s", destinationSubnet, network.Name()) - } - destinationBlockchainID = blockchainID - } - if originTransferrerAddress == "" { - addr, err := app.Prompt.CaptureAddress( - fmt.Sprintf("Enter the address of the Token Transferrer on %s", originSubnet), - ) - if err != nil { - return err - } - originTransferrerAddress = addr.Hex() - } else { - if err := prompts.ValidateAddress(originTransferrerAddress); err != nil { - return err - } - } - if destinationTransferrerAddress == "" { - addr, err := app.Prompt.CaptureAddress( - fmt.Sprintf("Enter the address of the Token Transferrer on %s", destinationSubnet), - ) - if err != nil { - return err - } - destinationTransferrerAddress = addr.Hex() - } else { - if err := prompts.ValidateAddress(destinationTransferrerAddress); err != nil { - return err - } - } - if keyName == "" { - keyName, err = prompts.CaptureKeyName(app.Prompt, "fund the transfer", app.GetKeyDir(), true) - if err != nil { - return err - } - } - originK, err := app.GetKey(keyName, network, false) - if err != nil { + if !receiverChainFlags.Defined() { + prompt := "Where are the funds going to?" + if cancel, err := contract.PromptChain( + app, + network, + prompt, + "", + &receiverChainFlags, + ); err != nil { return err + } else if cancel { + return nil } - privateKey := originK.PrivKeyHex() - var destinationAddr goethereumcommon.Address - if destinationAddrStr == "" && destinationKeyName == "" { - option, err := app.Prompt.CaptureList( - "Do you want to choose a stored key for the destination, or input a destination address?", - []string{"Key", "Address"}, - ) - if err != nil { - return err - } - switch option { - case "Key": - destinationKeyName, err = prompts.CaptureKeyName(app.Prompt, "receive the transfer", app.GetKeyDir(), true) - if err != nil { - return err - } - case "Address": - addr, err := app.Prompt.CaptureAddress( - "Enter the destination address", - ) - if err != nil { - return err - } - destinationAddrStr = addr.Hex() - } - } - switch { - case destinationAddrStr != "": - if err := prompts.ValidateAddress(destinationAddrStr); err != nil { - return err - } - destinationAddr = goethereumcommon.HexToAddress(destinationAddrStr) - case destinationKeyName != "": - destinationK, err := app.GetKey(destinationKeyName, network, false) - if err != nil { - return err - } - destinationAddrStr = destinationK.C() - destinationAddr = goethereumcommon.HexToAddress(destinationAddrStr) - default: - return fmt.Errorf("you should set the destination address or destination key") - } - if amountFlt == 0 { - amountFlt, err = captureAmount(true, "TOKEN units") - if err != nil { - return err - } - } - amount := new(big.Float).SetFloat64(amountFlt) - amount = amount.Mul(amount, new(big.Float).SetFloat64(float64(units.Avax))) - amount = amount.Mul(amount, new(big.Float).SetFloat64(float64(units.Avax))) - amountInt, _ := amount.Int(nil) - return ictt.Send( - originURL, - goethereumcommon.HexToAddress(originTransferrerAddress), - privateKey, - destinationBlockchainID, - goethereumcommon.HexToAddress(destinationTransferrerAddress), - destinationAddr, - amountInt, - ) } - if !send && !receive { - option, err := app.Prompt.CaptureList( - "Step of the transfer", - []string{"Send", "Receive"}, - ) - if err != nil { - return err - } - if option == "Send" { - send = true - } else { - receive = true - } + if (senderChainFlags.CChain && receiverChainFlags.CChain) || + (senderChainFlags.BlockchainName != "" && senderChainFlags.BlockchainName == receiverChainFlags.BlockchainName) { + return intraEvmSend(network, senderChainFlags) + } + + if !senderChainFlags.PChain && !senderChainFlags.XChain && !receiverChainFlags.PChain && !receiverChainFlags.XChain { + return interEvmSend(network, senderChainFlags, receiverChainFlags) + } + + senderDesc, err := contract.GetBlockchainDesc(senderChainFlags) + if err != nil { + return err + } + receiverDesc, err := contract.GetBlockchainDesc(receiverChainFlags) + if err != nil { + return err + } + if senderChainFlags.BlockchainName != "" || receiverChainFlags.BlockchainName != "" || senderChainFlags.XChain { + return fmt.Errorf("tranfer from %s to %s is not supported", senderDesc, receiverDesc) } if keyName == "" && ledgerIndex == wrongLedgerIndexVal { var useLedger bool - goalStr := "" - if send { - goalStr = "as the sender address" - } else { - goalStr = "as the destination address" + goalStr := "as the sender address" + if receiverChainFlags.XChain { + ux.Logger.PrintToUser("P->X transfer is an intra-account operation.") + ux.Logger.PrintToUser("Tokens will be transferred to the same account address on the other chain") + goalStr = "specify the sender/receiver address" + } + if senderChainFlags.CChain && receiverChainFlags.PChain { + ux.Logger.PrintToUser("C->P transfer is an intra-account operation.") + ux.Logger.PrintToUser("Tokens will be transferred to the same account address on the other chain") + goalStr = "as the sender/receiver address" } useLedger, keyName, err = prompts.GetKeyOrLedger(app.Prompt, goalStr, app.GetKeyDir(), true) if err != nil { @@ -443,19 +247,10 @@ func transferF(*cobra.Command, []string) error { } } - if amountFlt == 0 { - amountFlt, err = captureAmount(send, "AVAX units") - if err != nil { - return err - } - } - amount := uint64(amountFlt * float64(units.Avax)) - - fee := network.GenesisParams().TxFeeConfig.StaticFeeConfig.TxFee - var kc keychain.Keychain + var sk *key.SoftKey if keyName != "" { - sk, err := app.GetKey(keyName, network, false) + sk, err = app.GetKey(keyName, network, false) if err != nil { return err } @@ -471,291 +266,725 @@ func transferF(*cobra.Command, []string) error { return err } } + usingLedger := ledgerIndex != wrongLedgerIndexVal - var destinationAddr ids.ShortID - if send { - if destinationAddrStr == "" { - if PToP { - destinationAddrStr, err = app.Prompt.CapturePChainAddress("Destination address", network) - if err != nil { - return err - } - } else { - destinationAddrStr, err = app.Prompt.CaptureXChainAddress("Destination address", network) - if err != nil { - return err - } - } - } - destinationAddr, err = address.ParseToID(destinationAddrStr) + if amountFlt == 0 { + amountFlt, err = captureAmount("AVAX units") if err != nil { return err } - } else { - destinationAddr = kc.Addresses().List()[0] - destinationAddrStr, err = address.Format("P", key.GetHRP(network.ID), destinationAddr[:]) + } + amount := uint64(amountFlt * float64(units.Avax)) + + if destinationAddrStr == "" && !receiverChainFlags.XChain && + !(senderChainFlags.CChain && receiverChainFlags.PChain) { + format := prompts.EVMFormat + if receiverChainFlags.PChain { + format = prompts.PChainFormat + } + if receiverChainFlags.XChain { + format = prompts.XChainFormat + } + destinationAddrStr, err = prompts.PromptAddress( + app.Prompt, + "destination address", + app.GetKeyDir(), + app.GetKey, + "", + network, + format, + "destination address", + ) if err != nil { return err } } - usingLedger := ledgerIndex != wrongLedgerIndexVal + if senderChainFlags.PChain && receiverChainFlags.PChain { + return pToPSend( + network, + kc, + usingLedger, + destinationAddrStr, + amount, + ) + } + + if senderChainFlags.PChain && receiverChainFlags.CChain { + return pToCSend( + network, + kc, + usingLedger, + destinationAddrStr, + amount, + ) + } + if senderChainFlags.CChain && receiverChainFlags.PChain { + return cToPSend( + network, + kc, + sk, + usingLedger, + amount, + ) + } + if senderChainFlags.PChain && receiverChainFlags.XChain { + return pToXSend( + network, + kc, + usingLedger, + amount, + ) + } - ux.Logger.PrintToUser("") - ux.Logger.PrintToUser("this operation is going to:") - if send { - addr := kc.Addresses().List()[0] - addrStr, err := address.Format("P", key.GetHRP(network.ID), addr[:]) + return nil +} + +func captureAmount(tokenDesc string) (float64, error) { + promptStr := fmt.Sprintf("Amount to send (%s)", tokenDesc) + amountFlt, err := app.Prompt.CaptureFloat(promptStr, func(v float64) error { + if v <= 0 { + return fmt.Errorf("value %f must be greater than zero", v) + } + return nil + }) + if err != nil { + return 0, err + } + return amountFlt, nil +} + +func intraEvmSend( + network models.Network, + senderChain contract.ChainSpec, +) error { + privateKey, err := prompts.PromptPrivateKey( + app.Prompt, + "sender private key", + app.GetKeyDir(), + app.GetKey, + "", + "", + ) + if err != nil { + return err + } + destinationAddr, err := prompts.PromptAddress( + app.Prompt, + "destination address", + app.GetKeyDir(), + app.GetKey, + "", + network, + prompts.EVMFormat, + "destination address", + ) + if err != nil { + return err + } + amountFlt, err := app.Prompt.CaptureFloat( + "Amount to transfer", + func(f float64) error { + if f <= 0 { + return fmt.Errorf("not positive") + } + return nil + }, + ) + if err != nil { + return err + } + amountBigFlt := new(big.Float).SetFloat64(amountFlt) + amountBigFlt = amountBigFlt.Mul(amountBigFlt, new(big.Float).SetInt(vm.OneAvax)) + amount, _ := amountBigFlt.Int(nil) + senderURL, _, err := contract.GetBlockchainEndpoints( + app, + network, + senderChain, + true, + false, + ) + if err != nil { + return err + } + client, err := clievm.GetClient(senderURL) + if err != nil { + return err + } + return clievm.FundAddress(client, privateKey, destinationAddr, amount) +} + +func interEvmSend( + network models.Network, + senderChain contract.ChainSpec, + receiverChain contract.ChainSpec, +) error { + senderURL, _, err := contract.GetBlockchainEndpoints( + app, + network, + senderChain, + true, + false, + ) + if err != nil { + return err + } + receiverBlockchainID, err := contract.GetBlockchainID( + app, + network, + receiverChain, + ) + if err != nil { + return err + } + if originTransferrerAddress == "" { + addr, err := app.Prompt.CaptureAddress( + fmt.Sprintf("Enter the address of the Token Transferrer on %s", originSubnet), + ) if err != nil { return err } - if addr == destinationAddr && PToP { - return fmt.Errorf("sender addr is the same as destination addr") - } - ux.Logger.PrintToUser("- send %.9f AVAX from %s to destination address %s", float64(amount)/float64(units.Avax), addrStr, destinationAddrStr) - totalFee := 4 * fee - if !usingLedger { - totalFee = fee + originTransferrerAddress = addr.Hex() + } else { + if err := prompts.ValidateAddress(originTransferrerAddress); err != nil { + return err } - if PToX { - totalFee = 2 * fee + } + if destinationTransferrerAddress == "" { + addr, err := app.Prompt.CaptureAddress( + fmt.Sprintf("Enter the address of the Token Transferrer on %s", destinationSubnet), + ) + if err != nil { + return err } - ux.Logger.PrintToUser("- take a fee of %.9f AVAX from source address %s", float64(totalFee)/float64(units.Avax), addrStr) + destinationTransferrerAddress = addr.Hex() } else { - ux.Logger.PrintToUser("- receive %.9f AVAX at destination address %s", float64(amount)/float64(units.Avax), destinationAddrStr) + if err := prompts.ValidateAddress(destinationTransferrerAddress); err != nil { + return err + } } - ux.Logger.PrintToUser("") - - if !force { - confStr := "Confirm transfer" - conf, err := app.Prompt.CaptureNoYes(confStr) + if keyName == "" { + keyName, err = prompts.CaptureKeyName(app.Prompt, "fund the transfer", app.GetKeyDir(), true) if err != nil { return err } - if !conf { - ux.Logger.PrintToUser("Cancelled") - return nil - } } - - to := secp256k1fx.OutputOwners{ - Threshold: 1, - Addrs: []ids.ShortID{destinationAddr}, + originK, err := app.GetKey(keyName, network, false) + if err != nil { + return err } - - if send { - wallet, err := primary.MakeWallet( - context.Background(), - &primary.WalletConfig{ - URI: network.Endpoint, - AVAXKeychain: kc, - EthKeychain: secp256k1fx.NewKeychain(), - }, + privateKey := originK.PrivKeyHex() + var destinationAddr goethereumcommon.Address + if destinationAddrStr == "" && destinationKeyName == "" { + option, err := app.Prompt.CaptureList( + "Do you want to choose a stored key for the destination, or input a destination address?", + []string{"Key", "Address"}, ) if err != nil { return err } - amountPlusFee := amount - if PToP { - if usingLedger { - amountPlusFee += fee * 3 - } - } - if PToX { - amountPlusFee += fee - } - output := &avax.TransferableOutput{ - Asset: avax.Asset{ID: wallet.P().Builder().Context().AVAXAssetID}, - Out: &secp256k1fx.TransferOutput{ - Amt: amountPlusFee, - OutputOwners: to, - }, - } - outputs := []*avax.TransferableOutput{output} - var unsignedTx txs.UnsignedTx - if PToP && !usingLedger { - ux.Logger.PrintToUser("Issuing BaseTx P -> P") - unsignedTx, err = wallet.P().Builder().NewBaseTx( - outputs, - ) + switch option { + case "Key": + destinationKeyName, err = prompts.CaptureKeyName(app.Prompt, "receive the transfer", app.GetKeyDir(), true) if err != nil { - return fmt.Errorf("error building tx: %w", err) - } - } else { - ux.Logger.PrintToUser("Issuing ExportTx P -> X") - if usingLedger { - ux.Logger.PrintToUser("*** Please sign 'Export Tx / P to X Chain' transaction on the ledger device *** ") + return err } - unsignedTx, err = wallet.P().Builder().NewExportTx( - wallet.X().Builder().Context().BlockchainID, - outputs, + case "Address": + addr, err := app.Prompt.CaptureAddress( + "Enter the destination address", ) if err != nil { - return fmt.Errorf("error building tx: %w", err) + return err } + destinationAddrStr = addr.Hex() } - tx := txs.Tx{Unsigned: unsignedTx} - if err := wallet.P().Signer().Sign(context.Background(), &tx); err != nil { - return fmt.Errorf("error signing tx: %w", err) - } - - pContext := wallet.P().Builder().Context() - var pFeeCalculator avagofee.Calculator - if pContext.GasPrice != 0 { - pFeeCalculator = avagofee.NewDynamicCalculator(pContext.ComplexityWeights, pContext.GasPrice) - } else { - pFeeCalculator = avagofee.NewStaticCalculator(pContext.StaticFeeConfig) + } + switch { + case destinationAddrStr != "": + if err := prompts.ValidateAddress(destinationAddrStr); err != nil { + return err } - txFee, err := pFeeCalculator.CalculateFee(unsignedTx) + destinationAddr = goethereumcommon.HexToAddress(destinationAddrStr) + case destinationKeyName != "": + destinationK, err := app.GetKey(destinationKeyName, network, false) if err != nil { return err } - ux.Logger.PrintToUser("") - ux.Logger.PrintToUser("Paid fee: %.9f", float64(txFee)/float64(units.Avax)) - - ctx, cancel := utils.GetAPIContext() - defer cancel() - err = wallet.P().IssueTx( - &tx, - common.WithContext(ctx), - ) + destinationAddrStr = destinationK.C() + destinationAddr = goethereumcommon.HexToAddress(destinationAddrStr) + default: + return fmt.Errorf("you should set the destination address or destination key") + } + if amountFlt == 0 { + amountFlt, err = captureAmount("TOKEN units") if err != nil { - if ctx.Err() != nil { - err = fmt.Errorf("timeout issuing/verifying tx with ID %s: %w", tx.ID(), err) - } else { - err = fmt.Errorf("error issuing tx with ID %s: %w", tx.ID(), err) - } return err } - } else { - if receiveRecoveryStep == 0 { - wallet, err := primary.MakeWallet( - context.Background(), - &primary.WalletConfig{ - URI: network.Endpoint, - AVAXKeychain: kc, - EthKeychain: secp256k1fx.NewKeychain(), - }, - ) - if err != nil { - ux.Logger.PrintToUser(logging.LightRed.Wrap("ERROR: restart from this step by using the same command")) - return err - } - ux.Logger.PrintToUser("Issuing ImportTx P -> X") - if usingLedger { - ux.Logger.PrintToUser("*** Please sign ImportTx transaction on the ledger device *** ") - } - unsignedTx, err := wallet.X().Builder().NewImportTx( - avagoconstants.PlatformChainID, - &to, - ) - if err != nil { - ux.Logger.PrintToUser(logging.LightRed.Wrap("ERROR: restart from this step by using the same command")) - return fmt.Errorf("error building tx: %w", err) - } - tx := avmtxs.Tx{Unsigned: unsignedTx} - if err := wallet.X().Signer().Sign(context.Background(), &tx); err != nil { - ux.Logger.PrintToUser(logging.LightRed.Wrap("ERROR: restart from this step by using the same command")) - return fmt.Errorf("error signing tx: %w", err) - } + } + amount := new(big.Float).SetFloat64(amountFlt) + amount = amount.Mul(amount, new(big.Float).SetFloat64(float64(units.Avax))) + amount = amount.Mul(amount, new(big.Float).SetFloat64(float64(units.Avax))) + amountInt, _ := amount.Int(nil) + return ictt.Send( + senderURL, + goethereumcommon.HexToAddress(originTransferrerAddress), + privateKey, + receiverBlockchainID, + goethereumcommon.HexToAddress(destinationTransferrerAddress), + destinationAddr, + amountInt, + ) +} - ctx, cancel := utils.GetAPIContext() - defer cancel() - err = wallet.X().IssueTx( - &tx, - common.WithContext(ctx), - ) - if err != nil { - if ctx.Err() != nil { - err = fmt.Errorf("timeout issuing/verifying tx with ID %s: %w", tx.ID(), err) - } else { - err = fmt.Errorf("error issuing tx with ID %s: %w", tx.ID(), err) - } - ux.Logger.PrintToUser(logging.LightRed.Wrap("ERROR: restart from this step by using the same command")) - return err - } +func pToPSend( + network models.Network, + kc keychain.Keychain, + usingLedger bool, + destinationAddrStr string, + amount uint64, +) error { + ethKeychain := secp256k1fx.NewKeychain() + wallet, err := primary.MakeWallet( + context.Background(), + &primary.WalletConfig{ + URI: network.Endpoint, + AVAXKeychain: kc, + EthKeychain: ethKeychain, + }, + ) + if err != nil { + return err + } + destinationAddr, err := address.ParseToID(destinationAddrStr) + if err != nil { + return err + } + to := secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{destinationAddr}, + } + output := &avax.TransferableOutput{ + Asset: avax.Asset{ID: wallet.P().Builder().Context().AVAXAssetID}, + Out: &secp256k1fx.TransferOutput{ + Amt: amount, + OutputOwners: to, + }, + } + outputs := []*avax.TransferableOutput{output} + ux.Logger.PrintToUser("Issuing BaseTx P -> P") + if usingLedger { + ux.Logger.PrintToUser("*** Please sign 'Export Tx / P to X Chain' transaction on the ledger device *** ") + } + unsignedTx, err := wallet.P().Builder().NewBaseTx( + outputs, + ) + if err != nil { + return fmt.Errorf("error building tx: %w", err) + } + tx := txs.Tx{Unsigned: unsignedTx} + if err := wallet.P().Signer().Sign(context.Background(), &tx); err != nil { + return fmt.Errorf("error signing tx: %w", err) + } + ctx, cancel := utils.GetAPIContext() + defer cancel() + err = wallet.P().IssueTx( + &tx, + common.WithContext(ctx), + ) + if err != nil { + if ctx.Err() != nil { + err = fmt.Errorf("timeout issuing/verifying tx with ID %s: %w", tx.ID(), err) + } else { + err = fmt.Errorf("error issuing tx with ID %s: %w", tx.ID(), err) + } + return err + } + pContext := wallet.P().Builder().Context() + var pFeeCalculator avagofee.Calculator + if pContext.GasPrice != 0 { + pFeeCalculator = avagofee.NewDynamicCalculator(pContext.ComplexityWeights, pContext.GasPrice) + } else { + pFeeCalculator = avagofee.NewStaticCalculator(pContext.StaticFeeConfig) + } + txFee, err := pFeeCalculator.CalculateFee(unsignedTx) + if err != nil { + return err + } + ux.Logger.PrintToUser("Paid fee: %.9f", float64(txFee)/float64(units.Avax)) + return nil +} - if PToX { - return nil - } +func pToXSend( + network models.Network, + kc keychain.Keychain, + usingLedger bool, + amount uint64, +) error { + ethKeychain := secp256k1fx.NewKeychain() + wallet, err := primary.MakeWallet( + context.Background(), + &primary.WalletConfig{ + URI: network.Endpoint, + AVAXKeychain: kc, + EthKeychain: ethKeychain, + }, + ) + if err != nil { + return err + } + to := secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: kc.Addresses().List(), + } + if err := exportFromP( + amount, + wallet, + wallet.X().Builder().Context().BlockchainID, + "X", + to, + usingLedger, + ); err != nil { + return err + } + time.Sleep(5 * time.Second) + return importIntoX( + wallet, + avagoconstants.PlatformChainID, + "P", + to, + usingLedger, + ) +} - time.Sleep(2 * time.Second) - receiveRecoveryStep++ - } - if receiveRecoveryStep == 1 { - wallet, err := primary.MakeWallet( - context.Background(), - &primary.WalletConfig{ - URI: network.Endpoint, - AVAXKeychain: kc, - EthKeychain: secp256k1fx.NewKeychain(), - }, - ) - if err != nil { - ux.Logger.PrintToUser(logging.LightRed.Wrap(fmt.Sprintf("ERROR: restart from this step by using the same command with extra arguments: --%s %d", receiveRecoveryStepFlag, receiveRecoveryStep))) - return err - } - ux.Logger.PrintToUser("Issuing ExportTx X -> P") - _, err = subnet.IssueXToPExportTx( - wallet, - usingLedger, - true, - wallet.P().Builder().Context().AVAXAssetID, - amount+fee*1, - &to, - ) - if err != nil { - ux.Logger.PrintToUser(logging.LightRed.Wrap(fmt.Sprintf("ERROR: restart from this step by using the same command with extra arguments: --%s %d", receiveRecoveryStepFlag, receiveRecoveryStep))) - return err - } - time.Sleep(2 * time.Second) - receiveRecoveryStep++ +func exportFromP( + amount uint64, + wallet primary.Wallet, + blockchainID ids.ID, + blockchainAlias string, + to secp256k1fx.OutputOwners, + usingLedger bool, +) error { + output := &avax.TransferableOutput{ + Asset: avax.Asset{ID: wallet.P().Builder().Context().AVAXAssetID}, + Out: &secp256k1fx.TransferOutput{ + Amt: amount, + OutputOwners: to, + }, + } + outputs := []*avax.TransferableOutput{output} + ux.Logger.PrintToUser("Issuing ExportTx P -> %s", blockchainAlias) + if usingLedger { + ux.Logger.PrintToUser("*** Please sign 'Export Tx / P to %s Chain' transaction on the ledger device *** ", blockchainAlias) + } + unsignedTx, err := wallet.P().Builder().NewExportTx( + blockchainID, + outputs, + ) + if err != nil { + return fmt.Errorf("error building tx: %w", err) + } + tx := txs.Tx{Unsigned: unsignedTx} + if err := wallet.P().Signer().Sign(context.Background(), &tx); err != nil { + return fmt.Errorf("error signing tx: %w", err) + } + ctx, cancel := utils.GetAPIContext() + defer cancel() + err = wallet.P().IssueTx( + &tx, + common.WithContext(ctx), + ) + if err != nil { + if ctx.Err() != nil { + err = fmt.Errorf("timeout issuing/verifying tx with ID %s: %w", tx.ID(), err) + } else { + err = fmt.Errorf("error issuing tx with ID %s: %w", tx.ID(), err) } - if receiveRecoveryStep == 2 { - wallet, err := primary.MakeWallet( - context.Background(), - &primary.WalletConfig{ - URI: network.Endpoint, - AVAXKeychain: kc, - EthKeychain: secp256k1fx.NewKeychain(), - }, - ) - if err != nil { - ux.Logger.PrintToUser(logging.LightRed.Wrap(fmt.Sprintf("ERROR: restart from this step by using the same command with extra arguments: --%s %d", receiveRecoveryStepFlag, receiveRecoveryStep))) - return err - } - ux.Logger.PrintToUser("Issuing ImportTx X -> P") - _, err = subnet.IssuePFromXImportTx( - wallet, - usingLedger, - true, - &to, - ) - if err != nil { - ux.Logger.PrintToUser(logging.LightRed.Wrap(fmt.Sprintf("ERROR: restart from this step by using the same command with extra arguments: --%s %d", receiveRecoveryStepFlag, receiveRecoveryStep))) - return err - } + return err + } + return nil +} + +func importIntoX( + wallet primary.Wallet, + blockchainID ids.ID, + blockchainAlias string, + to secp256k1fx.OutputOwners, + usingLedger bool, +) error { + ux.Logger.PrintToUser("Issuing ImportTx %s -> X", blockchainAlias) + if usingLedger { + ux.Logger.PrintToUser("*** Please sign ImportTx transaction on the ledger device *** ") + } + unsignedTx, err := wallet.X().Builder().NewImportTx( + blockchainID, + &to, + ) + if err != nil { + return fmt.Errorf("error building tx: %w", err) + } + tx := avmtxs.Tx{Unsigned: unsignedTx} + if err := wallet.X().Signer().Sign(context.Background(), &tx); err != nil { + return fmt.Errorf("error signing tx: %w", err) + } + ctx, cancel := utils.GetAPIContext() + defer cancel() + err = wallet.X().IssueTx( + &tx, + common.WithContext(ctx), + ) + if err != nil { + if ctx.Err() != nil { + err = fmt.Errorf("timeout issuing/verifying tx with ID %s: %w", tx.ID(), err) + } else { + err = fmt.Errorf("error issuing tx with ID %s: %w", tx.ID(), err) } + return err + } + return nil +} + +func pToCSend( + network models.Network, + kc keychain.Keychain, + usingLedger bool, + destinationAddrStr string, + amount uint64, +) error { + ethKeychain := secp256k1fx.NewKeychain() + wallet, err := primary.MakeWallet( + context.Background(), + &primary.WalletConfig{ + URI: network.Endpoint, + AVAXKeychain: kc, + EthKeychain: ethKeychain, + }, + ) + if err != nil { + return err } + to := secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: kc.Addresses().List(), + } + if err := exportFromP( + amount, + wallet, + wallet.C().Builder().Context().BlockchainID, + "C", + to, + usingLedger, + ); err != nil { + return err + } + time.Sleep(5 * time.Second) + if err != nil { + return err + } + return importIntoC( + network, + wallet, + avagoconstants.PlatformChainID, + "P", + destinationAddrStr, + usingLedger, + ) +} +func importIntoC( + network models.Network, + wallet primary.Wallet, + blockchainID ids.ID, + blockchainAlias string, + destinationAddrStr string, + usingLedger bool, +) error { + ux.Logger.PrintToUser("Issuing ImportTx %s -> C", blockchainAlias) + if usingLedger { + ux.Logger.PrintToUser("*** Please sign ImportTx transaction on the ledger device *** ") + } + client, err := clievm.GetClient(network.BlockchainEndpoint("C")) + if err != nil { + return err + } + baseFee, err := clievm.EstimateBaseFee(client) + if err != nil { + return err + } + unsignedTx, err := wallet.C().Builder().NewImportTx( + blockchainID, + goethereumcommon.HexToAddress(destinationAddrStr), + baseFee, + ) + if err != nil { + return fmt.Errorf("error building tx: %w", err) + } + tx := evm.Tx{UnsignedAtomicTx: unsignedTx} + if err := wallet.C().Signer().SignAtomic(context.Background(), &tx); err != nil { + return fmt.Errorf("error signing tx: %w", err) + } + ctx, cancel := utils.GetAPIContext() + defer cancel() + err = wallet.C().IssueAtomicTx( + &tx, + common.WithContext(ctx), + ) + if err != nil { + if ctx.Err() != nil { + err = fmt.Errorf("timeout issuing/verifying tx with ID %s: %w", tx.ID(), err) + } else { + err = fmt.Errorf("error issuing tx with ID %s: %w", tx.ID(), err) + } + return err + } return nil } -func captureAmount(sending bool, tokenDesc string) (float64, error) { - var promptStr string - if sending { - promptStr = fmt.Sprintf("Amount to send (%s)", tokenDesc) - } else { - promptStr = fmt.Sprintf("Amount to receive (%s)", tokenDesc) +func cToPSend( + network models.Network, + kc keychain.Keychain, + sk *key.SoftKey, + usingLedger bool, + amount uint64, +) error { + ethKeychain := sk.KeyChain() + wallet, err := primary.MakeWallet( + context.Background(), + &primary.WalletConfig{ + URI: network.Endpoint, + AVAXKeychain: kc, + EthKeychain: ethKeychain, + }, + ) + if err != nil { + return err } - amountFlt, err := app.Prompt.CaptureFloat(promptStr, func(v float64) error { - if v <= 0 { - return fmt.Errorf("value %f must be greater than zero", v) + to := secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: kc.Addresses().List(), + } + if err := exportFromC( + network, + amount, + wallet, + avagoconstants.PlatformChainID, + "P", + to, + usingLedger, + ); err != nil { + return err + } + time.Sleep(5 * time.Second) + wallet, err = primary.MakeWallet( + context.Background(), + &primary.WalletConfig{ + URI: network.Endpoint, + AVAXKeychain: kc, + EthKeychain: ethKeychain, + }, + ) + if err != nil { + return err + } + return importIntoP( + wallet, + wallet.C().Builder().Context().BlockchainID, + "C", + to, + usingLedger, + ) +} + +func exportFromC( + network models.Network, + amount uint64, + wallet primary.Wallet, + blockchainID ids.ID, + blockchainAlias string, + to secp256k1fx.OutputOwners, + usingLedger bool, +) error { + ux.Logger.PrintToUser("Issuing ExportTx C -> %s", blockchainAlias) + if usingLedger { + ux.Logger.PrintToUser("*** Please sign ExportTx transaction on the ledger device *** ") + } + client, err := clievm.GetClient(network.BlockchainEndpoint("C")) + if err != nil { + return err + } + baseFee, err := clievm.EstimateBaseFee(client) + if err != nil { + return err + } + outputs := []*secp256k1fx.TransferOutput{ + { + Amt: amount, + OutputOwners: to, + }, + } + unsignedTx, err := wallet.C().Builder().NewExportTx( + blockchainID, + outputs, + baseFee, + ) + if err != nil { + return fmt.Errorf("error building tx: %w", err) + } + tx := evm.Tx{UnsignedAtomicTx: unsignedTx} + if err := wallet.C().Signer().SignAtomic(context.Background(), &tx); err != nil { + return fmt.Errorf("error signing tx: %w", err) + } + ctx, cancel := utils.GetAPIContext() + defer cancel() + err = wallet.C().IssueAtomicTx( + &tx, + common.WithContext(ctx), + ) + if err != nil { + if ctx.Err() != nil { + err = fmt.Errorf("timeout issuing/verifying tx with ID %s: %w", tx.ID(), err) + } else { + err = fmt.Errorf("error issuing tx with ID %s: %w", tx.ID(), err) } - return nil - }) + return err + } + return nil +} + +func importIntoP( + wallet primary.Wallet, + blockchainID ids.ID, + blockchainAlias string, + to secp256k1fx.OutputOwners, + usingLedger bool, +) error { + ux.Logger.PrintToUser("Issuing ImportTx %s -> P", blockchainAlias) + if usingLedger { + ux.Logger.PrintToUser("*** Please sign ImportTx transaction on the ledger device *** ") + } + unsignedTx, err := wallet.P().Builder().NewImportTx( + blockchainID, + &to, + ) if err != nil { - return 0, err + return fmt.Errorf("error building tx: %w", err) } - return amountFlt, nil + tx := txs.Tx{Unsigned: unsignedTx} + if err := wallet.P().Signer().Sign(context.Background(), &tx); err != nil { + return fmt.Errorf("error signing tx: %w", err) + } + ctx, cancel := utils.GetAPIContext() + defer cancel() + err = wallet.P().IssueTx( + &tx, + common.WithContext(ctx), + ) + if err != nil { + if ctx.Err() != nil { + err = fmt.Errorf("timeout issuing/verifying tx with ID %s: %w", tx.ID(), err) + } else { + err = fmt.Errorf("error issuing tx with ID %s: %w", tx.ID(), err) + } + return err + } + return nil } diff --git a/cmd/teleportercmd/deploy.go b/cmd/teleportercmd/deploy.go index fef0f1061..99715f997 100644 --- a/cmd/teleportercmd/deploy.go +++ b/cmd/teleportercmd/deploy.go @@ -59,7 +59,8 @@ func newDeployCmd() *cobra.Command { } networkoptions.AddNetworkFlagsToCmd(cmd, &deployFlags.Network, true, deploySupportedNetworkOptions) deployFlags.PrivateKeyFlags.AddToCmd(cmd, "to fund ICM deploy") - deployFlags.ChainFlags.AddToCmd(cmd, "deploy ICM", true) + deployFlags.ChainFlags.SetEnabled(true, true, false, false, true) + deployFlags.ChainFlags.AddToCmd(cmd, "deploy ICM into %s") cmd.Flags().BoolVar(&deployFlags.DeployMessenger, "deploy-messenger", true, "deploy Teleporter Messenger") cmd.Flags().BoolVar(&deployFlags.DeployRegistry, "deploy-registry", true, "deploy Teleporter Registry") cmd.Flags().StringVar(&deployFlags.RPCURL, "rpc-url", "", "use the given RPC URL to connect to the subnet") @@ -100,9 +101,7 @@ func CallDeploy(_ []string, flags DeployFlags) error { app, network, prompt, - false, "", - false, &flags.ChainFlags, ); err != nil { return err diff --git a/cmd/teleportercmd/relayercmd/configList.go b/cmd/teleportercmd/relayercmd/configList.go index 6c74bad29..5a197c7ee 100644 --- a/cmd/teleportercmd/relayercmd/configList.go +++ b/cmd/teleportercmd/relayercmd/configList.go @@ -86,13 +86,12 @@ func addBoth(network models.Network, configSpec ConfigSpec) (ConfigSpec, error) func getBlockchain(network models.Network, prompt string) (contract.ChainSpec, error) { chainSpec := contract.ChainSpec{} + chainSpec.SetEnabled(true, true, false, false, true) if cancel, err := contract.PromptChain( app, network, prompt, - false, "", - true, &chainSpec, ); err != nil { return chainSpec, err diff --git a/pkg/application/app.go b/pkg/application/app.go index 391484f24..8b3221d7c 100644 --- a/pkg/application/app.go +++ b/pkg/application/app.go @@ -596,7 +596,18 @@ func (app *Avalanche) GetBlockchainNamesOnNetwork(network models.Network) ([]str if err != nil { return nil, err } - if sc.Networks[network.Name()].BlockchainID != ids.Empty { + networkName := network.Name() + if sc.Networks[networkName].BlockchainID == ids.Empty { + for k := range sc.Networks { + sidecarNetwork, err := app.GetNetworkFromSidecarNetworkName(k) + if err == nil { + if sidecarNetwork.Kind == network.Kind && sidecarNetwork.Endpoint == network.Endpoint { + networkName = sidecarNetwork.Name() + } + } + } + } + if sc.Networks[networkName].BlockchainID != ids.Empty { filtered = append(filtered, blockchainName) } } @@ -837,3 +848,26 @@ func (app *Avalanche) ListClusterNames() ([]string, error) { } return maps.Keys(clustersConfig.Clusters), nil } + +func (app *Avalanche) GetNetworkFromSidecarNetworkName( + networkName string, +) (models.Network, error) { + switch { + case networkName == models.Local.String(): + return models.NewLocalNetwork(), nil + case strings.HasPrefix(networkName, "Cluster"): + // network names on sidecar can refer to a cluster in the form "Cluster " + // we use clusterName to find out the underlying network for the cluster + // (one of local, devnet, fuji, mainnet) + parts := strings.Split(networkName, " ") + if len(parts) != 2 { + return models.UndefinedNetwork, fmt.Errorf("expected 'Cluster clusterName' on network name %s", networkName) + } + return app.GetClusterNetwork(parts[1]) + case networkName == models.Fuji.String(): + return models.NewFujiNetwork(), nil + case networkName == models.Mainnet.String(): + return models.NewMainnetNetwork(), nil + } + return models.UndefinedNetwork, fmt.Errorf("unsupported network name") +} diff --git a/pkg/contract/chain.go b/pkg/contract/chain.go index 3b423d3dc..7459cc539 100644 --- a/pkg/contract/chain.go +++ b/pkg/contract/chain.go @@ -4,6 +4,7 @@ package contract import ( "fmt" + "strings" cmdflags "github.com/ava-labs/avalanche-cli/cmd/flags" "github.com/ava-labs/avalanche-cli/pkg/application" @@ -17,18 +18,29 @@ import ( ) type ChainSpec struct { - blockchainFlagName string - cChainFlagName string - blockchainIDFlagName string - BlockchainName string - CChain bool - BlockchainID string + BlockchainName string + blockchainNameFlagEnabled bool + blockchainNameFlagName string + CChain bool + cChainFlagEnabled bool + cChainFlagName string + PChain bool + pChainFlagEnabled bool + pChainFlagName string + XChain bool + xChainFlagEnabled bool + xChainFlagName string + BlockchainID string + blockchainIDFlagEnabled bool + blockchainIDFlagName string } const ( - defaultBlockchainFlagName = "blockchain" - defaultCChainFlagName = "c-chain" - defaultBlockchainIDFlagName = "blockchain-id" + defaultBlockchainNameFlagName = "blockchain" + defaultCChainFlagName = "c-chain" + defaultPChainFlagName = "p-chain" + defaultXChainFlagName = "x-chain" + defaultBlockchainIDFlagName = "blockchain-id" ) func (cs *ChainSpec) CheckMutuallyExclusiveFields() error { @@ -36,55 +48,125 @@ func (cs *ChainSpec) CheckMutuallyExclusiveFields() error { cs.BlockchainName != "", cs.BlockchainID != "", cs.CChain, + cs.PChain, + cs.XChain, }) { - return fmt.Errorf("%s, %s and %s are mutually exclusive flags", - cs.blockchainFlagName, - cs.cChainFlagName, - cs.blockchainIDFlagName, + flags := []string{} + if cs.blockchainNameFlagEnabled { + flags = append(flags, cs.blockchainNameFlagName) + } + if cs.cChainFlagEnabled { + flags = append(flags, cs.cChainFlagName) + } + if cs.pChainFlagEnabled { + flags = append(flags, cs.pChainFlagName) + } + if cs.xChainFlagEnabled { + flags = append(flags, cs.xChainFlagName) + } + if cs.blockchainIDFlagEnabled { + flags = append(flags, cs.blockchainIDFlagName) + } + return fmt.Errorf("%s are mutually exclusive flags", + strings.Join(flags, ", "), ) } return nil } func (cs *ChainSpec) Defined() bool { - return cs.BlockchainName != "" || cs.BlockchainID != "" || cs.CChain + if cs.blockchainNameFlagEnabled && cs.BlockchainName != "" { + return true + } + if cs.cChainFlagEnabled && cs.CChain { + return true + } + if cs.pChainFlagEnabled && cs.PChain { + return true + } + if cs.xChainFlagEnabled && cs.XChain { + return true + } + if cs.blockchainIDFlagEnabled && cs.BlockchainID != "" { + return true + } + return false } -func (cs *ChainSpec) fillDefaultFlagNames() { - if cs.blockchainFlagName == "" { - cs.blockchainFlagName = defaultBlockchainFlagName +func (cs *ChainSpec) fillDefaults() { + if cs.blockchainNameFlagName == "" { + cs.blockchainIDFlagEnabled = true + cs.blockchainNameFlagName = defaultBlockchainNameFlagName } if cs.cChainFlagName == "" { + cs.cChainFlagEnabled = true cs.cChainFlagName = defaultCChainFlagName } + if cs.pChainFlagName == "" { + cs.pChainFlagName = defaultPChainFlagName + } + if cs.xChainFlagName == "" { + cs.xChainFlagName = defaultXChainFlagName + } if cs.blockchainIDFlagName == "" { cs.blockchainIDFlagName = defaultBlockchainIDFlagName } } func (cs *ChainSpec) SetFlagNames( - blockchainFlagName string, + blockchainNameFlagName string, cChainFlagName string, + pChainFlagName string, + xChainFlagName string, blockchainIDFlagName string, ) { - cs.blockchainFlagName = blockchainFlagName + cs.blockchainNameFlagName = blockchainNameFlagName cs.cChainFlagName = cChainFlagName + cs.pChainFlagName = pChainFlagName + cs.xChainFlagName = xChainFlagName cs.blockchainIDFlagName = blockchainIDFlagName + cs.SetEnabled( + cs.blockchainNameFlagName != "", + cs.cChainFlagName != "", + cs.pChainFlagName != "", + cs.xChainFlagName != "", + cs.blockchainIDFlagName != "", + ) +} + +func (cs *ChainSpec) SetEnabled( + blockchainNameFlagEnabled bool, + cChainFlagEnabled bool, + pChainFlagEnabled bool, + xChainFlagEnabled bool, + blockchainIDFlagEnabled bool, +) { + cs.blockchainNameFlagEnabled = blockchainNameFlagEnabled + cs.cChainFlagEnabled = cChainFlagEnabled + cs.pChainFlagEnabled = pChainFlagEnabled + cs.xChainFlagEnabled = xChainFlagEnabled + cs.blockchainIDFlagEnabled = blockchainIDFlagEnabled } func (cs *ChainSpec) AddToCmd( cmd *cobra.Command, - goal string, - addBlockchainIDFlag bool, + goalFmt string, ) { - cs.fillDefaultFlagNames() - if cs.blockchainFlagName == defaultBlockchainFlagName { - cmd.Flags().StringVar(&cs.BlockchainName, "subnet", "", fmt.Sprintf("%s into the given CLI blockchain", goal)) + cs.fillDefaults() + if cs.blockchainNameFlagEnabled { + cmd.Flags().StringVar(&cs.BlockchainName, cs.blockchainNameFlagName, "", fmt.Sprintf(goalFmt, "the given CLI blockchain")) + } + if cs.cChainFlagEnabled { + cmd.Flags().BoolVar(&cs.CChain, cs.cChainFlagName, false, fmt.Sprintf(goalFmt, "C-Chain")) } - cmd.Flags().StringVar(&cs.BlockchainName, cs.blockchainFlagName, "", fmt.Sprintf("%s into the given CLI blockchain", goal)) - cmd.Flags().BoolVar(&cs.CChain, cs.cChainFlagName, false, fmt.Sprintf("%s into C-Chain", goal)) - if addBlockchainIDFlag { - cmd.Flags().StringVar(&cs.BlockchainID, cs.blockchainIDFlagName, "", fmt.Sprintf("%s into the given blockchain ID/Alias", goal)) + if cs.pChainFlagEnabled { + cmd.Flags().BoolVar(&cs.PChain, cs.pChainFlagName, false, fmt.Sprintf(goalFmt, "P-Chain")) + } + if cs.xChainFlagEnabled { + cmd.Flags().BoolVar(&cs.XChain, cs.xChainFlagName, false, fmt.Sprintf(goalFmt, "X-Chain")) + } + if cs.blockchainIDFlagEnabled { + cmd.Flags().StringVar(&cs.BlockchainID, cs.blockchainIDFlagName, "", fmt.Sprintf(goalFmt, "the given blockchain ID/Alias")) } } @@ -100,30 +182,39 @@ func GetBlockchainEndpoints( wsEndpoint string ) switch { + case chainSpec.BlockchainName != "": + sc, err := app.LoadSidecar(chainSpec.BlockchainName) + if err != nil { + return "", "", fmt.Errorf("failed to load sidecar: %w", err) + } + networkName := network.Name() + if sc.Networks[networkName].BlockchainID == ids.Empty { + // look into the cluster deploys + for k := range sc.Networks { + sidecarNetwork, err := app.GetNetworkFromSidecarNetworkName(k) + if err == nil { + if sidecarNetwork.Equals(network) { + networkName = sidecarNetwork.Name() + } + } + } + } + if len(sc.Networks[networkName].RPCEndpoints) > 0 { + rpcEndpoint = sc.Networks[networkName].RPCEndpoints[0] + } + if len(sc.Networks[networkName].WSEndpoints) > 0 { + wsEndpoint = sc.Networks[networkName].WSEndpoints[0] + } case chainSpec.CChain: rpcEndpoint = network.CChainEndpoint() wsEndpoint = network.CChainWSEndpoint() - case network.Kind == models.Local || network.Kind == models.Devnet: + case network.Kind == models.Local: blockchainID, err := GetBlockchainID(app, network, chainSpec) if err != nil { return "", "", err } rpcEndpoint = network.BlockchainEndpoint(blockchainID.String()) wsEndpoint = network.BlockchainWSEndpoint(blockchainID.String()) - case chainSpec.BlockchainName != "": - sc, err := app.LoadSidecar(chainSpec.BlockchainName) - if err != nil { - return "", "", fmt.Errorf("failed to load sidecar: %w", err) - } - if sc.Networks[network.Name()].BlockchainID == ids.Empty { - return "", "", fmt.Errorf("blockchain has not been deployed to %s", network.Name()) - } - if len(sc.Networks[network.Name()].RPCEndpoints) > 0 { - rpcEndpoint = sc.Networks[network.Name()].RPCEndpoints[0] - } - if len(sc.Networks[network.Name()].WSEndpoints) > 0 { - wsEndpoint = sc.Networks[network.Name()].WSEndpoints[0] - } } blockchainDesc, err := GetBlockchainDesc(chainSpec) if err != nil { @@ -225,6 +316,10 @@ func GetBlockchainDesc( blockchainDesc = chainSpec.BlockchainName case chainSpec.CChain: blockchainDesc = "C-Chain" + case chainSpec.PChain: + blockchainDesc = "P-Chain" + case chainSpec.XChain: + blockchainDesc = "X-Chain" case chainSpec.BlockchainID != "": blockchainDesc = chainSpec.BlockchainID default: @@ -293,24 +388,28 @@ func PromptChain( app *application.Avalanche, network models.Network, prompt string, - avoidCChain bool, - avoidBlockchain string, - includeCustom bool, + blockchainNameToAvoid string, chainSpec *ChainSpec, ) (bool, error) { - blockchainNames, err := app.GetBlockchainNamesOnNetwork(network) - if err != nil { - return false, err + var ( + err error + blockchainNames []string + ) + if chainSpec.blockchainNameFlagEnabled { + blockchainNames, err = app.GetBlockchainNamesOnNetwork(network) + if err != nil { + return false, err + } } - cancel, _, _, cChain, blockchainName, blockchainID, err := prompts.PromptChain( + cancel, pChain, xChain, cChain, blockchainName, blockchainID, err := prompts.PromptChain( app.Prompt, prompt, blockchainNames, - true, - true, - avoidCChain, - avoidBlockchain, - includeCustom, + chainSpec.pChainFlagEnabled, + chainSpec.xChainFlagEnabled, + chainSpec.cChainFlagEnabled, + blockchainNameToAvoid, + chainSpec.blockchainIDFlagEnabled, ) if err != nil || cancel { return cancel, err @@ -325,6 +424,8 @@ func PromptChain( } chainSpec.BlockchainName = blockchainName chainSpec.CChain = cChain + chainSpec.PChain = pChain + chainSpec.XChain = xChain chainSpec.BlockchainID = blockchainID return false, nil } diff --git a/pkg/models/network.go b/pkg/models/network.go index 08128c2fc..36581e138 100644 --- a/pkg/models/network.go +++ b/pkg/models/network.go @@ -169,3 +169,8 @@ func (n *Network) HandlePublicNetworkSimulation() { n.Endpoint = constants.LocalAPIEndpoint } } + +// Equals checks the underlying fields Kind and Endpoint +func (n *Network) Equals(n2 Network) bool { + return n.Kind == n2.Kind && n.Endpoint == n2.Endpoint +} diff --git a/pkg/networkoptions/network_options.go b/pkg/networkoptions/network_options.go index 7e1c32bcb..52cebbac1 100644 --- a/pkg/networkoptions/network_options.go +++ b/pkg/networkoptions/network_options.go @@ -99,27 +99,6 @@ func AddNetworkFlagsToCmd(cmd *cobra.Command, networkFlags *NetworkFlags, addEnd } } -func GetNetworkFromSidecarNetworkName( - app *application.Avalanche, - networkName string, -) (models.Network, error) { - switch { - case networkName == models.Local.String(): - return models.NewLocalNetwork(), nil - case strings.HasPrefix(networkName, Cluster.String()): - parts := strings.Split(networkName, " ") - if len(parts) != 2 { - return models.UndefinedNetwork, fmt.Errorf("expected 'Cluster clusterName' on network name %s", networkName) - } - return app.GetClusterNetwork(parts[1]) - case networkName == models.Fuji.String(): - return models.NewFujiNetwork(), nil - case networkName == models.Mainnet.String(): - return models.NewMainnetNetwork(), nil - } - return models.UndefinedNetwork, fmt.Errorf("unsupported network name") -} - func GetSupportedNetworkOptionsForSubnet( app *application.Avalanche, subnetName string, @@ -238,6 +217,17 @@ func GetNetworkFromCmdLineFlags( networkOption = Mainnet case networkFlags.ClusterName != "": networkOption = Cluster + case networkFlags.Endpoint != "": + switch networkFlags.Endpoint { + case constants.MainnetAPIEndpoint: + networkOption = Mainnet + case constants.FujiAPIEndpoint: + networkOption = Fuji + case constants.LocalAPIEndpoint: + networkOption = Local + default: + networkOption = Devnet + } } // unsupported option if networkOption != Undefined && !slices.Contains(supportedNetworkOptions, networkOption) { diff --git a/pkg/prompts/prompts.go b/pkg/prompts/prompts.go index d1b2983a5..028ad19c7 100644 --- a/pkg/prompts/prompts.go +++ b/pkg/prompts/prompts.go @@ -30,6 +30,7 @@ const ( Undefined AddressFormat = iota PChainFormat EVMFormat + XChainFormat ) const ( @@ -899,10 +900,10 @@ func PromptChain( prompter Prompter, prompt string, subnetNames []string, - avoidPChain bool, - avoidXChain bool, - avoidCChain bool, - avoidSubnet string, + includePChain bool, + includeXChain bool, + includeCChain bool, + avoidBlockchainName string, includeCustom bool, ) (bool, bool, bool, bool, string, string, error) { pChainOption := "P-Chain" @@ -910,16 +911,16 @@ func PromptChain( cChainOption := "C-Chain" notListedOption := "My blockchain isn't listed" subnetOptions := []string{} - if !avoidPChain { + if includePChain { subnetOptions = append(subnetOptions, pChainOption) } - if !avoidXChain { + if includeXChain { subnetOptions = append(subnetOptions, xChainOption) } - if !avoidCChain { + if includeCChain { subnetOptions = append(subnetOptions, cChainOption) } - subnetNames = utils.RemoveFromSlice(subnetNames, avoidSubnet) + subnetNames = utils.RemoveFromSlice(subnetNames, avoidBlockchainName) subnetOptions = append(subnetOptions, utils.Map(subnetNames, func(s string) string { return "Blockchain " + s })...) if includeCustom { subnetOptions = append(subnetOptions, customOption) @@ -1045,6 +1046,11 @@ func PromptAddress( if err != nil { return "", err } + case XChainFormat: + address, err = prompter.CaptureXChainAddress(customPrompt, network) + if err != nil { + return "", err + } case EVMFormat: addr, err := prompter.CaptureAddress(customPrompt) if err != nil { @@ -1081,6 +1087,8 @@ func CaptureKeyAddress( switch format { case PChainFormat: return k.P()[0], nil + case XChainFormat: + return k.X()[0], nil case EVMFormat: return k.C(), nil } diff --git a/pkg/prompts/validations.go b/pkg/prompts/validations.go index 3a96ddb7d..bfe2b54a6 100644 --- a/pkg/prompts/validations.go +++ b/pkg/prompts/validations.go @@ -267,6 +267,8 @@ func getXChainValidationFunc(network models.Network) func(string) error { return validateXChainMainAddress case models.Local: return validateXChainLocalAddress + case models.Devnet: + return validateXChainLocalAddress default: return func(string) error { return errors.New("unsupported network") diff --git a/tests/e2e/commands/key.go b/tests/e2e/commands/key.go index 31525c5e7..c27b87fa7 100644 --- a/tests/e2e/commands/key.go +++ b/tests/e2e/commands/key.go @@ -127,13 +127,12 @@ func KeyTransferSend(keyName string, targetAddr string, amount string) (string, "--local", "--key", keyName, - "--send", "--destination-addr", targetAddr, "--amount", amount, - "--fund-p-chain", - "--force", + "--p-chain-sender", + "--p-chain-receiver", "--" + constants.SkipUpdateFlag, } cmd := exec.Command(CLIBinary, args...)