forked from cerc-io/laconicd-deprecated
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 (
|
||||
// FlagAddress is the flag for the user's address on the command line.
|
||||
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 = "pubkey"
|
||||
// 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().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(FlagDevice, "d", false, "Output the address in a ledger device")
|
||||
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)
|
||||
isShowEthAddr := viper.GetBool(FlagETHAddress)
|
||||
isShowPubKey := viper.GetBool(FlagPublicKey)
|
||||
isShowDevice := viper.GetBool(FlagDevice)
|
||||
|
||||
@ -67,11 +71,13 @@ func runShowCmd(cmd *cobra.Command, args []string) (err error) {
|
||||
isOutputSet = tmp.Changed
|
||||
}
|
||||
|
||||
if isShowAddr && isShowPubKey {
|
||||
return errors.New("cannot use both --address and --pubkey at once")
|
||||
isShowEitherAddr := isShowAddr || isShowEthAddr
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
@ -80,6 +86,8 @@ func runShowCmd(cmd *cobra.Command, args []string) (err error) {
|
||||
switch {
|
||||
case isShowAddr:
|
||||
printKeyAddress(info, keyOutputFunction)
|
||||
case isShowEthAddr:
|
||||
printKeyEthAddress(info, keyOutputFunction)
|
||||
case isShowPubKey:
|
||||
printPubKey(info, keyOutputFunction)
|
||||
default:
|
||||
|
@ -148,6 +148,15 @@ func printKeyAddress(info cosmosKeys.Info, bechKeyOut bechKeyOutFn) {
|
||||
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) {
|
||||
ko, err := bechKeyOut(info)
|
||||
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
|
||||
func GetRPCAPIs(cliCtx context.CLIContext, key emintcrypto.PrivKeySecp256k1) []rpc.API {
|
||||
nonceLock := new(AddrLocker)
|
||||
return []rpc.API{
|
||||
{
|
||||
Namespace: "web3",
|
||||
@ -20,13 +21,13 @@ func GetRPCAPIs(cliCtx context.CLIContext, key emintcrypto.PrivKeySecp256k1) []r
|
||||
{
|
||||
Namespace: "eth",
|
||||
Version: "1.0",
|
||||
Service: NewPublicEthAPI(cliCtx, key),
|
||||
Service: NewPublicEthAPI(cliCtx, nonceLock, key),
|
||||
Public: true,
|
||||
},
|
||||
{
|
||||
Namespace: "personal",
|
||||
Version: "1.0",
|
||||
Service: NewPersonalEthAPI(cliCtx),
|
||||
Service: NewPersonalEthAPI(cliCtx, nonceLock),
|
||||
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"
|
||||
emintcrypto "github.com/cosmos/ethermint/crypto"
|
||||
emintkeys "github.com/cosmos/ethermint/keys"
|
||||
"github.com/cosmos/ethermint/rpc/args"
|
||||
"github.com/cosmos/ethermint/version"
|
||||
"github.com/cosmos/ethermint/x/evm/types"
|
||||
|
||||
@ -17,20 +18,26 @@ import (
|
||||
"github.com/ethereum/go-ethereum/common/hexutil"
|
||||
"github.com/ethereum/go-ethereum/rlp"
|
||||
"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.
|
||||
type PublicEthAPI struct {
|
||||
cliCtx context.CLIContext
|
||||
key emintcrypto.PrivKeySecp256k1
|
||||
nonceLock *AddrLocker
|
||||
}
|
||||
|
||||
// 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{
|
||||
cliCtx: cliCtx,
|
||||
key: key,
|
||||
nonceLock: nonceLock,
|
||||
}
|
||||
}
|
||||
|
||||
@ -200,9 +207,54 @@ func (e *PublicEthAPI) Sign(address common.Address, data hexutil.Bytes) (hexutil
|
||||
}
|
||||
|
||||
// SendTransaction sends an Ethereum transaction.
|
||||
func (e *PublicEthAPI) SendTransaction(args core.SendTxArgs) common.Hash {
|
||||
var h common.Hash
|
||||
return h
|
||||
func (e *PublicEthAPI) SendTransaction(args args.SendTxArgs) (common.Hash, error) {
|
||||
// TODO: Change this functionality to find an unlocked account by address
|
||||
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.
|
||||
@ -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
|
||||
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 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.
|
||||
|
@ -11,12 +11,14 @@ import (
|
||||
// PersonalEthAPI is the eth_ prefixed set of APIs in the Web3 JSON-RPC spec.
|
||||
type PersonalEthAPI struct {
|
||||
cliCtx sdkcontext.CLIContext
|
||||
nonceLock *AddrLocker
|
||||
}
|
||||
|
||||
// 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{
|
||||
cliCtx: cliCtx,
|
||||
nonceLock: nonceLock,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/ecdsa"
|
||||
"errors"
|
||||
"fmt"
|
||||
@ -10,8 +11,11 @@ import (
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/codec"
|
||||
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/cosmos-sdk/client/context"
|
||||
ethcmn "github.com/ethereum/go-ethereum/common"
|
||||
ethtypes "github.com/ethereum/go-ethereum/core/types"
|
||||
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
|
||||
}
|
||||
|
||||
// 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