diff --git a/cmd/keycmd/transfer.go b/cmd/keycmd/transfer.go index 10df746b4..e1462b590 100644 --- a/cmd/keycmd/transfer.go +++ b/cmd/keycmd/transfer.go @@ -216,7 +216,7 @@ func transferF(*cobra.Command, []string) error { if err != nil { return err } - if senderChainFlags.BlockchainName != "" || receiverChainFlags.BlockchainName != "" || senderChainFlags.XChain { + if senderChainFlags.BlockchainName != "" || receiverChainFlags.BlockchainName != "" || (senderChainFlags.XChain && !receiverChainFlags.XChain) { return fmt.Errorf("transfer from %s to %s is not supported", senderDesc, receiverDesc) } @@ -274,7 +274,7 @@ func transferF(*cobra.Command, []string) error { } amount := uint64(amountFlt * float64(units.Avax)) - if destinationAddrStr == "" && senderChainFlags.PChain && (receiverChainFlags.PChain || receiverChainFlags.CChain) { + if destinationAddrStr == "" && ((senderChainFlags.PChain && (receiverChainFlags.PChain || receiverChainFlags.CChain)) || (senderChainFlags.XChain && receiverChainFlags.XChain)) { if destinationKeyName != "" { k, err := app.GetKey(destinationKeyName, network, false) if err != nil { @@ -290,11 +290,21 @@ func transferF(*cobra.Command, []string) error { } destinationAddrStr = addrs[0] } + if receiverChainFlags.XChain { + addrs := k.X() + if len(addrs) == 0 { + return fmt.Errorf("unexpected null number of X-Chain addresses for key") + } + destinationAddrStr = addrs[0] + } } else { format := prompts.EVMFormat if receiverChainFlags.PChain { format = prompts.PChainFormat } + if receiverChainFlags.XChain { + format = prompts.XChainFormat + } destinationAddrStr, err = prompts.PromptAddress( app.Prompt, "destination address", @@ -347,6 +357,15 @@ func transferF(*cobra.Command, []string) error { amount, ) } + if senderChainFlags.XChain && receiverChainFlags.XChain { + return xToXSend( + network, + kc, + usingLedger, + destinationAddrStr, + amount, + ) + } return nil } @@ -743,6 +762,76 @@ func pToXSend( ) } +func xToXSend( + network models.Network, + kc keychain.Keychain, + usingLedger bool, + destinationAddrStr string, + amount uint64, +) error { + ctx, cancel := sdkutils.GetTimedContext(constants.WalletCreationTimeout) + defer cancel() + ethKeychain := secp256k1fx.NewKeychain() + wallet, err := primary.MakeWallet( + ctx, + network.Endpoint, + kc, + ethKeychain, + primary.WalletConfig{}, + ) + 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.X().Builder().Context().AVAXAssetID}, + Out: &secp256k1fx.TransferOutput{ + Amt: amount, + OutputOwners: to, + }, + } + outputs := []*avax.TransferableOutput{output} + ux.Logger.PrintToUser("Issuing BaseTx X -> X") + if usingLedger { + ux.Logger.PrintToUser("*** Please sign BaseTx transaction on the ledger device *** ") + } + unsignedTx, err := wallet.X().Builder().NewBaseTx( + outputs, + ) + if err != nil { + return fmt.Errorf("error building tx: %w", err) + } + tx := avmtxs.Tx{Unsigned: unsignedTx} + ctx, cancel = sdkutils.GetTimedContext(constants.SignatureTimeout) + defer cancel() + if err := wallet.X().Signer().Sign(ctx, &tx); err != nil { + return fmt.Errorf("error signing tx: %w", err) + } + ctx, cancel = sdkutils.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 + } + ux.Logger.PrintToUser("X-Chain Paid fee: %.9f AVAX", float64(wallet.X().Builder().Context().BaseTxFee)/float64(units.Avax)) + return nil +} + func exportFromP( amount uint64, wallet *primary.Wallet, diff --git a/tests/e2e/testcases/key/transfer/suite.go b/tests/e2e/testcases/key/transfer/suite.go index e4b7a68ab..f8768a8e7 100644 --- a/tests/e2e/testcases/key/transfer/suite.go +++ b/tests/e2e/testcases/key/transfer/suite.go @@ -429,6 +429,46 @@ var _ = ginkgo.Describe("[Key] transfer", func() { Should(gomega.Equal(ewoqKeyERCBalance1 - ewoqKeyERCBalance2)) gomega.Expect(keyBalance2 - keyERCBalance1).Should(gomega.Equal(amount)) }) + + ginkgo.It("can transfer from X-chain to X-chain with ewoq key and local key", func() { + amount := 0.2 + amountStr := fmt.Sprintf("%.2f", amount) + amountNAvax := uint64(amount * float64(units.Avax)) + commandArguments := []string{ + "--local", + "--key", + ewoqKeyName, + "--destination-key", + keyName, + "--x-chain-sender", + "--x-chain-receiver", + "--amount", + amountStr, + } + + output, err := commands.ListKeys("local", true, "", "") + gomega.Expect(err).Should(gomega.BeNil()) + _, keyBalance1, err := utils.ParseAddrBalanceFromKeyListOutput(output, keyName, "X-Chain") + gomega.Expect(err).Should(gomega.BeNil()) + _, ewoqKeyBalance1, err := utils.ParseAddrBalanceFromKeyListOutput(output, ewoqKeyName, "X-Chain") + gomega.Expect(err).Should(gomega.BeNil()) + + output, err = commands.KeyTransferSend(commandArguments) + gomega.Expect(err).Should(gomega.BeNil()) + + feeNAvax, err := utils.GetKeyTransferFee(output, "X-Chain") + gomega.Expect(err).Should(gomega.BeNil()) + + output, err = commands.ListKeys("local", true, "", "") + gomega.Expect(err).Should(gomega.BeNil()) + _, keyBalance2, err := utils.ParseAddrBalanceFromKeyListOutput(output, keyName, "X-Chain") + gomega.Expect(err).Should(gomega.BeNil()) + _, ewoqKeyBalance2, err := utils.ParseAddrBalanceFromKeyListOutput(output, ewoqKeyName, "X-Chain") + gomega.Expect(err).Should(gomega.BeNil()) + gomega.Expect(feeNAvax + amountNAvax). + Should(gomega.Equal(ewoqKeyBalance1 - ewoqKeyBalance2)) + gomega.Expect(keyBalance2 - keyBalance1).Should(gomega.Equal(amountNAvax)) + }) }) ginkgo.Context("with invalid input", func() { ginkgo.It("should fail when both key and ledger index were provided", func() { @@ -532,25 +572,6 @@ var _ = ginkgo.Describe("[Key] transfer", func() { }) }) ginkgo.Context("with unsupported paths", func() { - ginkgo.It("should fail when transferring from X-Chain to X-Chain", func() { - commandArguments := []string{ - "--local", - "--key", - ewoqKeyName, - "--destination-key", - ewoqKeyName, - "--amount", - "0.1", - "--x-chain-sender", - "--x-chain-receiver", - } - output, err := commands.KeyTransferSend(commandArguments) - - gomega.Expect(err).Should(gomega.HaveOccurred()) - gomega.Expect(output). - Should(gomega.ContainSubstring("transfer from X-Chain to X-Chain is not supported")) - }) - ginkgo.It("should fail when transferring from X-Chain to C-Chain", func() { commandArguments := []string{ "--local",