Skip to content

Commit

Permalink
merge: branch '3620-ethereum-rpc' into 'main'
Browse files Browse the repository at this point in the history
Minimum viable Ethereum RPC [#3620]

Closes #3620

See merge request accumulatenetwork/accumulate!1088
  • Loading branch information
firelizzard18 committed Jul 16, 2024
2 parents 13e9cc4 + e472276 commit ecb11ed
Show file tree
Hide file tree
Showing 15 changed files with 727 additions and 20 deletions.
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ require (
github.com/sergi/go-diff v1.2.0
github.com/ulikunitz/xz v0.5.11
github.com/vektra/mockery/v2 v2.42.3
gitlab.com/accumulatenetwork/core/schema v0.2.1-0.20240711192735-5b3657ff1135
gitlab.com/accumulatenetwork/core/schema v0.2.1-0.20240713035306-1121dbc75e5d
gitlab.com/firelizzard/go-script v0.0.0-20240404234115-d5f0a716003d
go.opentelemetry.io/otel v1.27.0
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.27.0
Expand Down Expand Up @@ -377,6 +377,7 @@ require (
github.com/uudashr/gocognit v1.1.2 // indirect
github.com/yagipy/maintidx v1.0.0 // indirect
github.com/yeya24/promlinter v0.2.0 // indirect
gitlab.com/accumulatenetwork/utils/jsonrpc v0.1.0
gitlab.com/bosi/decorder v0.4.1 // indirect
go.etcd.io/bbolt v1.3.6
go.uber.org/multierr v1.11.0 // indirect
Expand Down
6 changes: 4 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -1170,8 +1170,10 @@ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9dec
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
gitlab.com/accumulatenetwork/core/schema v0.2.1-0.20240711192735-5b3657ff1135 h1:DawYWpRLfW7HeTaN1k4idFrv1B+esoNAHjWbtGRqi80=
gitlab.com/accumulatenetwork/core/schema v0.2.1-0.20240711192735-5b3657ff1135/go.mod h1:FTl7W44SWhDenzAtvKkLu30Cin8DAr249mH4eg7BNLY=
gitlab.com/accumulatenetwork/core/schema v0.2.1-0.20240713035306-1121dbc75e5d h1:6crG3cl/LB7if3Lt3MbE3c8qgTzl/aLvdB+RmGpnDXE=
gitlab.com/accumulatenetwork/core/schema v0.2.1-0.20240713035306-1121dbc75e5d/go.mod h1:FTl7W44SWhDenzAtvKkLu30Cin8DAr249mH4eg7BNLY=
gitlab.com/accumulatenetwork/utils/jsonrpc v0.1.0 h1:t4Akt7LvcROop2bh++KRIhbv+NORJT1m5WWIsiEUIF8=
gitlab.com/accumulatenetwork/utils/jsonrpc v0.1.0/go.mod h1:Kopfqa//YI6CkXkVpHRMOZqODMNFwifB8IvKw+m52no=
gitlab.com/bosi/decorder v0.4.1 h1:VdsdfxhstabyhZovHafFw+9eJ6eU0d2CkFNJcZz/NU4=
gitlab.com/bosi/decorder v0.4.1/go.mod h1:jecSqWUew6Yle1pCr2eLWTensJMmsxHsBwt+PVbkAqA=
gitlab.com/ethan.reesor/vscode-notebooks/go-playbooks v0.0.0-20220417214602-1121b9fae118 h1:UnyYFTz6dWVMBzLUyqHPIQwMrdpiuE+CE7p/5kUfvmk=
Expand Down
96 changes: 96 additions & 0 deletions internal/api/ethereum/service.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
// Copyright 2024 The Accumulate Authors
//
// Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.

package ethimpl

import (
"context"
"math/big"

ethrpc "gitlab.com/accumulatenetwork/accumulate/pkg/api/ethereum"
"gitlab.com/accumulatenetwork/accumulate/pkg/api/v3"
"gitlab.com/accumulatenetwork/accumulate/pkg/errors"
"gitlab.com/accumulatenetwork/accumulate/pkg/types/encoding"
"gitlab.com/accumulatenetwork/accumulate/protocol"
)

type Service struct {
Network api.NetworkService
Query api.Querier
}

var _ ethrpc.Service = (*Service)(nil)

func (s *Service) EthChainId(ctx context.Context) (*ethrpc.Number, error) {
ns, err := s.Network.NetworkStatus(ctx, api.NetworkStatusOptions{Partition: protocol.Directory})
if err != nil {
return nil, err
}
cid := protocol.EthChainID(ns.Network.NetworkName)
return (*ethrpc.Number)(cid), nil
}

func (s *Service) EthBlockNumber(ctx context.Context) (*ethrpc.Number, error) {
// TODO: Is this the right number?
ns, err := s.Network.NetworkStatus(ctx, api.NetworkStatusOptions{Partition: protocol.Directory})
if err != nil {
return nil, err
}
return ethrpc.NewNumber(int64(ns.DirectoryHeight)), nil
}

func (s *Service) EthGasPrice(ctx context.Context) (*ethrpc.Number, error) {
ns, err := s.Network.NetworkStatus(ctx, api.NetworkStatusOptions{Partition: protocol.Directory})
if err != nil {
return nil, err
}
// Instead of the ACME precision, we have to use 18, because Ethereum
// wallets require native tokens to have a precision of 18.
//
// TODO: Verify this is the right result.
v := big.NewInt(1e18 / protocol.CreditPrecision)
v.Div(v, big.NewInt(int64(ns.Oracle.Price)))
return (*ethrpc.Number)(v), nil
}

func (s *Service) EthGetBalance(ctx context.Context, addr ethrpc.Address, block string) (*ethrpc.Number, error) {
u, err := protocol.LiteTokenAddressFromHash(addr[:], "ACME")
if err != nil {
return nil, err
}

var lite *protocol.LiteTokenAccount
_, err = api.Querier2{Querier: s.Query}.QueryAccountAs(ctx, u, nil, &lite)
if err != nil {
if errors.Is(err, errors.NotFound) {
return ethrpc.NewNumber(0), nil
}
return nil, err
}

value := new(big.Int)
value.Set(&lite.Balance)

// Adjust for precision
value.Mul(value, big.NewInt(1e18/protocol.AcmePrecision))

return (*ethrpc.Number)(value), nil
}

func (s *Service) EthGetBlockByNumber(ctx context.Context, block string, expand bool) (*ethrpc.BlockData, error) {
// TODO
return &ethrpc.BlockData{}, nil
}

func (s *Service) AccTypedData(_ context.Context, txn *protocol.Transaction, sig protocol.Signature) (*encoding.EIP712Call, error) {
if txn == nil {
return nil, errors.BadRequest.WithFormat("missing transaction")
}
if sig == nil {
return nil, errors.BadRequest.WithFormat("missing signature")
}
return protocol.NewEIP712Call(txn, sig)
}
16 changes: 15 additions & 1 deletion internal/node/http/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,13 @@ import (
"github.com/julienschmidt/httprouter"
"github.com/libp2p/go-libp2p/core/peer"
"github.com/multiformats/go-multiaddr"
ethimpl "gitlab.com/accumulatenetwork/accumulate/internal/api/ethereum"
"gitlab.com/accumulatenetwork/accumulate/internal/api/routing"
v2 "gitlab.com/accumulatenetwork/accumulate/internal/api/v2"
"gitlab.com/accumulatenetwork/accumulate/internal/logging"
"gitlab.com/accumulatenetwork/accumulate/internal/node/config"
"gitlab.com/accumulatenetwork/accumulate/internal/node/web"
ethrpc "gitlab.com/accumulatenetwork/accumulate/pkg/api/ethereum"
"gitlab.com/accumulatenetwork/accumulate/pkg/api/v3"
"gitlab.com/accumulatenetwork/accumulate/pkg/api/v3/jsonrpc"
"gitlab.com/accumulatenetwork/accumulate/pkg/api/v3/message"
Expand Down Expand Up @@ -126,6 +128,7 @@ func NewHandler(opts Options) (*Handler, error) {
return nil, errors.UnknownError.WithFormat("initialize websocket API: %w", err)
}

// JSON-RPC API v2
v2, err := v2.NewJrpc(v2.Options{
Logger: opts.Logger,
Describe: opts.Network,
Expand All @@ -141,7 +144,13 @@ func NewHandler(opts Options) (*Handler, error) {
return nil, errors.UnknownError.WithFormat("initialize API v2: %v", err)
}

// Set up mux
// Ethereum JSON-RPC
eth := ethrpc.NewHandler(&ethimpl.Service{
Network: selfClient,
Query: client,
})

// REST API
h.mux, err = rest.NewHandler(
v2,
rest.NodeService{NodeService: selfClient},
Expand All @@ -157,11 +166,16 @@ func NewHandler(opts Options) (*Handler, error) {
return nil, errors.UnknownError.WithFormat("register API v2: %v", err)
}

// Setup mux
v3h := ws.FallbackTo(v3)
h.mux.POST("/v3", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
v3h.ServeHTTP(w, r)
})

h.mux.POST("/eth", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
eth.ServeHTTP(w, r)
})

h.mux.GET("/", func(w http.ResponseWriter, _ *http.Request, _ httprouter.Params) {
w.Header().Set("Location", "/x")
w.WriteHeader(http.StatusTemporaryRedirect)
Expand Down
11 changes: 11 additions & 0 deletions pkg/api/ethereum/generate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// Copyright 2024 The Accumulate Authors
//
// Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.

package ethrpc

//go:generate go run gitlab.com/accumulatenetwork/core/schema/cmd/generate schema schema.yml -w schema_gen.go
//go:generate go run gitlab.com/accumulatenetwork/core/schema/cmd/generate types schema.yml -w types_gen.go
//go:generate go run github.com/rinchsan/gosimports/cmd/gosimports -w .
176 changes: 176 additions & 0 deletions pkg/api/ethereum/jsonrpc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
// Copyright 2024 The Accumulate Authors
//
// Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.

package ethrpc

import (
"context"
"encoding/json"
"fmt"
"net/http"

"gitlab.com/accumulatenetwork/accumulate/pkg/accumulate"
"gitlab.com/accumulatenetwork/accumulate/pkg/types/encoding"
"gitlab.com/accumulatenetwork/accumulate/protocol"
"gitlab.com/accumulatenetwork/utils/jsonrpc"
)

type JSONRPCClient struct {
Client jsonrpc.HTTPClient
Endpoint string
}

type JSONRPCHandler struct {
Service Service
}

var _ Service = (*JSONRPCClient)(nil)

func NewClient(endpoint string) Service {
return &JSONRPCClient{Endpoint: accumulate.ResolveWellKnownEndpoint(endpoint, "eth")}
}

func NewHandler(service Service) http.Handler {
return jsonrpc.HTTPHandler{H: &JSONRPCHandler{Service: service}}
}

func (c *JSONRPCClient) EthChainId(ctx context.Context) (*Number, error) {
return clientCall[*Number](c, ctx, "eth_chainId", []any{})
}

func (h *JSONRPCHandler) eth_chainId(ctx context.Context, _ json.RawMessage) (any, error) {
return h.Service.EthChainId(ctx)
}

func (h *JSONRPCHandler) net_version(ctx context.Context, _ json.RawMessage) (any, error) {
id, err := h.Service.EthChainId(ctx)
if err != nil {
return nil, err
}
return id.Int().Uint64(), nil
}

func (c *JSONRPCClient) EthBlockNumber(ctx context.Context) (*Number, error) {
return clientCall[*Number](c, ctx, "eth_blockNumber", []any{})
}

func (h *JSONRPCHandler) eth_blockNumber(ctx context.Context, _ json.RawMessage) (any, error) {
return h.Service.EthBlockNumber(ctx)
}

func (c *JSONRPCClient) EthGasPrice(ctx context.Context) (*Number, error) {
return clientCall[*Number](c, ctx, "eth_gasPrice", []any{})
}

func (h *JSONRPCHandler) eth_gasPrice(ctx context.Context, _ json.RawMessage) (any, error) {
return h.Service.EthGasPrice(ctx)
}

func (c *JSONRPCClient) EthGetBalance(ctx context.Context, addr Address, block string) (*Number, error) {
return clientCall[*Number](c, ctx, "eth_getBalance", []any{addr, block})
}

func (h *JSONRPCHandler) eth_getBalance(ctx context.Context, raw json.RawMessage) (any, error) {
return handlerCall2(ctx, raw, h.Service.EthGetBalance)
}

func (c *JSONRPCClient) EthGetBlockByNumber(ctx context.Context, block string, expand bool) (*BlockData, error) {
return clientCall[*BlockData](c, ctx, "eth_getBlockByNumber", []any{block, expand})
}

func (h *JSONRPCHandler) eth_getBlockByNumber(ctx context.Context, raw json.RawMessage) (any, error) {
return handlerCall2(ctx, raw, h.Service.EthGetBlockByNumber)
}

func (c *JSONRPCClient) AccTypedData(ctx context.Context, txn *protocol.Transaction, sig protocol.Signature) (*encoding.EIP712Call, error) {
return clientCall[*encoding.EIP712Call](c, ctx, "acc_typedData", []any{txn, sig})
}

func (h *JSONRPCHandler) acc_typedData(ctx context.Context, raw json.RawMessage) (any, error) {
return handlerCall2(ctx, raw, func(ctx context.Context, txn *protocol.Transaction, sig *signatureWrapper) (any, error) {
return h.Service.AccTypedData(ctx, txn, sig.V)
})
}

func clientCall[V any](c *JSONRPCClient, ctx context.Context, method string, params any) (V, error) {
var v V
err := c.Client.Call(ctx, c.Endpoint, method, params, &v)
return v, err
}

func handlerCall2[V1, V2, R any](ctx context.Context, raw json.RawMessage, fn func(context.Context, V1, V2) (R, error)) (any, error) {
var v1 V1
var v2 V2
err := decodeN(raw, &v1, &v2)
if err != nil {
return nil, err
}
return fn(ctx, v1, v2)
}

func decodeN(raw json.RawMessage, v ...any) error {
var params []json.RawMessage
err := json.Unmarshal(raw, &params)
if err != nil {
return jsonrpc.InvalidParams.With(err, err)
}
if len(params) != len(v) {
err := fmt.Errorf("expected %d parameters, got %d", len(v), len(params))
return jsonrpc.InvalidParams.With(err, err)
}
for i, param := range params {
err = json.Unmarshal(param, v[i])
if err != nil {
return jsonrpc.InvalidParams.With(err, err)
}
}
return nil
}

func (h *JSONRPCHandler) ServeJSONRPC(ctx context.Context, method string, params json.RawMessage) (result any, err error) {
switch method {
case "net_version":
return h.net_version(ctx, params)

case "eth_chainId":
return h.eth_chainId(ctx, params)
case "eth_blockNumber":
return h.eth_blockNumber(ctx, params)
case "eth_gasPrice":
return h.eth_gasPrice(ctx, params)
case "eth_getBalance":
return h.eth_getBalance(ctx, params)
case "eth_getBlockByNumber":
return h.eth_getBlockByNumber(ctx, params)

case "acc_typedData":
return h.acc_typedData(ctx, params)
}

/*
Missing "eth_getCode"
Missing "eth_estimateGas"
Missing "eth_getTransactionCount"
*/
return nil, &jsonrpc.Error{
Code: jsonrpc.MethodNotFound,
Message: fmt.Sprintf("%q is not a supported method", method),
}
}

type signatureWrapper struct {
V protocol.Signature
}

func (s *signatureWrapper) MarshalJSON() ([]byte, error) {
return json.Marshal(s.V)
}

func (s *signatureWrapper) UnmarshalJSON(b []byte) error {
var err error
s.V, err = protocol.UnmarshalSignatureJSON(b)
return err
}
Loading

0 comments on commit ecb11ed

Please sign in to comment.