diff --git a/README.md b/README.md index b9c2c4d6..3cc41cb1 100644 --- a/README.md +++ b/README.md @@ -74,3 +74,7 @@ Blockbook stores data the key-value store RocksDB. Database format is described ## API Blockbook API is described [here](/docs/api.md). + +## Environment variables + +List of environment variables that affect Blockbook's behavior is [here](/docs/env.md). diff --git a/bchain/basechain.go b/bchain/basechain.go index cecd2954..df494531 100644 --- a/bchain/basechain.go +++ b/bchain/basechain.go @@ -76,3 +76,8 @@ func (b *BaseChain) EthereumTypeGetSupportedStakingPools() []string { func (b *BaseChain) EthereumTypeGetStakingPoolsData(addrDesc AddressDescriptor) ([]StakingPoolData, error) { return nil, errors.New("not supported") } + +// EthereumTypeEthCall calls eth_call with given data and to address +func (b *BaseChain) EthereumTypeEthCall(data, to, from string) (string, error) { + return "", errors.New("not supported") +} diff --git a/bchain/coins/blockchain.go b/bchain/coins/blockchain.go index dcb51667..cdc877ca 100644 --- a/bchain/coins/blockchain.go +++ b/bchain/coins/blockchain.go @@ -342,6 +342,12 @@ func (c *blockChainWithMetrics) EthereumTypeGetStakingPoolsData(addrDesc bchain. return c.b.EthereumTypeGetStakingPoolsData(addrDesc) } +// EthereumTypeEthCall calls eth_call with given data and to address +func (c *blockChainWithMetrics) EthereumTypeEthCall(data, to, from string) (v string, err error) { + defer func(s time.Time) { c.observeRPCLatency("EthereumTypeEthCall", s, err) }(time.Now()) + return c.b.EthereumTypeEthCall(data, to, from) +} + type mempoolWithMetrics struct { mempool bchain.Mempool m *common.Metrics diff --git a/bchain/coins/eth/contract.go b/bchain/coins/eth/contract.go index 34916346..0d3bd237 100644 --- a/bchain/coins/eth/contract.go +++ b/bchain/coins/eth/contract.go @@ -273,14 +273,19 @@ func contractGetTransfersFromTx(tx *bchain.RpcTransaction) (bchain.TokenTransfer return r, nil } -func (b *EthereumRPC) ethCall(data, to string) (string, error) { +// EthereumTypeEthCall calls eth_call with given data and to address +func (b *EthereumRPC) EthereumTypeEthCall(data, to, from string) (string, error) { ctx, cancel := context.WithTimeout(context.Background(), b.Timeout) defer cancel() var r string - err := b.RPC.CallContext(ctx, &r, "eth_call", map[string]interface{}{ + args := map[string]interface{}{ "data": data, "to": to, - }, "latest") + } + if from != "" { + args["from"] = from + } + err := b.RPC.CallContext(ctx, &r, "eth_call", args, "latest") if err != nil { return "", err } @@ -289,7 +294,7 @@ func (b *EthereumRPC) ethCall(data, to string) (string, error) { func (b *EthereumRPC) fetchContractInfo(address string) (*bchain.ContractInfo, error) { var contract bchain.ContractInfo - data, err := b.ethCall(contractNameSignature, address) + data, err := b.EthereumTypeEthCall(contractNameSignature, address, "") if err != nil { // ignore the error from the eth_call - since geth v1.9.15 they changed the behavior // and returning error "execution reverted" for some non contract addresses @@ -300,14 +305,14 @@ func (b *EthereumRPC) fetchContractInfo(address string) (*bchain.ContractInfo, e } name := strings.TrimSpace(parseSimpleStringProperty(data)) if name != "" { - data, err = b.ethCall(contractSymbolSignature, address) + data, err = b.EthereumTypeEthCall(contractSymbolSignature, address, "") if err != nil { // glog.Warning(errors.Annotatef(err, "Contract SymbolSignature %v", address)) return nil, nil // return nil, errors.Annotatef(err, "erc20SymbolSignature %v", address) } symbol := strings.TrimSpace(parseSimpleStringProperty(data)) - data, _ = b.ethCall(contractDecimalsSignature, address) + data, _ = b.EthereumTypeEthCall(contractDecimalsSignature, address, "") // if err != nil { // glog.Warning(errors.Annotatef(err, "Contract DecimalsSignature %v", address)) // // return nil, errors.Annotatef(err, "erc20DecimalsSignature %v", address) @@ -340,7 +345,7 @@ func (b *EthereumRPC) EthereumTypeGetErc20ContractBalance(addrDesc, contractDesc addr := hexutil.Encode(addrDesc)[2:] contract := hexutil.Encode(contractDesc) req := contractBalanceOfSignature + "0000000000000000000000000000000000000000000000000000000000000000"[len(addr):] + addr - data, err := b.ethCall(req, contract) + data, err := b.EthereumTypeEthCall(req, contract, "") if err != nil { return nil, err } @@ -364,7 +369,7 @@ func (b *EthereumRPC) GetTokenURI(contractDesc bchain.AddressDescriptor, tokenID } // try ERC721 tokenURI method and ERC1155 uri method for _, method := range []string{erc721TokenURIMethodSignature, erc1155URIMethodSignature} { - data, err := b.ethCall(method+id, address) + data, err := b.EthereumTypeEthCall(method+id, address, "") if err == nil && data != "" { uri := parseSimpleStringProperty(data) // try to sanitize the URI returned from the contract diff --git a/bchain/coins/eth/stakingpool.go b/bchain/coins/eth/stakingpool.go index 30820390..4a40057a 100644 --- a/bchain/coins/eth/stakingpool.go +++ b/bchain/coins/eth/stakingpool.go @@ -61,7 +61,7 @@ func isZeroBigInt(b *big.Int) bool { func (b *EthereumRPC) everstakeBalanceTypeContractCall(signature, addr, contract string) (string, error) { req := signature + "0000000000000000000000000000000000000000000000000000000000000000"[len(addr):] + addr - return b.ethCall(req, contract) + return b.EthereumTypeEthCall(req, contract, "") } func (b *EthereumRPC) everstakeContractCallSimpleNumeric(signature, addr, contract string) (*big.Int, error) { diff --git a/bchain/types.go b/bchain/types.go index a27431ab..ee29be9d 100644 --- a/bchain/types.go +++ b/bchain/types.go @@ -335,6 +335,7 @@ type BlockChain interface { EthereumTypeGetErc20ContractBalance(addrDesc, contractDesc AddressDescriptor) (*big.Int, error) EthereumTypeGetSupportedStakingPools() []string EthereumTypeGetStakingPoolsData(addrDesc AddressDescriptor) ([]StakingPoolData, error) + EthereumTypeEthCall(data, to, from string) (string, error) GetTokenURI(contractDesc AddressDescriptor, tokenID *big.Int) (string, error) } diff --git a/blockbook-api.ts b/blockbook-api.ts index 2f08c6e7..ca722a37 100644 --- a/blockbook-api.ts +++ b/blockbook-api.ts @@ -261,6 +261,7 @@ export interface InternalStateColumn { } export interface BlockbookInfo { coin: string; + network: string; host: string; version: string; gitCommit: string; @@ -447,6 +448,14 @@ export interface WsMempoolFiltersReq { fromTimestamp: number; M?: number; } +export interface WsEthCallReq { + from?: string; + to: string; + data: string; +} +export interface WsEthCallRes { + data: string; +} export interface MempoolTxidFilterEntries { entries?: { [key: string]: string }; usedZeroedKey?: boolean; diff --git a/build/tools/typescriptify/typescriptify.go b/build/tools/typescriptify/typescriptify.go index 4b19d4a2..5e24f620 100644 --- a/build/tools/typescriptify/typescriptify.go +++ b/build/tools/typescriptify/typescriptify.go @@ -60,6 +60,8 @@ func main() { t.Add(server.WsFiatRatesForTimestampsReq{}) t.Add(server.WsFiatRatesTickersListReq{}) t.Add(server.WsMempoolFiltersReq{}) + t.Add(server.WsEthCallReq{}) + t.Add(server.WsEthCallRes{}) t.Add(bchain.MempoolTxidFilterEntries{}) err := t.ConvertToFile("blockbook-api.ts") diff --git a/docs/env.md b/docs/env.md new file mode 100644 index 00000000..98383453 --- /dev/null +++ b/docs/env.md @@ -0,0 +1,11 @@ +# Environment variables + +Some behavior of Blockbook can be modified by environment variables. The variables usually start with a coin shortcut to allow to run multiple Blockbooks on a single server. + +- `_WS_GETACCOUNTINFO_LIMIT` - Limits the number of `getAccountInfo` requests per websocket connection to reduce server abuse. Accepts number as input. + +- `_STAKING_POOL_CONTRACT` - The pool name and contract used for Ethereum staking. The format of the variable is `/`. If missing, staking support is disabled. + +- `COINGECKO_API_KEY` or `_COINGECKO_API_KEY` - API key for making requests to CoinGecko in the paid tier. + +- `_ALLOWED_ETH_CALL_CONTRACTS` - Contract addresses for which `ethCall` websocket requests can be made, as a comma-separated list. If omitted, `ethCall` is enabled for all addresses. diff --git a/server/public_ethereumtype_test.go b/server/public_ethereumtype_test.go index 1edb8ab2..b7a6c6c9 100644 --- a/server/public_ethereumtype_test.go +++ b/server/public_ethereumtype_test.go @@ -134,6 +134,27 @@ func httpTestsEthereumType(t *testing.T, ts *httptest.Server) { performHttpTests(tests, t, ts) } +var websocketTestsEthereumType = []websocketTest{ + { + name: "websocket getInfo", + req: websocketReq{ + Method: "getInfo", + }, + want: `{"id":"0","data":{"name":"Fakecoin","shortcut":"FAKE","network":"FAKE","decimals":18,"version":"unknown","bestHeight":4321001,"bestHash":"0x2b57e15e93a0ed197417a34c2498b7187df79099572c04a6b6e6ff418f74e6ee","block0Hash":"","testnet":true,"backend":{"version":"001001","subversion":"/Fakecoin:0.0.1/"}}}`, + }, + { + name: "websocket ethCall", + req: websocketReq{ + Method: "ethCall", + Params: WsEthCallReq{ + To: "0xcdA9FC258358EcaA88845f19Af595e908bb7EfE9", + Data: "0x4567", + }, + }, + want: `{"id":"1","data":{"data":"0x4567abcd"}}`, + }, +} + func initEthereumTypeDB(d *db.RocksDB) error { // add 0xa9059cbb transfer(address,uint256) signature wb := grocksdb.NewWriteBatch() @@ -238,4 +259,5 @@ func Test_PublicServer_EthereumType(t *testing.T) { defer ts.Close() httpTestsEthereumType(t, ts) + runWebsocketTests(t, ts, websocketTestsEthereumType) } diff --git a/server/public_test.go b/server/public_test.go index 84d5e1ca..9b8ed215 100644 --- a/server/public_test.go +++ b/server/public_test.go @@ -1484,9 +1484,20 @@ var websocketTestsBitcoinType = []websocketTest{ }, want: `{"id":"43","data":{"P":0,"M":1,"zeroedKey":false,"blockFilter":""}}`, }, + { + name: "websocket ethCall", + req: websocketReq{ + Method: "ethCall", + Params: WsEthCallReq{ + To: "0x123", + Data: "0x456", + }, + }, + want: `{"id":"44","data":{"error":{"message":"not supported"}}}`, + }, } -func runWebsocketTestsBitcoinType(t *testing.T, ts *httptest.Server, tests []websocketTest) { +func runWebsocketTests(t *testing.T, ts *httptest.Server, tests []websocketTest) { url := strings.Replace(ts.URL, "http://", "ws://", 1) + "/websocket" s, _, err := websocket.DefaultDialer.Dial(url, nil) if err != nil { @@ -1577,7 +1588,7 @@ func Test_PublicServer_BitcoinType(t *testing.T) { httpTestsBitcoinType(t, ts) socketioTestsBitcoinType(t, ts) - runWebsocketTestsBitcoinType(t, ts, websocketTestsBitcoinType) + runWebsocketTests(t, ts, websocketTestsBitcoinType) } func httpTestsBitcoinTypeExtendedIndex(t *testing.T, ts *httptest.Server) { @@ -1758,5 +1769,5 @@ func Test_PublicServer_BitcoinType_ExtendedIndex(t *testing.T) { defer ts.Close() httpTestsBitcoinTypeExtendedIndex(t, ts) - runWebsocketTestsBitcoinType(t, ts, websocketTestsBitcoinTypeExtendedIndex) + runWebsocketTests(t, ts, websocketTestsBitcoinTypeExtendedIndex) } diff --git a/server/websocket.go b/server/websocket.go index 9e628222..4a0745b4 100644 --- a/server/websocket.go +++ b/server/websocket.go @@ -4,6 +4,7 @@ import ( "encoding/json" "math/big" "net/http" + "os" "runtime/debug" "strconv" "strings" @@ -70,6 +71,7 @@ type WebsocketServer struct { fiatRatesSubscriptions map[string]map[*websocketChannel]string fiatRatesTokenSubscriptions map[*websocketChannel][]string fiatRatesSubscriptionsLock sync.Mutex + allowedEthCallContracts map[string]struct{} } // NewWebsocketServer creates new websocket interface to blockbook and returns its handle @@ -105,6 +107,14 @@ func NewWebsocketServer(db *db.RocksDB, chain bchain.BlockChain, mempool bchain. fiatRatesSubscriptions: make(map[string]map[*websocketChannel]string), fiatRatesTokenSubscriptions: make(map[*websocketChannel][]string), } + envEthCall := os.Getenv(strings.ToUpper(is.CoinShortcut) + "_ALLOWED_ETH_CALL_CONTRACTS") + if envEthCall != "" { + s.allowedEthCallContracts = make(map[string]struct{}) + for _, c := range strings.Split(envEthCall, ",") { + s.allowedEthCallContracts[strings.ToLower(c)] = struct{}{} + } + glog.Info("Support of ethCall for these contracts: ", envEthCall) + } return s, nil } @@ -391,6 +401,14 @@ var requestHandlers = map[string]func(*WebsocketServer, *websocketChannel, *WsRe } return }, + "ethCall": func(s *WebsocketServer, c *websocketChannel, req *WsReq) (rv interface{}, err error) { + r := WsEthCallReq{} + err = json.Unmarshal(req.Params, &r) + if err == nil { + rv, err = s.ethCall(&r) + } + return + }, "subscribeNewBlock": func(s *WebsocketServer, c *websocketChannel, req *WsReq) (rv interface{}, err error) { return s.subscribeNewBlock(c, req) }, @@ -747,6 +765,20 @@ func (s *WebsocketServer) getBlockFiltersBatch(r *WsBlockFiltersBatchReq) (res i }, nil } +func (s *WebsocketServer) ethCall(r *WsEthCallReq) (*WsEthCallRes, error) { + if s.allowedEthCallContracts != nil { + _, ok := s.allowedEthCallContracts[strings.ToLower(r.To)] + if !ok { + return nil, errors.New("Not supported") + } + } + data, err := s.chain.EthereumTypeEthCall(r.Data, r.To, r.From) + if err != nil { + return nil, err + } + return &WsEthCallRes{Data: data}, nil +} + type subscriptionResponse struct { Subscribed bool `json:"subscribed"` } diff --git a/server/ws_types.go b/server/ws_types.go index 0ebe0d19..248e1d32 100644 --- a/server/ws_types.go +++ b/server/ws_types.go @@ -138,3 +138,13 @@ type WsFiatRatesTickersListReq struct { Timestamp int64 `json:"timestamp,omitempty"` Token string `json:"token,omitempty"` } + +type WsEthCallReq struct { + From string `json:"from,omitempty"` + To string `json:"to"` + Data string `json:"data"` +} + +type WsEthCallRes struct { + Data string `json:"data"` +} diff --git a/static/test-websocket.html b/static/test-websocket.html index 3972aa53..4f2cc0f6 100644 --- a/static/test-websocket.html +++ b/static/test-websocket.html @@ -1,823 +1,1293 @@ - + - - - - - - - Blockbook Websocket Test Page - + + + +
+
+

