-
Notifications
You must be signed in to change notification settings - Fork 15
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
merge: branch '3620-ethereum-rpc' into 'main'
Minimum viable Ethereum RPC [#3620] Closes #3620 See merge request accumulatenetwork/accumulate!1088
- Loading branch information
Showing
15 changed files
with
727 additions
and
20 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 ðrpc.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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 . |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, ¶ms) | ||
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 | ||
} |
Oops, something went wrong.