diff --git a/bchain/coins/eth/ethrpc.go b/bchain/coins/eth/ethrpc.go index bcc454b7..3b2193d1 100644 --- a/bchain/coins/eth/ethrpc.go +++ b/bchain/coins/eth/ethrpc.go @@ -4,16 +4,20 @@ import ( "blockbook/bchain" "context" "encoding/json" + "fmt" "math/big" + "strconv" "sync" "time" "github.com/golang/glog" "github.com/juju/errors" + ethereum "github.com/ethereum/go-ethereum" ethcommon "github.com/ethereum/go-ethereum/common" ethtypes "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/ethclient" + "github.com/ethereum/go-ethereum/rpc" ) type EthereumNet uint32 @@ -26,6 +30,7 @@ const ( // EthRPC is an interface to JSON-RPC eth service. type EthRPC struct { client *ethclient.Client + rpc *rpc.Client timeout time.Duration rpcURL string Parser *EthParser @@ -49,12 +54,15 @@ func NewEthRPC(config json.RawMessage, pushHandler func(*bchain.MQMessage)) (bch if err != nil { return nil, errors.Annotatef(err, "Invalid configuration file") } - ec, err := ethclient.Dial(c.RPCURL) + rc, err := rpc.Dial(c.RPCURL) if err != nil { return nil, err } + ec := ethclient.NewClient(rc) + s := &EthRPC{ client: ec, + rpc: rc, rpcURL: c.RPCURL, } @@ -154,16 +162,15 @@ func (b *EthRPC) GetBlockHash(height uint32) (string, error) { } func (b *EthRPC) ethHeaderToBlockHeader(h *ethtypes.Header) (*bchain.BlockHeader, error) { - bh, err := b.getBestHeader() + hn := h.Number.Uint64() + c, err := b.computeConfirmations(hn) if err != nil { return nil, err } - hn := uint32(h.Number.Uint64()) - bn := uint32(bh.Number.Uint64()) return &bchain.BlockHeader{ Hash: ethHashToHash(h.Hash()), - Height: hn, - Confirmations: int(bn - hn), + Height: uint32(hn), + Confirmations: int(c), // Next // Prev @@ -180,27 +187,115 @@ func (b *EthRPC) GetBlockHeader(hash string) (*bchain.BlockHeader, error) { return b.ethHeaderToBlockHeader(h) } -func (b *EthRPC) GetBlock(hash string, height uint32) (*bchain.Block, error) { - ctx, cancel := context.WithTimeout(context.Background(), b.timeout) - defer cancel() - bk, err := b.client.BlockByHash(ctx, ethcommon.HexToHash(hash)) +func (b *EthRPC) computeConfirmations(n uint64) (uint32, error) { + bh, err := b.getBestHeader() + if err != nil { + return 0, err + } + bn := bh.Number.Uint64() + return uint32(bn - n), nil +} + +type rpcTransaction struct { + tx *ethtypes.Transaction + txExtraInfo +} + +type txExtraInfo struct { + BlockNumber *string + BlockHash ethcommon.Hash + From ethcommon.Address + TransactionIndex string `json:"transactionIndex"` +} + +type rpcBlock struct { + Hash ethcommon.Hash `json:"hash"` + Transactions []rpcTransaction `json:"transactions"` + UncleHashes []ethcommon.Hash `json:"uncles"` +} + +func ethTxToTx(rtx *rpcTransaction, blocktime int64, confirmations uint32) (*bchain.Tx, error) { + txid := ethHashToHash(rtx.tx.Hash()) + n, err := strconv.ParseInt(rtx.TransactionIndex, 16, 64) if err != nil { return nil, err } - // TODO maybe not the most optimal way to get the header - bbh, err := b.ethHeaderToBlockHeader(bk.Header()) - txs := bk.Transactions() - btxs := make([]bchain.Tx, len(txs)) - for i, tx := range txs { - btxs[i] = bchain.Tx{ - // Blocktime - Confirmations: uint32(bbh.Confirmations), - // Hex - // LockTime - // Time - Txid: ethHashToHash(tx.Hash()), - // Vin + var from, to string + ethTo := rtx.tx.To() + if ethTo != nil { + to = ethTo.Hex()[2:] + } + from = rtx.From.Hex()[2:] + return &bchain.Tx{ + Blocktime: blocktime, + Confirmations: confirmations, + // Hex + // LockTime + Time: blocktime, + Txid: txid, + Vin: []bchain.Vin{ + { + Addresses: []string{from}, + // Coinbase + // ScriptSig + // Sequence + // Txid + // Vout + }, + }, + Vout: []bchain.Vout{ + { + N: uint32(n), + Value: float64(rtx.tx.Value().Int64()), + ScriptPubKey: bchain.ScriptPubKey{ + // Hex + Addresses: []string{to}, + }, + }, + }, + }, nil +} + +func (b *EthRPC) GetBlock(hash string, height uint32) (*bchain.Block, error) { + ctx, cancel := context.WithTimeout(context.Background(), b.timeout) + defer cancel() + var raw json.RawMessage + err := b.rpc.CallContext(ctx, &raw, "eth_getBlockByHash", ethcommon.HexToHash(hash), true) + if err != nil { + return nil, err + } else if len(raw) == 0 { + return nil, ethereum.NotFound + } + // Decode header and transactions. + var head *ethtypes.Header + var body rpcBlock + if err := json.Unmarshal(raw, &head); err != nil { + return nil, err + } + if err := json.Unmarshal(raw, &body); err != nil { + return nil, err + } + // Quick-verify transaction and uncle lists. This mostly helps with debugging the server. + if head.UncleHash == ethtypes.EmptyUncleHash && len(body.UncleHashes) > 0 { + return nil, fmt.Errorf("server returned non-empty uncle list but block header indicates no uncles") + } + if head.UncleHash != ethtypes.EmptyUncleHash && len(body.UncleHashes) == 0 { + return nil, fmt.Errorf("server returned empty uncle list but block header indicates uncles") + } + if head.TxHash == ethtypes.EmptyRootHash && len(body.Transactions) > 0 { + return nil, fmt.Errorf("server returned non-empty transaction list but block header indicates no transactions") + } + if head.TxHash != ethtypes.EmptyRootHash && len(body.Transactions) == 0 { + return nil, fmt.Errorf("server returned empty transaction list but block header indicates transactions") + } + bbh, err := b.ethHeaderToBlockHeader(head) + btxs := make([]bchain.Tx, len(body.Transactions)) + for i, tx := range body.Transactions { + btx, err := ethTxToTx(&tx, int64(head.Time.Uint64()), uint32(bbh.Confirmations)) + if err != nil { + return nil, err } + btxs[i] = *btx } bbk := bchain.Block{ BlockHeader: *bbh, @@ -209,32 +304,49 @@ func (b *EthRPC) GetBlock(hash string, height uint32) (*bchain.Block, error) { return &bbk, nil } -func (b *EthRPC) GetMempool() ([]string, error) { - panic("not implemented") -} - func (b *EthRPC) GetTransaction(txid string) (*bchain.Tx, error) { - // bh, err := b.getBestHeader() - // if err != nil { - // return nil, err - // } ctx, cancel := context.WithTimeout(context.Background(), b.timeout) defer cancel() - tx, _, err := b.client.TransactionByHash(ctx, ethcommon.StringToHash(txid)) + var json *rpcTransaction + err := b.rpc.CallContext(ctx, &json, "eth_getTransactionByHash", ethcommon.HexToHash(txid)) if err != nil { return nil, err + } else if json == nil { + return nil, ethereum.NotFound + } else if _, r, _ := json.tx.RawSignatureValues(); r == nil { + return nil, fmt.Errorf("server returned transaction without signature") } - btx := bchain.Tx{ - // Blocktime - // Confirmations - // Hex - // LockTime - // Time - Txid: ethHashToHash(tx.Hash()), - // Vin - // Vout + var btx *bchain.Tx + if json.BlockNumber == nil { + // mempool tx + btx, err = ethTxToTx(json, 0, 0) + if err != nil { + return nil, err + } + } else { + // non mempool tx - we must read the block header to get the block time + n, err := strconv.ParseInt((*json.BlockNumber)[2:], 16, 64) + if err != nil { + return nil, err + } + h, err := b.client.HeaderByHash(ctx, json.BlockHash) + if err != nil { + return nil, err + } + confirmations, err := b.computeConfirmations(uint64(n)) + if err != nil { + return nil, err + } + btx, err = ethTxToTx(json, h.Time.Int64(), confirmations) + if err != nil { + return nil, err + } } - return &btx, nil + return btx, nil +} + +func (b *EthRPC) GetMempool() ([]string, error) { + panic("not implemented") } func (b *EthRPC) EstimateSmartFee(blocks int, conservative bool) (float64, error) { diff --git a/bchain/types.go b/bchain/types.go index ef0f6ddc..acd880aa 100644 --- a/bchain/types.go +++ b/bchain/types.go @@ -13,6 +13,7 @@ type Vin struct { Vout uint32 `json:"vout"` ScriptSig ScriptSig `json:"scriptSig"` Sequence uint32 `json:"sequence"` + Addresses []string `json:"addresses,omitempty"` } type ScriptPubKey struct {