Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Wallet APIs for local development #335

Merged
merged 8 commits into from
Jul 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,4 @@ start:
.PHONY: start-local
start-local:
rm -rf db/
go run cmd/main/main.go --flow-network-id=flow-emulator --coinbase=FACF71692421039876a5BB4F10EF7A439D8ef61E --coa-address=f8d6e0586b0a20c7 --coa-key=2619878f0e2ff438d17835c2a4561cb87b4d24d72d12ec34569acd0dd4af7c21 --coa-resource-create=true --gas-price=0 --log-writer=console
go run cmd/main/main.go --flow-network-id=flow-emulator --coinbase=FACF71692421039876a5BB4F10EF7A439D8ef61E --coa-address=f8d6e0586b0a20c7 --coa-key=2619878f0e2ff438d17835c2a4561cb87b4d24d72d12ec34569acd0dd4af7c21 --wallet-api-key=2619878f0e2ff438d17835c2a4561cb87b4d24d72d12ec34569acd0dd4af7c21 --coa-resource-create=true --gas-price=0 --log-writer=console
48 changes: 8 additions & 40 deletions api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ func SupportedAPIs(
streamAPI *StreamAPI,
pullAPI *PullAPI,
debugAPI *DebugAPI,
walletAPI *WalletAPI,
config *config.Config,
) []rpc.API {
apis := []rpc.API{{
Expand Down Expand Up @@ -63,6 +64,13 @@ func SupportedAPIs(
})
}

if walletAPI != nil {
apis = append(apis, rpc.API{
Namespace: "eth",
Service: walletAPI,
})
}

return apis
}

Expand Down Expand Up @@ -962,46 +970,6 @@ This is because a decision to not support this API was made either because we do
ever or we don't support it at this phase.
*/

// Accounts returns the collection of accounts this node manages.
func (b *BlockChainAPI) Accounts() []common.Address {
return []common.Address{}
}

// Sign calculates an ECDSA signature for:
// keccak256("\x19Ethereum Signed Message:\n" + len(message) + message).
//
// Note, the produced signature conforms to the secp256k1 curve R, S and V values,
// where the V value will be 27 or 28 for legacy reasons.
//
// The account associated with addr must be unlocked.
//
// https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_sign
func (b *BlockChainAPI) Sign(
addr common.Address,
data hexutil.Bytes,
) (hexutil.Bytes, error) {
return nil, errs.ErrNotSupported
}

// SignTransaction will sign the given transaction with the from account.
// The node needs to have the private key of the account corresponding with
// the given from address and it needs to be unlocked.
func (b *BlockChainAPI) SignTransaction(
ctx context.Context,
args TransactionArgs,
) (*SignTransactionResult, error) {
return nil, errs.ErrNotSupported
}

// SendTransaction creates a transaction for the given argument, sign it
// and submit it to the transaction pool.
func (b *BlockChainAPI) SendTransaction(
ctx context.Context,
args TransactionArgs,
) (common.Hash, error) {
return common.Hash{}, errs.ErrNotSupported
}

