Skip to content

Commit 97dd328

Browse files
authored
Merge pull request #1 from bcdevtools/feat/send-evm-tx
feat: add new feature send evm legacy transfer tx
2 parents f7e0f02 + dd65361 commit 97dd328

File tree

14 files changed

+300
-88
lines changed

14 files changed

+300
-88
lines changed

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,14 @@ devd query debug_traceTransaction [0xHash] [--tracer callTracer] [--rpc http://l
7171
# devd q trace 0xHash --tracer callTracer
7272
```
7373

74+
### Tx tools
75+
76+
#### Send EVM transaction
77+
78+
```bash
79+
devd tx send [to] [amount] [--rpc http://localhost:8545]
80+
```
81+
7482
### Convert tools
7583

7684
#### Convert address between different formats

cmd/query/balance.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ func GetQueryBalanceCommand() *cobra.Command {
1818
Short: "Get ERC-20 token information. If account address is provided, it will query the balance of the account (bech32 is accepted).",
1919
Args: cobra.MinimumNArgs(1),
2020
Run: func(cmd *cobra.Command, args []string) {
21-
evmAddrs, err := getEvmAddressFromAnyFormatAddress(args...)
21+
evmAddrs, err := utils.GetEvmAddressFromAnyFormatAddress(args...)
2222
if err != nil {
2323
utils.PrintlnStdErr("ERR:", err)
2424
return

cmd/query/debug_traceTransaction.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ func GetQueryTraceTxCommand() *cobra.Command {
4242
params = append(params, types.NewJsonRpcRawQueryParam(fmt.Sprintf(`{"tracer":"%s"}`, tracer)))
4343
}
4444

45-
bz, err := doQuery(
45+
bz, err := types.DoEvmQuery(
4646
rpc,
4747
types.NewJsonRpcQueryBuilder(
4848
"debug_traceTransaction",

cmd/query/erc20.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ func GetQueryErc20Command() *cobra.Command {
1818
Run: func(cmd *cobra.Command, args []string) {
1919
ethClient8545, _ := mustGetEthClient(cmd, true)
2020

21-
evmAddrs, err := getEvmAddressFromAnyFormatAddress(args...)
21+
evmAddrs, err := utils.GetEvmAddressFromAnyFormatAddress(args...)
2222
utils.ExitOnErr(err, "failed to get evm address from input")
2323

2424
contextHeight := readContextHeightFromFlag(cmd)

cmd/query/eth_getBlockByNumber.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ func GetQueryBlockCommand() *cobra.Command {
5656
paramBlockNumber = types.NewJsonRpcInt64QueryParam(blockNumber.Int64())
5757
}
5858

59-
bz, err := doQuery(
59+
bz, err := types.DoEvmQuery(
6060
rpc,
6161
types.NewJsonRpcQueryBuilder(
6262
"eth_getBlockByNumber",

cmd/query/evm.go

Lines changed: 0 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -1,100 +1,17 @@
11
package query
22

33
import (
4-
"bytes"
54
"context"
6-
"encoding/hex"
75
"fmt"
8-
"github.com/bcdevtools/devd/v2/cmd/types"
96
"github.com/bcdevtools/devd/v2/cmd/utils"
107
"github.com/bcdevtools/devd/v2/constants"
11-
sdk "github.com/cosmos/cosmos-sdk/types"
12-
"github.com/ethereum/go-ethereum/common"
138
"github.com/ethereum/go-ethereum/ethclient"
14-
"github.com/pkg/errors"
159
"github.com/spf13/cobra"
16-
"io"
1710
"math/big"
18-
"net/http"
1911
"os"
20-
"regexp"
2112
"strings"
22-
"time"
2313
)
2414

25-
const generalQueryTimeout = 3 * time.Second
26-
27-
func doQuery(host string, qb types.JsonRpcQueryBuilder, optionalTimeout time.Duration) ([]byte, error) {
28-
var timeout = optionalTimeout
29-
if optionalTimeout == 0 {
30-
timeout = generalQueryTimeout
31-
}
32-
if timeout < time.Second {
33-
timeout = time.Second
34-
}
35-
36-
httpClient := http.Client{
37-
Timeout: timeout,
38-
}
39-
40-
fmt.Println("Querying", host, strings.ReplaceAll(strings.ReplaceAll(qb.String(), "\n", " "), " ", ""))
41-
42-
resp, err := httpClient.Post(host, "application/json", bytes.NewBuffer([]byte(qb.String())))
43-
if err != nil {
44-
return nil, err
45-
}
46-
47-
if resp.StatusCode != http.StatusOK {
48-
return nil, fmt.Errorf("non-OK status code: %d", resp.StatusCode)
49-
}
50-
51-
defer func() {
52-
_ = resp.Body.Close()
53-
}()
54-
55-
bz, err := io.ReadAll(resp.Body)
56-
if err != nil {
57-
return nil, errors.Wrap(err, "failed to read response body")
58-
}
59-
60-
return bz, nil
61-
}
62-
63-
func getEvmAddressFromAnyFormatAddress(addrs ...string) (evmAddrs []common.Address, err error) {
64-
for _, addr := range addrs {
65-
normalizedAddr := strings.ToLower(addr)
66-
67-
if regexp.MustCompile(`^(0x)?[a-f\d]{40}$`).MatchString(normalizedAddr) {
68-
evmAddrs = append(evmAddrs, common.HexToAddress(normalizedAddr))
69-
} else if regexp.MustCompile(`^(0x)?[a-f\d]{64}$`).MatchString(normalizedAddr) {
70-
err = fmt.Errorf("ERR: invalid address format: %s", normalizedAddr)
71-
return
72-
} else { // bech32
73-
spl := strings.Split(normalizedAddr, "1")
74-
if len(spl) != 2 || len(spl[0]) < 1 || len(spl[1]) < 1 {
75-
err = fmt.Errorf("ERR: invalid bech32 address: %s", normalizedAddr)
76-
return
77-
}
78-
79-
var bz []byte
80-
bz, err = sdk.GetFromBech32(normalizedAddr, spl[0])
81-
if err != nil {
82-
err = fmt.Errorf("ERR: failed to decode bech32 address %s: %s", normalizedAddr, err)
83-
return
84-
}
85-
86-
if len(bz) != 20 {
87-
err = fmt.Errorf("ERR: bech32 address %s has invalid length, must be 20 bytes, got %s %d bytes", normalizedAddr, hex.EncodeToString(bz), len(bz))
88-
return
89-
}
90-
91-
evmAddrs = append(evmAddrs, common.BytesToAddress(bz))
92-
}
93-
}
94-
95-
return
96-
}
97-
9815
func mustGetEthClient(cmd *cobra.Command, fallbackDeprecatedFlagHost bool) (ethClient8545 *ethclient.Client, rpc string) {
9916
var inputSource string
10017
var err error

cmd/root.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"github.com/bcdevtools/devd/v2/cmd/debug"
88
"github.com/bcdevtools/devd/v2/cmd/hash"
99
"github.com/bcdevtools/devd/v2/cmd/query"
10+
"github.com/bcdevtools/devd/v2/cmd/tx"
1011
"github.com/bcdevtools/devd/v2/cmd/types"
1112
"github.com/bcdevtools/devd/v2/constants"
1213
"github.com/spf13/cobra"
@@ -39,6 +40,7 @@ func init() {
3940
rootCmd.AddCommand(query.Commands())
4041
rootCmd.AddCommand(hash.Commands())
4142
rootCmd.AddCommand(check.Commands())
43+
rootCmd.AddCommand(tx.Commands())
4244

4345
rootCmd.PersistentFlags().Bool("help", false, "show help")
4446
}

cmd/tx/evm.go

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
package tx
2+
3+
import (
4+
"context"
5+
"crypto/ecdsa"
6+
"fmt"
7+
"github.com/bcdevtools/devd/v2/cmd/utils"
8+
"github.com/bcdevtools/devd/v2/constants"
9+
"github.com/ethereum/go-ethereum/common"
10+
"github.com/ethereum/go-ethereum/common/hexutil"
11+
"github.com/ethereum/go-ethereum/crypto"
12+
"github.com/ethereum/go-ethereum/ethclient"
13+
"github.com/spf13/cobra"
14+
"os"
15+
"strings"
16+
)
17+
18+
func mustGetEthClient(cmd *cobra.Command) (ethClient8545 *ethclient.Client, rpc string) {
19+
var inputSource string
20+
var err error
21+
22+
if rpcFromFlagRpc, _ := cmd.Flags().GetString(flagRpc); len(rpcFromFlagRpc) > 0 {
23+
rpc = rpcFromFlagRpc
24+
inputSource = "flag"
25+
} else if rpcFromEnv := os.Getenv(constants.ENV_EVM_RPC); len(rpcFromEnv) > 0 {
26+
rpc = rpcFromEnv
27+
inputSource = "environment variable"
28+
} else {
29+
rpc = constants.DEFAULT_EVM_RPC
30+
inputSource = "default"
31+
}
32+
33+
utils.PrintlnStdErr("INF: Connecting to EVM Json-RPC", rpc, fmt.Sprintf("(from %s)", inputSource))
34+
35+
ethClient8545, err = ethclient.Dial(rpc)
36+
utils.ExitOnErr(err, "failed to connect to EVM Json-RPC")
37+
38+
// pre-flight check to ensure the connection is working
39+
_, err = ethClient8545.BlockNumber(context.Background())
40+
if err != nil && strings.Contains(err.Error(), "connection refused") {
41+
utils.PrintlnStdErr("ERR: failed to connect to EVM Json-RPC, please check the connection and try again.")
42+
utils.PrintfStdErr("ERR: if you are using a custom EVM Json-RPC, please provide it via flag '--%s <your_custom>' or setting environment variable 'export %s=<your_custom>'.\n", flagRpc, constants.ENV_EVM_RPC)
43+
os.Exit(1)
44+
}
45+
46+
return
47+
}
48+
49+
func mustSecretEvmAccount(cmd *cobra.Command) (privKey string, ecdsaPrivateKey *ecdsa.PrivateKey, ecdsaPubKey *ecdsa.PublicKey, account *common.Address) {
50+
var inputSource string
51+
var err error
52+
var ok bool
53+
54+
if secretFromFlag, _ := cmd.Flags().GetString(flagSecretKey); len(secretFromFlag) > 0 {
55+
privKey = secretFromFlag
56+
inputSource = "flag"
57+
} else if secretFromEnv := os.Getenv(constants.ENV_SECRET_KEY); len(secretFromEnv) > 0 {
58+
privKey = secretFromEnv
59+
inputSource = "environment variable"
60+
} else {
61+
utils.PrintlnStdErr("ERR: secret key is required")
62+
utils.PrintfStdErr("ERR: secret key can be set by flag '--%s <your_secret_key>' or environment variable 'export %s=<your_secret_key>'.\n", flagSecretKey, constants.ENV_SECRET_KEY)
63+
os.Exit(1)
64+
}
65+
66+
privKey = strings.TrimPrefix(privKey, "0x")
67+
68+
pKeyBytes, err := hexutil.Decode("0x" + privKey)
69+
if err != nil {
70+
utils.PrintlnStdErr("ERR: failed to decode secret key")
71+
os.Exit(1)
72+
}
73+
74+
ecdsaPrivateKey, err = crypto.ToECDSA(pKeyBytes)
75+
if err != nil {
76+
utils.PrintlnStdErr("ERR: failed to convert secret key to ECDSA")
77+
os.Exit(1)
78+
}
79+
80+
publicKey := ecdsaPrivateKey.Public()
81+
ecdsaPubKey, ok = publicKey.(*ecdsa.PublicKey)
82+
if !ok {
83+
utils.PrintlnStdErr("ERR: failed to convert secret public key to ECDSA")
84+
os.Exit(1)
85+
}
86+
87+
fromAddress := crypto.PubkeyToAddress(*ecdsaPubKey)
88+
account = &fromAddress
89+
90+
fmt.Println("Account Address:", account.Hex(), "(from", inputSource, ")")
91+
92+
return
93+
}

cmd/tx/root.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package tx
2+
3+
import (
4+
"github.com/bcdevtools/devd/v2/constants"
5+
"github.com/spf13/cobra"
6+
)
7+
8+
const (
9+
flagRpc = "rpc"
10+
flagSecretKey = "secret-key"
11+
)
12+
13+
const (
14+
flagEvmRpcDesc = "EVM Json-RPC endpoint, default is " + constants.DEFAULT_EVM_RPC + ", can be set by environment variable " + constants.ENV_EVM_RPC
15+
flagSecretKeyDesc = "Secret key of the account, can be set by environment variable " + constants.ENV_SECRET_KEY
16+
)
17+
18+
// Commands registers a sub-tree of commands
19+
func Commands() *cobra.Command {
20+
cmd := &cobra.Command{
21+
Use: "tx",
22+
Short: "Tx commands",
23+
}
24+
25+
cmd.AddCommand(
26+
GetSendEvmTxCommand(),
27+
)
28+
29+
return cmd
30+
}

cmd/tx/send.go

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package tx
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"encoding/hex"
7+
"fmt"
8+
"github.com/bcdevtools/devd/v2/cmd/utils"
9+
ethtypes "github.com/ethereum/go-ethereum/core/types"
10+
"github.com/spf13/cobra"
11+
"math/big"
12+
)
13+
14+
func GetSendEvmTxCommand() *cobra.Command {
15+
cmd := &cobra.Command{
16+
Use: "send [to] [amount]",
17+
Short: "Send some token to an address via EVM transfer",
18+
Args: cobra.ExactArgs(2),
19+
Run: func(cmd *cobra.Command, args []string) {
20+
ethClient8545, _ := mustGetEthClient(cmd)
21+
22+
evmAddrs, err := utils.GetEvmAddressFromAnyFormatAddress(args[0])
23+
utils.ExitOnErr(err, "failed to get evm address from input")
24+
25+
receiverAddr := evmAddrs[0]
26+
amount, ok := new(big.Int).SetString(args[1], 10)
27+
if !ok {
28+
utils.ExitOnErr(fmt.Errorf("invalid amount %s", args[1]), "failed to parse amount")
29+
}
30+
display, _, _, err := utils.ConvertNumberIntoDisplayWithExponent(amount, 18)
31+
utils.ExitOnErr(err, "failed to convert amount into display with exponent")
32+
33+
_, ecdsaPrivateKey, _, from := mustSecretEvmAccount(cmd)
34+
35+
nonce, err := ethClient8545.NonceAt(context.Background(), *from, nil)
36+
utils.ExitOnErr(err, "failed to get nonce of sender")
37+
38+
chainId, err := ethClient8545.ChainID(context.Background())
39+
utils.ExitOnErr(err, "failed to get chain ID")
40+
41+
txData := ethtypes.LegacyTx{
42+
Nonce: nonce,
43+
GasPrice: big.NewInt(20_000_000_000),
44+
Gas: 21000,
45+
To: &receiverAddr,
46+
Value: amount,
47+
}
48+
tx := ethtypes.NewTx(&txData)
49+
50+
fmt.Println("Send", display, "from", from.Hex(), "to", receiverAddr.Hex())
51+
fmt.Println("EIP155 Chain ID:", chainId.String(), "and nonce", txData.Nonce)
52+
53+
signedTx, err := ethtypes.SignTx(tx, ethtypes.LatestSignerForChainID(chainId), ecdsaPrivateKey)
54+
utils.ExitOnErr(err, "failed to sign tx")
55+
56+
var buf bytes.Buffer
57+
err = signedTx.EncodeRLP(&buf)
58+
utils.ExitOnErr(err, "failed to encode tx")
59+
60+
rawTxRLPHex := hex.EncodeToString(buf.Bytes())
61+
fmt.Printf("RawTx: 0x%s\n", rawTxRLPHex)
62+
63+
err = ethClient8545.SendTransaction(context.Background(), signedTx)
64+
utils.ExitOnErr(err, "failed to send tx")
65+
},
66+
}
67+
68+
cmd.Flags().String(flagRpc, "", flagEvmRpcDesc)
69+
cmd.Flags().String(flagSecretKey, "", flagSecretKeyDesc)
70+
71+
return cmd
72+
}

0 commit comments

Comments
 (0)