Skip to content
This repository was archived by the owner on Nov 30, 2021. It is now read-only.

Commit 28aaba0

Browse files
authored
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
1 parent 2ca42cc commit 28aaba0

File tree

8 files changed

+213
-18
lines changed

8 files changed

+213
-18
lines changed

keys/show.go

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ import (
1919
const (
2020
// FlagAddress is the flag for the user's address on the command line.
2121
FlagAddress = "address"
22+
// FlagAddress is the flag for the user's address on the command line.
23+
FlagETHAddress = "ethwallet"
2224
// FlagPublicKey represents the user's public key on the command line.
2325
FlagPublicKey = "pubkey"
2426
// FlagBechPrefix defines a desired Bech32 prefix encoding for a key.
@@ -38,6 +40,7 @@ func showKeysCmd() *cobra.Command {
3840

3941
cmd.Flags().String(FlagBechPrefix, sdk.PrefixAccount, "The Bech32 prefix encoding for a key (acc|val|cons)")
4042
cmd.Flags().BoolP(FlagAddress, "a", false, "Output the address only (overrides --output)")
43+
cmd.Flags().BoolP(FlagETHAddress, "w", false, "Output the Ethereum address only (overrides --output)")
4144
cmd.Flags().BoolP(FlagPublicKey, "p", false, "Output the public key only (overrides --output)")
4245
cmd.Flags().BoolP(FlagDevice, "d", false, "Output the address in a ledger device")
4346
cmd.Flags().Bool(flags.FlagIndentResponse, false, "Add indent to JSON response")
@@ -58,6 +61,7 @@ func runShowCmd(cmd *cobra.Command, args []string) (err error) {
5861
}
5962

6063
isShowAddr := viper.GetBool(FlagAddress)
64+
isShowEthAddr := viper.GetBool(FlagETHAddress)
6165
isShowPubKey := viper.GetBool(FlagPublicKey)
6266
isShowDevice := viper.GetBool(FlagDevice)
6367

@@ -67,11 +71,13 @@ func runShowCmd(cmd *cobra.Command, args []string) (err error) {
6771
isOutputSet = tmp.Changed
6872
}
6973

70-
if isShowAddr && isShowPubKey {
71-
return errors.New("cannot use both --address and --pubkey at once")
74+
isShowEitherAddr := isShowAddr || isShowEthAddr
75+
76+
if isShowEitherAddr && isShowPubKey {
77+
return errors.New("cannot get address, with --address or --ethwallet, and --pubkey at once")
7278
}
7379

74-
if isOutputSet && (isShowAddr || isShowPubKey) {
80+
if isOutputSet && (isShowEitherAddr || isShowPubKey) {
7581
return errors.New("cannot use --output with --address or --pubkey")
7682
}
7783

@@ -80,6 +86,8 @@ func runShowCmd(cmd *cobra.Command, args []string) (err error) {
8086
switch {
8187
case isShowAddr:
8288
printKeyAddress(info, keyOutputFunction)
89+
case isShowEthAddr:
90+
printKeyEthAddress(info, keyOutputFunction)
8391
case isShowPubKey:
8492
printPubKey(info, keyOutputFunction)
8593
default:

keys/utils.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,15 @@ func printKeyAddress(info cosmosKeys.Info, bechKeyOut bechKeyOutFn) {
148148
fmt.Println(ko.Address)
149149
}
150150

151+
func printKeyEthAddress(info cosmosKeys.Info, bechKeyOut bechKeyOutFn) {
152+
ko, err := bechKeyOut(info)
153+
if err != nil {
154+
panic(err)
155+
}
156+
157+
fmt.Println(ko.ETHAddress)
158+
}
159+
151160
func printPubKey(info cosmosKeys.Info, bechKeyOut bechKeyOutFn) {
152161
ko, err := bechKeyOut(info)
153162
if err != nil {

rpc/addrlock.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package rpc
2+
3+
import (
4+
"sync"
5+
6+
"github.com/ethereum/go-ethereum/common"
7+
)
8+
9+
// AddrLocker is a mutex structure used to avoid querying outdated account data
10+
type AddrLocker struct {
11+
mu sync.Mutex
12+
locks map[common.Address]*sync.Mutex
13+
}
14+
15+
// lock returns the lock of the given address.
16+
func (l *AddrLocker) lock(address common.Address) *sync.Mutex {
17+
l.mu.Lock()
18+
defer l.mu.Unlock()
19+
if l.locks == nil {
20+
l.locks = make(map[common.Address]*sync.Mutex)
21+
}
22+
if _, ok := l.locks[address]; !ok {
23+
l.locks[address] = new(sync.Mutex)
24+
}
25+
return l.locks[address]
26+
}
27+
28+
// LockAddr locks an account's mutex. This is used to prevent another tx getting the
29+
// same nonce until the lock is released. The mutex prevents the (an identical nonce) from
30+
// being read again during the time that the first transaction is being signed.
31+
func (l *AddrLocker) LockAddr(address common.Address) {
32+
l.lock(address).Lock()
33+
}
34+
35+
// UnlockAddr unlocks the mutex of the given account.
36+
func (l *AddrLocker) UnlockAddr(address common.Address) {
37+
l.lock(address).Unlock()
38+
}

rpc/apis.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010

1111
// GetRPCAPIs returns the list of all APIs
1212
func GetRPCAPIs(cliCtx context.CLIContext, key emintcrypto.PrivKeySecp256k1) []rpc.API {
13+
nonceLock := new(AddrLocker)
1314
return []rpc.API{
1415
{
1516
Namespace: "web3",
@@ -20,13 +21,13 @@ func GetRPCAPIs(cliCtx context.CLIContext, key emintcrypto.PrivKeySecp256k1) []r
2021
{
2122
Namespace: "eth",
2223
Version: "1.0",
23-
Service: NewPublicEthAPI(cliCtx, key),
24+
Service: NewPublicEthAPI(cliCtx, nonceLock, key),
2425
Public: true,
2526
},
2627
{
2728
Namespace: "personal",
2829
Version: "1.0",
29-
Service: NewPersonalEthAPI(cliCtx),
30+
Service: NewPersonalEthAPI(cliCtx, nonceLock),
3031
Public: false,
3132
},
3233
}

rpc/args/send_tx.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package args
2+
3+
import (
4+
"github.com/ethereum/go-ethereum/common"
5+
"github.com/ethereum/go-ethereum/common/hexutil"
6+
)
7+
8+
// SendTxArgs represents the arguments to sumbit a new transaction into the transaction pool.
9+
// Duplicate struct definition since geth struct is in internal package
10+
// Ref: https://github.com/ethereum/go-ethereum/blob/release/1.9/internal/ethapi/api.go#L1346
11+
type SendTxArgs struct {
12+
From common.Address `json:"from"`
13+
To *common.Address `json:"to"`
14+
Gas *hexutil.Uint64 `json:"gas"`
15+
GasPrice *hexutil.Big `json:"gasPrice"`
16+
Value *hexutil.Big `json:"value"`
17+
Nonce *hexutil.Uint64 `json:"nonce"`
18+
// We accept "data" and "input" for backwards-compatibility reasons. "input" is the
19+
// newer name and should be preferred by clients.
20+
Data *hexutil.Bytes `json:"data"`
21+
Input *hexutil.Bytes `json:"input"`
22+
}

rpc/eth_api.go

Lines changed: 64 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
authutils "github.com/cosmos/cosmos-sdk/x/auth/client/utils"
1010
emintcrypto "github.com/cosmos/ethermint/crypto"
1111
emintkeys "github.com/cosmos/ethermint/keys"
12+
"github.com/cosmos/ethermint/rpc/args"
1213
"github.com/cosmos/ethermint/version"
1314
"github.com/cosmos/ethermint/x/evm/types"
1415

@@ -17,20 +18,26 @@ import (
1718
"github.com/ethereum/go-ethereum/common/hexutil"
1819
"github.com/ethereum/go-ethereum/rlp"
1920
"github.com/ethereum/go-ethereum/rpc"
20-
"github.com/ethereum/go-ethereum/signer/core"
21+
22+
"github.com/cosmos/cosmos-sdk/client/flags"
23+
"github.com/spf13/viper"
2124
)
2225

2326
// PublicEthAPI is the eth_ prefixed set of APIs in the Web3 JSON-RPC spec.
2427
type PublicEthAPI struct {
25-
cliCtx context.CLIContext
26-
key emintcrypto.PrivKeySecp256k1
28+
cliCtx context.CLIContext
29+
key emintcrypto.PrivKeySecp256k1
30+
nonceLock *AddrLocker
2731
}
2832

2933
// NewPublicEthAPI creates an instance of the public ETH Web3 API.
30-
func NewPublicEthAPI(cliCtx context.CLIContext, key emintcrypto.PrivKeySecp256k1) *PublicEthAPI {
34+
func NewPublicEthAPI(cliCtx context.CLIContext, nonceLock *AddrLocker,
35+
key emintcrypto.PrivKeySecp256k1) *PublicEthAPI {
36+
3137
return &PublicEthAPI{
32-
cliCtx: cliCtx,
33-
key: key,
38+
cliCtx: cliCtx,
39+
key: key,
40+
nonceLock: nonceLock,
3441
}
3542
}
3643

@@ -200,9 +207,54 @@ func (e *PublicEthAPI) Sign(address common.Address, data hexutil.Bytes) (hexutil
200207
}
201208

202209
// SendTransaction sends an Ethereum transaction.
203-
func (e *PublicEthAPI) SendTransaction(args core.SendTxArgs) common.Hash {
204-
var h common.Hash
205-
return h
210+
func (e *PublicEthAPI) SendTransaction(args args.SendTxArgs) (common.Hash, error) {
211+
// TODO: Change this functionality to find an unlocked account by address
212+
if e.key == nil || !bytes.Equal(e.key.PubKey().Address().Bytes(), args.From.Bytes()) {
213+
return common.Hash{}, keystore.ErrLocked
214+
}
215+
216+
// Mutex lock the address' nonce to avoid assigning it to multiple requests
217+
if args.Nonce == nil {
218+
e.nonceLock.LockAddr(args.From)
219+
defer e.nonceLock.UnlockAddr(args.From)
220+
}
221+
222+
// Assemble transaction from fields
223+
tx, err := types.GenerateFromArgs(args, e.cliCtx)
224+
if err != nil {
225+
return common.Hash{}, err
226+
}
227+
228+
// ChainID must be set as flag to send transaction
229+
chainID := viper.GetString(flags.FlagChainID)
230+
// parse the chainID from a string to a base-10 integer
231+
intChainID, ok := new(big.Int).SetString(chainID, 10)
232+
if !ok {
233+
return common.Hash{}, fmt.Errorf(
234+
fmt.Sprintf("Invalid chainID: %s, must be integer format", chainID))
235+
}
236+
237+
// Sign transaction
238+
tx.Sign(intChainID, e.key.ToECDSA())
239+
240+
// Encode transaction by default Tx encoder
241+
txEncoder := authutils.GetTxEncoder(e.cliCtx.Codec)
242+
txBytes, err := txEncoder(tx)
243+
if err != nil {
244+
return common.Hash{}, err
245+
}
246+
247+
// Broadcast transaction
248+
res, err := e.cliCtx.BroadcastTx(txBytes)
249+
// If error is encountered on the node, the broadcast will not return an error
250+
// TODO: Remove res log
251+
fmt.Println(res.RawLog)
252+
if err != nil {
253+
return common.Hash{}, err
254+
}
255+
256+
// Return RLP encoded bytes
257+
return tx.Hash(), nil
206258
}
207259

208260
// SendRawTransaction send a raw Ethereum transaction.
@@ -225,12 +277,14 @@ func (e *PublicEthAPI) SendRawTransaction(data hexutil.Bytes) (common.Hash, erro
225277
// TODO: Possibly log the contract creation address (if recipient address is nil) or tx data
226278
res, err := e.cliCtx.BroadcastTx(txBytes)
227279
// If error is encountered on the node, the broadcast will not return an error
280+
// TODO: Remove res log
228281
fmt.Println(res.RawLog)
229282
if err != nil {
230283
return common.Hash{}, err
231284
}
232285

233-
return common.HexToHash(res.TxHash), nil
286+
// Return RLP encoded bytes
287+
return tx.Hash(), nil
234288
}
235289

236290
// CallArgs represents arguments to a smart contract call as provided by RPC clients.

rpc/personal_api.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,15 @@ import (
1010

1111
// PersonalEthAPI is the eth_ prefixed set of APIs in the Web3 JSON-RPC spec.
1212
type PersonalEthAPI struct {
13-
cliCtx sdkcontext.CLIContext
13+
cliCtx sdkcontext.CLIContext
14+
nonceLock *AddrLocker
1415
}
1516

1617
// NewPersonalEthAPI creates an instance of the public ETH Web3 API.
17-
func NewPersonalEthAPI(cliCtx sdkcontext.CLIContext) *PersonalEthAPI {
18+
func NewPersonalEthAPI(cliCtx sdkcontext.CLIContext, nonceLock *AddrLocker) *PersonalEthAPI {
1819
return &PersonalEthAPI{
19-
cliCtx: cliCtx,
20+
cliCtx: cliCtx,
21+
nonceLock: nonceLock,
2022
}
2123
}
2224

x/evm/types/msg.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package types
22

33
import (
4+
"bytes"
45
"crypto/ecdsa"
56
"errors"
67
"fmt"
@@ -10,8 +11,11 @@ import (
1011

1112
"github.com/cosmos/cosmos-sdk/codec"
1213
sdk "github.com/cosmos/cosmos-sdk/types"
14+
authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
15+
"github.com/cosmos/ethermint/rpc/args"
1316
"github.com/cosmos/ethermint/types"
1417

18+
"github.com/cosmos/cosmos-sdk/client/context"
1519
ethcmn "github.com/ethereum/go-ethereum/common"
1620
ethtypes "github.com/ethereum/go-ethereum/core/types"
1721
ethcrypto "github.com/ethereum/go-ethereum/crypto"
@@ -355,3 +359,60 @@ func recoverEthSig(R, S, Vb *big.Int, sigHash ethcmn.Hash) (ethcmn.Address, erro
355359

356360
return addr, nil
357361
}
362+
363+
// PopulateFromArgs populates tx message with args (used in RPC API)
364+
func GenerateFromArgs(args args.SendTxArgs, ctx context.CLIContext) (msg *EthereumTxMsg, err error) {
365+
var nonce uint64
366+
367+
var gasLimit uint64
368+
369+
amount := (*big.Int)(args.Value)
370+
371+
gasPrice := (*big.Int)(args.GasPrice)
372+
373+
if args.GasPrice == nil {
374+
// Set default gas price
375+
// TODO: Change to min gas price from context once available through server/daemon
376+
gasPrice = big.NewInt(20)
377+
}
378+
379+
if args.Nonce == nil {
380+
// Get nonce (sequence) from account
381+
from := sdk.AccAddress(args.From.Bytes())
382+
_, nonce, err = authtypes.NewAccountRetriever(ctx).GetAccountNumberSequence(from)
383+
if err != nil {
384+
return nil, err
385+
}
386+
} else {
387+
nonce = (uint64)(*args.Nonce)
388+
}
389+
390+
if args.Data != nil && args.Input != nil && !bytes.Equal(*args.Data, *args.Input) {
391+
return nil, fmt.Errorf(`both "data" and "input" are set and not equal. Please use "input" to pass transaction call data`)
392+
}
393+
394+
// Sets input to either Input or Data, if both are set and not equal error above returns
395+
var input []byte
396+
if args.Input != nil {
397+
input = *args.Input
398+
} else if args.Data != nil {
399+
input = *args.Data
400+
}
401+
402+
if args.To == nil {
403+
// Contract creation
404+
if len(input) == 0 {
405+
return nil, fmt.Errorf("contract creation without any data provided")
406+
}
407+
}
408+
409+
if args.Gas == nil {
410+
// Estimate the gas usage if necessary.
411+
// TODO: Set gas based on estimate when simulating txs are setup
412+
gasLimit = 22000
413+
} else {
414+
gasLimit = (uint64)(*args.Gas)
415+
}
416+
417+
return newEthereumTxMsg(nonce, args.To, amount, gasLimit, gasPrice, input), nil
418+
}

0 commit comments

Comments
 (0)