Blockbook Websocket Test Page

+
+
+
+ +
+
+ +
+
+ +
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+ + + +
+
+
+
+
+
+
+
+
+ +
+
+
+ + +
+
+ + + + + +
+
+
+
+
+
+
+
+
+ +
+
+
+ +
+
+
+
+
+
+
+
+
+ +
+
+
+ +
+
+ + + + +
+
+
+
+
+
+
+
+
+ +
+
+
+ +
+
+
+
+
+
+
+
+
+ +
+
+
+ +
+
+
+
+
+
+
+
+
+ +
+
+
+ +
+
+ +
+
+
+
+
+
+
+
+
+ +
+
+ +
+
+
+
+
+
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+
+
+
+
+ +
+
+ +
+
+ +
+
+
+
+
+
+
+ +
+
+ +
+
+ +
+
+
+
+
+
+
+ +
+
+ +
+
+ +
+
+
+
+
+
+
+ +
+
+ + +
+
+
+
+
+
+
+ +
+
+
+ + + +
+
+
+
+
+
+
+
+ +
+
+
+ + + +
+
+
+
+
+
+
+
+ +
+
+ +
+
+ +
+
+
+
+
+
+
+ +
+
+ +
+
+ +
+
+
+
+
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+
+
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+
+
+
+

+ + - - - -
-
-

