diff --git a/bchain/basechain.go b/bchain/basechain.go index 8fbd2023..1de31a48 100644 --- a/bchain/basechain.go +++ b/bchain/basechain.go @@ -39,6 +39,11 @@ func (b *BaseChain) EthereumTypeGetNonce(addrDesc AddressDescriptor) (uint64, er return 0, errors.New("Not supported") } +// EthereumTypeEstimateGas is not supported +func (b *BaseChain) EthereumTypeEstimateGas(params map[string]interface{}) (uint64, error) { + return 0, errors.New("Not supported") +} + // EthereumTypeGetErc20ContractInfo is not supported func (b *BaseChain) EthereumTypeGetErc20ContractInfo(contractDesc AddressDescriptor) (*Erc20Contract, error) { return nil, errors.New("Not supported") diff --git a/bchain/coins/blockchain.go b/bchain/coins/blockchain.go index 52799ced..bf28ff8d 100644 --- a/bchain/coins/blockchain.go +++ b/bchain/coins/blockchain.go @@ -252,6 +252,11 @@ func (c *blockChainWithMetrics) EthereumTypeGetNonce(addrDesc bchain.AddressDesc return c.b.EthereumTypeGetNonce(addrDesc) } +func (c *blockChainWithMetrics) EthereumTypeEstimateGas(params map[string]interface{}) (v uint64, err error) { + defer func(s time.Time) { c.observeRPCLatency("EthereumTypeEstimateGas", s, err) }(time.Now()) + return c.b.EthereumTypeEstimateGas(params) +} + func (c *blockChainWithMetrics) EthereumTypeGetErc20ContractInfo(contractDesc bchain.AddressDescriptor) (v *bchain.Erc20Contract, err error) { defer func(s time.Time) { c.observeRPCLatency("EthereumTypeGetErc20ContractInfo", s, err) }(time.Now()) return c.b.EthereumTypeGetErc20ContractInfo(contractDesc) diff --git a/bchain/coins/eth/ethrpc.go b/bchain/coins/eth/ethrpc.go index 0d7c575e..347edd4e 100644 --- a/bchain/coins/eth/ethrpc.go +++ b/bchain/coins/eth/ethrpc.go @@ -632,18 +632,42 @@ func (b *EthereumRPC) EstimateFee(blocks int) (big.Int, error) { func (b *EthereumRPC) EstimateSmartFee(blocks int, conservative bool) (big.Int, error) { ctx, cancel := context.WithTimeout(context.Background(), b.timeout) defer cancel() - // TODO - what parameters of msg to use to get better estimate, maybe more data from the wallet are needed - a := ethcommon.HexToAddress("0x1234567890123456789012345678901234567890") - msg := ethereum.CallMsg{ - To: &a, - } - g, err := b.client.EstimateGas(ctx, msg) var r big.Int - if err != nil { - return r, err + gp, err := b.client.SuggestGasPrice(ctx) + if err == nil && b != nil { + r = *gp } - r.SetUint64(g) - return r, nil + return r, err +} + +func getStringFromMap(p string, params map[string]interface{}) (string, bool) { + v, ok := params[p] + if ok { + s, ok := v.(string) + return s, ok + } + return "", false +} + +// EthereumTypeEstimateGas returns estimation of gas consumption for given transaction parameters +func (b *EthereumRPC) EthereumTypeEstimateGas(params map[string]interface{}) (uint64, error) { + ctx, cancel := context.WithTimeout(context.Background(), b.timeout) + defer cancel() + msg := ethereum.CallMsg{} + s, ok := getStringFromMap("from", params) + if ok && len(s) > 0 { + msg.From = ethcommon.HexToAddress(s) + } + s, ok = getStringFromMap("to", params) + if ok && len(s) > 0 { + a := ethcommon.HexToAddress(s) + msg.To = &a + } + s, ok = getStringFromMap("data", params) + if ok && len(s) > 0 { + msg.Data = ethcommon.FromHex(s) + } + return b.client.EstimateGas(ctx, msg) } // SendRawTransaction sends raw transaction diff --git a/bchain/types.go b/bchain/types.go index d14f9e0e..be6496a2 100644 --- a/bchain/types.go +++ b/bchain/types.go @@ -216,6 +216,7 @@ type BlockChain interface { // EthereumType specific EthereumTypeGetBalance(addrDesc AddressDescriptor) (*big.Int, error) EthereumTypeGetNonce(addrDesc AddressDescriptor) (uint64, error) + EthereumTypeEstimateGas(params map[string]interface{}) (uint64, error) EthereumTypeGetErc20ContractInfo(contractDesc AddressDescriptor) (*Erc20Contract, error) EthereumTypeGetErc20ContractBalance(addrDesc, contractDesc AddressDescriptor) (*big.Int, error) } diff --git a/server/websocket.go b/server/websocket.go index 8226343e..2807d648 100644 --- a/server/websocket.go +++ b/server/websocket.go @@ -6,8 +6,10 @@ import ( "blockbook/common" "blockbook/db" "encoding/json" + "math/big" "net/http" "runtime/debug" + "strconv" "sync" "sync/atomic" "time" @@ -225,6 +227,9 @@ var requestHandlers = map[string]func(*WebsocketServer, *websocketChannel, *webs "getInfo": func(s *WebsocketServer, c *websocketChannel, req *websocketReq) (rv interface{}, err error) { return s.getInfo() }, + "estimateFee": func(s *WebsocketServer, c *websocketChannel, req *websocketReq) (rv interface{}, err error) { + return s.estimateFee(c, req.Params) + }, "sendTransaction": func(s *WebsocketServer, c *websocketChannel, req *websocketReq) (rv interface{}, err error) { r := struct { Hex string `json:"hex"` @@ -360,6 +365,72 @@ func (s *WebsocketServer) getInfo() (interface{}, error) { }, nil } +func (s *WebsocketServer) estimateFee(c *websocketChannel, params []byte) (interface{}, error) { + type estimateFeeReq struct { + Blocks []int `json:"blocks"` + Specific map[string]interface{} `json:"specific"` + } + type estimateFeeRes struct { + FeePerTx string `json:"feePerTx,omitempty"` + FeePerUnit string `json:"feePerUnit,omitempty"` + FeeLimit string `json:"feeLimit,omitempty"` + } + var r estimateFeeReq + err := json.Unmarshal(params, &r) + if err != nil { + return nil, err + } + res := make([]estimateFeeRes, len(r.Blocks)) + if s.chainParser.GetChainType() == bchain.ChainEthereumType { + gas, err := s.chain.EthereumTypeEstimateGas(r.Specific) + if err != nil { + return nil, err + } + sg := strconv.FormatUint(gas, 10) + for i, b := range r.Blocks { + fee, err := s.chain.EstimateSmartFee(b, true) + if err != nil { + return nil, err + } + res[i].FeePerUnit = fee.String() + res[i].FeeLimit = sg + fee.Mul(&fee, new(big.Int).SetUint64(gas)) + res[i].FeePerTx = fee.String() + } + } else { + conservative := true + v, ok := r.Specific["conservative"] + if ok { + vc, ok := v.(bool) + if ok { + conservative = vc + } + } + txSize := 0 + v, ok = r.Specific["txsize"] + if ok { + f, ok := v.(float64) + if ok { + txSize = int(f) + } + } + for i, b := range r.Blocks { + fee, err := s.chain.EstimateSmartFee(b, conservative) + if err != nil { + return nil, err + } + res[i].FeePerUnit = fee.String() + if txSize > 0 { + fee.Mul(&fee, big.NewInt(int64(txSize))) + fee.Add(&fee, big.NewInt(500)) + fee.Div(&fee, big.NewInt(1000)) + res[i].FeePerTx = fee.String() + } + } + } + return res, nil +} + func (s *WebsocketServer) sendTransaction(tx string) (res resultSendTransaction, err error) { txid, err := s.chain.SendRawTransaction(tx) if err != nil { @@ -461,7 +532,7 @@ func (s *WebsocketServer) OnNewBlock(hash string, height uint32) { // OnNewTxAddr is a callback that broadcasts info about a tx affecting subscribed address func (s *WebsocketServer) OnNewTxAddr(tx *bchain.Tx, addrDesc bchain.AddressDescriptor, isOutput bool) { - // check if there is any subscription but release lock immediately, GetTransactionFromBchainTx may take some time + // check if there is any subscription but release the lock immediately, GetTransactionFromBchainTx may take some time s.addressSubscriptionsLock.Lock() as, ok := s.addressSubscriptions[string(addrDesc)] s.addressSubscriptionsLock.Unlock() diff --git a/static/test-websocket.html b/static/test-websocket.html index 606a5c13..0a367f6c 100644 --- a/static/test-websocket.html +++ b/static/test-websocket.html @@ -97,6 +97,15 @@ }; } + function getInfo() { + const method = 'getInfo'; + const params = { + }; + send(method, params, function (result) { + document.getElementById('getInfoResult').innerText = JSON.stringify(result).replace(/,/g, ", "); + }); + } + function getAccountInfo() { const descriptor = document.getElementById('getAccountInfoDescriptor').value.trim(); const selectDetails = document.getElementById('getAccountInfoDetails'); @@ -121,13 +130,31 @@ }); } - function getInfo() { - const method = 'getInfo'; - const params = { - }; - send(method, params, function (result) { - document.getElementById('getInfoResult').innerText = JSON.stringify(result).replace(/,/g, ", "); - }); + function estimateFee() { + try { + var blocks = document.getElementById('estimateFeeBlocks').value.split(","); + var specific = document.getElementById('estimateFeeSpecific').value.trim(); + if (specific) { + // example for bitcoin type: {"conservative": false,"txsize":1234} + // example for ethereum type: {"from":"0x65513ecd11fd3a5b1fefdcc6a500b025008405a2","to":"0x65513ecd11fd3a5b1fefdcc6a500b025008405a2","data":"0xabcd"} + specific = JSON.parse(specific) + } + else { + specific = undefined; + } + blocks = blocks.map(s => parseInt(s.trim())); + const method = 'estimateFee'; + const params = { + blocks, + specific + }; + send(method, params, function (result) { + document.getElementById('estimateFeeResult').innerText = JSON.stringify(result).replace(/,/g, ", "); + }); + } + catch (e) { + document.getElementById('estimateFeeResult').innerText = e; + } } function sendTransaction() { @@ -198,41 +225,6 @@ }); } - function getBlockHeader() { - var param = document.getElementById('getBlockHeaderParam').value.trim(); - lookupBlockHash(isHash(param) ? param : parseInt(param), function (result) { - console.log('getBlockHeader sent successfully'); - console.log(result); - document.getElementById('getBlockHeaderResult').innerText = JSON.stringify(result).replace(/,/g, ", "); - }); - } - - function isHash(str) { - var re = /[0-9A-Fa-f]{64}/g; - return re.test(str); - } - - function lookupBlockHash(heightOrHash, f) { - const method = 'getBlockHeader'; - const params = [heightOrHash]; - return socket.send({ method, params }, f); - } - - function estimateFee() { - var blocks = document.getElementById('estimateFeeBlocks').value.trim(); - estimateTxFee(parseInt(blocks), function (result) { - console.log('estimateFee sent successfully'); - console.log(result); - document.getElementById('estimateFeeResult').innerText = JSON.stringify(result).replace(/,/g, ", "); - }); - } - - function estimateTxFee(blocks, f) { - const method = 'estimateFee'; - const params = [blocks]; - return socket.send({ method, params }, f); - } - @@ -252,6 +244,13 @@ +
+
+ +
+
+
+
@@ -279,41 +278,24 @@
- -
-
- -
-
-
-
+