Implement eth_sendTransaction (#104)
* Set up framework for sending transaction with correct args and nonce mutex locking * Set up printing ethereum address through emintkeys and getting chainid from flags * Implemented defaults for eth_sendTransaction * Fix bug with no data provided * Updated comments and error, as well as RLP encoded tx bytes for return instead of amino encoded
This commit is contained in:
parent
2ca42cc155
commit
28aaba0695
14
keys/show.go
14
keys/show.go
@ -19,6 +19,8 @@ import (
|
|||||||
const (
|
const (
|
||||||
// FlagAddress is the flag for the user's address on the command line.
|
// FlagAddress is the flag for the user's address on the command line.
|
||||||
FlagAddress = "address"
|
FlagAddress = "address"
|
||||||
|
// FlagAddress is the flag for the user's address on the command line.
|
||||||
|
FlagETHAddress = "ethwallet"
|
||||||
// FlagPublicKey represents the user's public key on the command line.
|
// FlagPublicKey represents the user's public key on the command line.
|
||||||
FlagPublicKey = "pubkey"
|
FlagPublicKey = "pubkey"
|
||||||
// FlagBechPrefix defines a desired Bech32 prefix encoding for a key.
|
// FlagBechPrefix defines a desired Bech32 prefix encoding for a key.
|
||||||
@ -38,6 +40,7 @@ func showKeysCmd() *cobra.Command {
|
|||||||
|
|
||||||
cmd.Flags().String(FlagBechPrefix, sdk.PrefixAccount, "The Bech32 prefix encoding for a key (acc|val|cons)")
|
cmd.Flags().String(FlagBechPrefix, sdk.PrefixAccount, "The Bech32 prefix encoding for a key (acc|val|cons)")
|
||||||
cmd.Flags().BoolP(FlagAddress, "a", false, "Output the address only (overrides --output)")
|
cmd.Flags().BoolP(FlagAddress, "a", false, "Output the address only (overrides --output)")
|
||||||
|
cmd.Flags().BoolP(FlagETHAddress, "w", false, "Output the Ethereum address only (overrides --output)")
|
||||||
cmd.Flags().BoolP(FlagPublicKey, "p", false, "Output the public key only (overrides --output)")
|
cmd.Flags().BoolP(FlagPublicKey, "p", false, "Output the public key only (overrides --output)")
|
||||||
cmd.Flags().BoolP(FlagDevice, "d", false, "Output the address in a ledger device")
|
cmd.Flags().BoolP(FlagDevice, "d", false, "Output the address in a ledger device")
|
||||||
cmd.Flags().Bool(flags.FlagIndentResponse, false, "Add indent to JSON response")
|
cmd.Flags().Bool(flags.FlagIndentResponse, false, "Add indent to JSON response")
|
||||||
@ -58,6 +61,7 @@ func runShowCmd(cmd *cobra.Command, args []string) (err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
isShowAddr := viper.GetBool(FlagAddress)
|
isShowAddr := viper.GetBool(FlagAddress)
|
||||||
|
isShowEthAddr := viper.GetBool(FlagETHAddress)
|
||||||
isShowPubKey := viper.GetBool(FlagPublicKey)
|
isShowPubKey := viper.GetBool(FlagPublicKey)
|
||||||
isShowDevice := viper.GetBool(FlagDevice)
|
isShowDevice := viper.GetBool(FlagDevice)
|
||||||
|
|
||||||
@ -67,11 +71,13 @@ func runShowCmd(cmd *cobra.Command, args []string) (err error) {
|
|||||||
isOutputSet = tmp.Changed
|
isOutputSet = tmp.Changed
|
||||||
}
|
}
|
||||||
|
|
||||||
if isShowAddr && isShowPubKey {
|
isShowEitherAddr := isShowAddr || isShowEthAddr
|
||||||
return errors.New("cannot use both --address and --pubkey at once")
|
|
||||||
|
if isShowEitherAddr && isShowPubKey {
|
||||||
|
return errors.New("cannot get address, with --address or --ethwallet, and --pubkey at once")
|
||||||
}
|
}
|
||||||
|
|
||||||
if isOutputSet && (isShowAddr || isShowPubKey) {
|
if isOutputSet && (isShowEitherAddr || isShowPubKey) {
|
||||||
return errors.New("cannot use --output with --address or --pubkey")
|
return errors.New("cannot use --output with --address or --pubkey")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -80,6 +86,8 @@ func runShowCmd(cmd *cobra.Command, args []string) (err error) {
|
|||||||
switch {
|
switch {
|
||||||
case isShowAddr:
|
case isShowAddr:
|
||||||
printKeyAddress(info, keyOutputFunction)
|
printKeyAddress(info, keyOutputFunction)
|
||||||
|
case isShowEthAddr:
|
||||||
|
printKeyEthAddress(info, keyOutputFunction)
|
||||||
case isShowPubKey:
|
case isShowPubKey:
|
||||||
printPubKey(info, keyOutputFunction)
|
printPubKey(info, keyOutputFunction)
|
||||||
default:
|
default:
|
||||||
|
@ -148,6 +148,15 @@ func printKeyAddress(info cosmosKeys.Info, bechKeyOut bechKeyOutFn) {
|
|||||||
fmt.Println(ko.Address)
|
fmt.Println(ko.Address)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func printKeyEthAddress(info cosmosKeys.Info, bechKeyOut bechKeyOutFn) {
|
||||||
|
ko, err := bechKeyOut(info)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println(ko.ETHAddress)
|
||||||
|
}
|
||||||
|
|
||||||
func printPubKey(info cosmosKeys.Info, bechKeyOut bechKeyOutFn) {
|
func printPubKey(info cosmosKeys.Info, bechKeyOut bechKeyOutFn) {
|
||||||
ko, err := bechKeyOut(info)
|
ko, err := bechKeyOut(info)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
38
rpc/addrlock.go
Normal file
38
rpc/addrlock.go
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
package rpc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AddrLocker is a mutex structure used to avoid querying outdated account data
|
||||||
|
type AddrLocker struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
locks map[common.Address]*sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
// lock returns the lock of the given address.
|
||||||
|
func (l *AddrLocker) lock(address common.Address) *sync.Mutex {
|
||||||
|
l.mu.Lock()
|
||||||
|
defer l.mu.Unlock()
|
||||||
|
if l.locks == nil {
|
||||||
|
l.locks = make(map[common.Address]*sync.Mutex)
|
||||||
|
}
|
||||||
|
if _, ok := l.locks[address]; !ok {
|
||||||
|
l.locks[address] = new(sync.Mutex)
|
||||||
|
}
|
||||||
|
return l.locks[address]
|
||||||
|
}
|
||||||
|
|
||||||
|
// LockAddr locks an account's mutex. This is used to prevent another tx getting the
|
||||||
|
// same nonce until the lock is released. The mutex prevents the (an identical nonce) from
|
||||||
|
// being read again during the time that the first transaction is being signed.
|
||||||
|
func (l *AddrLocker) LockAddr(address common.Address) {
|
||||||
|
l.lock(address).Lock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnlockAddr unlocks the mutex of the given account.
|
||||||
|
func (l *AddrLocker) UnlockAddr(address common.Address) {
|
||||||
|
l.lock(address).Unlock()
|
||||||
|
}
|
@ -10,6 +10,7 @@ import (
|
|||||||
|
|
||||||
// GetRPCAPIs returns the list of all APIs
|
// GetRPCAPIs returns the list of all APIs
|
||||||
func GetRPCAPIs(cliCtx context.CLIContext, key emintcrypto.PrivKeySecp256k1) []rpc.API {
|
func GetRPCAPIs(cliCtx context.CLIContext, key emintcrypto.PrivKeySecp256k1) []rpc.API {
|
||||||
|
nonceLock := new(AddrLocker)
|
||||||
return []rpc.API{
|
return []rpc.API{
|
||||||
{
|
{
|
||||||
Namespace: "web3",
|
Namespace: "web3",
|
||||||
@ -20,13 +21,13 @@ func GetRPCAPIs(cliCtx context.CLIContext, key emintcrypto.PrivKeySecp256k1) []r
|
|||||||
{
|
{
|
||||||
Namespace: "eth",
|
Namespace: "eth",
|
||||||
Version: "1.0",
|
Version: "1.0",
|
||||||
Service: NewPublicEthAPI(cliCtx, key),
|
Service: NewPublicEthAPI(cliCtx, nonceLock, key),
|
||||||
Public: true,
|
Public: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Namespace: "personal",
|
Namespace: "personal",
|
||||||
Version: "1.0",
|
Version: "1.0",
|
||||||
Service: NewPersonalEthAPI(cliCtx),
|
Service: NewPersonalEthAPI(cliCtx, nonceLock),
|
||||||
Public: false,
|
Public: false,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
22
rpc/args/send_tx.go
Normal file
22
rpc/args/send_tx.go
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
package args
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/ethereum/go-ethereum/common"
|
||||||
|
"github.com/ethereum/go-ethereum/common/hexutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SendTxArgs represents the arguments to sumbit a new transaction into the transaction pool.
|
||||||
|
// Duplicate struct definition since geth struct is in internal package
|
||||||
|
// Ref: https://github.com/ethereum/go-ethereum/blob/release/1.9/internal/ethapi/api.go#L1346
|
||||||
|
type SendTxArgs struct {
|
||||||
|
From common.Address `json:"from"`
|
||||||
|
To *common.Address `json:"to"`
|
||||||
|
Gas *hexutil.Uint64 `json:"gas"`
|
||||||
|
GasPrice *hexutil.Big `json:"gasPrice"`
|
||||||
|
Value *hexutil.Big `json:"value"`
|
||||||
|
Nonce *hexutil.Uint64 `json:"nonce"`
|
||||||
|
// We accept "data" and "input" for backwards-compatibility reasons. "input" is the
|
||||||
|
// newer name and should be preferred by clients.
|
||||||
|
Data *hexutil.Bytes `json:"data"`
|
||||||
|
Input *hexutil.Bytes `json:"input"`
|
||||||
|
}
|
@ -9,6 +9,7 @@ import (
|
|||||||
authutils "github.com/cosmos/cosmos-sdk/x/auth/client/utils"
|
authutils "github.com/cosmos/cosmos-sdk/x/auth/client/utils"
|
||||||
emintcrypto "github.com/cosmos/ethermint/crypto"
|
emintcrypto "github.com/cosmos/ethermint/crypto"
|
||||||
emintkeys "github.com/cosmos/ethermint/keys"
|
emintkeys "github.com/cosmos/ethermint/keys"
|
||||||
|
"github.com/cosmos/ethermint/rpc/args"
|
||||||
"github.com/cosmos/ethermint/version"
|
"github.com/cosmos/ethermint/version"
|
||||||
"github.com/cosmos/ethermint/x/evm/types"
|
"github.com/cosmos/ethermint/x/evm/types"
|
||||||
|
|
||||||
@ -17,20 +18,26 @@ import (
|
|||||||
"github.com/ethereum/go-ethereum/common/hexutil"
|
"github.com/ethereum/go-ethereum/common/hexutil"
|
||||||
"github.com/ethereum/go-ethereum/rlp"
|
"github.com/ethereum/go-ethereum/rlp"
|
||||||
"github.com/ethereum/go-ethereum/rpc"
|
"github.com/ethereum/go-ethereum/rpc"
|
||||||
"github.com/ethereum/go-ethereum/signer/core"
|
|
||||||
|
"github.com/cosmos/cosmos-sdk/client/flags"
|
||||||
|
"github.com/spf13/viper"
|
||||||
)
|
)
|
||||||
|
|
||||||
// PublicEthAPI is the eth_ prefixed set of APIs in the Web3 JSON-RPC spec.
|
// PublicEthAPI is the eth_ prefixed set of APIs in the Web3 JSON-RPC spec.
|
||||||
type PublicEthAPI struct {
|
type PublicEthAPI struct {
|
||||||
cliCtx context.CLIContext
|
cliCtx context.CLIContext
|
||||||
key emintcrypto.PrivKeySecp256k1
|
key emintcrypto.PrivKeySecp256k1
|
||||||
|
nonceLock *AddrLocker
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewPublicEthAPI creates an instance of the public ETH Web3 API.
|
// NewPublicEthAPI creates an instance of the public ETH Web3 API.
|
||||||
func NewPublicEthAPI(cliCtx context.CLIContext, key emintcrypto.PrivKeySecp256k1) *PublicEthAPI {
|
func NewPublicEthAPI(cliCtx context.CLIContext, nonceLock *AddrLocker,
|
||||||
|
key emintcrypto.PrivKeySecp256k1) *PublicEthAPI {
|
||||||
|
|
||||||
return &PublicEthAPI{
|
return &PublicEthAPI{
|
||||||
cliCtx: cliCtx,
|
cliCtx: cliCtx,
|
||||||
key: key,
|
key: key,
|
||||||
|
nonceLock: nonceLock,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -200,9 +207,54 @@ func (e *PublicEthAPI) Sign(address common.Address, data hexutil.Bytes) (hexutil
|
|||||||
}
|
}
|
||||||
|
|
||||||
// SendTransaction sends an Ethereum transaction.
|
// SendTransaction sends an Ethereum transaction.
|
||||||
func (e *PublicEthAPI) SendTransaction(args core.SendTxArgs) common.Hash {
|
func (e *PublicEthAPI) SendTransaction(args args.SendTxArgs) (common.Hash, error) {
|
||||||
var h common.Hash
|
// TODO: Change this functionality to find an unlocked account by address
|
||||||
return h
|
if e.key == nil || !bytes.Equal(e.key.PubKey().Address().Bytes(), args.From.Bytes()) {
|
||||||
|
return common.Hash{}, keystore.ErrLocked
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mutex lock the address' nonce to avoid assigning it to multiple requests
|
||||||
|
if args.Nonce == nil {
|
||||||
|
e.nonceLock.LockAddr(args.From)
|
||||||
|
defer e.nonceLock.UnlockAddr(args.From)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assemble transaction from fields
|
||||||
|
tx, err := types.GenerateFromArgs(args, e.cliCtx)
|
||||||
|
if err != nil {
|
||||||
|
return common.Hash{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChainID must be set as flag to send transaction
|
||||||
|
chainID := viper.GetString(flags.FlagChainID)
|
||||||
|
// parse the chainID from a string to a base-10 integer
|
||||||
|
intChainID, ok := new(big.Int).SetString(chainID, 10)
|
||||||
|
if !ok {
|
||||||
|
return common.Hash{}, fmt.Errorf(
|
||||||
|
fmt.Sprintf("Invalid chainID: %s, must be integer format", chainID))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sign transaction
|
||||||
|
tx.Sign(intChainID, e.key.ToECDSA())
|
||||||
|
|
||||||
|
// Encode transaction by default Tx encoder
|
||||||
|
txEncoder := authutils.GetTxEncoder(e.cliCtx.Codec)
|
||||||
|
txBytes, err := txEncoder(tx)
|
||||||
|
if err != nil {
|
||||||
|
return common.Hash{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Broadcast transaction
|
||||||
|
res, err := e.cliCtx.BroadcastTx(txBytes)
|
||||||
|
// If error is encountered on the node, the broadcast will not return an error
|
||||||
|
// TODO: Remove res log
|
||||||
|
fmt.Println(res.RawLog)
|
||||||
|
if err != nil {
|
||||||
|
return common.Hash{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return RLP encoded bytes
|
||||||
|
return tx.Hash(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// SendRawTransaction send a raw Ethereum transaction.
|
// SendRawTransaction send a raw Ethereum transaction.
|
||||||
@ -225,12 +277,14 @@ func (e *PublicEthAPI) SendRawTransaction(data hexutil.Bytes) (common.Hash, erro
|
|||||||
// TODO: Possibly log the contract creation address (if recipient address is nil) or tx data
|
// TODO: Possibly log the contract creation address (if recipient address is nil) or tx data
|
||||||
res, err := e.cliCtx.BroadcastTx(txBytes)
|
res, err := e.cliCtx.BroadcastTx(txBytes)
|
||||||
// If error is encountered on the node, the broadcast will not return an error
|
// If error is encountered on the node, the broadcast will not return an error
|
||||||
|
// TODO: Remove res log
|
||||||
fmt.Println(res.RawLog)
|
fmt.Println(res.RawLog)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return common.Hash{}, err
|
return common.Hash{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return common.HexToHash(res.TxHash), nil
|
// Return RLP encoded bytes
|
||||||
|
return tx.Hash(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// CallArgs represents arguments to a smart contract call as provided by RPC clients.
|
// CallArgs represents arguments to a smart contract call as provided by RPC clients.
|
||||||
|
@ -10,13 +10,15 @@ import (
|
|||||||
|
|
||||||
// PersonalEthAPI is the eth_ prefixed set of APIs in the Web3 JSON-RPC spec.
|
// PersonalEthAPI is the eth_ prefixed set of APIs in the Web3 JSON-RPC spec.
|
||||||
type PersonalEthAPI struct {
|
type PersonalEthAPI struct {
|
||||||
cliCtx sdkcontext.CLIContext
|
cliCtx sdkcontext.CLIContext
|
||||||
|
nonceLock *AddrLocker
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewPersonalEthAPI creates an instance of the public ETH Web3 API.
|
// NewPersonalEthAPI creates an instance of the public ETH Web3 API.
|
||||||
func NewPersonalEthAPI(cliCtx sdkcontext.CLIContext) *PersonalEthAPI {
|
func NewPersonalEthAPI(cliCtx sdkcontext.CLIContext, nonceLock *AddrLocker) *PersonalEthAPI {
|
||||||
return &PersonalEthAPI{
|
return &PersonalEthAPI{
|
||||||
cliCtx: cliCtx,
|
cliCtx: cliCtx,
|
||||||
|
nonceLock: nonceLock,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package types
|
package types
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"crypto/ecdsa"
|
"crypto/ecdsa"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
@ -10,8 +11,11 @@ import (
|
|||||||
|
|
||||||
"github.com/cosmos/cosmos-sdk/codec"
|
"github.com/cosmos/cosmos-sdk/codec"
|
||||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||||
|
authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
|
||||||
|
"github.com/cosmos/ethermint/rpc/args"
|
||||||
"github.com/cosmos/ethermint/types"
|
"github.com/cosmos/ethermint/types"
|
||||||
|
|
||||||
|
"github.com/cosmos/cosmos-sdk/client/context"
|
||||||
ethcmn "github.com/ethereum/go-ethereum/common"
|
ethcmn "github.com/ethereum/go-ethereum/common"
|
||||||
ethtypes "github.com/ethereum/go-ethereum/core/types"
|
ethtypes "github.com/ethereum/go-ethereum/core/types"
|
||||||
ethcrypto "github.com/ethereum/go-ethereum/crypto"
|
ethcrypto "github.com/ethereum/go-ethereum/crypto"
|
||||||
@ -355,3 +359,60 @@ func recoverEthSig(R, S, Vb *big.Int, sigHash ethcmn.Hash) (ethcmn.Address, erro
|
|||||||
|
|
||||||
return addr, nil
|
return addr, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PopulateFromArgs populates tx message with args (used in RPC API)
|
||||||
|
func GenerateFromArgs(args args.SendTxArgs, ctx context.CLIContext) (msg *EthereumTxMsg, err error) {
|
||||||
|
var nonce uint64
|
||||||
|
|
||||||
|
var gasLimit uint64
|
||||||
|
|
||||||
|
amount := (*big.Int)(args.Value)
|
||||||
|
|
||||||
|
gasPrice := (*big.Int)(args.GasPrice)
|
||||||
|
|
||||||
|
if args.GasPrice == nil {
|
||||||
|
// Set default gas price
|
||||||
|
// TODO: Change to min gas price from context once available through server/daemon
|
||||||
|
gasPrice = big.NewInt(20)
|
||||||
|
}
|
||||||
|
|
||||||
|
if args.Nonce == nil {
|
||||||
|
// Get nonce (sequence) from account
|
||||||
|
from := sdk.AccAddress(args.From.Bytes())
|
||||||
|
_, nonce, err = authtypes.NewAccountRetriever(ctx).GetAccountNumberSequence(from)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
nonce = (uint64)(*args.Nonce)
|
||||||
|
}
|
||||||
|
|
||||||
|
if args.Data != nil && args.Input != nil && !bytes.Equal(*args.Data, *args.Input) {
|
||||||
|
return nil, fmt.Errorf(`both "data" and "input" are set and not equal. Please use "input" to pass transaction call data`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sets input to either Input or Data, if both are set and not equal error above returns
|
||||||
|
var input []byte
|
||||||
|
if args.Input != nil {
|
||||||
|
input = *args.Input
|
||||||
|
} else if args.Data != nil {
|
||||||
|
input = *args.Data
|
||||||
|
}
|
||||||
|
|
||||||
|
if args.To == nil {
|
||||||
|
// Contract creation
|
||||||
|
if len(input) == 0 {
|
||||||
|
return nil, fmt.Errorf("contract creation without any data provided")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if args.Gas == nil {
|
||||||
|
// Estimate the gas usage if necessary.
|
||||||
|
// TODO: Set gas based on estimate when simulating txs are setup
|
||||||
|
gasLimit = 22000
|
||||||
|
} else {
|
||||||
|
gasLimit = (uint64)(*args.Gas)
|
||||||
|
}
|
||||||
|
|
||||||
|
return newEthereumTxMsg(nonce, args.To, amount, gasLimit, gasPrice, input), nil
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user