Add ethCall websocket request

This commit is contained in:
Martin Boehm 2024-09-06 14:06:02 +02:00
parent a1434979fa
commit 3cf7e6abe8
15 changed files with 1409 additions and 816 deletions

View File

@ -74,3 +74,7 @@ Blockbook stores data the key-value store RocksDB. Database format is described
## API ## API
Blockbook API is described [here](/docs/api.md). Blockbook API is described [here](/docs/api.md).
## Environment variables
List of environment variables that affect Blockbook's behavior is [here](/docs/env.md).

View File

@ -76,3 +76,8 @@ func (b *BaseChain) EthereumTypeGetSupportedStakingPools() []string {
func (b *BaseChain) EthereumTypeGetStakingPoolsData(addrDesc AddressDescriptor) ([]StakingPoolData, error) { func (b *BaseChain) EthereumTypeGetStakingPoolsData(addrDesc AddressDescriptor) ([]StakingPoolData, error) {
return nil, errors.New("not supported") 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")
}

View File

@ -342,6 +342,12 @@ func (c *blockChainWithMetrics) EthereumTypeGetStakingPoolsData(addrDesc bchain.
return c.b.EthereumTypeGetStakingPoolsData(addrDesc) 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 { type mempoolWithMetrics struct {
mempool bchain.Mempool mempool bchain.Mempool
m *common.Metrics m *common.Metrics

View File

@ -273,14 +273,19 @@ func contractGetTransfersFromTx(tx *bchain.RpcTransaction) (bchain.TokenTransfer
return r, nil 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) ctx, cancel := context.WithTimeout(context.Background(), b.Timeout)
defer cancel() defer cancel()
var r string var r string
err := b.RPC.CallContext(ctx, &r, "eth_call", map[string]interface{}{ args := map[string]interface{}{
"data": data, "data": data,
"to": to, "to": to,
}, "latest") }
if from != "" {
args["from"] = from
}
err := b.RPC.CallContext(ctx, &r, "eth_call", args, "latest")
if err != nil { if err != nil {
return "", err return "", err
} }
@ -289,7 +294,7 @@ func (b *EthereumRPC) ethCall(data, to string) (string, error) {
func (b *EthereumRPC) fetchContractInfo(address string) (*bchain.ContractInfo, error) { func (b *EthereumRPC) fetchContractInfo(address string) (*bchain.ContractInfo, error) {
var contract bchain.ContractInfo var contract bchain.ContractInfo
data, err := b.ethCall(contractNameSignature, address) data, err := b.EthereumTypeEthCall(contractNameSignature, address, "")
if err != nil { if err != nil {
// ignore the error from the eth_call - since geth v1.9.15 they changed the behavior // 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 // 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)) name := strings.TrimSpace(parseSimpleStringProperty(data))
if name != "" { if name != "" {
data, err = b.ethCall(contractSymbolSignature, address) data, err = b.EthereumTypeEthCall(contractSymbolSignature, address, "")
if err != nil { if err != nil {
// glog.Warning(errors.Annotatef(err, "Contract SymbolSignature %v", address)) // glog.Warning(errors.Annotatef(err, "Contract SymbolSignature %v", address))
return nil, nil return nil, nil
// return nil, errors.Annotatef(err, "erc20SymbolSignature %v", address) // return nil, errors.Annotatef(err, "erc20SymbolSignature %v", address)
} }
symbol := strings.TrimSpace(parseSimpleStringProperty(data)) symbol := strings.TrimSpace(parseSimpleStringProperty(data))
data, _ = b.ethCall(contractDecimalsSignature, address) data, _ = b.EthereumTypeEthCall(contractDecimalsSignature, address, "")
// if err != nil { // if err != nil {
// glog.Warning(errors.Annotatef(err, "Contract DecimalsSignature %v", address)) // glog.Warning(errors.Annotatef(err, "Contract DecimalsSignature %v", address))
// // return nil, errors.Annotatef(err, "erc20DecimalsSignature %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:] addr := hexutil.Encode(addrDesc)[2:]
contract := hexutil.Encode(contractDesc) contract := hexutil.Encode(contractDesc)
req := contractBalanceOfSignature + "0000000000000000000000000000000000000000000000000000000000000000"[len(addr):] + addr req := contractBalanceOfSignature + "0000000000000000000000000000000000000000000000000000000000000000"[len(addr):] + addr
data, err := b.ethCall(req, contract) data, err := b.EthereumTypeEthCall(req, contract, "")
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -364,7 +369,7 @@ func (b *EthereumRPC) GetTokenURI(contractDesc bchain.AddressDescriptor, tokenID
} }
// try ERC721 tokenURI method and ERC1155 uri method // try ERC721 tokenURI method and ERC1155 uri method
for _, method := range []string{erc721TokenURIMethodSignature, erc1155URIMethodSignature} { for _, method := range []string{erc721TokenURIMethodSignature, erc1155URIMethodSignature} {
data, err := b.ethCall(method+id, address) data, err := b.EthereumTypeEthCall(method+id, address, "")
if err == nil && data != "" { if err == nil && data != "" {
uri := parseSimpleStringProperty(data) uri := parseSimpleStringProperty(data)
// try to sanitize the URI returned from the contract // try to sanitize the URI returned from the contract

View File

@ -61,7 +61,7 @@ func isZeroBigInt(b *big.Int) bool {
func (b *EthereumRPC) everstakeBalanceTypeContractCall(signature, addr, contract string) (string, error) { func (b *EthereumRPC) everstakeBalanceTypeContractCall(signature, addr, contract string) (string, error) {
req := signature + "0000000000000000000000000000000000000000000000000000000000000000"[len(addr):] + addr 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) { func (b *EthereumRPC) everstakeContractCallSimpleNumeric(signature, addr, contract string) (*big.Int, error) {

View File

@ -335,6 +335,7 @@ type BlockChain interface {
EthereumTypeGetErc20ContractBalance(addrDesc, contractDesc AddressDescriptor) (*big.Int, error) EthereumTypeGetErc20ContractBalance(addrDesc, contractDesc AddressDescriptor) (*big.Int, error)
EthereumTypeGetSupportedStakingPools() []string EthereumTypeGetSupportedStakingPools() []string
EthereumTypeGetStakingPoolsData(addrDesc AddressDescriptor) ([]StakingPoolData, error) EthereumTypeGetStakingPoolsData(addrDesc AddressDescriptor) ([]StakingPoolData, error)
EthereumTypeEthCall(data, to, from string) (string, error)
GetTokenURI(contractDesc AddressDescriptor, tokenID *big.Int) (string, error) GetTokenURI(contractDesc AddressDescriptor, tokenID *big.Int) (string, error)
} }

View File

@ -261,6 +261,7 @@ export interface InternalStateColumn {
} }
export interface BlockbookInfo { export interface BlockbookInfo {
coin: string; coin: string;
network: string;
host: string; host: string;
version: string; version: string;
gitCommit: string; gitCommit: string;
@ -447,6 +448,14 @@ export interface WsMempoolFiltersReq {
fromTimestamp: number; fromTimestamp: number;
M?: number; M?: number;
} }
export interface WsEthCallReq {
from?: string;
to: string;
data: string;
}
export interface WsEthCallRes {
data: string;
}
export interface MempoolTxidFilterEntries { export interface MempoolTxidFilterEntries {
entries?: { [key: string]: string }; entries?: { [key: string]: string };
usedZeroedKey?: boolean; usedZeroedKey?: boolean;

View File

@ -60,6 +60,8 @@ func main() {
t.Add(server.WsFiatRatesForTimestampsReq{}) t.Add(server.WsFiatRatesForTimestampsReq{})
t.Add(server.WsFiatRatesTickersListReq{}) t.Add(server.WsFiatRatesTickersListReq{})
t.Add(server.WsMempoolFiltersReq{}) t.Add(server.WsMempoolFiltersReq{})
t.Add(server.WsEthCallReq{})
t.Add(server.WsEthCallRes{})
t.Add(bchain.MempoolTxidFilterEntries{}) t.Add(bchain.MempoolTxidFilterEntries{})
err := t.ConvertToFile("blockbook-api.ts") err := t.ConvertToFile("blockbook-api.ts")

11
docs/env.md Normal file
View File

@ -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.
- `<coin shortcut>_WS_GETACCOUNTINFO_LIMIT` - Limits the number of `getAccountInfo` requests per websocket connection to reduce server abuse. Accepts number as input.
- `<coin shortcut>_STAKING_POOL_CONTRACT` - The pool name and contract used for Ethereum staking. The format of the variable is `<pool name>/<pool contract>`. If missing, staking support is disabled.
- `COINGECKO_API_KEY` or `<coin shortcut>_COINGECKO_API_KEY` - API key for making requests to CoinGecko in the paid tier.
- `<coin shortcut>_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.

View File

@ -134,6 +134,27 @@ func httpTestsEthereumType(t *testing.T, ts *httptest.Server) {
performHttpTests(tests, t, ts) 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 { func initEthereumTypeDB(d *db.RocksDB) error {
// add 0xa9059cbb transfer(address,uint256) signature // add 0xa9059cbb transfer(address,uint256) signature
wb := grocksdb.NewWriteBatch() wb := grocksdb.NewWriteBatch()
@ -238,4 +259,5 @@ func Test_PublicServer_EthereumType(t *testing.T) {
defer ts.Close() defer ts.Close()
httpTestsEthereumType(t, ts) httpTestsEthereumType(t, ts)
runWebsocketTests(t, ts, websocketTestsEthereumType)
} }

View File

@ -1484,9 +1484,20 @@ var websocketTestsBitcoinType = []websocketTest{
}, },
want: `{"id":"43","data":{"P":0,"M":1,"zeroedKey":false,"blockFilter":""}}`, 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" url := strings.Replace(ts.URL, "http://", "ws://", 1) + "/websocket"
s, _, err := websocket.DefaultDialer.Dial(url, nil) s, _, err := websocket.DefaultDialer.Dial(url, nil)
if err != nil { if err != nil {
@ -1577,7 +1588,7 @@ func Test_PublicServer_BitcoinType(t *testing.T) {
httpTestsBitcoinType(t, ts) httpTestsBitcoinType(t, ts)
socketioTestsBitcoinType(t, ts) socketioTestsBitcoinType(t, ts)
runWebsocketTestsBitcoinType(t, ts, websocketTestsBitcoinType) runWebsocketTests(t, ts, websocketTestsBitcoinType)
} }
func httpTestsBitcoinTypeExtendedIndex(t *testing.T, ts *httptest.Server) { func httpTestsBitcoinTypeExtendedIndex(t *testing.T, ts *httptest.Server) {
@ -1758,5 +1769,5 @@ func Test_PublicServer_BitcoinType_ExtendedIndex(t *testing.T) {
defer ts.Close() defer ts.Close()
httpTestsBitcoinTypeExtendedIndex(t, ts) httpTestsBitcoinTypeExtendedIndex(t, ts)
runWebsocketTestsBitcoinType(t, ts, websocketTestsBitcoinTypeExtendedIndex) runWebsocketTests(t, ts, websocketTestsBitcoinTypeExtendedIndex)
} }

View File

@ -4,6 +4,7 @@ import (
"encoding/json" "encoding/json"
"math/big" "math/big"
"net/http" "net/http"
"os"
"runtime/debug" "runtime/debug"
"strconv" "strconv"
"strings" "strings"
@ -70,6 +71,7 @@ type WebsocketServer struct {
fiatRatesSubscriptions map[string]map[*websocketChannel]string fiatRatesSubscriptions map[string]map[*websocketChannel]string
fiatRatesTokenSubscriptions map[*websocketChannel][]string fiatRatesTokenSubscriptions map[*websocketChannel][]string
fiatRatesSubscriptionsLock sync.Mutex fiatRatesSubscriptionsLock sync.Mutex
allowedEthCallContracts map[string]struct{}
} }
// NewWebsocketServer creates new websocket interface to blockbook and returns its handle // 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), fiatRatesSubscriptions: make(map[string]map[*websocketChannel]string),
fiatRatesTokenSubscriptions: make(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 return s, nil
} }
@ -391,6 +401,14 @@ var requestHandlers = map[string]func(*WebsocketServer, *websocketChannel, *WsRe
} }
return 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) { "subscribeNewBlock": func(s *WebsocketServer, c *websocketChannel, req *WsReq) (rv interface{}, err error) {
return s.subscribeNewBlock(c, req) return s.subscribeNewBlock(c, req)
}, },
@ -747,6 +765,20 @@ func (s *WebsocketServer) getBlockFiltersBatch(r *WsBlockFiltersBatchReq) (res i
}, nil }, 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 { type subscriptionResponse struct {
Subscribed bool `json:"subscribed"` Subscribed bool `json:"subscribed"`
} }

View File

@ -138,3 +138,13 @@ type WsFiatRatesTickersListReq struct {
Timestamp int64 `json:"timestamp,omitempty"` Timestamp int64 `json:"timestamp,omitempty"`
Token string `json:"token,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"`
}

File diff suppressed because it is too large Load Diff

View File

@ -134,6 +134,11 @@ func (c *fakeBlockChainEthereumType) EthereumTypeGetErc20ContractBalance(addrDes
return big.NewInt(1000000000 + int64(addrDesc[0])*1000 + int64(contractDesc[0])), nil 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 // GetTokenURI returns URI derived from the input contractDesc
func (c *fakeBlockChainEthereumType) GetTokenURI(contractDesc bchain.AddressDescriptor, tokenID *big.Int) (string, error) { func (c *fakeBlockChainEthereumType) GetTokenURI(contractDesc bchain.AddressDescriptor, tokenID *big.Int) (string, error) {
return "https://ipfs.io/ipfs/" + contractDesc.String()[3:] + ".json", nil return "https://ipfs.io/ipfs/" + contractDesc.String()[3:] + ".json", nil