Skip to content

Commit

Permalink
Merge pull request #335 from onflow/gregor/wallet
Browse files Browse the repository at this point in the history
Add Wallet APIs for local development
  • Loading branch information
m-Peter authored Jul 8, 2024
2 parents 5127995 + c2e9016 commit 50a7728
Show file tree
Hide file tree
Showing 5 changed files with 194 additions and 45 deletions.
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
}

// 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(),
Gas: uint64(*args.Gas),
GasPrice: args.GasPrice.ToInt(),
Data: data,
})

signed, err := types.SignTx(tx, evmEmulator.GetDefaultSigner(), w.config.WalletKey)
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)
}

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
}

// todo validate Config values
return cfg, nil
}

0 comments on commit 50a7728

Please sign in to comment.