From 453134b90385e5219aad330dc63d4fdcc9c3471a Mon Sep 17 00:00:00 2001 From: Roshan <48975233+Pythonberg1997@users.noreply.github.com> Date: Tue, 31 Jan 2023 20:05:58 +0800 Subject: [PATCH] feat: add support for EVM jsonrpc (#90) --- go.mod | 21 +- go.sum | 27 ++ server/apis.go | 79 ++++ server/config/config.go | 140 +++++- server/config/toml.go | 30 ++ server/constructors_test.go | 1 - server/export_test.go | 1 - server/grpc/server_test.go | 4 +- server/json_rpc.go | 156 +++++++ server/jsonrpc/backend/account_info.go | 36 ++ server/jsonrpc/backend/backend.go | 155 +++++++ server/jsonrpc/backend/blocks.go | 193 +++++++++ server/jsonrpc/backend/chain_info.go | 24 ++ server/jsonrpc/backend/unimplemented.go | 251 +++++++++++ server/jsonrpc/namespaces/ethereum/eth/api.go | 405 ++++++++++++++++++ server/jsonrpc/namespaces/ethereum/net/api.go | 56 +++ server/mock/app.go | 3 +- server/rollback.go | 7 +- server/rosetta/client_offline.go | 1 - server/rosetta/client_online.go | 13 +- server/rosetta/config.go | 3 +- server/rosetta/lib/errors/errors.go | 3 +- .../lib/internal/service/construction.go | 3 +- server/rosetta/lib/internal/service/data.go | 1 + server/start.go | 59 +++ server/websockets.go | 226 ++++++++++ 26 files changed, 1868 insertions(+), 30 deletions(-) create mode 100644 server/apis.go create mode 100644 server/json_rpc.go create mode 100644 server/jsonrpc/backend/account_info.go create mode 100644 server/jsonrpc/backend/backend.go create mode 100644 server/jsonrpc/backend/blocks.go create mode 100644 server/jsonrpc/backend/chain_info.go create mode 100644 server/jsonrpc/backend/unimplemented.go create mode 100644 server/jsonrpc/namespaces/ethereum/eth/api.go create mode 100644 server/jsonrpc/namespaces/ethereum/net/api.go create mode 100644 server/websockets.go diff --git a/go.mod b/go.mod index 9b217ccf6c..6c971af3b2 100644 --- a/go.mod +++ b/go.mod @@ -29,6 +29,7 @@ require ( github.com/google/uuid v1.3.0 github.com/gorilla/handlers v1.5.1 github.com/gorilla/mux v1.8.0 + github.com/gorilla/websocket v1.5.0 github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 github.com/grpc-ecosystem/grpc-gateway v1.16.0 github.com/hashicorp/golang-lru v0.5.5-0.20210104140557-80c98217689d @@ -45,6 +46,7 @@ require ( github.com/prysmaticlabs/prysm v0.0.0-20220124113610-e26cde5e091b github.com/rakyll/statik v0.1.7 github.com/regen-network/cosmos-proto v0.3.1 + github.com/rs/cors v1.8.2 github.com/rs/zerolog v1.27.0 github.com/spf13/cast v1.5.0 github.com/spf13/cobra v1.6.0 @@ -60,6 +62,7 @@ require ( github.com/willf/bitset v1.1.3 golang.org/x/crypto v0.1.0 golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e + golang.org/x/net v0.1.0 golang.org/x/text v0.4.0 google.golang.org/genproto v0.0.0-20221014213838-99cd37c6964a google.golang.org/grpc v1.50.1 @@ -71,6 +74,8 @@ require ( require ( filippo.io/edwards25519 v1.0.0-rc.1 // indirect github.com/ChainSafe/go-schnorrkel v0.0.0-20200405005733-88cbf1b4c40d // indirect + github.com/StackExchange/wmi v0.0.0-20210224194228-fe8f1750fd46 // indirect + github.com/VictoriaMetrics/fastcache v1.6.0 // indirect github.com/Workiva/go-datastructures v1.0.53 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/btcsuite/btcd/btcec/v2 v2.2.0 // indirect @@ -85,6 +90,7 @@ require ( github.com/creachadair/taskgroup v0.3.2 // indirect github.com/danieljoos/wincred v1.1.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/deckarep/golang-set v1.8.0 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f // indirect github.com/dgraph-io/badger/v2 v2.2007.4 // indirect @@ -95,21 +101,25 @@ require ( github.com/felixge/httpsnoop v1.0.1 // indirect github.com/ferranbt/fastssz v0.0.0-20210905181407-59cf6761a7d5 // indirect github.com/fsnotify/fsnotify v1.5.4 // indirect + github.com/gin-gonic/gin v1.7.0 // indirect github.com/go-kit/kit v0.12.0 // indirect github.com/go-kit/log v0.2.1 // indirect github.com/go-logfmt/logfmt v0.5.1 // indirect + github.com/go-ole/go-ole v1.2.6 // indirect + github.com/go-stack/stack v1.8.0 // indirect github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 // indirect github.com/golang/glog v1.0.0 // indirect github.com/golang/snappy v0.0.4 // indirect github.com/google/btree v1.0.1 // indirect github.com/google/orderedcode v0.0.1 // indirect - github.com/gorilla/websocket v1.5.0 // indirect github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c // indirect github.com/gtank/merlin v0.1.1 // indirect github.com/gtank/ristretto255 v0.1.2 // indirect github.com/hashicorp/go-immutable-radix v1.3.1 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/herumi/bls-eth-go-binary v0.0.0-20210917013441-d37c07cfda4e // indirect + github.com/holiman/bloomfilter/v2 v2.0.3 // indirect + github.com/holiman/uint256 v1.2.1 // indirect github.com/inconshreveable/mousetrap v1.0.1 // indirect github.com/jmhodges/levigo v1.0.0 // indirect github.com/keybase/go-keychain v0.0.0-20190712205309-48d3d31d256d // indirect @@ -118,6 +128,7 @@ require ( github.com/lib/pq v1.10.6 // indirect github.com/libp2p/go-buffer-pool v0.1.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-runewidth v0.0.9 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect github.com/mimoo/StrobeGo v0.0.0-20210601165009-122bf33a46e0 // indirect github.com/minio/highwayhash v1.0.2 // indirect @@ -125,17 +136,19 @@ require ( github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect github.com/mtibben/percent v0.2.1 // indirect + github.com/olekukonko/tablewriter v0.0.5 // indirect github.com/pelletier/go-toml v1.9.5 // indirect github.com/pelletier/go-toml/v2 v2.0.5 // indirect github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_model v0.2.0 // indirect github.com/prometheus/procfs v0.8.0 // indirect + github.com/prometheus/tsdb v0.10.0 // indirect github.com/prysmaticlabs/eth2-types v0.0.0-20210303084904-c9735a06829d // indirect github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 // indirect - github.com/rs/cors v1.8.2 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/sasha-s/go-deadlock v0.3.1 // indirect + github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible // indirect github.com/sirupsen/logrus v1.9.0 // indirect github.com/spf13/afero v1.8.2 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect @@ -143,16 +156,18 @@ require ( github.com/supranational/blst v0.3.8-0.20220526154634-513d2456b344 // indirect github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 // indirect github.com/thomaso-mirodin/intmath v0.0.0-20160323211736-5dc6d854e46e // indirect + github.com/tklauser/go-sysconf v0.3.10 // indirect + github.com/tklauser/numcpus v0.4.0 // indirect github.com/tyler-smith/go-bip39 v1.1.0 // indirect github.com/urfave/cli/v2 v2.3.0 // indirect github.com/wealdtech/go-bytesutil v1.1.1 // indirect github.com/wealdtech/go-eth2-types/v2 v2.5.2 // indirect github.com/zondax/hid v0.9.1-0.20220302062450-5552068d2266 // indirect go.etcd.io/bbolt v1.3.6 // indirect - golang.org/x/net v0.1.0 // indirect golang.org/x/sys v0.1.0 // indirect golang.org/x/term v0.1.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect nhooyr.io/websocket v1.8.6 // indirect diff --git a/go.sum b/go.sum index 1df4e45e7e..bb7f6796a8 100644 --- a/go.sum +++ b/go.sum @@ -114,6 +114,7 @@ github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRF github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156/go.mod h1:Cb/ax3seSYIx7SuZdm2G2xzfwmv3TPSk2ucNfQESPXM= +github.com/allegro/bigcache v1.2.1 h1:hg1sY1raCwic3Vnsvje6TT7/pnZba83LeFck5NrFKSc= github.com/allegro/bigcache v1.2.1/go.mod h1:Cb/ax3seSYIx7SuZdm2G2xzfwmv3TPSk2ucNfQESPXM= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= @@ -264,6 +265,7 @@ github.com/cosmos/gorocksdb v1.2.0 h1:d0l3jJG8M4hBouIZq0mDUHZ+zjOx044J3nGRskwTb4 github.com/cosmos/gorocksdb v1.2.0/go.mod h1:aaKvKItm514hKfNJpUJXnnOWeBnk2GL4+Qw9NHizILw= github.com/cosmos/iavl v0.19.4 h1:t82sN+Y0WeqxDLJRSpNd8YFX5URIrT+p8n6oJbJ2Dok= github.com/cosmos/iavl v0.19.4/go.mod h1:X9PKD3J0iFxdmgNLa7b2LYWdsGd90ToV5cAONApkEPw= +github.com/cosmos/ibc-go/v5 v5.0.0-rc2 h1:7rGkZwojUwK1Dvgze3ZP5K1upIKygudxU3WaeFFU2ts= github.com/cosmos/keyring v1.1.7-0.20210622111912-ef00f8ac3d76 h1:DdzS1m6o/pCqeZ8VOAit/gyATedRgjvkVI+UCrLpyuU= github.com/cosmos/keyring v1.1.7-0.20210622111912-ef00f8ac3d76/go.mod h1:0mkLWIoZuQ7uBoospo5Q9zIpqq6rYCPJDSUdeCJvPM8= github.com/cosmos/ledger-cosmos-go v0.11.1 h1:9JIYsGnXP613pb2vPjFeMMjBI5lEDsEaF6oYorTy6J4= @@ -294,6 +296,7 @@ github.com/davidlazar/go-crypto v0.0.0-20170701192655-dcfb0a7ac018/go.mod h1:rQY github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c/go.mod h1:6UhI8N9EjYm1c2odKpFpAYeR8dsBeM7PtzQhRgxRr9U= github.com/deckarep/golang-set v0.0.0-20180603214616-504e848d77ea/go.mod h1:93vsz/8Wt4joVM7c2AVqh+YRMiUSc14yDtF28KmMOgQ= github.com/deckarep/golang-set v1.7.1/go.mod h1:93vsz/8Wt4joVM7c2AVqh+YRMiUSc14yDtF28KmMOgQ= +github.com/deckarep/golang-set v1.8.0 h1:sk9/l/KqpunDwP7pSjUg0keiOOLEnOBHzykLrsPppp4= github.com/deckarep/golang-set v1.8.0/go.mod h1:5nI87KwE7wgsBU1F4GKAw2Qod7p5kyS383rP6+o6qqo= github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0= github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= @@ -320,6 +323,7 @@ github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUn github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WAFKLNi6ZS0675eEUC9y3AlwSbQu1Y= github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= +github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91 h1:Izz0+t1Z5nI16/II7vuEo/nHjodOg0p7+OiDpjX5t1E= github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= github.com/dnaeon/go-vcr v1.1.0/go.mod h1:M7tiix8f0r6mKKJ3Yq/kqU1OYf3MnfmBWVbPx/yU9ko= github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= @@ -329,6 +333,7 @@ github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDD github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= github.com/dop251/goja v0.0.0-20211011172007-d99e4b8cbf48/go.mod h1:R9ET47fwRVRPZnOGvHxxhuZcbrMCuiqOz3Rlrh4KSnk= +github.com/dop251/goja v0.0.0-20220405120441-9037c2b61cbf h1:Yt+4K30SdjOkRoRRm3vYNQgR+/ZIy0RmeUDZo7Y8zeQ= github.com/dop251/goja_nodejs v0.0.0-20210225215109-d91c329300e7/go.mod h1:hn7BA7c8pLvoGndExHudxTDKZ84Pyvv+90pbBjbTz0Y= github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= @@ -341,6 +346,7 @@ github.com/eapache/go-resiliency v1.2.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5m github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= github.com/eclipse/paho.mqtt.golang v1.2.0/go.mod h1:H9keYFcgq3Qr5OUJm/JZI/i6U7joQ8SYLhZwfeOo6Ts= +github.com/edsrzf/mmap-go v1.0.0 h1:CEBF7HpRnUCSJgGUb5h1Gm7e3VkmVDrR8lvWVLtrOFw= github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= github.com/emicklei/dot v0.11.0/go.mod h1:DeV7GvQtIw4h2u73RKBkkFdvVAz0D9fzeJrgPW6gy/s= @@ -371,6 +377,7 @@ github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSw github.com/ferranbt/fastssz v0.0.0-20210120143747-11b9eff30ea9/go.mod h1:DyEu2iuLBnb/T51BlsiO3yLYdJC6UbGMrIkqK1KmQxM= github.com/ferranbt/fastssz v0.0.0-20210905181407-59cf6761a7d5 h1:6dVcS0LktRSyEEgldFY4N9J17WjUoiJStttH+RZj0Wo= github.com/ferranbt/fastssz v0.0.0-20210905181407-59cf6761a7d5/go.mod h1:S8yiDeAXy8f88W4Ul+0dBMPx49S05byYbmZD6Uv94K4= +github.com/fjl/memsize v0.0.0-20190710130421-bcb5799ab5e5 h1:FtmdgXiUlNeRsoNMFlKLDt+S+6hbjVMEW6RGQ7aUf7c= github.com/fjl/memsize v0.0.0-20190710130421-bcb5799ab5e5/go.mod h1:VvhXpOYNQvB+uIk2RvXzuaQtkQJzzIx6lSBe1xv7hi0= github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= github.com/flynn/noise v1.0.0/go.mod h1:xbMo+0i6+IGbYdJhF31t2eR1BIU0CYc12+BNAKwUTag= @@ -391,6 +398,7 @@ github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmV github.com/garyburd/redigo v1.1.1-0.20170914051019-70e1b1943d4f/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY= github.com/garyburd/redigo v1.6.0/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY= github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff/go.mod h1:x7DCsMOv1taUwEWCzT4cmDeAkigA5/QCwUodaVOe8Ww= +github.com/gballet/go-libpcsclite v0.0.0-20191108122812-4678299bea08 h1:f6D9Hr8xV8uYKlyuj8XIruxlh9WjVjdh1gIicAS7ays= github.com/gballet/go-libpcsclite v0.0.0-20191108122812-4678299bea08/go.mod h1:x7DCsMOv1taUwEWCzT4cmDeAkigA5/QCwUodaVOe8Ww= github.com/getkin/kin-openapi v0.53.0/go.mod h1:7Yn5whZr5kJi6t+kShccXS8ae1APpYTW6yheSwk8Yi4= github.com/getkin/kin-openapi v0.61.0/go.mod h1:7Yn5whZr5kJi6t+kShccXS8ae1APpYTW6yheSwk8Yi4= @@ -428,6 +436,7 @@ github.com/go-logr/logr v0.2.1/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTg github.com/go-ole/go-ole v1.2.1/go.mod h1:7FAglXiTm7HKlQRDeOQ6ZNUHidzCWXuZWq/1dTyBNF8= github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0= github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg= @@ -441,6 +450,7 @@ github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD87 github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE= github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= +github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyLFfuisuzeidWPMPWmECqU= github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg= github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= @@ -463,7 +473,9 @@ github.com/gofrs/uuid v3.3.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRx github.com/gogo/gateway v1.1.0 h1:u0SuhL9+Il+UbjM9VIE3ntfRujKbvVpFvNB4HbjeVQ0= github.com/gogo/gateway v1.1.0/go.mod h1:S7rR8FRQyG3QFESeSv4l2WnsyzlCLG0CzBbUUo/mbic= github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s= +github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= +github.com/golang-jwt/jwt/v4 v4.3.0 h1:kHL1vqdqWNfATmA0FNMdmZNMyZI1U6O31X4rlIPoBog= github.com/golang-jwt/jwt/v4 v4.3.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/golang/gddo v0.0.0-20200528160355-8d077c1d8f4c/go.mod h1:sam69Hju0uq+5uvLJUMDlsKlQ21Vrs1Kd/1YFPNYdOU= @@ -614,6 +626,7 @@ github.com/gxed/hashland/murmur3 v0.0.1/go.mod h1:KjXop02n4/ckmZSnY2+HKcLud/tcmv github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE= github.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-bexpr v0.1.10 h1:9kuI5PFotCboP3dkDYFr/wi0gg0QVbSNz5oFRpxn4uE= github.com/hashicorp/go-bexpr v0.1.10/go.mod h1:oxlubA2vC/gFVfX1A6JGp7ls7uCDlfJn732ehYYg+g0= github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= @@ -649,13 +662,17 @@ github.com/hdevalence/ed25519consensus v0.0.0-20220222234857-c00d1f31bab3/go.mod github.com/herumi/bls-eth-go-binary v0.0.0-20210130185500-57372fb27371/go.mod h1:luAnRm3OsMQeokhGzpYmc0ZKwawY7o87PUEP11Z7r7U= github.com/herumi/bls-eth-go-binary v0.0.0-20210917013441-d37c07cfda4e h1:wCMygKUQhmcQAjlk2Gquzq6dLmyMv2kF+llRspoRgrk= github.com/herumi/bls-eth-go-binary v0.0.0-20210917013441-d37c07cfda4e/go.mod h1:luAnRm3OsMQeokhGzpYmc0ZKwawY7o87PUEP11Z7r7U= +github.com/holiman/bloomfilter/v2 v2.0.3 h1:73e0e/V0tCydx14a0SCYS/EWCxgwLZ18CZcZKVu0fao= github.com/holiman/bloomfilter/v2 v2.0.3/go.mod h1:zpoh+gs7qcpqrHr3dB55AMiJwo0iURXE7ZOP9L9hSkA= github.com/holiman/uint256 v1.2.0/go.mod h1:y4ga/t+u+Xwd7CpDgZESaRcWy0I7XMlTMA25ApIH5Jw= +github.com/holiman/uint256 v1.2.1 h1:XRtyuda/zw2l+Bq/38n5XUoEF72aSOu/77Thd9pPp2o= +github.com/holiman/uint256 v1.2.1/go.mod h1:y4ga/t+u+Xwd7CpDgZESaRcWy0I7XMlTMA25ApIH5Jw= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg= github.com/huin/goupnp v1.0.0/go.mod h1:n9v9KO1tAxYH82qOn+UTIFQDmx5n1Zxd/ClZDMX7Bnc= github.com/huin/goupnp v1.0.2/go.mod h1:0dxJBVBHqTMjIUMkESDTNgOOx/Mw5wYIfyFmdzSamkM= github.com/huin/goupnp v1.0.3-0.20220313090229-ca81a64b4204/go.mod h1:ZxNlw5WqJj6wSsRK5+YfflQGXYfccj5VgQsMNixHM7Y= +github.com/huin/goupnp v1.0.3 h1:N8No57ls+MnjlB+JPiCVSOyy/ot7MJTqlo7rn+NYSqQ= github.com/huin/goutil v0.0.0-20170803182201-1ca381bf3150/go.mod h1:PpLOETDnJ0o3iZrZfqZzyLl6l7F3c6L1oWn7OICBi6o= github.com/ianlancetaylor/cgosymbolizer v0.0.0-20200424224625-be1b05b0b279/go.mod h1:a5aratAVTWyz+nJMmDsN8O4XTfaLfdAsB1ysCmZX5Bw= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= @@ -708,6 +725,7 @@ github.com/ipfs/go-log/v2 v2.1.3/go.mod h1:/8d0SH3Su5Ooc31QlL1WysJhvyOTDCjcCZ9Ax github.com/ipfs/go-log/v2 v2.3.0/go.mod h1:QqGoj30OTpnKaG/LKTGTxoP2mmQtjVMEnK72gynbe/g= github.com/ipfs/go-log/v2 v2.4.0/go.mod h1:nPZnh7Cj7lwS3LpRU5Mwr2ol1c2gXIEXuF6aywqrtmo= github.com/jackpal/go-nat-pmp v1.0.2-0.20160603034137-1fa385a6f458/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= +github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus= github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= github.com/jbenet/go-cienv v0.1.0/go.mod h1:TqNnHUmJgXau0nCzC7kXWeotg3J9W34CUv5Djy1+FlA= github.com/jbenet/go-temp-err-catcher v0.0.0-20150120210811-aac704a3f4f2/go.mod h1:8GXXJV31xl8whumTzdZsTt3RnUIiPqzkyf7mxToRCMs= @@ -1005,6 +1023,7 @@ github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RR github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/pointerstructure v1.2.0 h1:O+i9nHnXS3l/9Wu7r4NrEdwA2VFTicjUEN1uBnDo34A= github.com/mitchellh/pointerstructure v1.2.0/go.mod h1:BRAsLI5zgXmw97Lf6s25bs8ohIXc3tViBH44KcwB2g4= github.com/moby/sys/mountinfo v0.5.0/go.mod h1:3bMD3Rg+zkqx8MRYPi7Pyb0Ie97QEBmdxbhnCLlSvSU= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -1242,6 +1261,7 @@ github.com/regen-network/cosmos-proto v0.3.1/go.mod h1:jO0sVX6a1B36nmE8C9xBFXpNw github.com/regen-network/protobuf v1.3.3-alpha.regen.1 h1:OHEc+q5iIAXpqiqFKeLpu5NwTIkVXUs48vFMwzqpqY4= github.com/regen-network/protobuf v1.3.3-alpha.regen.1/go.mod h1:2DjTFR1HhMQhiWC5sZ4OhQ3+NtdbZ6oBDKQwq5Ou+FI= github.com/retailnext/hllpp v1.0.1-0.20180308014038-101a6d2f8b52/go.mod h1:RDpi1RftBQPUCDRw6SmxeaREsAaRKnOclghuzp/WRzc= +github.com/rjeczalik/notify v0.9.1 h1:CLCKso/QK1snAlnhNR/CNvNiFU2saUtjV0bx3EwNeCE= github.com/rjeczalik/notify v0.9.1/go.mod h1:rKwnCoCGeuQnwBtTSPL9Dad03Vh2n40ePRrjvIXnJho= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= @@ -1342,6 +1362,7 @@ github.com/spf13/viper v1.13.0 h1:BWSJ/M+f+3nmdz9bxB+bWX28kkALN2ok11D0rSo8EJU= github.com/spf13/viper v1.13.0/go.mod h1:Icm2xNL3/8uyh/wFuB1jI7TiTNKp8632Nwegu+zgdYw= github.com/src-d/envconfig v1.0.0/go.mod h1:Q9YQZ7BKITldTBnoxsE5gOeB5y66RyPXeue/R4aaNBc= github.com/status-im/keycard-go v0.0.0-20190316090335-8537d3370df4/go.mod h1:RZLeN1LMWmRsyYjvAu+I6Dm9QmlDaIIt+Y+4Kd7Tp+Q= +github.com/status-im/keycard-go v0.0.0-20200402102358-957c09536969 h1:Oo2KZNP70KE0+IUJSidPj/BFS/RXNHmKIJOdckzml2E= github.com/status-im/keycard-go v0.0.0-20200402102358-957c09536969/go.mod h1:RZLeN1LMWmRsyYjvAu+I6Dm9QmlDaIIt+Y+4Kd7Tp+Q= github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= @@ -1393,8 +1414,10 @@ github.com/tinylib/msgp v1.1.5/go.mod h1:eQsjooMTnV42mHu917E26IogZ2930nFyBQdofk1 github.com/tjfoc/gmsm v1.3.0/go.mod h1:HaUcFuY0auTiaHB9MHFGCPx5IaLhTUd2atbCFBQXn9w= github.com/tklauser/go-sysconf v0.3.5/go.mod h1:MkWzOF4RMCshBAMXuhXJs64Rte09mITnppBXY/rYEFI= github.com/tklauser/go-sysconf v0.3.10 h1:IJ1AZGZRWbY8T5Vfk04D9WOA5WSejdflXxP03OUqALw= +github.com/tklauser/go-sysconf v0.3.10/go.mod h1:C8XykCvCb+Gn0oNCWPIlcb0RuglQTYaQ2hGm7jmxEFk= github.com/tklauser/numcpus v0.2.2/go.mod h1:x3qojaO3uyYt0i56EW/VUYs7uBvdl2fkfZFu0T9wgjM= github.com/tklauser/numcpus v0.4.0 h1:E53Dm1HjH1/R2/aoCtXtPgzmElmn51aOkhCFSuZq//o= +github.com/tklauser/numcpus v0.4.0/go.mod h1:1+UI3pD8NW14VMwdgJNJ1ESk2UnwhAnz5hMwiKKqXCQ= github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/trailofbits/go-mutexasserts v0.0.0-20200708152505-19999e7d3cef/go.mod h1:+SV/613m53DNAmlXPTWGZhIyt4E/qDvn9g/lOPRiy0A= github.com/ttacon/chalk v0.0.0-20160626202418-22c06c80ed31/go.mod h1:onvgF043R+lC5RZ8IT9rBXDaEDnpnw/Cl+HFiw+v/7Q= @@ -1677,6 +1700,7 @@ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0 h1:cu5kTvlzcw1Q5S9f5ip1/cpiB4nXvw1XYzFPGgzLUOY= golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -1784,6 +1808,7 @@ golang.org/x/sys v0.0.0-20211116061358-0a5406a5449c/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -2053,6 +2078,7 @@ gopkg.in/jcmturner/dnsutils.v1 v1.0.1/go.mod h1:m3v+5svpVOhtFAP/wSz+yzh4Mc0Fg7eR gopkg.in/jcmturner/goidentity.v3 v3.0.0/go.mod h1:oG2kH0IvSYNIu80dVAyu/yoefjq1mNfM5bm88whjWx4= gopkg.in/jcmturner/gokrb5.v7 v7.5.0/go.mod h1:l8VISx+WGYp+Fp7KRbsiUuXTTOnxIc3Tuvyavf11/WM= gopkg.in/jcmturner/rpc.v1 v1.1.0/go.mod h1:YIdkC4XfD6GXbzje11McwsDuOlZQSb9W4vfLvuNnlv8= +gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce h1:+JknDZhAj8YMt7GC73Ei8pv4MzjDUNPHgQWJdtMAaDU= gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce/go.mod h1:5AcXVHNjg+BDxry382+8OKon8SEWiKktQR07RKPsv1c= gopkg.in/olebedev/go-duktape.v3 v3.0.0-20200619000410-60c24ae608a6/go.mod h1:uAJfkITjFhyEEuUfm7bsmCZRbW5WRq8s9EY8HZ6hCns= gopkg.in/redis.v4 v4.2.4/go.mod h1:8KREHdypkCEojGKQcjMqAODMICIVwZAONWq8RowTITA= @@ -2061,6 +2087,7 @@ gopkg.in/src-d/go-cli.v0 v0.0.0-20181105080154-d492247bbc0d/go.mod h1:z+K8VcOYVY gopkg.in/src-d/go-log.v1 v1.0.1/go.mod h1:GN34hKP0g305ysm2/hctJ0Y8nWP3zxXXJ8GFabTyABE= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/urfave/cli.v1 v1.20.0 h1:NdAVW6RYxDif9DhDHaAortIu956m2c0v+09AZBPTbE0= gopkg.in/urfave/cli.v1 v1.20.0/go.mod h1:vuBzUtMdQeixQj8LVd+/98pzhxNGQoyuPBlsXHOQNO0= gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= diff --git a/server/apis.go b/server/apis.go new file mode 100644 index 0000000000..1ce6a4bb6a --- /dev/null +++ b/server/apis.go @@ -0,0 +1,79 @@ +package server + +import ( + "github.com/ethereum/go-ethereum/rpc" + rpcclient "github.com/tendermint/tendermint/rpc/jsonrpc/client" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/server/jsonrpc/backend" + "github.com/cosmos/cosmos-sdk/server/jsonrpc/namespaces/ethereum/eth" + "github.com/cosmos/cosmos-sdk/server/jsonrpc/namespaces/ethereum/net" +) + +// RPC namespaces and API version +const ( + // Ethereum namespaces + + EthNamespace = "eth" + NetNamespace = "net" + + apiVersion = "1.0" +) + +// APICreator creates the JSON-RPC API implementations. +type APICreator = func( + ctx *Context, + clientCtx client.Context, + tendermintWebsocketClient *rpcclient.WSClient, +) []rpc.API + +// apiCreators defines the JSON-RPC API namespaces. +var apiCreators map[string]APICreator + +func init() { + apiCreators = map[string]APICreator{ + EthNamespace: func(ctx *Context, + clientCtx client.Context, + tmWSClient *rpcclient.WSClient, + ) []rpc.API { + evmBackend := backend.NewBackend(ctx.Viper, ctx.Logger, clientCtx) + return []rpc.API{ + { + Namespace: EthNamespace, + Version: apiVersion, + Service: eth.NewPublicAPI(ctx.Logger, evmBackend), + Public: true, + }, + } + }, + NetNamespace: func(_ *Context, clientCtx client.Context, _ *rpcclient.WSClient) []rpc.API { + return []rpc.API{ + { + Namespace: NetNamespace, + Version: apiVersion, + Service: net.NewPublicAPI(clientCtx), + Public: true, + }, + } + }, + } +} + +// GetRPCAPIs returns the list of all APIs +func GetRPCAPIs(ctx *Context, + clientCtx client.Context, + tmWSClient *rpcclient.WSClient, + selectedAPIs []string, +) []rpc.API { + var apis []rpc.API + + for _, ns := range selectedAPIs { + if creator, ok := apiCreators[ns]; ok { + apis = append(apis, creator(ctx, clientCtx, tmWSClient)...) + } else { + ctx.Logger.Error("invalid namespace value", "namespace", ns) + } + } + + return apis +} diff --git a/server/config/config.go b/server/config/config.go index 549626ae18..f69d67cb04 100644 --- a/server/config/config.go +++ b/server/config/config.go @@ -1,9 +1,12 @@ package config import ( + "errors" "fmt" "math" + "path" "strings" + "time" "github.com/spf13/viper" @@ -26,6 +29,12 @@ const ( // DefaultGRPCWebAddress defines the default address to bind the gRPC-web server to. DefaultGRPCWebAddress = "0.0.0.0:9091" + // DefaultJSONRPCAddress is the default address the JSON-RPC server binds to. + DefaultJSONRPCAddress = "0.0.0.0:8545" + + // DefaultJSONRPCWsAddress is the default address the JSON-RPC WebSocket server binds to. + DefaultJSONRPCWsAddress = "0.0.0.0:8546" + // DefaultGRPCMaxRecvMsgSize defines the default gRPC max message size in // bytes the server can receive. DefaultGRPCMaxRecvMsgSize = 1024 * 1024 * 10 @@ -33,6 +42,13 @@ const ( // DefaultGRPCMaxSendMsgSize defines the default gRPC max message size in // bytes the server can send. DefaultGRPCMaxSendMsgSize = math.MaxInt32 + + DefaultHTTPTimeout = 30 * time.Second + + DefaultHTTPIdleTimeout = 120 * time.Second + + // DefaultMaxOpenConnections represents the amount of open connections (unlimited = 0) + DefaultMaxOpenConnections = 0 ) // BaseConfig defines the server's basic configuration @@ -196,6 +212,33 @@ type StateSyncConfig struct { SnapshotKeepRecent uint32 `mapstructure:"snapshot-keep-recent"` } +// JSONRPCConfig defines configuration for the EVM RPC server. +type JSONRPCConfig struct { + // API defines a list of JSON-RPC namespaces that should be enabled + API []string `mapstructure:"api"` + // Address defines the HTTP server to listen on + Address string `mapstructure:"address"` + // WsAddress defines the WebSocket server to listen on + WsAddress string `mapstructure:"ws-address"` + // Enable defines if the EVM RPC server should be enabled. + Enable bool `mapstructure:"enable"` + // HTTPTimeout is the read/write timeout of http json-rpc server. + HTTPTimeout time.Duration `mapstructure:"http-timeout"` + // HTTPIdleTimeout is the idle timeout of http json-rpc server. + HTTPIdleTimeout time.Duration `mapstructure:"http-idle-timeout"` + // MaxOpenConnections sets the maximum number of simultaneous connections + // for the server listener. + MaxOpenConnections int `mapstructure:"max-open-connections"` +} + +// TLSConfig defines the certificate and matching private key for the server. +type TLSConfig struct { + // CertificatePath the file path for the certificate .pem file + CertificatePath string `mapstructure:"certificate-path"` + // KeyPath the file path for the key .pem file + KeyPath string `mapstructure:"key-path"` +} + // UpgradeConfig defines the upgrading configuration. type UpgradeConfig struct { Name string `mapstructure:"name"` @@ -215,6 +258,8 @@ type Config struct { Rosetta RosettaConfig `mapstructure:"rosetta"` GRPCWeb GRPCWebConfig `mapstructure:"grpc-web"` StateSync StateSyncConfig `mapstructure:"state-sync"` + JSONRPC JSONRPCConfig `mapstructure:"json-rpc"` + TLS TLSConfig `mapstructure:"tls"` } // SetMinGasPrices sets the validator's minimum gas prices. @@ -296,6 +341,8 @@ func DefaultConfig() *Config { SnapshotInterval: 0, SnapshotKeepRecent: 2, }, + JSONRPC: *DefaultJSONRPCConfig(), + TLS: *DefaultTLSConfig(), } } @@ -380,10 +427,23 @@ func GetConfig(v *viper.Viper) (Config, error) { SnapshotInterval: v.GetUint64("state-sync.snapshot-interval"), SnapshotKeepRecent: v.GetUint32("state-sync.snapshot-keep-recent"), }, + JSONRPC: JSONRPCConfig{ + API: v.GetStringSlice("json-rpc.api"), + Address: v.GetString("json-rpc.address"), + WsAddress: v.GetString("json-rpc.ws-address"), + Enable: v.GetBool("json-rpc.enable"), + HTTPTimeout: v.GetDuration("json-rpc.http-timeout"), + HTTPIdleTimeout: v.GetDuration("json-rpc.http-idle-timeout"), + MaxOpenConnections: v.GetInt("json-rpc.max-open-connections"), + }, + TLS: TLSConfig{ + CertificatePath: v.GetString("tls.certificate-path"), + KeyPath: v.GetString("tls.key-path"), + }, }, nil } -// ValidateBasic returns an error if min-gas-prices field is empty in BaseConfig. Otherwise, it returns nil. +// ValidateBasic returns an error if something is wrong with the config. func (c Config) ValidateBasic() error { if c.BaseConfig.MinGasPrices == "" { return sdkerrors.ErrAppConfig.Wrap("set min gas price in app.toml or flag or env variable") @@ -394,5 +454,83 @@ func (c Config) ValidateBasic() error { ) } + if err := c.JSONRPC.Validate(); err != nil { + return sdkerrors.ErrAppConfig.Wrapf("invalid json-rpc config value: %s", err.Error()) + } + + if err := c.TLS.Validate(); err != nil { + return sdkerrors.ErrAppConfig.Wrapf("invalid tls config value: %s", err.Error()) + } + + return nil +} + +// GetDefaultAPINamespaces returns the default list of JSON-RPC namespaces that should be enabled +func GetDefaultAPINamespaces() []string { + return []string{"eth", "net"} +} + +// DefaultJSONRPCConfig returns an EVM config with the JSON-RPC API enabled by default +func DefaultJSONRPCConfig() *JSONRPCConfig { + return &JSONRPCConfig{ + Enable: true, + API: GetDefaultAPINamespaces(), + Address: DefaultJSONRPCAddress, + WsAddress: DefaultJSONRPCWsAddress, + HTTPTimeout: DefaultHTTPTimeout, + HTTPIdleTimeout: DefaultHTTPIdleTimeout, + MaxOpenConnections: DefaultMaxOpenConnections, + } +} + +// Validate returns an error if the JSON-RPC configuration fields are invalid. +func (c JSONRPCConfig) Validate() error { + if c.Enable && len(c.API) == 0 { + return errors.New("cannot enable JSON-RPC without defining any API namespace") + } + + if c.HTTPTimeout < 0 { + return errors.New("JSON-RPC HTTP timeout duration cannot be negative") + } + + if c.HTTPIdleTimeout < 0 { + return errors.New("JSON-RPC HTTP idle timeout duration cannot be negative") + } + + // check for duplicates + seenAPIs := make(map[string]bool) + for _, api := range c.API { + if seenAPIs[api] { + return fmt.Errorf("repeated API namespace '%s'", api) + } + + seenAPIs[api] = true + } + + return nil +} + +// DefaultTLSConfig returns the default TLS configuration +func DefaultTLSConfig() *TLSConfig { + return &TLSConfig{ + CertificatePath: "", + KeyPath: "", + } +} + +// Validate returns an error if the TLS certificate and key file extensions are invalid. +func (c TLSConfig) Validate() error { + certExt := path.Ext(c.CertificatePath) + + if c.CertificatePath != "" && certExt != ".pem" { + return fmt.Errorf("invalid extension %s for certificate path %s, expected '.pem'", certExt, c.CertificatePath) + } + + keyExt := path.Ext(c.KeyPath) + + if c.KeyPath != "" && keyExt != ".pem" { + return fmt.Errorf("invalid extension %s for key path %s, expected '.pem'", keyExt, c.KeyPath) + } + return nil } diff --git a/server/config/toml.go b/server/config/toml.go index 8c9bc64731..b6977dd661 100644 --- a/server/config/toml.go +++ b/server/config/toml.go @@ -250,6 +250,36 @@ snapshot-interval = {{ .StateSync.SnapshotInterval }} # snapshot-keep-recent specifies the number of recent snapshots to keep and serve (0 to keep all). snapshot-keep-recent = {{ .StateSync.SnapshotKeepRecent }} + +############################################################################### +### JSONRPC Configuration ### +############################################################################### + +[json-rpc] + +api = "{{range $index, $elmt := .JSONRPC.API}}{{if $index}},{{$elmt}}{{else}}{{$elmt}}{{end}}{{end}}" + +address = "{{ .JSONRPC.Address }}" + +ws-address = "{{ .JSONRPC.WsAddress }}" + +enable = {{ .JSONRPC.Enable }} + +http-timeout = "{{ .JSONRPC.HTTPTimeout }}" + +http-idle-timeout = "{{ .JSONRPC.HTTPIdleTimeout }}" + +max-open-connections = {{ .JSONRPC.MaxOpenConnections }} + +############################################################################### +### TLS Configuration ### +############################################################################### + +[tls] + +certificate-path = "{{ .TLS.CertificatePath }}" + +key-path = "{{ .TLS.KeyPath }}" ` var configTemplate *template.Template diff --git a/server/constructors_test.go b/server/constructors_test.go index 646311b457..f07831bc33 100644 --- a/server/constructors_test.go +++ b/server/constructors_test.go @@ -5,7 +5,6 @@ import ( "testing" "github.com/stretchr/testify/require" - dbm "github.com/tendermint/tm-db" ) diff --git a/server/export_test.go b/server/export_test.go index 34374d7337..4890715cbc 100644 --- a/server/export_test.go +++ b/server/export_test.go @@ -11,7 +11,6 @@ import ( "github.com/spf13/cobra" "github.com/stretchr/testify/require" - abci "github.com/tendermint/tendermint/abci/types" tmjson "github.com/tendermint/tendermint/libs/json" "github.com/tendermint/tendermint/libs/log" diff --git a/server/grpc/server_test.go b/server/grpc/server_test.go index d6956a7f45..537919354e 100644 --- a/server/grpc/server_test.go +++ b/server/grpc/server_test.go @@ -9,12 +9,9 @@ import ( "testing" "time" - "github.com/cosmos/cosmos-sdk/codec" "github.com/jhump/protoreflect/grpcreflect" - "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" - "google.golang.org/grpc" "google.golang.org/grpc/metadata" rpb "google.golang.org/grpc/reflection/grpc_reflection_v1alpha" @@ -22,6 +19,7 @@ import ( "github.com/cosmos/cosmos-sdk/client" reflectionv1 "github.com/cosmos/cosmos-sdk/client/grpc/reflection" clienttx "github.com/cosmos/cosmos-sdk/client/tx" + "github.com/cosmos/cosmos-sdk/codec" reflectionv2 "github.com/cosmos/cosmos-sdk/server/grpc/reflection/v2alpha1" "github.com/cosmos/cosmos-sdk/simapp" "github.com/cosmos/cosmos-sdk/testutil/network" diff --git a/server/json_rpc.go b/server/json_rpc.go new file mode 100644 index 0000000000..98ee5ebcac --- /dev/null +++ b/server/json_rpc.go @@ -0,0 +1,156 @@ +package server + +import ( + "net" + "net/http" + "time" + + ethlog "github.com/ethereum/go-ethereum/log" + ethrpc "github.com/ethereum/go-ethereum/rpc" + "github.com/gorilla/mux" + "github.com/rs/cors" + tmlog "github.com/tendermint/tendermint/libs/log" + rpcclient "github.com/tendermint/tendermint/rpc/jsonrpc/client" + "golang.org/x/net/netutil" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/server/config" + "github.com/cosmos/cosmos-sdk/server/types" +) + +// StartJSONRPC starts the JSON-RPC server +// this server is only meant to be used for wallets connection, so the wallets can sign EIP712 typed msg +// it doesn't include other unnecessary EVM rpc apis like eth_call +func StartJSONRPC(ctx *Context, + clientCtx client.Context, + tmRPCAddr, + tmEndpoint string, + config *config.Config, +) (*http.Server, chan struct{}, error) { + tmWsClient := ConnectTmWS(tmRPCAddr, tmEndpoint, ctx.Logger) + + logger := ctx.Logger.With("module", "json-rpc-server") + ethlog.Root().SetHandler(ethlog.FuncHandler(func(r *ethlog.Record) error { + switch r.Lvl { + case ethlog.LvlTrace, ethlog.LvlDebug: + logger.Debug(r.Msg, r.Ctx...) + case ethlog.LvlInfo, ethlog.LvlWarn: + logger.Info(r.Msg, r.Ctx...) + case ethlog.LvlError, ethlog.LvlCrit: + logger.Error(r.Msg, r.Ctx...) + } + return nil + })) + + rpcServer := ethrpc.NewServer() + + rpcAPIArr := config.JSONRPC.API + + apis := GetRPCAPIs(ctx, clientCtx, tmWsClient, rpcAPIArr) + + for _, api := range apis { + if err := rpcServer.RegisterName(api.Namespace, api.Service); err != nil { + ctx.Logger.Error( + "failed to register service in JSON RPC namespace", + "namespace", api.Namespace, + "service", api.Service, + ) + return nil, nil, err + } + } + + r := mux.NewRouter() + r.HandleFunc("/", rpcServer.ServeHTTP).Methods("POST") + + handlerWithCors := cors.Default() + if config.API.EnableUnsafeCORS { + handlerWithCors = cors.AllowAll() + } + + httpSrv := &http.Server{ + Addr: config.JSONRPC.Address, + Handler: handlerWithCors.Handler(r), + ReadHeaderTimeout: config.JSONRPC.HTTPTimeout, + ReadTimeout: config.JSONRPC.HTTPTimeout, + WriteTimeout: config.JSONRPC.HTTPTimeout, + IdleTimeout: config.JSONRPC.HTTPIdleTimeout, + } + httpSrvDone := make(chan struct{}, 1) + + ln, err := Listen(httpSrv.Addr, config) + if err != nil { + return nil, nil, err + } + + errCh := make(chan error) + go func() { + ctx.Logger.Info("Starting JSON-RPC server", "address", config.JSONRPC.Address) + if err := httpSrv.Serve(ln); err != nil { + if err == http.ErrServerClosed { + close(httpSrvDone) + return + } + + ctx.Logger.Error("failed to start JSON-RPC server", "error", err.Error()) + errCh <- err + } + }() + + select { + case err := <-errCh: + ctx.Logger.Error("failed to boot JSON-RPC server", "error", err.Error()) + return nil, nil, err + case <-time.After(types.ServerStartTime): // assume JSON RPC server started successfully + } + + ctx.Logger.Info("Starting JSON WebSocket server", "address", config.JSONRPC.WsAddress) + + // allocate separate WS connection to Tendermint + wsSrv := NewWebsocketsServer(ctx.Logger, config) + wsSrv.Start() + return httpSrv, httpSrvDone, nil +} + +func ConnectTmWS(tmRPCAddr, tmEndpoint string, logger tmlog.Logger) *rpcclient.WSClient { + tmWsClient, err := rpcclient.NewWS(tmRPCAddr, tmEndpoint, + rpcclient.MaxReconnectAttempts(256), + rpcclient.ReadWait(120*time.Second), + rpcclient.WriteWait(120*time.Second), + rpcclient.PingPeriod(50*time.Second), + rpcclient.OnReconnect(func() { + logger.Debug("EVM RPC reconnects to Tendermint WS", "address", tmRPCAddr+tmEndpoint) + }), + ) + + if err != nil { + logger.Error( + "Tendermint WS client could not be created", + "address", tmRPCAddr+tmEndpoint, + "error", err, + ) + } else if err := tmWsClient.OnStart(); err != nil { + logger.Error( + "Tendermint WS client could not start", + "address", tmRPCAddr+tmEndpoint, + "error", err, + ) + } + + return tmWsClient +} + +// Listen starts a net.Listener on the tcp network on the given address. +// If there is a specified MaxOpenConnections in the config, it will also set the limitListener. +func Listen(addr string, config *config.Config) (net.Listener, error) { + if addr == "" { + addr = ":http" + } + ln, err := net.Listen("tcp", addr) + if err != nil { + return nil, err + } + if config.JSONRPC.MaxOpenConnections > 0 { + ln = netutil.LimitListener(ln, config.JSONRPC.MaxOpenConnections) + } + return ln, err +} diff --git a/server/jsonrpc/backend/account_info.go b/server/jsonrpc/backend/account_info.go new file mode 100644 index 0000000000..5070a8b0e5 --- /dev/null +++ b/server/jsonrpc/backend/account_info.go @@ -0,0 +1,36 @@ +package backend + +import ( + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + rpctypes "github.com/evmos/ethermint/rpc/types" + + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" +) + +func (b *Backend) GetBalance(address common.Address, blockNrOrHash rpctypes.BlockNumberOrHash) (*hexutil.Big, error) { + blockNum, err := b.BlockNumberFromTendermint(blockNrOrHash) + if err != nil { + return nil, err + } + + _, err = b.TendermintBlockByNumber(blockNum) + if err != nil { + return nil, err + } + + req := &banktypes.QueryBalanceRequest{ + Address: address.String(), + Denom: "BNB", + } + queryClient := banktypes.NewQueryClient(b.clientCtx) + + res, err := queryClient.Balance(b.ctx, req) + if err != nil { + return (*hexutil.Big)(big.NewInt(0)), err + } + + return (*hexutil.Big)(res.Balance.Amount.BigInt()), nil +} diff --git a/server/jsonrpc/backend/backend.go b/server/jsonrpc/backend/backend.go new file mode 100644 index 0000000000..bd98326d9d --- /dev/null +++ b/server/jsonrpc/backend/backend.go @@ -0,0 +1,155 @@ +package backend + +import ( + "context" + "math/big" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + ethtypes "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/params" + "github.com/ethereum/go-ethereum/rpc" + "github.com/ethereum/go-ethereum/signer/core/apitypes" + rpctypes "github.com/evmos/ethermint/rpc/types" + "github.com/evmos/ethermint/server/config" + ethermint "github.com/evmos/ethermint/types" + evmtypes "github.com/evmos/ethermint/x/evm/types" + "github.com/spf13/viper" + "github.com/tendermint/tendermint/libs/log" + tmrpctypes "github.com/tendermint/tendermint/rpc/core/types" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/crypto/keyring" + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// BackendI implements the EVM backend. +// this server is only meant to be used for wallets connection, so the wallets can sign EIP712 typed msg +// it doesn't include other unnecessary EVM rpc apis like eth_call +type BackendI interface { + EVMBackend +} + +// EVMBackend implements the functionality shared within ethereum namespaces +// as defined by EIP-1474: https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1474.md +// Implemented by Backend. +type EVMBackend interface { + // Node specific queries + Accounts() ([]common.Address, error) + Syncing() (interface{}, error) + SetEtherbase(etherbase common.Address) bool + SetGasPrice(gasPrice hexutil.Big) bool + ImportRawKey(privkey, password string) (common.Address, error) + ListAccounts() ([]common.Address, error) + NewMnemonic(uid string, language keyring.Language, hdPath, bip39Passphrase string, algo keyring.SignatureAlgo) (*keyring.Record, error) + UnprotectedAllowed() bool + RPCGasCap() uint64 // global gas cap for eth_call over rpc: DoS protection + RPCEVMTimeout() time.Duration // global timeout for eth_call over rpc: DoS protection + RPCTxFeeCap() float64 // RPCTxFeeCap is the global transaction fee(price * gaslimit) cap for send-transaction variants. The unit is ether. + RPCMinGasPrice() int64 + + // Sign Tx + Sign(address common.Address, data hexutil.Bytes) (hexutil.Bytes, error) + SendTransaction(args evmtypes.TransactionArgs) (common.Hash, error) + SignTypedData(address common.Address, typedData apitypes.TypedData) (hexutil.Bytes, error) + + // Blocks Info + BlockNumber() (hexutil.Uint64, error) + GetBlockByNumber(blockNum rpctypes.BlockNumber, fullTx bool) (map[string]interface{}, error) + GetBlockByHash(hash common.Hash, fullTx bool) (map[string]interface{}, error) + GetBlockTransactionCountByHash(hash common.Hash) *hexutil.Uint + GetBlockTransactionCountByNumber(blockNum rpctypes.BlockNumber) *hexutil.Uint + TendermintBlockByNumber(blockNum rpctypes.BlockNumber) (*tmrpctypes.ResultBlock, error) + TendermintBlockResultByNumber(height *int64) (*tmrpctypes.ResultBlockResults, error) + TendermintBlockByHash(blockHash common.Hash) (*tmrpctypes.ResultBlock, error) + BlockNumberFromTendermint(blockNrOrHash rpctypes.BlockNumberOrHash) (rpctypes.BlockNumber, error) + BlockNumberFromTendermintByHash(blockHash common.Hash) (*big.Int, error) + EthMsgsFromTendermintBlock(block *tmrpctypes.ResultBlock, blockRes *tmrpctypes.ResultBlockResults) []*evmtypes.MsgEthereumTx + BlockBloom(blockRes *tmrpctypes.ResultBlockResults) (ethtypes.Bloom, error) + HeaderByNumber(blockNum rpctypes.BlockNumber) (*ethtypes.Header, error) + HeaderByHash(blockHash common.Hash) (*ethtypes.Header, error) + RPCBlockFromTendermintBlock(resBlock *tmrpctypes.ResultBlock, blockRes *tmrpctypes.ResultBlockResults, fullTx bool) (map[string]interface{}, error) + EthBlockByNumber(blockNum rpctypes.BlockNumber) (*ethtypes.Block, error) + EthBlockFromTendermintBlock(resBlock *tmrpctypes.ResultBlock, blockRes *tmrpctypes.ResultBlockResults) (*ethtypes.Block, error) + + // Account Info + GetCode(address common.Address, blockNrOrHash rpctypes.BlockNumberOrHash) (hexutil.Bytes, error) + GetBalance(address common.Address, blockNrOrHash rpctypes.BlockNumberOrHash) (*hexutil.Big, error) + GetStorageAt(address common.Address, key string, blockNrOrHash rpctypes.BlockNumberOrHash) (hexutil.Bytes, error) + GetProof(address common.Address, storageKeys []string, blockNrOrHash rpctypes.BlockNumberOrHash) (*rpctypes.AccountResult, error) + GetTransactionCount(address common.Address, blockNum rpctypes.BlockNumber) (*hexutil.Uint64, error) + + // Chain Info + ChainID() (*hexutil.Big, error) + ChainConfig() *params.ChainConfig + GlobalMinGasPrice() (sdk.Dec, error) + BaseFee(blockRes *tmrpctypes.ResultBlockResults) (*big.Int, error) + CurrentHeader() *ethtypes.Header + PendingTransactions() ([]*sdk.Tx, error) + GetCoinbase() (sdk.AccAddress, error) + FeeHistory(blockCount rpc.DecimalOrHex, lastBlock rpc.BlockNumber, rewardPercentiles []float64) (*rpctypes.FeeHistoryResult, error) + SuggestGasTipCap(baseFee *big.Int) (*big.Int, error) + + // Tx Info + GetTransactionByHash(txHash common.Hash) (*rpctypes.RPCTransaction, error) + GetTxByEthHash(txHash common.Hash) (*ethermint.TxResult, error) + GetTxByTxIndex(height int64, txIndex uint) (*ethermint.TxResult, error) + GetTransactionByBlockAndIndex(block *tmrpctypes.ResultBlock, idx hexutil.Uint) (*rpctypes.RPCTransaction, error) + GetTransactionReceipt(hash common.Hash) (map[string]interface{}, error) + GetTransactionByBlockHashAndIndex(hash common.Hash, idx hexutil.Uint) (*rpctypes.RPCTransaction, error) + GetTransactionByBlockNumberAndIndex(blockNum rpctypes.BlockNumber, idx hexutil.Uint) (*rpctypes.RPCTransaction, error) + + // Send Transaction + Resend(args evmtypes.TransactionArgs, gasPrice *hexutil.Big, gasLimit *hexutil.Uint64) (common.Hash, error) + SendRawTransaction(data hexutil.Bytes) (common.Hash, error) + SetTxDefaults(args evmtypes.TransactionArgs) (evmtypes.TransactionArgs, error) + EstimateGas(args evmtypes.TransactionArgs, blockNrOptional *rpctypes.BlockNumber) (hexutil.Uint64, error) + DoCall(args evmtypes.TransactionArgs, blockNr rpctypes.BlockNumber) (*evmtypes.MsgEthereumTxResponse, error) + GasPrice() (*hexutil.Big, error) + + // Filter API + GetLogs(hash common.Hash) ([][]*ethtypes.Log, error) + GetLogsByHeight(height *int64) ([][]*ethtypes.Log, error) + BloomStatus() (uint64, uint64) + + // Tracing + TraceTransaction(hash common.Hash, config *evmtypes.TraceConfig) (interface{}, error) + TraceBlock(height rpctypes.BlockNumber, config *evmtypes.TraceConfig, block *tmrpctypes.ResultBlock) ([]*evmtypes.TxTraceResult, error) +} + +var _ BackendI = (*Backend)(nil) + +// Backend implements the BackendI interface +type Backend struct { + ctx context.Context + clientCtx client.Context + logger log.Logger + chainID *big.Int + cfg config.Config +} + +// NewBackend creates a new Backend instance for cosmos and ethereum namespaces +func NewBackend( + viper *viper.Viper, + logger log.Logger, + clientCtx client.Context, +) *Backend { + chainID, err := ethermint.ParseChainID(clientCtx.ChainID) + if err != nil { + panic(err) + } + + appConf, err := config.GetConfig(viper) + if err != nil { + panic(err) + } + + return &Backend{ + ctx: context.Background(), + clientCtx: clientCtx, + logger: logger.With("json-rpc", "backend"), + chainID: chainID, + cfg: appConf, + } +} diff --git a/server/jsonrpc/backend/blocks.go b/server/jsonrpc/backend/blocks.go new file mode 100644 index 0000000000..027a257a92 --- /dev/null +++ b/server/jsonrpc/backend/blocks.go @@ -0,0 +1,193 @@ +package backend + +import ( + "fmt" + "math/big" + "strconv" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + ethtypes "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/trie" + rpctypes "github.com/evmos/ethermint/rpc/types" + "github.com/pkg/errors" + tmrpctypes "github.com/tendermint/tendermint/rpc/core/types" + tmtypes "github.com/tendermint/tendermint/types" + "google.golang.org/grpc" + "google.golang.org/grpc/metadata" + + grpctypes "github.com/cosmos/cosmos-sdk/types/grpc" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" +) + +// BlockNumber returns the current block number in abci app state. Because abci +// app state could lag behind from tendermint the latest block, it's more stable for +// the client to use the latest block number in abci app state than tendermint +// rpc. +func (b *Backend) BlockNumber() (hexutil.Uint64, error) { + // do any grpc query, ignore the response and use the returned block height + var header metadata.MD + queryClient := banktypes.NewQueryClient(b.clientCtx) + + _, err := queryClient.Balance(b.ctx, &banktypes.QueryBalanceRequest{Address: "0x0000000000000000000000000000000000000000", Denom: "BNB"}, grpc.Header(&header)) + if err != nil { + return hexutil.Uint64(0), err + } + + blockHeightHeader := header.Get(grpctypes.GRPCBlockHeightHeader) + if headerLen := len(blockHeightHeader); headerLen != 1 { + return 0, fmt.Errorf("unexpected '%s' gRPC header length; got %d, expected: %d", grpctypes.GRPCBlockHeightHeader, headerLen, 1) + } + + height, err := strconv.ParseUint(blockHeightHeader[0], 10, 64) + if err != nil { + return 0, fmt.Errorf("failed to parse block height: %w", err) + } + + return hexutil.Uint64(height), nil +} + +// GetBlockByNumber returns the JSON-RPC compatible Ethereum block identified by +// block number. Depending on fullTx it either returns the full transaction +// objects or if false only the hashes of the transactions. +func (b *Backend) GetBlockByNumber(blockNum rpctypes.BlockNumber, fullTx bool) (map[string]interface{}, error) { + resBlock, err := b.TendermintBlockByNumber(blockNum) + if err != nil { + return nil, nil + } + + // return if requested block height is greater than the current one + if resBlock == nil || resBlock.Block == nil { + return nil, nil + } + + res, err := b.RPCBlockFromTendermintBlock(resBlock, nil, false) + if err != nil { + b.logger.Debug("GetEthBlockFromTendermint failed", "height", blockNum, "error", err.Error()) + return nil, err + } + + return res, nil +} + +// TendermintBlockByNumber returns a Tendermint-formatted block for a given +// block number +func (b *Backend) TendermintBlockByNumber(blockNum rpctypes.BlockNumber) (*tmrpctypes.ResultBlock, error) { + height := blockNum.Int64() + if height <= 0 { + // fetch the latest block number from the app state, more accurate than the tendermint block store state. + n, err := b.BlockNumber() + if err != nil { + return nil, err + } + height = int64(n) + } + resBlock, err := b.clientCtx.Client.Block(b.ctx, &height) + if err != nil { + b.logger.Debug("tendermint client failed to get block", "height", height, "error", err.Error()) + return nil, err + } + + if resBlock.Block == nil { + b.logger.Debug("TendermintBlockByNumber block not found", "height", height) + return nil, nil + } + + return resBlock, nil +} + +// BlockNumberFromTendermint returns the BlockNumber from BlockNumberOrHash +func (b *Backend) BlockNumberFromTendermint(blockNrOrHash rpctypes.BlockNumberOrHash) (rpctypes.BlockNumber, error) { + switch { + case blockNrOrHash.BlockHash == nil && blockNrOrHash.BlockNumber == nil: + return rpctypes.EthEarliestBlockNumber, fmt.Errorf("types BlockHash and BlockNumber cannot be both nil") + case blockNrOrHash.BlockNumber != nil: + return *blockNrOrHash.BlockNumber, nil + default: + return rpctypes.EthEarliestBlockNumber, nil + } +} + +// HeaderByNumber returns the block header identified by height. +func (b *Backend) HeaderByNumber(blockNum rpctypes.BlockNumber) (*ethtypes.Header, error) { + resBlock, err := b.TendermintBlockByNumber(blockNum) + if err != nil { + return nil, err + } + + if resBlock == nil { + return nil, errors.Errorf("block not found for height %d", blockNum) + } + + ethHeader := EthHeaderFromTendermint(resBlock.Block.Header) + return ethHeader, nil +} + +// RPCBlockFromTendermintBlock returns a JSON-RPC compatible Ethereum block from a +// given Tendermint block and its block result. +func (b *Backend) RPCBlockFromTendermintBlock( + resBlock *tmrpctypes.ResultBlock, + blockRes *tmrpctypes.ResultBlockResults, + fullTx bool, +) (map[string]interface{}, error) { + block := resBlock.Block + + formattedBlock := rpctypes.FormatBlock( + block.Header, block.Size(), + 0, new(big.Int).SetUint64(0), + []interface{}{}, ethtypes.Bloom{}, common.Address{}, nil, + ) + return formattedBlock, nil +} + +// EthBlockByNumber returns the Ethereum Block identified by number. +func (b *Backend) EthBlockByNumber(blockNum rpctypes.BlockNumber) (*ethtypes.Block, error) { + resBlock, err := b.TendermintBlockByNumber(blockNum) + if err != nil { + return nil, err + } + if resBlock == nil { + // block not found + return nil, fmt.Errorf("block not found for height %d", blockNum) + } + + return b.EthBlockFromTendermintBlock(resBlock, nil) +} + +// EthBlockFromTendermintBlock returns an Ethereum Block type from Tendermint block +// EthBlockFromTendermintBlock +func (b *Backend) EthBlockFromTendermintBlock( + resBlock *tmrpctypes.ResultBlock, + blockRes *tmrpctypes.ResultBlockResults, +) (*ethtypes.Block, error) { + block := resBlock.Block + + ethHeader := EthHeaderFromTendermint(block.Header) + + ethBlock := ethtypes.NewBlock(ethHeader, nil, nil, nil, trie.NewStackTrie(nil)) + return ethBlock, nil +} + +func EthHeaderFromTendermint(header tmtypes.Header) *ethtypes.Header { + txHash := ethtypes.EmptyRootHash + if len(header.DataHash) == 0 { + txHash = common.BytesToHash(header.DataHash) + } + + return ðtypes.Header{ + ParentHash: common.BytesToHash(header.LastBlockID.Hash.Bytes()), + UncleHash: ethtypes.EmptyUncleHash, + Coinbase: common.BytesToAddress(header.ProposerAddress), + Root: common.BytesToHash(header.AppHash), + TxHash: txHash, + ReceiptHash: ethtypes.EmptyRootHash, + Difficulty: big.NewInt(0), + Number: big.NewInt(header.Height), + GasLimit: 0, + GasUsed: 0, + Time: uint64(header.Time.UTC().Unix()), + Extra: []byte{}, + MixDigest: common.Hash{}, + Nonce: ethtypes.BlockNonce{}, + } +} diff --git a/server/jsonrpc/backend/chain_info.go b/server/jsonrpc/backend/chain_info.go new file mode 100644 index 0000000000..b75427379e --- /dev/null +++ b/server/jsonrpc/backend/chain_info.go @@ -0,0 +1,24 @@ +package backend + +import ( + "github.com/ethereum/go-ethereum/common/hexutil" + ethtypes "github.com/ethereum/go-ethereum/core/types" + rpctypes "github.com/evmos/ethermint/rpc/types" + ethermint "github.com/evmos/ethermint/types" +) + +// ChainID is the chain id for the current chain config. +func (b *Backend) ChainID() (*hexutil.Big, error) { + chainID, err := ethermint.ParseChainID(b.clientCtx.ChainID) + if err != nil { + panic(err) + } + + return (*hexutil.Big)(chainID), nil +} + +// CurrentHeader returns the latest block header +func (b *Backend) CurrentHeader() *ethtypes.Header { + header, _ := b.HeaderByNumber(rpctypes.EthLatestBlockNumber) + return header +} diff --git a/server/jsonrpc/backend/unimplemented.go b/server/jsonrpc/backend/unimplemented.go new file mode 100644 index 0000000000..ab98294cee --- /dev/null +++ b/server/jsonrpc/backend/unimplemented.go @@ -0,0 +1,251 @@ +package backend + +import ( + "fmt" + "math/big" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + ethtypes "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/params" + "github.com/ethereum/go-ethereum/rpc" + "github.com/ethereum/go-ethereum/signer/core/apitypes" + rpctypes "github.com/evmos/ethermint/rpc/types" + ethermint "github.com/evmos/ethermint/types" + evmtypes "github.com/evmos/ethermint/x/evm/types" + tmrpctypes "github.com/tendermint/tendermint/rpc/core/types" + + "github.com/cosmos/cosmos-sdk/crypto/keyring" + sdk "github.com/cosmos/cosmos-sdk/types" +) + +func (b *Backend) TendermintBlockResultByNumber(height *int64) (*tmrpctypes.ResultBlockResults, error) { + return nil, fmt.Errorf("should not be called") +} + +func (b *Backend) EthMsgsFromTendermintBlock(block *tmrpctypes.ResultBlock, blockRes *tmrpctypes.ResultBlockResults) []*evmtypes.MsgEthereumTx { + panic("should not be called") +} + +func (b *Backend) ChainConfig() *params.ChainConfig { + panic("should not be called") +} + +func (b *Backend) GetCoinbase() (sdk.AccAddress, error) { + return nil, fmt.Errorf("should not be called") +} + +func (b *Backend) FeeHistory(blockCount rpc.DecimalOrHex, lastBlock rpc.BlockNumber, rewardPercentiles []float64) (*rpctypes.FeeHistoryResult, error) { + return nil, fmt.Errorf("should not be called") +} + +func (b *Backend) SuggestGasTipCap(baseFee *big.Int) (*big.Int, error) { + return nil, fmt.Errorf("should not be called") +} + +func (b *Backend) RPCFilterCap() int32 { + panic("should not be called") +} + +func (b *Backend) RPCLogsCap() int32 { + panic("should not be called") +} + +func (b *Backend) RPCBlockRangeCap() int32 { + panic("should not be called") +} + +func (b *Backend) Accounts() ([]common.Address, error) { + return nil, fmt.Errorf("should not be called") +} + +func (b *Backend) Syncing() (interface{}, error) { + return nil, fmt.Errorf("should not be called") +} + +func (b *Backend) SetEtherbase(etherbase common.Address) bool { + panic("should not be called") +} + +func (b *Backend) SetGasPrice(gasPrice hexutil.Big) bool { + panic("should not be called") +} + +func (b *Backend) ImportRawKey(privkey, password string) (common.Address, error) { + return common.Address{}, fmt.Errorf("should not be called") +} + +func (b *Backend) ListAccounts() ([]common.Address, error) { + return nil, fmt.Errorf("should not be called") +} + +func (b *Backend) NewMnemonic(uid string, language keyring.Language, hdPath, bip39Passphrase string, algo keyring.SignatureAlgo) (*keyring.Record, error) { + return nil, fmt.Errorf("should not be called") +} + +func (b *Backend) UnprotectedAllowed() bool { + panic("should not be called") +} + +func (b *Backend) RPCGasCap() uint64 { + panic("should not be called") +} + +func (b *Backend) RPCEVMTimeout() time.Duration { + panic("should not be called") +} + +func (b *Backend) RPCTxFeeCap() float64 { + panic("should not be called") +} + +func (b *Backend) RPCMinGasPrice() int64 { + panic("should not be called") +} + +func (b *Backend) Sign(address common.Address, data hexutil.Bytes) (hexutil.Bytes, error) { + return nil, fmt.Errorf("should not be called") +} + +func (b *Backend) SendTransaction(args evmtypes.TransactionArgs) (common.Hash, error) { + return common.Hash{}, fmt.Errorf("should not be called") +} + +func (b *Backend) SignTypedData(address common.Address, typedData apitypes.TypedData) (hexutil.Bytes, error) { + return nil, fmt.Errorf("should not be called") +} + +func (b *Backend) GetCode(address common.Address, blockNrOrHash rpctypes.BlockNumberOrHash) (hexutil.Bytes, error) { + return nil, fmt.Errorf("should not be called") +} + +func (b *Backend) GetStorageAt(address common.Address, key string, blockNrOrHash rpctypes.BlockNumberOrHash) (hexutil.Bytes, error) { + return nil, fmt.Errorf("should not be called") +} + +func (b *Backend) GetProof(address common.Address, storageKeys []string, blockNrOrHash rpctypes.BlockNumberOrHash) (*rpctypes.AccountResult, error) { + return nil, fmt.Errorf("should not be called") +} + +func (b *Backend) GetTransactionCount(address common.Address, blockNum rpctypes.BlockNumber) (*hexutil.Uint64, error) { + return nil, fmt.Errorf("should not be called") +} + +func (b *Backend) GetTransactionByHash(txHash common.Hash) (*rpctypes.RPCTransaction, error) { + return nil, fmt.Errorf("should not be called") +} + +func (b *Backend) GetTxByEthHash(txHash common.Hash) (*ethermint.TxResult, error) { + return nil, fmt.Errorf("should not be called") +} + +func (b *Backend) GetTxByTxIndex(height int64, txIndex uint) (*ethermint.TxResult, error) { + return nil, fmt.Errorf("should not be called") +} + +func (b *Backend) GetTransactionByBlockAndIndex(block *tmrpctypes.ResultBlock, idx hexutil.Uint) (*rpctypes.RPCTransaction, error) { + return nil, fmt.Errorf("should not be called") +} + +func (b *Backend) GetTransactionReceipt(hash common.Hash) (map[string]interface{}, error) { + return nil, fmt.Errorf("should not be called") +} + +func (b *Backend) GetTransactionByBlockHashAndIndex(hash common.Hash, idx hexutil.Uint) (*rpctypes.RPCTransaction, error) { + return nil, fmt.Errorf("should not be called") +} + +func (b *Backend) GetTransactionByBlockNumberAndIndex(blockNum rpctypes.BlockNumber, idx hexutil.Uint) (*rpctypes.RPCTransaction, error) { + return nil, fmt.Errorf("should not be called") +} + +func (b *Backend) GetLogs(hash common.Hash) ([][]*ethtypes.Log, error) { + return nil, fmt.Errorf("should not be called") +} + +func (b *Backend) GetLogsByHeight(height *int64) ([][]*ethtypes.Log, error) { + return nil, fmt.Errorf("should not be called") +} + +func (b *Backend) BloomStatus() (uint64, uint64) { + panic("should not be called") +} + +func (b *Backend) TraceTransaction(hash common.Hash, config *evmtypes.TraceConfig) (interface{}, error) { + return nil, fmt.Errorf("should not be called") +} + +func (b *Backend) TraceBlock(height rpctypes.BlockNumber, config *evmtypes.TraceConfig, block *tmrpctypes.ResultBlock) ([]*evmtypes.TxTraceResult, error) { + return nil, fmt.Errorf("should not be called") +} + +func (b *Backend) GetBlockByHash(hash common.Hash, fullTx bool) (map[string]interface{}, error) { + return nil, fmt.Errorf("should not be called") +} + +func (b *Backend) GetBlockTransactionCountByHash(hash common.Hash) *hexutil.Uint { + panic("should not be called") +} + +func (b *Backend) GetBlockTransactionCountByNumber(blockNum rpctypes.BlockNumber) *hexutil.Uint { + panic("should not be called") +} + +func (b *Backend) GetBlockTransactionCount(block *tmrpctypes.ResultBlock) *hexutil.Uint { + panic("should not be called") +} + +func (b *Backend) TendermintBlockByHash(blockHash common.Hash) (*tmrpctypes.ResultBlock, error) { + return nil, fmt.Errorf("should not be called") +} + +func (b *Backend) BlockNumberFromTendermintByHash(blockHash common.Hash) (*big.Int, error) { + return nil, fmt.Errorf("should not be called") +} + +func (b *Backend) HeaderByHash(blockHash common.Hash) (*ethtypes.Header, error) { + return nil, fmt.Errorf("should not be called") +} + +func (b *Backend) BlockBloom(blockRes *tmrpctypes.ResultBlockResults) (ethtypes.Bloom, error) { + return ethtypes.Bloom{}, fmt.Errorf("should not be called") +} + +func (b *Backend) PendingTransactions() ([]*sdk.Tx, error) { + return nil, fmt.Errorf("should not be called") +} + +func (b *Backend) GlobalMinGasPrice() (sdk.Dec, error) { + return sdk.Dec{}, fmt.Errorf("should not be called") +} + +func (b *Backend) BaseFee(blockRes *tmrpctypes.ResultBlockResults) (*big.Int, error) { + return nil, fmt.Errorf("should not be called") +} + +func (b *Backend) EstimateGas(args evmtypes.TransactionArgs, blockNrOptional *rpctypes.BlockNumber) (hexutil.Uint64, error) { + return hexutil.Uint64(0), fmt.Errorf("should not be called") +} + +func (b *Backend) DoCall( + args evmtypes.TransactionArgs, blockNr rpctypes.BlockNumber, +) (*evmtypes.MsgEthereumTxResponse, error) { + return nil, fmt.Errorf("should not be called") +} + +func (b *Backend) Resend(args evmtypes.TransactionArgs, gasPrice *hexutil.Big, gasLimit *hexutil.Uint64) (common.Hash, error) { + return common.Hash{}, fmt.Errorf("should not be called") +} + +func (b *Backend) SendRawTransaction(data hexutil.Bytes) (common.Hash, error) { + return common.Hash{}, fmt.Errorf("should not be called") +} + +func (b *Backend) SetTxDefaults(args evmtypes.TransactionArgs) (evmtypes.TransactionArgs, error) { + return evmtypes.TransactionArgs{}, fmt.Errorf("should not be called") +} + +func (b *Backend) GasPrice() (*hexutil.Big, error) { + return (*hexutil.Big)(big.NewInt(0)), fmt.Errorf("should not be called") +} diff --git a/server/jsonrpc/namespaces/ethereum/eth/api.go b/server/jsonrpc/namespaces/ethereum/eth/api.go new file mode 100644 index 0000000000..340a7958ce --- /dev/null +++ b/server/jsonrpc/namespaces/ethereum/eth/api.go @@ -0,0 +1,405 @@ +package eth + +import ( + "context" + "fmt" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + ethtypes "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/rpc" + "github.com/ethereum/go-ethereum/signer/core/apitypes" + rpctypes "github.com/evmos/ethermint/rpc/types" + ethermint "github.com/evmos/ethermint/types" + evmtypes "github.com/evmos/ethermint/x/evm/types" + "github.com/tendermint/tendermint/libs/log" + + "github.com/cosmos/cosmos-sdk/server/jsonrpc/backend" +) + +type EthereumAPI interface { + // Getting Blocks + // + // Retrieves information from a particular block in the blockchain. + BlockNumber() (hexutil.Uint64, error) + GetBlockByNumber(ethBlockNum rpctypes.BlockNumber, fullTx bool) (map[string]interface{}, error) + GetBlockByHash(hash common.Hash, fullTx bool) (map[string]interface{}, error) + GetBlockTransactionCountByHash(hash common.Hash) *hexutil.Uint + GetBlockTransactionCountByNumber(blockNum rpctypes.BlockNumber) *hexutil.Uint + + // Reading Transactions + // + // Retrieves information on the state data for addresses regardless of whether + // it is a user or a smart contract. + GetTransactionByHash(hash common.Hash) (*rpctypes.RPCTransaction, error) + GetTransactionCount(address common.Address, blockNrOrHash rpctypes.BlockNumberOrHash) (*hexutil.Uint64, error) + GetTransactionReceipt(hash common.Hash) (map[string]interface{}, error) + GetTransactionByBlockHashAndIndex(hash common.Hash, idx hexutil.Uint) (*rpctypes.RPCTransaction, error) + GetTransactionByBlockNumberAndIndex(blockNum rpctypes.BlockNumber, idx hexutil.Uint) (*rpctypes.RPCTransaction, error) + // eth_getBlockReceipts + + // Writing Transactions + // + // Allows developers to both send ETH from one address to another, write data + // on-chain, and interact with smart contracts. + SendRawTransaction(data hexutil.Bytes) (common.Hash, error) + SendTransaction(args evmtypes.TransactionArgs) (common.Hash, error) + // eth_sendPrivateTransaction + // eth_cancel PrivateTransaction + + // Account Information + // + // Returns information regarding an address's stored on-chain data. + Accounts() ([]common.Address, error) + GetBalance(address common.Address, blockNrOrHash rpctypes.BlockNumberOrHash) (*hexutil.Big, error) + GetStorageAt(address common.Address, key string, blockNrOrHash rpctypes.BlockNumberOrHash) (hexutil.Bytes, error) + GetCode(address common.Address, blockNrOrHash rpctypes.BlockNumberOrHash) (hexutil.Bytes, error) + GetProof(address common.Address, storageKeys []string, blockNrOrHash rpctypes.BlockNumberOrHash) (*rpctypes.AccountResult, error) + + // EVM/Smart Contract Execution + // + // Allows developers to read data from the blockchain which includes executing + // smart contracts. However, no data is published to the Ethereum network. + Call(args evmtypes.TransactionArgs, blockNrOrHash rpctypes.BlockNumberOrHash, _ *rpctypes.StateOverride) (hexutil.Bytes, error) + + // Chain Information + // + // Returns information on the Ethereum network and internal settings. + ProtocolVersion() hexutil.Uint + GasPrice() (*hexutil.Big, error) + EstimateGas(args evmtypes.TransactionArgs, blockNrOptional *rpctypes.BlockNumber) (hexutil.Uint64, error) + FeeHistory(blockCount rpc.DecimalOrHex, lastBlock rpc.BlockNumber, rewardPercentiles []float64) (*rpctypes.FeeHistoryResult, error) + MaxPriorityFeePerGas() (*hexutil.Big, error) + ChainId() (*hexutil.Big, error) + + // Getting Uncles + // + // Returns information on uncle blocks are which are network rejected blocks and replaced by a canonical block instead. + GetUncleByBlockHashAndIndex(hash common.Hash, idx hexutil.Uint) map[string]interface{} + GetUncleByBlockNumberAndIndex(number, idx hexutil.Uint) map[string]interface{} + GetUncleCountByBlockHash(hash common.Hash) hexutil.Uint + GetUncleCountByBlockNumber(blockNum rpctypes.BlockNumber) hexutil.Uint + + // Proof of Work + Hashrate() hexutil.Uint64 + Mining() bool + + // Other + Syncing() (interface{}, error) + Coinbase() (string, error) + Sign(address common.Address, data hexutil.Bytes) (hexutil.Bytes, error) + GetTransactionLogs(txHash common.Hash) ([]*ethtypes.Log, error) + SignTypedData(address common.Address, typedData apitypes.TypedData) (hexutil.Bytes, error) + FillTransaction(args evmtypes.TransactionArgs) (*rpctypes.SignTransactionResult, error) + Resend(ctx context.Context, args evmtypes.TransactionArgs, gasPrice *hexutil.Big, gasLimit *hexutil.Uint64) (common.Hash, error) + GetPendingTransactions() ([]*rpctypes.RPCTransaction, error) + // eth_signTransaction (on Ethereum.org) + // eth_getCompilers (on Ethereum.org) + // eth_compileSolidity (on Ethereum.org) + // eth_compileLLL (on Ethereum.org) + // eth_compileSerpent (on Ethereum.org) + // eth_getWork (on Ethereum.org) + // eth_submitWork (on Ethereum.org) + // eth_submitHashrate (on Ethereum.org) +} + +var _ EthereumAPI = (*PublicAPI)(nil) + +// PublicAPI is the eth_ prefixed set of APIs in the Web3 JSON-RPC spec. +type PublicAPI struct { + ctx context.Context + logger log.Logger + backend backend.EVMBackend +} + +// NewPublicAPI creates an instance of the public ETH Web3 API. +func NewPublicAPI(logger log.Logger, backend backend.EVMBackend) *PublicAPI { + api := &PublicAPI{ + ctx: context.Background(), + logger: logger.With("json-rpc", "public-api"), + backend: backend, + } + + return api +} + +// ///////////////////////////////////////////////////////////////////////////// +// / Blocks /// +// ///////////////////////////////////////////////////////////////////////////// + +// BlockNumber returns the current block number. +func (e *PublicAPI) BlockNumber() (hexutil.Uint64, error) { + e.logger.Debug("eth_blockNumber") + return e.backend.BlockNumber() +} + +// GetBlockByNumber returns the block identified by number. +func (e *PublicAPI) GetBlockByNumber(ethBlockNum rpctypes.BlockNumber, fullTx bool) (map[string]interface{}, error) { + e.logger.Debug("eth_getBlockByNumber", "number", ethBlockNum, "full", fullTx) + return e.backend.GetBlockByNumber(ethBlockNum, fullTx) +} + +// GetBlockByHash returns the block identified by hash. +func (e *PublicAPI) GetBlockByHash(hash common.Hash, fullTx bool) (map[string]interface{}, error) { + e.logger.Debug("eth_getBlockByHash", "hash", hash.Hex(), "full", fullTx) + return e.backend.GetBlockByHash(hash, fullTx) +} + +// ///////////////////////////////////////////////////////////////////////////// +// / Read Txs /// +// ///////////////////////////////////////////////////////////////////////////// + +// GetTransactionByHash returns the transaction identified by hash. +func (e *PublicAPI) GetTransactionByHash(hash common.Hash) (*rpctypes.RPCTransaction, error) { + e.logger.Debug("eth_getTransactionByHash", "hash", hash.Hex()) + return e.backend.GetTransactionByHash(hash) +} + +// GetTransactionCount returns the number of transactions at the given address up to the given block number. +func (e *PublicAPI) GetTransactionCount(address common.Address, blockNrOrHash rpctypes.BlockNumberOrHash) (*hexutil.Uint64, error) { + e.logger.Debug("eth_getTransactionCount", "address", address.Hex(), "block number or hash", blockNrOrHash) + blockNum, err := e.backend.BlockNumberFromTendermint(blockNrOrHash) + if err != nil { + return nil, err + } + return e.backend.GetTransactionCount(address, blockNum) +} + +// GetTransactionReceipt returns the transaction receipt identified by hash. +func (e *PublicAPI) GetTransactionReceipt(hash common.Hash) (map[string]interface{}, error) { + hexTx := hash.Hex() + e.logger.Debug("eth_getTransactionReceipt", "hash", hexTx) + return e.backend.GetTransactionReceipt(hash) +} + +// GetBlockTransactionCountByHash returns the number of transactions in the block identified by hash. +func (e *PublicAPI) GetBlockTransactionCountByHash(hash common.Hash) *hexutil.Uint { + e.logger.Debug("eth_getBlockTransactionCountByHash", "hash", hash.Hex()) + return e.backend.GetBlockTransactionCountByHash(hash) +} + +// GetBlockTransactionCountByNumber returns the number of transactions in the block identified by number. +func (e *PublicAPI) GetBlockTransactionCountByNumber(blockNum rpctypes.BlockNumber) *hexutil.Uint { + e.logger.Debug("eth_getBlockTransactionCountByNumber", "height", blockNum.Int64()) + return e.backend.GetBlockTransactionCountByNumber(blockNum) +} + +// GetTransactionByBlockHashAndIndex returns the transaction identified by hash and index. +func (e *PublicAPI) GetTransactionByBlockHashAndIndex(hash common.Hash, idx hexutil.Uint) (*rpctypes.RPCTransaction, error) { + e.logger.Debug("eth_getTransactionByBlockHashAndIndex", "hash", hash.Hex(), "index", idx) + return e.backend.GetTransactionByBlockHashAndIndex(hash, idx) +} + +// GetTransactionByBlockNumberAndIndex returns the transaction identified by number and index. +func (e *PublicAPI) GetTransactionByBlockNumberAndIndex(blockNum rpctypes.BlockNumber, idx hexutil.Uint) (*rpctypes.RPCTransaction, error) { + e.logger.Debug("eth_getTransactionByBlockNumberAndIndex", "number", blockNum, "index", idx) + return e.backend.GetTransactionByBlockNumberAndIndex(blockNum, idx) +} + +// ///////////////////////////////////////////////////////////////////////////// +// / Write Txs /// +// ///////////////////////////////////////////////////////////////////////////// + +// SendRawTransaction send a raw Ethereum transaction. +func (e *PublicAPI) SendRawTransaction(data hexutil.Bytes) (common.Hash, error) { + e.logger.Debug("eth_sendRawTransaction", "length", len(data)) + return e.backend.SendRawTransaction(data) +} + +// SendTransaction sends an Ethereum transaction. +func (e *PublicAPI) SendTransaction(args evmtypes.TransactionArgs) (common.Hash, error) { + e.logger.Debug("eth_sendTransaction", "args", args.String()) + return e.backend.SendTransaction(args) +} + +// ///////////////////////////////////////////////////////////////////////////// +// / Account Information /// +// ///////////////////////////////////////////////////////////////////////////// + +// Accounts returns the list of accounts available to this node. +func (e *PublicAPI) Accounts() ([]common.Address, error) { + e.logger.Debug("eth_accounts") + return e.backend.Accounts() +} + +// GetBalance returns the provided account's balance up to the provided block number. +func (e *PublicAPI) GetBalance(address common.Address, blockNrOrHash rpctypes.BlockNumberOrHash) (*hexutil.Big, error) { + e.logger.Debug("eth_getBalance", "address", address.String(), "block number or hash", blockNrOrHash) + return e.backend.GetBalance(address, blockNrOrHash) +} + +// GetStorageAt returns the contract storage at the given address, block number, and key. +func (e *PublicAPI) GetStorageAt(address common.Address, key string, blockNrOrHash rpctypes.BlockNumberOrHash) (hexutil.Bytes, error) { + e.logger.Debug("eth_getStorageAt", "address", address.Hex(), "key", key, "block number or hash", blockNrOrHash) + return e.backend.GetStorageAt(address, key, blockNrOrHash) +} + +// GetCode returns the contract code at the given address and block number. +func (e *PublicAPI) GetCode(address common.Address, blockNrOrHash rpctypes.BlockNumberOrHash) (hexutil.Bytes, error) { + e.logger.Debug("eth_getCode", "address", address.Hex(), "block number or hash", blockNrOrHash) + return e.backend.GetCode(address, blockNrOrHash) +} + +// GetProof returns an account object with proof and any storage proofs +func (e *PublicAPI) GetProof(address common.Address, + storageKeys []string, + blockNrOrHash rpctypes.BlockNumberOrHash, +) (*rpctypes.AccountResult, error) { + e.logger.Debug("eth_getProof", "address", address.Hex(), "keys", storageKeys, "block number or hash", blockNrOrHash) + return e.backend.GetProof(address, storageKeys, blockNrOrHash) +} + +// Call performs a raw contract call. +func (e *PublicAPI) Call(args evmtypes.TransactionArgs, + blockNrOrHash rpctypes.BlockNumberOrHash, + _ *rpctypes.StateOverride, +) (hexutil.Bytes, error) { + e.logger.Debug("eth_call", "args", args.String(), "block number or hash", blockNrOrHash) + return nil, fmt.Errorf("should not be called") +} + +// ///////////////////////////////////////////////////////////////////////////// +// / Chain Information /// +// ///////////////////////////////////////////////////////////////////////////// + +// ProtocolVersion returns the supported Ethereum protocol version. +func (e *PublicAPI) ProtocolVersion() hexutil.Uint { + e.logger.Debug("eth_protocolVersion") + return hexutil.Uint(ethermint.ProtocolVersion) +} + +// GasPrice returns the current gas price based on Ethermint's gas price oracle. +func (e *PublicAPI) GasPrice() (*hexutil.Big, error) { + e.logger.Debug("eth_gasPrice") + return e.backend.GasPrice() +} + +// EstimateGas returns an estimate of gas usage for the given smart contract call. +func (e *PublicAPI) EstimateGas(args evmtypes.TransactionArgs, blockNrOptional *rpctypes.BlockNumber) (hexutil.Uint64, error) { + e.logger.Debug("eth_estimateGas") + return e.backend.EstimateGas(args, blockNrOptional) +} + +func (e *PublicAPI) FeeHistory(blockCount rpc.DecimalOrHex, + lastBlock rpc.BlockNumber, + rewardPercentiles []float64, +) (*rpctypes.FeeHistoryResult, error) { + e.logger.Debug("eth_feeHistory") + return e.backend.FeeHistory(blockCount, lastBlock, rewardPercentiles) +} + +// MaxPriorityFeePerGas returns a suggestion for a gas tip cap for dynamic fee transactions. +func (e *PublicAPI) MaxPriorityFeePerGas() (*hexutil.Big, error) { + e.logger.Debug("eth_maxPriorityFeePerGas") + return nil, fmt.Errorf("should not be called") +} + +// ChainId is the EIP-155 replay-protection chain id for the current ethereum chain config. +func (e *PublicAPI) ChainId() (*hexutil.Big, error) { + e.logger.Debug("eth_chainId") + return e.backend.ChainID() +} + +// ///////////////////////////////////////////////////////////////////////////// +// / Uncles /// +// ///////////////////////////////////////////////////////////////////////////// + +// GetUncleByBlockHashAndIndex returns the uncle identified by hash and index. Always returns nil. +func (e *PublicAPI) GetUncleByBlockHashAndIndex(hash common.Hash, idx hexutil.Uint) map[string]interface{} { + return nil +} + +// GetUncleByBlockNumberAndIndex returns the uncle identified by number and index. Always returns nil. +func (e *PublicAPI) GetUncleByBlockNumberAndIndex(number, idx hexutil.Uint) map[string]interface{} { + return nil +} + +// GetUncleCountByBlockHash returns the number of uncles in the block identified by hash. Always zero. +func (e *PublicAPI) GetUncleCountByBlockHash(hash common.Hash) hexutil.Uint { + return 0 +} + +// GetUncleCountByBlockNumber returns the number of uncles in the block identified by number. Always zero. +func (e *PublicAPI) GetUncleCountByBlockNumber(blockNum rpctypes.BlockNumber) hexutil.Uint { + return 0 +} + +// ///////////////////////////////////////////////////////////////////////////// +// / Proof of Work /// +// ///////////////////////////////////////////////////////////////////////////// + +// Hashrate returns the current node's hashrate. Always 0. +func (e *PublicAPI) Hashrate() hexutil.Uint64 { + e.logger.Debug("eth_hashrate") + return 0 +} + +// Mining returns whether this node is currently mining. Always false. +func (e *PublicAPI) Mining() bool { + e.logger.Debug("eth_mining") + return false +} + +// ///////////////////////////////////////////////////////////////////////////// +// / Other /// +// ///////////////////////////////////////////////////////////////////////////// + +// Syncing returns false in case the node is currently not syncing with the network. It can be up-to-date or has not +// yet received the latest block headers from its pears. In case it is synchronizing: +// - startingBlock: block number this node started to synchronize from +// - currentBlock: block number this node is currently importing +// - highestBlock: block number of the highest block header this node has received from peers +// - pulledStates: number of state entries processed until now +// - knownStates: number of known state entries that still need to be pulled +func (e *PublicAPI) Syncing() (interface{}, error) { + e.logger.Debug("eth_syncing") + return e.backend.Syncing() +} + +// Coinbase is the address that staking rewards will be sent to (alias for Etherbase). +func (e *PublicAPI) Coinbase() (string, error) { + e.logger.Debug("eth_coinbase") + return "", fmt.Errorf("should not be called") +} + +// Sign signs the provided data using the private key of address via Geth's signature standard. +func (e *PublicAPI) Sign(address common.Address, data hexutil.Bytes) (hexutil.Bytes, error) { + e.logger.Debug("eth_sign", "address", address.Hex(), "data", common.Bytes2Hex(data)) + return e.backend.Sign(address, data) +} + +// GetTransactionLogs returns the logs given a transaction hash. +func (e *PublicAPI) GetTransactionLogs(txHash common.Hash) ([]*ethtypes.Log, error) { + e.logger.Debug("eth_getTransactionLogs", "hash", txHash) + return nil, fmt.Errorf("should not be called") +} + +// SignTypedData signs EIP-712 conformal typed data +func (e *PublicAPI) SignTypedData(address common.Address, typedData apitypes.TypedData) (hexutil.Bytes, error) { + e.logger.Debug("eth_signTypedData", "address", address.Hex(), "data", typedData) + return e.backend.SignTypedData(address, typedData) +} + +// FillTransaction fills the defaults (nonce, gas, gasPrice or 1559 fields) +// on a given unsigned transaction, and returns it to the caller for further +// processing (signing + broadcast). +func (e *PublicAPI) FillTransaction(args evmtypes.TransactionArgs) (*rpctypes.SignTransactionResult, error) { + return nil, fmt.Errorf("should not be called") +} + +// Resend accepts an existing transaction and a new gas price and limit. It will remove +// the given transaction from the pool and reinsert it with the new gas price and limit. +func (e *PublicAPI) Resend(_ context.Context, + args evmtypes.TransactionArgs, + gasPrice *hexutil.Big, + gasLimit *hexutil.Uint64, +) (common.Hash, error) { + e.logger.Debug("eth_resend", "args", args.String()) + return e.backend.Resend(args, gasPrice, gasLimit) +} + +// GetPendingTransactions returns the transactions that are in the transaction pool +// and have a from address that is one of the accounts this node manages. +func (e *PublicAPI) GetPendingTransactions() ([]*rpctypes.RPCTransaction, error) { + return nil, fmt.Errorf("should not be called") +} diff --git a/server/jsonrpc/namespaces/ethereum/net/api.go b/server/jsonrpc/namespaces/ethereum/net/api.go new file mode 100644 index 0000000000..8a5e087d0e --- /dev/null +++ b/server/jsonrpc/namespaces/ethereum/net/api.go @@ -0,0 +1,56 @@ +package net + +import ( + "context" + "fmt" + + ethermint "github.com/evmos/ethermint/types" + rpcclient "github.com/tendermint/tendermint/rpc/client" + + "github.com/cosmos/cosmos-sdk/client" +) + +// PublicAPI is the eth_ prefixed set of APIs in the Web3 JSON-RPC spec. +type PublicAPI struct { + networkVersion uint64 + tmClient rpcclient.Client +} + +// NewPublicAPI creates an instance of the public Net Web3 API. +func NewPublicAPI(clientCtx client.Context) *PublicAPI { + // parse the chainID from an integer string + chainIDEpoch, err := ethermint.ParseChainID(clientCtx.ChainID) + if err != nil { + panic(err) + } + + return &PublicAPI{ + networkVersion: chainIDEpoch.Uint64(), + tmClient: clientCtx.Client, + } +} + +// Version returns the current ethereum protocol version. +func (s *PublicAPI) Version() string { + return fmt.Sprintf("%d", s.networkVersion) +} + +// Listening returns if client is actively listening for network connections. +func (s *PublicAPI) Listening() bool { + ctx := context.Background() + netInfo, err := s.tmClient.NetInfo(ctx) + if err != nil { + return false + } + return netInfo.Listening +} + +// PeerCount returns the number of peers currently connected to the client. +func (s *PublicAPI) PeerCount() int { + ctx := context.Background() + netInfo, err := s.tmClient.NetInfo(ctx) + if err != nil { + return 0 + } + return len(netInfo.Peers) +} diff --git a/server/mock/app.go b/server/mock/app.go index 8132b538d6..4427774d20 100644 --- a/server/mock/app.go +++ b/server/mock/app.go @@ -6,10 +6,9 @@ import ( "fmt" "path/filepath" - "github.com/tendermint/tendermint/types" - abci "github.com/tendermint/tendermint/abci/types" "github.com/tendermint/tendermint/libs/log" + "github.com/tendermint/tendermint/types" bam "github.com/cosmos/cosmos-sdk/baseapp" "github.com/cosmos/cosmos-sdk/codec" diff --git a/server/rollback.go b/server/rollback.go index c30e2f6f58..a88fdd92ac 100644 --- a/server/rollback.go +++ b/server/rollback.go @@ -3,12 +3,13 @@ package server import ( "fmt" - "github.com/cosmos/cosmos-sdk/client/flags" - serverconfig "github.com/cosmos/cosmos-sdk/server/config" - "github.com/cosmos/cosmos-sdk/server/types" "github.com/spf13/cobra" tmcmd "github.com/tendermint/tendermint/cmd/tendermint/commands" "github.com/tendermint/tendermint/node" + + "github.com/cosmos/cosmos-sdk/client/flags" + serverconfig "github.com/cosmos/cosmos-sdk/server/config" + "github.com/cosmos/cosmos-sdk/server/types" ) // NewRollbackCmd creates a command to rollback tendermint and multistore state by one height. diff --git a/server/rosetta/client_offline.go b/server/rosetta/client_offline.go index 1dca56de22..64af0dc037 100644 --- a/server/rosetta/client_offline.go +++ b/server/rosetta/client_offline.go @@ -7,7 +7,6 @@ import ( "github.com/coinbase/rosetta-sdk-go/types" crgerrs "github.com/cosmos/cosmos-sdk/server/rosetta/lib/errors" - sdk "github.com/cosmos/cosmos-sdk/types" ) diff --git a/server/rosetta/client_online.go b/server/rosetta/client_online.go index f3a24ac7da..2ffa8a3f5b 100644 --- a/server/rosetta/client_online.go +++ b/server/rosetta/client_online.go @@ -11,26 +11,21 @@ import ( "strconv" "time" - "github.com/cosmos/cosmos-sdk/version" - - abcitypes "github.com/tendermint/tendermint/abci/types" - rosettatypes "github.com/coinbase/rosetta-sdk-go/types" - "google.golang.org/grpc/metadata" - + abcitypes "github.com/tendermint/tendermint/abci/types" + tmrpc "github.com/tendermint/tendermint/rpc/client" "github.com/tendermint/tendermint/rpc/client/http" "google.golang.org/grpc" + "google.golang.org/grpc/metadata" crgerrs "github.com/cosmos/cosmos-sdk/server/rosetta/lib/errors" crgtypes "github.com/cosmos/cosmos-sdk/server/rosetta/lib/types" - sdk "github.com/cosmos/cosmos-sdk/types" grpctypes "github.com/cosmos/cosmos-sdk/types/grpc" + "github.com/cosmos/cosmos-sdk/version" authtx "github.com/cosmos/cosmos-sdk/x/auth/tx" auth "github.com/cosmos/cosmos-sdk/x/auth/types" bank "github.com/cosmos/cosmos-sdk/x/bank/types" - - tmrpc "github.com/tendermint/tendermint/rpc/client" ) // interface assertion diff --git a/server/rosetta/config.go b/server/rosetta/config.go index a6492cd1f1..f0952f72f5 100644 --- a/server/rosetta/config.go +++ b/server/rosetta/config.go @@ -8,11 +8,10 @@ import ( "github.com/coinbase/rosetta-sdk-go/types" "github.com/spf13/pflag" - crg "github.com/cosmos/cosmos-sdk/server/rosetta/lib/server" - clientflags "github.com/cosmos/cosmos-sdk/client/flags" "github.com/cosmos/cosmos-sdk/codec" codectypes "github.com/cosmos/cosmos-sdk/codec/types" + crg "github.com/cosmos/cosmos-sdk/server/rosetta/lib/server" sdk "github.com/cosmos/cosmos-sdk/types" ) diff --git a/server/rosetta/lib/errors/errors.go b/server/rosetta/lib/errors/errors.go index ebceb070ce..489226f1dc 100644 --- a/server/rosetta/lib/errors/errors.go +++ b/server/rosetta/lib/errors/errors.go @@ -6,10 +6,9 @@ package errors import ( "fmt" + "github.com/coinbase/rosetta-sdk-go/types" grpccodes "google.golang.org/grpc/codes" grpcstatus "google.golang.org/grpc/status" - - "github.com/coinbase/rosetta-sdk-go/types" ) // ListErrors lists all the registered errors diff --git a/server/rosetta/lib/internal/service/construction.go b/server/rosetta/lib/internal/service/construction.go index 6f2d0f85da..671df3080f 100644 --- a/server/rosetta/lib/internal/service/construction.go +++ b/server/rosetta/lib/internal/service/construction.go @@ -7,11 +7,10 @@ import ( "strconv" "strings" - sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/coinbase/rosetta-sdk-go/types" "github.com/cosmos/cosmos-sdk/server/rosetta/lib/errors" + sdk "github.com/cosmos/cosmos-sdk/types" ) // ConstructionCombine Combine creates a network-specific transaction from an unsigned transaction diff --git a/server/rosetta/lib/internal/service/data.go b/server/rosetta/lib/internal/service/data.go index c814ff4c72..e7193880f7 100644 --- a/server/rosetta/lib/internal/service/data.go +++ b/server/rosetta/lib/internal/service/data.go @@ -4,6 +4,7 @@ import ( "context" "github.com/coinbase/rosetta-sdk-go/types" + "github.com/cosmos/cosmos-sdk/server/rosetta/lib/errors" crgtypes "github.com/cosmos/cosmos-sdk/server/rosetta/lib/types" ) diff --git a/server/start.go b/server/start.go index b0536138a0..b1c3f4025b 100644 --- a/server/start.go +++ b/server/start.go @@ -3,6 +3,7 @@ package server // DONTCOVER import ( + "context" "fmt" "net" "net/http" @@ -78,6 +79,19 @@ const ( flagGRPCAddress = "grpc.address" flagGRPCWebEnable = "grpc-web.enable" flagGRPCWebAddress = "grpc-web.address" + + // jsonrpc-related flags + JSONRPCEnable = "json-rpc.enable" + JSONRPCAPI = "json-rpc.api" + JSONRPCAddress = "json-rpc.address" + JSONWsAddress = "json-rpc.ws-address" + JSONRPCHTTPTimeout = "json-rpc.http-timeout" + JSONRPCHTTPIdleTimeout = "json-rpc.http-idle-timeout" + JSONRPCMaxOpenConnections = "json-rpc.max-open-connections" + + // tls-related flags + TLSCertPath = "tls.certificate-path" + TLSKeyPath = "tls.key-path" ) // StartCmd runs the service passed in, either stand-alone or in-process with @@ -188,6 +202,17 @@ is performed. Note, when enabled, gRPC will also be automatically enabled. cmd.Flags().Bool(FlagDisableIAVLFastNode, false, "Disable fast node for IAVL tree") + cmd.Flags().Bool(JSONRPCEnable, true, "Define if the JSON-RPC server should be enabled") + cmd.Flags().StringSlice(JSONRPCAPI, serverconfig.GetDefaultAPINamespaces(), "Defines a list of JSON-RPC namespaces that should be enabled") + cmd.Flags().String(JSONRPCAddress, serverconfig.DefaultJSONRPCAddress, "the JSON-RPC server address to listen on") + cmd.Flags().String(JSONWsAddress, serverconfig.DefaultJSONRPCWsAddress, "the JSON-RPC WS server address to listen on") + cmd.Flags().Duration(JSONRPCHTTPTimeout, serverconfig.DefaultHTTPTimeout, "Sets a read/write timeout for json-rpc http server (0=infinite)") + cmd.Flags().Duration(JSONRPCHTTPIdleTimeout, serverconfig.DefaultHTTPIdleTimeout, "Sets a idle timeout for json-rpc http server (0=infinite)") + cmd.Flags().Int(JSONRPCMaxOpenConnections, serverconfig.DefaultMaxOpenConnections, "Sets the maximum number of simultaneous connections for the server listener") //nolint:lll + + cmd.Flags().String(TLSCertPath, "", "the cert.pem file path for the server TLS configuration") + cmd.Flags().String(TLSKeyPath, "", "the key.pem file path for the server TLS configuration") + // add support for all Tendermint-specific command line options tcmd.AddNodeFlags(cmd) return cmd @@ -441,6 +466,40 @@ func startInProcess(ctx *Context, clientCtx client.Context, appCreator types.App } } + var ( + httpSrv *http.Server + httpSrvDone chan struct{} + ) + + if config.JSONRPC.Enable { + genDoc, err := genDocProvider() + if err != nil { + return err + } + + clientCtx := clientCtx.WithChainID(genDoc.ChainID) + + tmEndpoint := "/websocket" + tmRPCAddr := cfg.RPC.ListenAddress + httpSrv, httpSrvDone, err = StartJSONRPC(ctx, clientCtx, tmRPCAddr, tmEndpoint, &config) + if err != nil { + return err + } + defer func() { + shutdownCtx, cancelFn := context.WithTimeout(context.Background(), 10*time.Second) + defer cancelFn() + if err := httpSrv.Shutdown(shutdownCtx); err != nil { + ctx.Logger.Error("HTTP server shutdown produced a warning", "error", err.Error()) + } else { + ctx.Logger.Info("HTTP server shut down, waiting 5 sec") + select { + case <-time.Tick(5 * time.Second): + case <-httpSrvDone: + } + } + }() + } + // At this point it is safe to block the process if we're in gRPC only mode as // we do not need to start Rosetta or handle any Tendermint related processes. if gRPCOnly { diff --git a/server/websockets.go b/server/websockets.go new file mode 100644 index 0000000000..1cff61f863 --- /dev/null +++ b/server/websockets.go @@ -0,0 +1,226 @@ +package server + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "math/big" + "net/http" + "sync" + + "github.com/ethereum/go-ethereum/rpc" + "github.com/evmos/ethermint/rpc/ethereum/pubsub" + "github.com/gorilla/mux" + "github.com/gorilla/websocket" + "github.com/pkg/errors" + "github.com/tendermint/tendermint/libs/log" + + "github.com/cosmos/cosmos-sdk/server/config" +) + +type WebsocketsServer interface { + Start() +} + +type ErrorResponseJSON struct { + Jsonrpc string `json:"jsonrpc"` + Error *ErrorMessageJSON `json:"error"` + ID *big.Int `json:"id"` +} + +type ErrorMessageJSON struct { + Code *big.Int `json:"code"` + Message string `json:"message"` +} + +type websocketsServer struct { + rpcAddr string // listen address of rest-server + wsAddr string // listen address of ws server + certFile string + keyFile string + logger log.Logger +} + +func NewWebsocketsServer(logger log.Logger, cfg *config.Config) WebsocketsServer { + logger = logger.With("module", "websocket-server") + + return &websocketsServer{ + rpcAddr: cfg.JSONRPC.Address, + wsAddr: cfg.JSONRPC.WsAddress, + certFile: cfg.TLS.CertificatePath, + keyFile: cfg.TLS.KeyPath, + logger: logger, + } +} + +func (s *websocketsServer) Start() { + ws := mux.NewRouter() + ws.Handle("/", s) + + go func() { + var err error + /* #nosec G114 -- http functions have no support for timeouts */ + if s.certFile == "" || s.keyFile == "" { + err = http.ListenAndServe(s.wsAddr, ws) + } else { + err = http.ListenAndServeTLS(s.wsAddr, s.certFile, s.keyFile, ws) + } + + if err != nil { + if err == http.ErrServerClosed { + return + } + + s.logger.Error("failed to start HTTP server for WS", "error", err.Error()) + } + }() +} + +func (s *websocketsServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { + upgrader := websocket.Upgrader{ + CheckOrigin: func(r *http.Request) bool { + return true + }, + } + + conn, err := upgrader.Upgrade(w, r, nil) + if err != nil { + s.logger.Debug("websocket upgrade failed", "error", err.Error()) + return + } + + s.readLoop(&wsConn{ + mux: new(sync.Mutex), + conn: conn, + }) +} + +func (s *websocketsServer) sendErrResponse(wsConn *wsConn, msg string) { + res := &ErrorResponseJSON{ + Jsonrpc: "2.0", + Error: &ErrorMessageJSON{ + Code: big.NewInt(-32600), + Message: msg, + }, + ID: nil, + } + + _ = wsConn.WriteJSON(res) +} + +type wsConn struct { + conn *websocket.Conn + mux *sync.Mutex +} + +func (w *wsConn) WriteJSON(v interface{}) error { + w.mux.Lock() + defer w.mux.Unlock() + + return w.conn.WriteJSON(v) +} + +func (w *wsConn) Close() error { + w.mux.Lock() + defer w.mux.Unlock() + + return w.conn.Close() +} + +func (w *wsConn) ReadMessage() (messageType int, p []byte, err error) { + // not protected by write mutex + + return w.conn.ReadMessage() +} + +func (s *websocketsServer) readLoop(wsConn *wsConn) { + // subscriptions of current connection + subscriptions := make(map[rpc.ID]pubsub.UnsubscribeFunc) + defer func() { + // cancel all subscriptions when connection closed + for _, unsubFn := range subscriptions { + unsubFn() + } + }() + + for { + _, mb, err := wsConn.ReadMessage() + if err != nil { + _ = wsConn.Close() + s.logger.Error("read message error, breaking read loop", "error", err.Error()) + return + } + + if isBatch(mb) { + if err := s.tcpGetAndSendResponse(wsConn, mb); err != nil { + s.sendErrResponse(wsConn, err.Error()) + } + continue + } + + var msg map[string]interface{} + if err = json.Unmarshal(mb, &msg); err != nil { + s.sendErrResponse(wsConn, err.Error()) + continue + } + + _, ok := msg["id"].(float64) + if !ok { + s.sendErrResponse( + wsConn, + fmt.Errorf("invalid type for connection ID: %T", msg["id"]).Error(), + ) + continue + } + + if err := s.tcpGetAndSendResponse(wsConn, mb); err != nil { + s.sendErrResponse(wsConn, err.Error()) + } + } +} + +// tcpGetAndSendResponse connects to the rest-server over tcp, posts a JSON-RPC request, and sends the response +// to the client over websockets +func (s *websocketsServer) tcpGetAndSendResponse(wsConn *wsConn, mb []byte) error { + req, err := http.NewRequestWithContext(context.Background(), "POST", "http://"+s.rpcAddr, bytes.NewBuffer(mb)) + if err != nil { + return errors.Wrap(err, "Could not build request") + } + + req.Header.Set("Content-Type", "application/json") + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return errors.Wrap(err, "Could not perform request") + } + + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return errors.Wrap(err, "could not read body from response") + } + + var wsSend interface{} + err = json.Unmarshal(body, &wsSend) + if err != nil { + return errors.Wrap(err, "failed to unmarshal rest-server response") + } + + return wsConn.WriteJSON(wsSend) +} + +// copy from github.com/ethereum/go-ethereum/rpc/json.go +// isBatch returns true when the first non-whitespace characters is '[' +func isBatch(raw []byte) bool { + for _, c := range raw { + // skip insignificant whitespace (http://www.ietf.org/rfc/rfc4627.txt) + if c == 0x20 || c == 0x09 || c == 0x0a || c == 0x0d { + continue + } + return c == '[' + } + return false +}