From 17c2359fc4cdd33d8f018969106edc0609b738f7 Mon Sep 17 00:00:00 2001 From: Ian Denhardt Date: Sun, 8 May 2022 21:42:10 -0400 Subject: [PATCH 1/2] console: support `data:` field to {send,sign}Transaction. This adds support for the data: field in console calls to sendTransaction & signTransaction, with the same semantics as in web3.js. --- pkg/console/console.go | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/pkg/console/console.go b/pkg/console/console.go index a9acb5a..238f2b0 100644 --- a/pkg/console/console.go +++ b/pkg/console/console.go @@ -544,6 +544,18 @@ func (b *bridge) HmyGetListAccounts(call jsre.Call) (goja.Value, error) { return call.VM.ToValue(accounts), nil } +func getTxData(txObj *goja.Object) ([]byte, error) { + dataObj := txObj.Get("data") + if dataObj != nil { + dataStr := dataObj.Export().(string) + if !strings.HasPrefix(dataStr, "0x") { + return nil, fmt.Errorf("Invalid data literal: %q", dataStr) + } + return hex.DecodeString(dataStr[2:]) + } + return nil, nil +} + func (b *bridge) HmySignTransaction(call jsre.Call) (goja.Value, error) { txObj := call.Arguments[0].ToObject(call.VM) password := call.Arguments[1].String() @@ -553,6 +565,10 @@ func (b *bridge) HmySignTransaction(call jsre.Call) (goja.Value, error) { gasLimit := getStringFromJsObjWithDefault(txObj, "gas", "1000000") amount := getStringFromJsObjWithDefault(txObj, "value", "0") gasPrice := getStringFromJsObjWithDefault(txObj, "gasPrice", "1") + input, err := getTxData(txObj) + if err != nil { + return nil, err + } networkHandler := rpc.NewHTTPHandler(b.console.nodeUrl) chanId, err := common.StringToChainID(b.console.net) @@ -598,7 +614,7 @@ func (b *bridge) HmySignTransaction(call jsre.Call) (goja.Value, error) { toP, uint32(b.console.shardId), uint32(b.console.shardId), amt, gPrice, - []byte{}, + input, ) if err != nil { return nil, err @@ -635,6 +651,10 @@ func (b *bridge) HmySendTransaction(call jsre.Call) (goja.Value, error) { gasLimit := getStringFromJsObjWithDefault(txObj, "gas", "1000000") amount := getStringFromJsObjWithDefault(txObj, "value", "0") gasPrice := getStringFromJsObjWithDefault(txObj, "gasPrice", "1") + input, err := getTxData(txObj) + if err != nil { + return nil, err + } networkHandler := rpc.NewHTTPHandler(b.console.nodeUrl) chanId, err := common.StringToChainID(b.console.net) @@ -680,7 +700,7 @@ func (b *bridge) HmySendTransaction(call jsre.Call) (goja.Value, error) { toP, uint32(b.console.shardId), uint32(b.console.shardId), amt, gPrice, - []byte{}, + input, ) if err != nil { return nil, err From 5cbf0c66dcba9fffd12eaa2fbd95950ba3608c35 Mon Sep 17 00:00:00 2001 From: MaxMustermann2 <82761650+MaxMustermann2@users.noreply.github.com> Date: Wed, 19 Oct 2022 14:08:50 +0000 Subject: [PATCH 2/2] cmd: add support for `data` signing --- cmd/subcommands/ethtransfer.go | 81 ++++++++++++++++++++++++++++++-- cmd/subcommands/transfer.go | 11 ++++- pkg/console/console.go | 49 +++++++++++++------ pkg/transaction/controller.go | 1 - pkg/transaction/ethcontroller.go | 55 +++++++++++++--------- pkg/transaction/util.go | 17 +++++++ 6 files changed, 170 insertions(+), 44 deletions(-) create mode 100644 pkg/transaction/util.go diff --git a/cmd/subcommands/ethtransfer.go b/cmd/subcommands/ethtransfer.go index 3596943..ce47115 100644 --- a/cmd/subcommands/ethtransfer.go +++ b/cmd/subcommands/ethtransfer.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "io/ioutil" + "os" "strconv" "strings" "time" @@ -43,9 +44,13 @@ func ethHandlerForShard(node string) (*rpc.HTTPMessenger, error) { // Note that the vars need to be set before calling this handler. func ethHandlerForTransaction(txLog *transactionLog) error { from := fromAddress.String() - networkHandler, err := ethHandlerForShard(node) - if handlerForError(txLog, err) != nil { - return err + var networkHandler *rpc.HTTPMessenger + if !offlineSign { + var err error + networkHandler, err = ethHandlerForShard(node) + if handlerForError(txLog, err) != nil { + return err + } } var ctrlr *transaction.EthController @@ -98,12 +103,17 @@ func ethHandlerForTransaction(txLog *transactionLog) error { gLimit = uint64(tempLimit) } + dataByte, err := transaction.StringToByte(data) + if err != nil { + return handlerForError(txLog, err) + } + txLog.TimeSigned = time.Now().UTC().Format(timeFormat) // Approximate time of signature err = ctrlr.ExecuteEthTransaction( nonce, gLimit, toAddress.String(), amt, gPrice, - []byte{}, + dataByte, ) if dryRun { @@ -187,6 +197,9 @@ func ethOpts(ctlr *transaction.EthController) { if dryRun { ctlr.Behavior.DryRun = true } + if offlineSign { + ctlr.Behavior.OfflineSign = true + } if useLedgerWallet { ctlr.Behavior.SigningImpl = transaction.Ledger } @@ -204,6 +217,9 @@ func init() { Create an Ethereum compatible transaction, sign it, and send off to the Harmony blockchain `, PreRunE: func(cmd *cobra.Command, args []string) error { + if offlineSign { + dryRun = true + } if givenFilePath == "" { for _, flagName := range [...]string{"from", "to", "amount", "chain-id"} { _ = cmd.MarkFlagRequired(flagName) @@ -269,15 +285,72 @@ Create an Ethereum compatible transaction, sign it, and send off to the Harmony cmdEthTransfer.Flags().Var(&fromAddress, "from", "sender's one address, keystore must exist locally") cmdEthTransfer.Flags().Var(&toAddress, "to", "the destination one address") cmdEthTransfer.Flags().BoolVar(&dryRun, "dry-run", false, "do not send signed transaction") + cmdEthTransfer.Flags().BoolVar(&offlineSign, "offline-sign", false, "output offline signing") cmdEthTransfer.Flags().BoolVar(&trueNonce, "true-nonce", false, "send transaction with on-chain nonce") cmdEthTransfer.Flags().StringVar(&amount, "amount", "0", "amount to send (ONE)") cmdEthTransfer.Flags().StringVar(&gasPrice, "gas-price", "100", "gas price to pay (NANO)") cmdEthTransfer.Flags().StringVar(&gasLimit, "gas-limit", "", "gas limit") cmdEthTransfer.Flags().StringVar(&inputNonce, "nonce", "", "set nonce for tx") + cmdEthTransfer.Flags().StringVar(&data, "data", "", "transaction data") cmdEthTransfer.Flags().StringVar(&targetChain, "chain-id", "", "what chain ID to target") cmdEthTransfer.Flags().Uint32Var(&timeout, "timeout", defaultTimeout, "set timeout in seconds. Set to 0 to not wait for confirm") cmdEthTransfer.Flags().BoolVar(&userProvidesPassphrase, "passphrase", false, ppPrompt) cmdEthTransfer.Flags().StringVar(&passphraseFilePath, "passphrase-file", "", "path to a file containing the passphrase") RootCmd.AddCommand(cmdEthTransfer) + + cmdOfflineSignEthTransfer := &cobra.Command{ + Use: "offline-sign-eth-transfer", + Short: "Send a Offline Signed Ethereum transaction", + Args: cobra.ExactArgs(0), + Long: ` +Send a offline signed transaction to the Harmony blockchain (on the same shard) +`, + PreRunE: func(cmd *cobra.Command, args []string) error { + if givenFilePath == "" { + return fmt.Errorf("must give a offline-signed file") + } + return nil + }, + RunE: func(cmd *cobra.Command, args []string) error { + var txLogs []*transactionLog + + networkHandler := rpc.NewHTTPHandler(node) + + openFile, err := os.Open(givenFilePath) + if err != nil { + return err + } + defer openFile.Close() + + err = json.NewDecoder(openFile).Decode(&txLogs) + if err != nil { + return err + } + + for _, txLog := range txLogs { + if len(txLog.Errors) > 0 { + continue + } + + ctrlr := transaction.NewEthController(networkHandler, nil, nil, *chainName.chainID, ethOpts) + err := ctrlr.ExecuteRawTransaction(txLog.RawTxn) + if handlerForError(txLog, err) != nil { + txLog.Errors = append(txLog.Errors, err.Error()) + continue + } + + if txHash := ctrlr.TransactionHash(); txHash != nil { + txLog.TxHash = *txHash + } + + txLog.Receipt = ctrlr.Receipt()["result"] + } + + fmt.Println(common.ToJSONUnsafe(txLogs, true)) + return nil + }, + } + + RootCmd.AddCommand(cmdOfflineSignEthTransfer) } diff --git a/cmd/subcommands/transfer.go b/cmd/subcommands/transfer.go index 5e00050..269ae7a 100644 --- a/cmd/subcommands/transfer.go +++ b/cmd/subcommands/transfer.go @@ -42,6 +42,7 @@ var ( transferFileFlags []transferFlags timeout uint32 timeFormat = "2006-01-02 15:04:05.000000" + data string ) type transactionLog struct { @@ -169,6 +170,11 @@ func handlerForTransaction(txLog *transactionLog) error { gLimit = uint64(tempLimit) } + dataByte, err := transaction.StringToByte(data) + if err != nil { + return handlerForError(txLog, err) + } + addr := toAddress.String() txLog.TimeSigned = time.Now().UTC().Format(timeFormat) // Approximate time of signature @@ -177,7 +183,7 @@ func handlerForTransaction(txLog *transactionLog) error { &addr, fromShardID, toShardID, amt, gPrice, - []byte{}, + dataByte, ) if dryRun { @@ -414,6 +420,7 @@ Create a transaction, sign it, and send off to the Harmony blockchain cmdTransfer.Flags().StringVar(&inputNonce, "nonce", "", "set nonce for tx") cmdTransfer.Flags().Uint32Var(&fromShardID, "from-shard", 0, "source shard id") cmdTransfer.Flags().Uint32Var(&toShardID, "to-shard", 0, "target shard id") + cmdTransfer.Flags().StringVar(&data, "data", "", "transaction data") cmdTransfer.Flags().StringVar(&targetChain, "chain-id", "", "what chain ID to target") cmdTransfer.Flags().Uint32Var(&timeout, "timeout", defaultTimeout, "set timeout in seconds. Set to 0 to not wait for confirm") cmdTransfer.Flags().BoolVar(&userProvidesPassphrase, "passphrase", false, ppPrompt) @@ -449,7 +456,7 @@ Get Nonce From a Account Short: "Send a Offline Signed transaction", Args: cobra.ExactArgs(0), Long: ` -Send a offline signed to the Harmony blockchain +Send a offline signed transaction to the Harmony blockchain `, PreRunE: func(cmd *cobra.Command, args []string) error { if givenFilePath == "" { diff --git a/pkg/console/console.go b/pkg/console/console.go index a9acb5a..79665f4 100644 --- a/pkg/console/console.go +++ b/pkg/console/console.go @@ -4,18 +4,6 @@ import ( "encoding/hex" "errors" "fmt" - ethereum_rpc "github.com/ethereum/go-ethereum/rpc" - "github.com/harmony-one/go-sdk/pkg/account" - "github.com/harmony-one/go-sdk/pkg/address" - "github.com/harmony-one/go-sdk/pkg/common" - "github.com/harmony-one/go-sdk/pkg/console/jsre" - "github.com/harmony-one/go-sdk/pkg/console/jsre/deps" - "github.com/harmony-one/go-sdk/pkg/console/prompt" - "github.com/harmony-one/go-sdk/pkg/console/web3ext" - "github.com/harmony-one/go-sdk/pkg/rpc" - "github.com/harmony-one/go-sdk/pkg/store" - "github.com/harmony-one/go-sdk/pkg/transaction" - "github.com/harmony-one/harmony/accounts" "io" "io/ioutil" "math/big" @@ -29,6 +17,19 @@ import ( "syscall" "time" + ethereum_rpc "github.com/ethereum/go-ethereum/rpc" + "github.com/harmony-one/go-sdk/pkg/account" + "github.com/harmony-one/go-sdk/pkg/address" + "github.com/harmony-one/go-sdk/pkg/common" + "github.com/harmony-one/go-sdk/pkg/console/jsre" + "github.com/harmony-one/go-sdk/pkg/console/jsre/deps" + "github.com/harmony-one/go-sdk/pkg/console/prompt" + "github.com/harmony-one/go-sdk/pkg/console/web3ext" + "github.com/harmony-one/go-sdk/pkg/rpc" + "github.com/harmony-one/go-sdk/pkg/store" + "github.com/harmony-one/go-sdk/pkg/transaction" + "github.com/harmony-one/harmony/accounts" + "github.com/dop251/goja" "github.com/mattn/go-colorable" "github.com/peterh/liner" @@ -544,6 +545,18 @@ func (b *bridge) HmyGetListAccounts(call jsre.Call) (goja.Value, error) { return call.VM.ToValue(accounts), nil } +func getTxData(txObj *goja.Object) ([]byte, error) { + dataObj := txObj.Get("data") + if dataObj != nil { + dataStr := dataObj.Export().(string) + if !strings.HasPrefix(dataStr, "0x") { + return nil, fmt.Errorf("Invalid data literal: %q", dataStr) + } + return hex.DecodeString(dataStr[2:]) + } + return nil, nil +} + func (b *bridge) HmySignTransaction(call jsre.Call) (goja.Value, error) { txObj := call.Arguments[0].ToObject(call.VM) password := call.Arguments[1].String() @@ -553,6 +566,10 @@ func (b *bridge) HmySignTransaction(call jsre.Call) (goja.Value, error) { gasLimit := getStringFromJsObjWithDefault(txObj, "gas", "1000000") amount := getStringFromJsObjWithDefault(txObj, "value", "0") gasPrice := getStringFromJsObjWithDefault(txObj, "gasPrice", "1") + input, err := transaction.StringToByte(getStringFromJsObjWithDefault(txObj, "data", "")) + if err != nil { + return nil, err + } networkHandler := rpc.NewHTTPHandler(b.console.nodeUrl) chanId, err := common.StringToChainID(b.console.net) @@ -598,7 +615,7 @@ func (b *bridge) HmySignTransaction(call jsre.Call) (goja.Value, error) { toP, uint32(b.console.shardId), uint32(b.console.shardId), amt, gPrice, - []byte{}, + input, ) if err != nil { return nil, err @@ -635,6 +652,10 @@ func (b *bridge) HmySendTransaction(call jsre.Call) (goja.Value, error) { gasLimit := getStringFromJsObjWithDefault(txObj, "gas", "1000000") amount := getStringFromJsObjWithDefault(txObj, "value", "0") gasPrice := getStringFromJsObjWithDefault(txObj, "gasPrice", "1") + input, err := transaction.StringToByte(getStringFromJsObjWithDefault(txObj, "data", "")) + if err != nil { + return nil, err + } networkHandler := rpc.NewHTTPHandler(b.console.nodeUrl) chanId, err := common.StringToChainID(b.console.net) @@ -680,7 +701,7 @@ func (b *bridge) HmySendTransaction(call jsre.Call) (goja.Value, error) { toP, uint32(b.console.shardId), uint32(b.console.shardId), amt, gPrice, - []byte{}, + input, ) if err != nil { return nil, err diff --git a/pkg/transaction/controller.go b/pkg/transaction/controller.go index 21c75b5..bd9310a 100644 --- a/pkg/transaction/controller.go +++ b/pkg/transaction/controller.go @@ -205,7 +205,6 @@ func (C *Controller) setAmount(amount numeric.Dec) { return } } - C.transactionForRPC.params["transfer-amount"] = amountInAtto } diff --git a/pkg/transaction/ethcontroller.go b/pkg/transaction/ethcontroller.go index 9df42aa..16760e5 100644 --- a/pkg/transaction/ethcontroller.go +++ b/pkg/transaction/ethcontroller.go @@ -134,34 +134,37 @@ func (C *EthController) setAmount(amount numeric.Dec) { }) return } - balanceRPCReply, err := C.messenger.SendRPC( - rpc.Method.GetBalance, - p{address.ToBech32(C.sender.account.Address), "latest"}, - ) - if err != nil { - C.executionError = err - return - } - currentBalance, _ := balanceRPCReply["result"].(string) - bal, _ := new(big.Int).SetString(currentBalance[2:], 16) - balance := numeric.NewDecFromBigInt(bal) + gasAsDec := C.transactionForRPC.params["gas-price"].(numeric.Dec) gasAsDec = gasAsDec.Mul(numeric.NewDec(int64(C.transactionForRPC.params["gas-limit"].(uint64)))) amountInAtto := amount.Mul(oneAsDec) total := amountInAtto.Add(gasAsDec) - if total.GT(balance) { - balanceInOne := balance.Quo(oneAsDec) - C.executionError = ErrBadTransactionParam - errorMsg := fmt.Sprintf( - "insufficient balance of %s in shard %d for the requested transfer of %s", - balanceInOne.String(), C.transactionForRPC.params["from-shard"].(uint32), amount.String(), + if !C.Behavior.OfflineSign { + balanceRPCReply, err := C.messenger.SendRPC( + rpc.Method.GetBalance, + p{address.ToBech32(C.sender.account.Address), "latest"}, ) - C.transactionErrors = append(C.transactionErrors, &Error{ - ErrMessage: &errorMsg, - TimestampOfRejection: time.Now().Unix(), - }) - return + if err != nil { + C.executionError = err + return + } + currentBalance, _ := balanceRPCReply["result"].(string) + bal, _ := new(big.Int).SetString(currentBalance[2:], 16) + balance := numeric.NewDecFromBigInt(bal) + if total.GT(balance) { + balanceInOne := balance.Quo(oneAsDec) + C.executionError = ErrBadTransactionParam + errorMsg := fmt.Sprintf( + "insufficient balance of %s in shard %d for the requested transfer of %s", + balanceInOne.String(), C.transactionForRPC.params["from-shard"].(uint32), amount.String(), + ) + C.transactionErrors = append(C.transactionErrors, &Error{ + ErrMessage: &errorMsg, + TimestampOfRejection: time.Now().Unix(), + }) + return + } } C.transactionForRPC.params["transfer-amount"] = amountInAtto } @@ -304,4 +307,10 @@ func (C *EthController) ExecuteEthTransaction( return C.executionError } -// TODO: add logic to create staking transactions in the SDK. +func (C *EthController) ExecuteRawTransaction(txn string) error { + C.transactionForRPC.signature = &txn + + C.sendSignedTx() + C.txConfirmation() + return C.executionError +} diff --git a/pkg/transaction/util.go b/pkg/transaction/util.go new file mode 100644 index 0000000..6100de7 --- /dev/null +++ b/pkg/transaction/util.go @@ -0,0 +1,17 @@ +package transaction + +import ( + "encoding/hex" + "fmt" + "strings" +) + +func StringToByte(dataStr string) ([]byte, error) { + if len(dataStr) == 0 { + return []byte{}, nil + } + if !strings.HasPrefix(dataStr, "0x") { + return nil, fmt.Errorf("invalid data literal: %q", dataStr) + } + return hex.DecodeString(dataStr[2:]) +}