// GetProof returns the Merkle-proof for a given account and optionally some storage keys.
func (b *BlockChainAPI) GetProof(
ctx context.Context,
Expand Down
138 changes: 138 additions & 0 deletions api/wallet.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
package api

import (
"context"
"errors"
"fmt"

evmEmulator "github.com/onflow/flow-go/fvm/evm/emulator"
"github.com/onflow/go-ethereum/accounts"
"github.com/onflow/go-ethereum/common"
"github.com/onflow/go-ethereum/common/hexutil"
"github.com/onflow/go-ethereum/core/types"
"github.com/onflow/go-ethereum/crypto"
"github.com/onflow/go-ethereum/rpc"

"github.com/onflow/flow-evm-gateway/config"
)

type WalletAPI struct {
net *BlockChainAPI
config *config.Config
}

func NewWalletAPI(config *config.Config, net *BlockChainAPI) *WalletAPI {
return &WalletAPI{
net: net,
config: config,
}
}

// Accounts returns the collection of accounts this node manages.
func (w *WalletAPI) Accounts() ([]common.Address, error) {
return []common.Address{
crypto.PubkeyToAddress(w.config.WalletKey.PublicKey),
}, nil
}
Comment on lines +31 to +36
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add error handling for unset WalletKey.

The Accounts function should handle the case where WalletKey is not set to avoid potential runtime errors.

func (w *WalletAPI) Accounts() ([]common.Address, error) {
+  if w.config.WalletKey == nil {
+    return nil, fmt.Errorf("wallet key is not set")
+  }
  return []common.Address{
    crypto.PubkeyToAddress(w.config.WalletKey.PublicKey),
  }, nil
}
Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Accounts returns the collection of accounts this node manages.
func (w *WalletAPI) Accounts() ([]common.Address, error) {
return []common.Address{
crypto.PubkeyToAddress(w.config.WalletKey.PublicKey),
}, nil
}
func (w *WalletAPI) Accounts() ([]common.Address, error) {
if w.config.WalletKey == nil {
return nil, fmt.Errorf("wallet key is not set")
}
return []common.Address{
crypto.PubkeyToAddress(w.config.WalletKey.PublicKey),
}, nil
}


// Sign calculates an ECDSA signature for:
// keccak256("\x19Ethereum Signed Message:\n" + len(message) + message).
//
// Note, the produced signature conforms to the secp256k1 curve R, S and V values,
// where the V value will be 27 or 28 for legacy reasons.
//
// The account associated with addr must be unlocked.
//
// https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_sign
func (w *WalletAPI) Sign(
addr common.Address,
data hexutil.Bytes,
) (hexutil.Bytes, error) {
// Transform the given message to the following format:
// keccak256("\x19Ethereum Signed Message:\n"${message length}${message})
hash := accounts.TextHash(data)
// Sign the hash using plain ECDSA operations
signature, err := crypto.Sign(hash, w.config.WalletKey)
if err == nil {
// Transform V from 0/1 to 27/28 according to the yellow paper
signature[64] += 27
}

return signature, err
}

// SignTransaction will sign the given transaction with the from account.
// The node needs to have the private key of the account corresponding with
// the given from address and it needs to be unlocked.
func (w *WalletAPI) SignTransaction(
ctx context.Context,
args TransactionArgs,
) (*SignTransactionResult, error) {
if args.Gas == nil {
return nil, errors.New("gas not specified")
}
if args.GasPrice == nil && (args.MaxPriorityFeePerGas == nil || args.MaxFeePerGas == nil) {
return nil, errors.New("missing gasPrice or maxFeePerGas/maxPriorityFeePerGas")
}

accounts, err := w.Accounts()
if err != nil {
return nil, err
}
from := accounts[0]

nonce := uint64(0)
if args.Nonce != nil {
nonce = uint64(*args.Nonce)
} else {
num := rpc.BlockNumberOrHashWithNumber(rpc.LatestBlockNumber)
n, err := w.net.GetTransactionCount(ctx, from, &num)
if err != nil {
return nil, err
}
nonce = uint64(*n)
}

var data []byte
if args.Data != nil {
data = *args.Data
}

tx := types.NewTx(&types.LegacyTx{
Nonce: nonce,
To: args.To,
Value: args.Value.ToInt(),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here, regarding:

  • args.Value
  • args.Gas
  • args.GasPrice
    We should check whether they are set.

Gas: uint64(*args.Gas),
GasPrice: args.GasPrice.ToInt(),
Data: data,
})

signed, err := types.SignTx(tx, evmEmulator.GetDefaultSigner(), w.config.WalletKey)
Copy link
Collaborator

@m-Peter m-Peter Jul 8, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Technically, I think we should be signing with the address in args.From, but since we have one PrivateKey in the config, this is good enough for development.

if err != nil {
return nil, fmt.Errorf("error signing EVM transaction: %w", err)
}

raw, err := signed.MarshalBinary()
if err != nil {
return nil, err
}

return &SignTransactionResult{
Raw: raw,
Tx: tx,
}, nil
}

// SendTransaction creates a transaction for the given argument, sign it
// and submit it to the transaction pool.
func (w *WalletAPI) SendTransaction(
ctx context.Context,
args TransactionArgs,
) (common.Hash, error) {
signed, err := w.SignTransaction(ctx, args)
if err != nil {
return common.Hash{}, err
}

return w.net.SendRawTransaction(ctx, signed.Raw)
}
6 changes: 6 additions & 0 deletions bootstrap/bootstrap.go
Original file line number Diff line number Diff line change
Expand Up @@ -344,11 +344,17 @@ func startServer(
debugAPI = api.NewDebugAPI(trace, blocks, logger)
}

var walletAPI *api.WalletAPI
if cfg.WalletEnabled {
walletAPI = api.NewWalletAPI(cfg, blockchainAPI)
}
Comment on lines +347 to +350
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add error handling for walletAPI initialization.

The Start function should handle potential errors during walletAPI initialization.

if cfg.WalletEnabled {
  walletAPI = api.NewWalletAPI(cfg, blockchainAPI)
+  if walletAPI == nil {
+    return fmt.Errorf("failed to initialize wallet API")
+  }
}
Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
var walletAPI *api.WalletAPI
if cfg.WalletEnabled {
walletAPI = api.NewWalletAPI(cfg, blockchainAPI)
}
var walletAPI *api.WalletAPI
if cfg.WalletEnabled {
walletAPI = api.NewWalletAPI(cfg, blockchainAPI)
if walletAPI == nil {
return fmt.Errorf("failed to initialize wallet API")
}
}


supportedAPIs := api.SupportedAPIs(
blockchainAPI,
streamAPI,
pullAPI,
debugAPI,
walletAPI,
cfg,
)

Expand Down
45 changes: 41 additions & 4 deletions config/config.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package config

import (
"crypto/ecdsa"
"flag"
"fmt"
"io"
Expand All @@ -16,6 +17,7 @@ import (
"github.com/onflow/flow-go/fvm/evm/types"
flowGo "github.com/onflow/flow-go/model/flow"
"github.com/onflow/go-ethereum/common"
gethCrypto "github.com/onflow/go-ethereum/crypto"
"github.com/rs/zerolog"
)

Expand Down Expand Up @@ -85,13 +87,37 @@ type Config struct {
TracesBucketName string
// TracesEnabled sets whether the node is supporting transaction traces.
TracesEnabled bool
// WalletEnabled sets whether wallet APIs are enabled
WalletEnabled bool
// WalletKey used for signing transactions
WalletKey *ecdsa.PrivateKey
}

func FromFlags() (*Config, error) {
cfg := &Config{}
var evmNetwork, coinbase, gas, coa, key, keysPath, flowNetwork, logLevel, logWriter, filterExpiry, accessSporkHosts, cloudKMSKeys, cloudKMSProjectID, cloudKMSLocationID, cloudKMSKeyRingID string
var streamTimeout int
var initHeight, forceStartHeight uint64
var (
evmNetwork,
coinbase,
gas,
coa,
key,
keysPath,
flowNetwork,
logLevel,
logWriter,
filterExpiry,
accessSporkHosts,
cloudKMSKeys,
cloudKMSProjectID,
cloudKMSLocationID,
cloudKMSKeyRingID,
walletKey string

streamTimeout int

initHeight,
forceStartHeight uint64
)

// parse from flags
flag.StringVar(&cfg.DatabaseDir, "database-dir", "./db", "Path to the directory for the database")
Expand All @@ -116,13 +142,14 @@ func FromFlags() (*Config, error) {
flag.StringVar(&cfg.AddressHeader, "address-header", "", "Address header that contains the client IP, this is useful when the server is behind a proxy that sets the source IP of the client. Leave empty if no proxy is used.")
flag.Uint64Var(&cfg.HeartbeatInterval, "heartbeat-interval", 100, "Heartbeat interval for AN event subscription")
flag.IntVar(&streamTimeout, "stream-timeout", 3, "Defines the timeout in seconds the server waits for the event to be sent to the client")
flag.Uint64Var(&forceStartHeight, "force-start-height", 0, "Force set starting Cadence height. This should only be used locally or for testing, never in production.")
flag.Uint64Var(&forceStartHeight, "force-start-height", 0, "Force set starting Cadence height. WARNING: This should only be used locally or for testing, never in production.")
flag.StringVar(&filterExpiry, "filter-expiry", "5m", "Filter defines the time it takes for an idle filter to expire")
flag.StringVar(&cfg.TracesBucketName, "traces-gcp-bucket", "", "GCP bucket name where transaction traces are stored")
flag.StringVar(&cloudKMSProjectID, "coa-cloud-kms-project-id", "", "The project ID containing the KMS keys, e.g. 'flow-evm-gateway'")
flag.StringVar(&cloudKMSLocationID, "coa-cloud-kms-location-id", "", "The location ID where the key ring is grouped into, e.g. 'global'")
flag.StringVar(&cloudKMSKeyRingID, "coa-cloud-kms-key-ring-id", "", "The key ring ID where the KMS keys exist, e.g. 'tx-signing'")
flag.StringVar(&cloudKMSKeys, "coa-cloud-kms-keys", "", `Names of the KMS keys and their versions as a comma separated list, e.g. "gw-key-6@1,gw-key-7@1,gw-key-8@1"`)
flag.StringVar(&walletKey, "wallet-api-key", "", "ECDSA private key used for wallet APIs. WARNING: This should only be used locally or for testing, never in production.")
flag.Parse()

if coinbase == "" {
Expand Down Expand Up @@ -262,6 +289,16 @@ func FromFlags() (*Config, error) {

cfg.TracesEnabled = cfg.TracesBucketName != ""

if walletKey != "" {
var k, err = gethCrypto.HexToECDSA(walletKey)
if err != nil {
return nil, fmt.Errorf("wrong private key for wallet API: %w", err)
}

cfg.WalletKey = k
cfg.WalletEnabled = true
}
Comment on lines +292 to +300
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add validation for WalletKey field.

The FromFlags function should validate the WalletKey field to ensure it is properly set.

if walletKey != "" {
  var k, err = gethCrypto.HexToECDSA(walletKey)
  if err != nil {
    return nil, fmt.Errorf("wrong private key for wallet API: %w", err)
  }
  cfg.WalletKey = k
  cfg.WalletEnabled = true
+} else {
+  cfg.WalletEnabled = false
}
Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if walletKey != "" {
var k, err = gethCrypto.HexToECDSA(walletKey)
if err != nil {
return nil, fmt.Errorf("wrong private key for wallet API: %w", err)
}
cfg.WalletKey = k
cfg.WalletEnabled = true
}
if walletKey != "" {
var k, err = gethCrypto.HexToECDSA(walletKey)
if err != nil {
return nil, fmt.Errorf("wrong private key for wallet API: %w", err)
}
cfg.WalletKey = k
cfg.WalletEnabled = true
} else {
cfg.WalletEnabled = false
}


// todo validate Config values
return cfg, nil
}
Loading