Blockbook Websocket Test Page

-
-
-
- -
-
- -
-
- -
-
-
-
- -
-
-
-
-
-
- -
-
-
-
-
-
- -
-
- -
-
-
-
-
-
-
-
-
- -
-
-
- - - -
-
-
-
-
-
-
-
-
-
- -
-
-
- - -
-
- - - - - -
-
-
-
-
-
-
-
-
-
- -
-
-
- -
-
-
-
-
-
-
-
-
-
- -
-
-
- -
-
- - - - -
-
-
-
-
-
-
-
-
-
- -
-
-
- -
-
-
-
-
-
-
-
-
-
- -
-
-
- -
-
-
-
-
-
-
-
-
-
- -
-
-
- -
-
- -
-
-
-
-
-
-
-
-
-
- -
-
- -
-
-
-
-
-
-
-
-
- -
-
- -
-
- -
-
- -
-
-
-
-
-
-
- -
-
- -
-
- -
-
-
-
-
-
-
- -
-
- -
-
- -
-
-
-
-
-
-
- -
-
- -
-
- -
-
-
-
-
-
-
- -
-
- - -
-
-
-
-
-
-
- -
-
-
- - - -
-
-
-
-
-
-
-
- -
-
- -
-
- -
-
-
-
-
-
-
- -
-
- -
-
- -
-
-
-
-
-
-
- -
-
- -
-
- -
-
- -
-
-
-
-
-
-
- -
-
- -
-
- -
-
- -
-
- -
-
-
-
-
-
-

- - - diff --git a/tests/dbtestdata/fakechain_ethereumtype.go b/tests/dbtestdata/fakechain_ethereumtype.go index 73e30b12..09abdfe9 100644 --- a/tests/dbtestdata/fakechain_ethereumtype.go +++ b/tests/dbtestdata/fakechain_ethereumtype.go @@ -134,6 +134,11 @@ func (c *fakeBlockChainEthereumType) EthereumTypeGetErc20ContractBalance(addrDes return big.NewInt(1000000000 + int64(addrDesc[0])*1000 + int64(contractDesc[0])), nil } +// EthereumTypeEthCall calls eth_call with given data and to address +func (c *fakeBlockChainEthereumType) EthereumTypeEthCall(data, to, from string) (string, error) { + return data + "abcd", nil +} + // GetTokenURI returns URI derived from the input contractDesc func (c *fakeBlockChainEthereumType) GetTokenURI(contractDesc bchain.AddressDescriptor, tokenID *big.Int) (string, error) { return "https://ipfs.io/ipfs/" + contractDesc.String()[3:] + ".json", nil