Merge branch 'ethereum' into indexv4

This commit is contained in:
Martin Boehm 2018-12-20 16:07:34 +01:00
commit a7b8994419
62 changed files with 5379 additions and 1328 deletions

4
Gopkg.lock generated
View File

@ -46,8 +46,8 @@
[[projects]]
name = "github.com/ethereum/go-ethereum"
packages = [".","common","common/hexutil","common/math","core/types","crypto","crypto/secp256k1","crypto/sha3","ethclient","ethdb","log","metrics","p2p/netutil","params","rlp","rpc","trie"]
revision = "89451f7c382ad2185987ee369f16416f89c28a7d"
version = "v1.8.15"
revision = "8bbe72075e4e16442c4e28d999edee12e294329e"
version = "v1.8.17"
[[projects]]
name = "github.com/go-stack/stack"

View File

@ -4,10 +4,27 @@ import (
"blockbook/bchain"
"blockbook/common"
"blockbook/db"
"encoding/json"
"math/big"
"time"
)
// GetAddressOption specifies what data returns GetAddress api call
type GetAddressOption int
const (
// Basic - only that address is indexed and some basic info
Basic GetAddressOption = iota
// Balance - only balances
Balance
// TxidHistory - balances and txids, subject to paging
TxidHistory
// TxHistoryLight - balances and easily obtained tx data (not requiring request to backend), subject to paging
TxHistoryLight
// TxHistory - balances and full tx data, subject to paging
TxHistory
)
// APIError extends error by information if the error details should be returned to the end user
type APIError struct {
Text string
@ -26,97 +43,169 @@ func NewAPIError(s string, public bool) error {
}
}
// ScriptSig contains input script
type ScriptSig struct {
Hex string `json:"hex"`
Asm string `json:"asm,omitempty"`
// Amount is datatype holding amounts
type Amount big.Int
// MarshalJSON Amount serialization
func (a *Amount) MarshalJSON() (out []byte, err error) {
if a == nil {
return []byte(`"0"`), nil
}
return []byte(`"` + (*big.Int)(a).String() + `"`), nil
}
func (a *Amount) String() string {
if a == nil {
return ""
}
return (*big.Int)(a).String()
}
// DecimalString returns amount with decimal point placed according to parameter d
func (a *Amount) DecimalString(d int) string {
return bchain.AmountToDecimalString((*big.Int)(a), d)
}
// AsBigInt returns big.Int type for the Amount (empty if Amount is nil)
func (a *Amount) AsBigInt() big.Int {
if a == nil {
return *new(big.Int)
}
return big.Int(*a)
}
// Vin contains information about single transaction input
type Vin struct {
Txid string `json:"txid"`
Vout uint32 `json:"vout"`
Txid string `json:"txid,omitempty"`
Vout uint32 `json:"vout,omitempty"`
Sequence int64 `json:"sequence,omitempty"`
N int `json:"n"`
ScriptSig ScriptSig `json:"scriptSig"`
AddrDesc bchain.AddressDescriptor `json:"-"`
Addresses []string `json:"addresses"`
Searchable bool `json:"-"`
Value string `json:"value"`
ValueSat big.Int `json:"-"`
}
// ScriptPubKey contains output script and addresses derived from it
type ScriptPubKey struct {
Hex string `json:"hex"`
ValueSat *Amount `json:"value,omitempty"`
Hex string `json:"hex,omitempty"`
Asm string `json:"asm,omitempty"`
AddrDesc bchain.AddressDescriptor `json:"-"`
Addresses []string `json:"addresses"`
Searchable bool `json:"-"`
Type string `json:"type,omitempty"`
}
// Vout contains information about single transaction output
type Vout struct {
Value string `json:"value"`
ValueSat big.Int `json:"-"`
N int `json:"n"`
ScriptPubKey ScriptPubKey `json:"scriptPubKey"`
Spent bool `json:"spent"`
SpentTxID string `json:"spentTxId,omitempty"`
SpentIndex int `json:"spentIndex,omitempty"`
SpentHeight int `json:"spentHeight,omitempty"`
ValueSat *Amount `json:"value,omitempty"`
N int `json:"n"`
Spent bool `json:"spent,omitempty"`
SpentTxID string `json:"spentTxId,omitempty"`
SpentIndex int `json:"spentIndex,omitempty"`
SpentHeight int `json:"spentHeight,omitempty"`
Hex string `json:"hex,omitempty"`
Asm string `json:"asm,omitempty"`
AddrDesc bchain.AddressDescriptor `json:"-"`
Addresses []string `json:"addresses"`
Searchable bool `json:"-"`
Type string `json:"type,omitempty"`
}
// Erc20Token contains info about ERC20 token held by an address
type Erc20Token struct {
Contract string `json:"contract"`
Transfers int `json:"transfers"`
Name string `json:"name"`
Symbol string `json:"symbol"`
Decimals int `json:"decimals"`
BalanceSat *Amount `json:"balance,omitempty"`
ContractIndex string `json:"-"`
}
// TokenTransfer contains info about a token transfer done in a transaction
type TokenTransfer struct {
Type string `json:"type"`
From string `json:"from"`
To string `json:"to"`
Token string `json:"token"`
Name string `json:"name"`
Symbol string `json:"symbol"`
Decimals int `json:"decimals"`
Value *Amount `json:"value"`
}
// EthereumSpecific contains ethereum specific transaction data
type EthereumSpecific struct {
Status int `json:"status"` // 1 OK, 0 Fail, -1 pending
Nonce uint64 `json:"nonce"`
GasLimit *big.Int `json:"gaslimit"`
GasUsed *big.Int `json:"gasused"`
GasPrice *Amount `json:"gasprice"`
}
// Tx holds information about a transaction
type Tx struct {
Txid string `json:"txid"`
Version int32 `json:"version,omitempty"`
Locktime uint32 `json:"locktime,omitempty"`
Vin []Vin `json:"vin"`
Vout []Vout `json:"vout"`
Blockhash string `json:"blockhash,omitempty"`
Blockheight int `json:"blockheight"`
Confirmations uint32 `json:"confirmations"`
Time int64 `json:"time,omitempty"`
Blocktime int64 `json:"blocktime"`
ValueOut string `json:"valueOut"`
ValueOutSat big.Int `json:"-"`
Size int `json:"size,omitempty"`
ValueIn string `json:"valueIn"`
ValueInSat big.Int `json:"-"`
Fees string `json:"fees"`
FeesSat big.Int `json:"-"`
Hex string `json:"hex"`
Txid string `json:"txid"`
Version int32 `json:"version,omitempty"`
Locktime uint32 `json:"locktime,omitempty"`
Vin []Vin `json:"vin"`
Vout []Vout `json:"vout"`
Blockhash string `json:"blockhash,omitempty"`
Blockheight int `json:"blockheight"`
Confirmations uint32 `json:"confirmations"`
Time int64 `json:"time,omitempty"`
Blocktime int64 `json:"blocktime"`
Size int `json:"size,omitempty"`
ValueOutSat *Amount `json:"value"`
ValueInSat *Amount `json:"valueIn,omitempty"`
FeesSat *Amount `json:"fees,omitempty"`
Hex string `json:"hex,omitempty"`
CoinSpecificData interface{} `json:"-"`
CoinSpecificJSON json.RawMessage `json:"-"`
TokenTransfers []TokenTransfer `json:"tokentransfers,omitempty"`
EthereumSpecific *EthereumSpecific `json:"ethereumspecific,omitempty"`
}
// Paging contains information about paging for address, blocks and block
type Paging struct {
Page int `json:"page"`
TotalPages int `json:"totalPages"`
ItemsOnPage int `json:"itemsOnPage"`
Page int `json:"page,omitempty"`
TotalPages int `json:"totalPages,omitempty"`
ItemsOnPage int `json:"itemsOnPage,omitempty"`
}
const (
// AddressFilterVoutOff disables filtering of transactions by vout
AddressFilterVoutOff = -1
// AddressFilterVoutInputs specifies that only txs where the address is as input are returned
AddressFilterVoutInputs = -2
// AddressFilterVoutOutputs specifies that only txs where the address is as output are returned
AddressFilterVoutOutputs = -3
)
// AddressFilter is used to filter data returned from GetAddress api method
type AddressFilter struct {
Vout int
Contract string
FromHeight uint32
ToHeight uint32
}
// Address holds information about address and its transactions
type Address struct {
Paging
AddrStr string `json:"addrStr"`
Balance string `json:"balance"`
TotalReceived string `json:"totalReceived"`
TotalSent string `json:"totalSent"`
UnconfirmedBalance string `json:"unconfirmedBalance"`
UnconfirmedTxApperances int `json:"unconfirmedTxApperances"`
TxApperances int `json:"txApperances"`
Transactions []*Tx `json:"txs,omitempty"`
Txids []string `json:"transactions,omitempty"`
AddrStr string `json:"addrStr"`
BalanceSat *Amount `json:"balance"`
TotalReceivedSat *Amount `json:"totalReceived,omitempty"`
TotalSentSat *Amount `json:"totalSent,omitempty"`
UnconfirmedBalanceSat *Amount `json:"unconfirmedBalance"`
UnconfirmedTxApperances int `json:"unconfirmedTxApperances"`
TxApperances int `json:"txApperances"`
Transactions []*Tx `json:"transactions,omitempty"`
Txids []string `json:"txids,omitempty"`
Nonce string `json:"nonce,omitempty"`
Erc20Contract *bchain.Erc20Contract `json:"erc20contract,omitempty"`
Erc20Tokens []Erc20Token `json:"erc20tokens,omitempty"`
Filter string `json:"-"`
}
// AddressUtxo holds information about address and its transactions
type AddressUtxo struct {
Txid string `json:"txid"`
Vout uint32 `json:"vout"`
Amount string `json:"amount"`
AmountSat big.Int `json:"satoshis"`
Vout int32 `json:"vout"`
AmountSat *Amount `json:"value"`
Height int `json:"height,omitempty"`
Confirmations int `json:"confirmations"`
}
@ -150,6 +239,7 @@ type BlockbookInfo struct {
InSyncMempool bool `json:"inSyncMempool"`
LastMempoolTime time.Time `json:"lastMempoolTime"`
MempoolSize int `json:"mempoolSize"`
Decimals int `json:"decimals"`
DbSize int64 `json:"dbSize"`
DbSizeFromColumns int64 `json:"dbSizeFromColumns,omitempty"`
DbColumns []common.InternalStateColumn `json:"dbColumns,omitempty"`

51
api/types_test.go Normal file
View File

@ -0,0 +1,51 @@
// build unittest
package api
import (
"encoding/json"
"math/big"
"reflect"
"testing"
)
func TestAmount_MarshalJSON(t *testing.T) {
type amounts struct {
A1 Amount `json:"a1"`
A2 Amount `json:"a2,omitempty"`
PA1 *Amount `json:"pa1"`
PA2 *Amount `json:"pa2,omitempty"`
}
tests := []struct {
name string
a amounts
want string
}{
{
name: "empty",
want: `{"a1":"0","a2":"0","pa1":null}`,
},
{
name: "1",
a: amounts{
A1: (Amount)(*big.NewInt(123456)),
A2: (Amount)(*big.NewInt(787901)),
PA1: (*Amount)(big.NewInt(234567)),
PA2: (*Amount)(big.NewInt(890123)),
},
want: `{"a1":"123456","a2":"787901","pa1":"234567","pa2":"890123"}`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
b, err := json.Marshal(&tt.a)
if err != nil {
t.Errorf("json.Marshal() error = %v", err)
return
}
if !reflect.DeepEqual(string(b), tt.want) {
t.Errorf("json.Marshal() = %v, want %v", string(b), tt.want)
}
})
}
}

220
api/typesv1.go Normal file
View File

@ -0,0 +1,220 @@
package api
import (
"blockbook/bchain"
"math/big"
)
// ScriptSigV1 is used for legacy api v1
type ScriptSigV1 struct {
Hex string `json:"hex,omitempty"`
Asm string `json:"asm,omitempty"`
}
// VinV1 is used for legacy api v1
type VinV1 struct {
Txid string `json:"txid"`
Vout uint32 `json:"vout"`
Sequence int64 `json:"sequence,omitempty"`
N int `json:"n"`
ScriptSig ScriptSigV1 `json:"scriptSig"`
AddrDesc bchain.AddressDescriptor `json:"-"`
Addresses []string `json:"addresses"`
Searchable bool `json:"-"`
Value string `json:"value"`
ValueSat big.Int `json:"-"`
}
// ScriptPubKeyV1 is used for legacy api v1
type ScriptPubKeyV1 struct {
Hex string `json:"hex,omitempty"`
Asm string `json:"asm,omitempty"`
AddrDesc bchain.AddressDescriptor `json:"-"`
Addresses []string `json:"addresses"`
Searchable bool `json:"-"`
Type string `json:"type,omitempty"`
}
// VoutV1 is used for legacy api v1
type VoutV1 struct {
Value string `json:"value"`
ValueSat big.Int `json:"-"`
N int `json:"n"`
ScriptPubKey ScriptPubKeyV1 `json:"scriptPubKey"`
Spent bool `json:"spent"`
SpentTxID string `json:"spentTxId,omitempty"`
SpentIndex int `json:"spentIndex,omitempty"`
SpentHeight int `json:"spentHeight,omitempty"`
}
// TxV1 is used for legacy api v1
type TxV1 struct {
Txid string `json:"txid"`
Version int32 `json:"version,omitempty"`
Locktime uint32 `json:"locktime,omitempty"`
Vin []VinV1 `json:"vin"`
Vout []VoutV1 `json:"vout"`
Blockhash string `json:"blockhash,omitempty"`
Blockheight int `json:"blockheight"`
Confirmations uint32 `json:"confirmations"`
Time int64 `json:"time,omitempty"`
Blocktime int64 `json:"blocktime"`
ValueOut string `json:"valueOut"`
ValueOutSat big.Int `json:"-"`
Size int `json:"size,omitempty"`
ValueIn string `json:"valueIn"`
ValueInSat big.Int `json:"-"`
Fees string `json:"fees"`
FeesSat big.Int `json:"-"`
Hex string `json:"hex"`
}
// AddressV1 is used for legacy api v1
type AddressV1 struct {
Paging
AddrStr string `json:"addrStr"`
Balance string `json:"balance"`
TotalReceived string `json:"totalReceived"`
TotalSent string `json:"totalSent"`
UnconfirmedBalance string `json:"unconfirmedBalance"`
UnconfirmedTxApperances int `json:"unconfirmedTxApperances"`
TxApperances int `json:"txApperances"`
Transactions []*TxV1 `json:"txs,omitempty"`
Txids []string `json:"transactions,omitempty"`
}
// AddressUtxoV1 is used for legacy api v1
type AddressUtxoV1 struct {
Txid string `json:"txid"`
Vout uint32 `json:"vout"`
Amount string `json:"amount"`
AmountSat big.Int `json:"satoshis"`
Height int `json:"height,omitempty"`
Confirmations int `json:"confirmations"`
}
// BlockV1 contains information about block
type BlockV1 struct {
Paging
bchain.BlockInfo
TxCount int `json:"TxCount"`
Transactions []*TxV1 `json:"txs,omitempty"`
}
// TxToV1 converts Tx to TxV1
func (w *Worker) TxToV1(tx *Tx) *TxV1 {
d := w.chainParser.AmountDecimals()
vinV1 := make([]VinV1, len(tx.Vin))
for i := range tx.Vin {
v := &tx.Vin[i]
vinV1[i] = VinV1{
AddrDesc: v.AddrDesc,
Addresses: v.Addresses,
N: v.N,
ScriptSig: ScriptSigV1{
Asm: v.Asm,
Hex: v.Hex,
},
Searchable: v.Searchable,
Sequence: v.Sequence,
Txid: v.Txid,
Value: v.ValueSat.DecimalString(d),
ValueSat: v.ValueSat.AsBigInt(),
Vout: v.Vout,
}
}
voutV1 := make([]VoutV1, len(tx.Vout))
for i := range tx.Vout {
v := &tx.Vout[i]
voutV1[i] = VoutV1{
N: v.N,
ScriptPubKey: ScriptPubKeyV1{
AddrDesc: v.AddrDesc,
Addresses: v.Addresses,
Asm: v.Asm,
Hex: v.Hex,
Searchable: v.Searchable,
Type: v.Type,
},
Spent: v.Spent,
SpentHeight: v.SpentHeight,
SpentIndex: v.SpentIndex,
SpentTxID: v.SpentTxID,
Value: v.ValueSat.DecimalString(d),
ValueSat: v.ValueSat.AsBigInt(),
}
}
return &TxV1{
Blockhash: tx.Blockhash,
Blockheight: tx.Blockheight,
Blocktime: tx.Blocktime,
Confirmations: tx.Confirmations,
Fees: tx.FeesSat.DecimalString(d),
FeesSat: tx.FeesSat.AsBigInt(),
Hex: tx.Hex,
Locktime: tx.Locktime,
Size: tx.Size,
Time: tx.Time,
Txid: tx.Txid,
ValueIn: tx.ValueInSat.DecimalString(d),
ValueInSat: tx.ValueInSat.AsBigInt(),
ValueOut: tx.ValueOutSat.DecimalString(d),
ValueOutSat: tx.ValueOutSat.AsBigInt(),
Version: tx.Version,
Vin: vinV1,
Vout: voutV1,
}
}
func (w *Worker) transactionsToV1(txs []*Tx) []*TxV1 {
v1 := make([]*TxV1, len(txs))
for i := range txs {
v1[i] = w.TxToV1(txs[i])
}
return v1
}
// AddressToV1 converts Address to AddressV1
func (w *Worker) AddressToV1(a *Address) *AddressV1 {
d := w.chainParser.AmountDecimals()
return &AddressV1{
AddrStr: a.AddrStr,
Balance: a.BalanceSat.DecimalString(d),
Paging: a.Paging,
TotalReceived: a.TotalReceivedSat.DecimalString(d),
TotalSent: a.TotalSentSat.DecimalString(d),
Transactions: w.transactionsToV1(a.Transactions),
TxApperances: a.TxApperances,
Txids: a.Txids,
UnconfirmedBalance: a.UnconfirmedBalanceSat.DecimalString(d),
UnconfirmedTxApperances: a.UnconfirmedTxApperances,
}
}
// AddressUtxoToV1 converts []AddressUtxo to []AddressUtxoV1
func (w *Worker) AddressUtxoToV1(au []AddressUtxo) []AddressUtxoV1 {
d := w.chainParser.AmountDecimals()
v1 := make([]AddressUtxoV1, len(au))
for i := range au {
utxo := &au[i]
v1[i] = AddressUtxoV1{
AmountSat: utxo.AmountSat.AsBigInt(),
Amount: utxo.AmountSat.DecimalString(d),
Confirmations: utxo.Confirmations,
Height: utxo.Height,
Txid: utxo.Txid,
Vout: uint32(utxo.Vout),
}
}
return v1
}
// BlockToV1 converts Address to Address1
func (w *Worker) BlockToV1(b *Block) *BlockV1 {
return &BlockV1{
BlockInfo: b.BlockInfo,
Paging: b.Paging,
Transactions: w.transactionsToV1(b.Transactions),
TxCount: b.TxCount,
}
}

View File

@ -2,9 +2,11 @@ package api
import (
"blockbook/bchain"
"blockbook/bchain/coins/eth"
"blockbook/common"
"blockbook/db"
"bytes"
"encoding/json"
"fmt"
"math/big"
"strconv"
@ -20,6 +22,7 @@ type Worker struct {
txCache *db.TxCache
chain bchain.BlockChain
chainParser bchain.BlockChainParser
chainType bchain.ChainType
is *common.InternalState
}
@ -30,6 +33,7 @@ func NewWorker(db *db.RocksDB, chain bchain.BlockChain, txCache *db.TxCache, is
txCache: txCache,
chain: chain,
chainParser: chain.GetChainParser(),
chainType: chain.GetChainParser().GetChainType(),
is: is,
}
return w, nil
@ -47,7 +51,7 @@ func (w *Worker) getAddressesFromVout(vout *bchain.Vout) (bchain.AddressDescript
// setSpendingTxToVout is helper function, that finds transaction that spent given output and sets it to the output
// there is no direct index for the operation, it must be found using addresses -> txaddresses -> tx
func (w *Worker) setSpendingTxToVout(vout *Vout, txid string, height uint32) error {
err := w.db.GetAddrDescTransactions(vout.ScriptPubKey.AddrDesc, height, ^uint32(0), func(t string, index uint32, isOutput bool) error {
err := w.db.GetAddrDescTransactions(vout.AddrDesc, height, ^uint32(0), func(t string, index int32, isOutput bool) error {
if isOutput == false {
tsp, err := w.db.GetTxAddresses(t)
if err != nil {
@ -55,7 +59,7 @@ func (w *Worker) setSpendingTxToVout(vout *Vout, txid string, height uint32) err
} else if tsp == nil {
glog.Warning("DB inconsistency: tx ", t, ": not found in txAddresses")
} else if len(tsp.Inputs) > int(index) {
if tsp.Inputs[index].ValueSat.Cmp(&vout.ValueSat) == 0 {
if tsp.Inputs[index].ValueSat.Cmp((*big.Int)(vout.ValueSat)) == 0 {
spentTx, spentHeight, err := w.txCache.GetTransaction(t)
if err != nil {
glog.Warning("Tx ", t, ": not found")
@ -80,7 +84,7 @@ func (w *Worker) setSpendingTxToVout(vout *Vout, txid string, height uint32) err
// GetSpendingTxid returns transaction id of transaction that spent given output
func (w *Worker) GetSpendingTxid(txid string, n int) (string, error) {
start := time.Now()
tx, err := w.GetTransaction(txid, false)
tx, err := w.GetTransaction(txid, false, false)
if err != nil {
return "", err
}
@ -96,18 +100,27 @@ func (w *Worker) GetSpendingTxid(txid string, n int) (string, error) {
}
// GetTransaction reads transaction data from txid
func (w *Worker) GetTransaction(txid string, spendingTxs bool) (*Tx, error) {
start := time.Now()
func (w *Worker) GetTransaction(txid string, spendingTxs bool, specificJSON bool) (*Tx, error) {
bchainTx, height, err := w.txCache.GetTransaction(txid)
if err != nil {
return nil, NewAPIError(fmt.Sprintf("Tx not found, %v", err), true)
}
return w.GetTransactionFromBchainTx(bchainTx, height, spendingTxs, specificJSON)
}
// GetTransactionFromBchainTx reads transaction data from txid
func (w *Worker) GetTransactionFromBchainTx(bchainTx *bchain.Tx, height uint32, spendingTxs bool, specificJSON bool) (*Tx, error) {
var err error
var ta *db.TxAddresses
var tokens []TokenTransfer
var ethSpecific *EthereumSpecific
var blockhash string
if bchainTx.Confirmations > 0 {
ta, err = w.db.GetTxAddresses(txid)
if err != nil {
return nil, errors.Annotatef(err, "GetTxAddresses %v", txid)
if w.chainType == bchain.ChainBitcoinType {
ta, err = w.db.GetTxAddresses(bchainTx.Txid)
if err != nil {
return nil, errors.Annotatef(err, "GetTxAddresses %v", bchainTx.Txid)
}
}
blockhash, err = w.db.GetBlockHash(height)
if err != nil {
@ -115,6 +128,7 @@ func (w *Worker) GetTransaction(txid string, spendingTxs bool) (*Tx, error) {
}
}
var valInSat, valOutSat, feesSat big.Int
var pValInSat *big.Int
vins := make([]Vin, len(bchainTx.Vin))
for i := range bchainTx.Vin {
bchainVin := &bchainTx.Vin[i]
@ -123,46 +137,55 @@ func (w *Worker) GetTransaction(txid string, spendingTxs bool) (*Tx, error) {
vin.N = i
vin.Vout = bchainVin.Vout
vin.Sequence = int64(bchainVin.Sequence)
vin.ScriptSig.Hex = bchainVin.ScriptSig.Hex
// bchainVin.Txid=="" is coinbase transaction
if bchainVin.Txid != "" {
// load spending addresses from TxAddresses
tas, err := w.db.GetTxAddresses(bchainVin.Txid)
if err != nil {
return nil, errors.Annotatef(err, "GetTxAddresses %v", bchainVin.Txid)
}
if tas == nil {
// mempool transactions are not in TxAddresses but confirmed should be there, log a problem
if bchainTx.Confirmations > 0 {
glog.Warning("DB inconsistency: tx ", bchainVin.Txid, ": not found in txAddresses")
}
// try to load from backend
otx, _, err := w.txCache.GetTransaction(bchainVin.Txid)
vin.Hex = bchainVin.ScriptSig.Hex
if w.chainType == bchain.ChainBitcoinType {
// bchainVin.Txid=="" is coinbase transaction
if bchainVin.Txid != "" {
// load spending addresses from TxAddresses
tas, err := w.db.GetTxAddresses(bchainVin.Txid)
if err != nil {
return nil, errors.Annotatef(err, "txCache.GetTransaction %v", bchainVin.Txid)
return nil, errors.Annotatef(err, "GetTxAddresses %v", bchainVin.Txid)
}
if len(otx.Vout) > int(vin.Vout) {
vout := &otx.Vout[vin.Vout]
vin.ValueSat = vout.ValueSat
vin.AddrDesc, vin.Addresses, vin.Searchable, err = w.getAddressesFromVout(vout)
if tas == nil {
// mempool transactions are not in TxAddresses but confirmed should be there, log a problem
if bchainTx.Confirmations > 0 {
glog.Warning("DB inconsistency: tx ", bchainVin.Txid, ": not found in txAddresses")
}
// try to load from backend
otx, _, err := w.txCache.GetTransaction(bchainVin.Txid)
if err != nil {
glog.Errorf("getAddressesFromVout error %v, vout %+v", err, vout)
}
}
} else {
if len(tas.Outputs) > int(vin.Vout) {
output := &tas.Outputs[vin.Vout]
vin.ValueSat = output.ValueSat
vin.Value = w.chainParser.AmountToDecimalString(&vin.ValueSat)
vin.AddrDesc = output.AddrDesc
vin.Addresses, vin.Searchable, err = output.Addresses(w.chainParser)
if err != nil {
glog.Errorf("output.Addresses error %v, tx %v, output %v", err, bchainVin.Txid, i)
return nil, errors.Annotatef(err, "txCache.GetTransaction %v", bchainVin.Txid)
}
if len(otx.Vout) > int(vin.Vout) {
vout := &otx.Vout[vin.Vout]
vin.ValueSat = (*Amount)(&vout.ValueSat)
vin.AddrDesc, vin.Addresses, vin.Searchable, err = w.getAddressesFromVout(vout)
if err != nil {
glog.Errorf("getAddressesFromVout error %v, vout %+v", err, vout)
}
}
} else {
if len(tas.Outputs) > int(vin.Vout) {
output := &tas.Outputs[vin.Vout]
vin.ValueSat = (*Amount)(&output.ValueSat)
vin.AddrDesc = output.AddrDesc
vin.Addresses, vin.Searchable, err = output.Addresses(w.chainParser)
if err != nil {
glog.Errorf("output.Addresses error %v, tx %v, output %v", err, bchainVin.Txid, i)
}
}
}
valInSat.Add(&valInSat, (*big.Int)(vin.ValueSat))
}
} else if w.chainType == bchain.ChainEthereumType {
if len(bchainVin.Addresses) > 0 {
vin.AddrDesc, err = w.chainParser.GetAddrDescFromAddress(bchainVin.Addresses[0])
if err != nil {
glog.Errorf("GetAddrDescFromAddress error %v, tx %v, bchainVin %v", err, bchainTx.Txid, bchainVin)
}
vin.Addresses = bchainVin.Addresses
vin.Searchable = true
}
vin.Value = w.chainParser.AmountToDecimalString(&vin.ValueSat)
valInSat.Add(&valInSat, &vin.ValueSat)
}
}
vouts := make([]Vout, len(bchainTx.Vout))
@ -170,11 +193,10 @@ func (w *Worker) GetTransaction(txid string, spendingTxs bool) (*Tx, error) {
bchainVout := &bchainTx.Vout[i]
vout := &vouts[i]
vout.N = i
vout.ValueSat = bchainVout.ValueSat
vout.Value = w.chainParser.AmountToDecimalString(&bchainVout.ValueSat)
vout.ValueSat = (*Amount)(&bchainVout.ValueSat)
valOutSat.Add(&valOutSat, &bchainVout.ValueSat)
vout.ScriptPubKey.Hex = bchainVout.ScriptPubKey.Hex
vout.ScriptPubKey.AddrDesc, vout.ScriptPubKey.Addresses, vout.ScriptPubKey.Searchable, err = w.getAddressesFromVout(bchainVout)
vout.Hex = bchainVout.ScriptPubKey.Hex
vout.AddrDesc, vout.Addresses, vout.Searchable, err = w.getAddressesFromVout(bchainVout)
if err != nil {
glog.V(2).Infof("getAddressesFromVout error %v, %v, output %v", err, bchainTx.Txid, bchainVout.N)
}
@ -183,60 +205,132 @@ func (w *Worker) GetTransaction(txid string, spendingTxs bool) (*Tx, error) {
if spendingTxs && vout.Spent {
err = w.setSpendingTxToVout(vout, bchainTx.Txid, height)
if err != nil {
glog.Errorf("setSpendingTxToVout error %v, %v, output %v", err, vout.ScriptPubKey.AddrDesc, vout.N)
glog.Errorf("setSpendingTxToVout error %v, %v, output %v", err, vout.AddrDesc, vout.N)
}
}
}
}
// for coinbase transactions valIn is 0
feesSat.Sub(&valInSat, &valOutSat)
if feesSat.Sign() == -1 {
feesSat.SetUint64(0)
if w.chainType == bchain.ChainBitcoinType {
// for coinbase transactions valIn is 0
feesSat.Sub(&valInSat, &valOutSat)
if feesSat.Sign() == -1 {
feesSat.SetUint64(0)
}
pValInSat = &valInSat
} else if w.chainType == bchain.ChainEthereumType {
ets, err := w.chainParser.EthereumTypeGetErc20FromTx(bchainTx)
if err != nil {
glog.Errorf("GetErc20FromTx error %v, %v", err, bchainTx)
}
tokens = make([]TokenTransfer, len(ets))
for i := range ets {
e := &ets[i]
cd, err := w.chainParser.GetAddrDescFromAddress(e.Contract)
if err != nil {
glog.Errorf("GetAddrDescFromAddress error %v, contract %v", err, e.Contract)
continue
}
erc20c, err := w.chain.EthereumTypeGetErc20ContractInfo(cd)
if err != nil {
glog.Errorf("GetErc20ContractInfo error %v, contract %v", err, e.Contract)
}
if erc20c == nil {
erc20c = &bchain.Erc20Contract{Name: e.Contract}
}
tokens[i] = TokenTransfer{
Type: "ERC20",
Token: e.Contract,
From: e.From,
To: e.To,
Decimals: erc20c.Decimals,
Value: (*Amount)(&e.Tokens),
Name: erc20c.Name,
Symbol: erc20c.Symbol,
}
}
ethTxData := eth.GetEthereumTxData(bchainTx)
// mempool txs do not have fees yet
if ethTxData.GasUsed != nil {
feesSat.Mul(ethTxData.GasPrice, ethTxData.GasUsed)
}
if len(bchainTx.Vout) > 0 {
valOutSat = bchainTx.Vout[0].ValueSat
}
ethSpecific = &EthereumSpecific{
GasLimit: ethTxData.GasLimit,
GasPrice: (*Amount)(ethTxData.GasPrice),
GasUsed: ethTxData.GasUsed,
Nonce: ethTxData.Nonce,
Status: ethTxData.Status,
}
}
// for now do not return size, we would have to compute vsize of segwit transactions
// size:=len(bchainTx.Hex) / 2
r := &Tx{
Blockhash: blockhash,
Blockheight: int(height),
Blocktime: bchainTx.Blocktime,
Confirmations: bchainTx.Confirmations,
Fees: w.chainParser.AmountToDecimalString(&feesSat),
FeesSat: feesSat,
Locktime: bchainTx.LockTime,
Time: bchainTx.Time,
Txid: bchainTx.Txid,
ValueIn: w.chainParser.AmountToDecimalString(&valInSat),
ValueInSat: valInSat,
ValueOut: w.chainParser.AmountToDecimalString(&valOutSat),
ValueOutSat: valOutSat,
Version: bchainTx.Version,
Hex: bchainTx.Hex,
Vin: vins,
Vout: vouts,
var sj json.RawMessage
if specificJSON {
sj, err = w.chain.GetTransactionSpecific(bchainTx)
if err != nil {
return nil, err
}
}
if spendingTxs {
glog.Info("GetTransaction ", txid, " finished in ", time.Since(start))
r := &Tx{
Blockhash: blockhash,
Blockheight: int(height),
Blocktime: bchainTx.Blocktime,
Confirmations: bchainTx.Confirmations,
FeesSat: (*Amount)(&feesSat),
Locktime: bchainTx.LockTime,
Time: bchainTx.Time,
Txid: bchainTx.Txid,
ValueInSat: (*Amount)(pValInSat),
ValueOutSat: (*Amount)(&valOutSat),
Version: bchainTx.Version,
Hex: bchainTx.Hex,
Vin: vins,
Vout: vouts,
CoinSpecificData: bchainTx.CoinSpecificData,
CoinSpecificJSON: sj,
TokenTransfers: tokens,
EthereumSpecific: ethSpecific,
}
return r, nil
}
func (w *Worker) getAddressTxids(addrDesc bchain.AddressDescriptor, mempool bool) ([]string, error) {
func (w *Worker) getAddressTxids(addrDesc bchain.AddressDescriptor, mempool bool, filter *AddressFilter) ([]string, error) {
var err error
txids := make([]string, 0, 4)
if !mempool {
err = w.db.GetAddrDescTransactions(addrDesc, 0, ^uint32(0), func(txid string, vout uint32, isOutput bool) error {
addFilteredTxid := func(txid string, vout int32, isOutput bool) error {
if filter.Vout == AddressFilterVoutOff ||
(filter.Vout == AddressFilterVoutInputs && !isOutput) ||
(filter.Vout == AddressFilterVoutOutputs && isOutput) ||
(vout == int32(filter.Vout)) {
txids = append(txids, txid)
return nil
})
}
return nil
}
if mempool {
o, err := w.chain.GetMempoolTransactionsForAddrDesc(addrDesc)
if err != nil {
return nil, err
}
for _, m := range o {
vout := m.Vout
isOutput := true
if vout < 0 {
isOutput = false
vout = ^vout
}
addFilteredTxid(m.Txid, vout, isOutput)
}
} else {
m, err := w.chain.GetMempoolTransactionsForAddrDesc(addrDesc)
to := filter.ToHeight
if to == 0 {
to = ^uint32(0)
}
err = w.db.GetAddrDescTransactions(addrDesc, filter.FromHeight, to, addFilteredTxid)
if err != nil {
return nil, err
}
txids = append(txids, m...)
}
return txids, nil
}
@ -244,8 +338,8 @@ func (w *Worker) getAddressTxids(addrDesc bchain.AddressDescriptor, mempool bool
func (t *Tx) getAddrVoutValue(addrDesc bchain.AddressDescriptor) *big.Int {
var val big.Int
for _, vout := range t.Vout {
if bytes.Equal(vout.ScriptPubKey.AddrDesc, addrDesc) {
val.Add(&val, &vout.ValueSat)
if bytes.Equal(vout.AddrDesc, addrDesc) && vout.ValueSat != nil {
val.Add(&val, (*big.Int)(vout.ValueSat))
}
}
return &val
@ -254,8 +348,8 @@ func (t *Tx) getAddrVoutValue(addrDesc bchain.AddressDescriptor) *big.Int {
func (t *Tx) getAddrVinValue(addrDesc bchain.AddressDescriptor) *big.Int {
var val big.Int
for _, vin := range t.Vin {
if bytes.Equal(vin.AddrDesc, addrDesc) {
val.Add(&val, &vin.ValueSat)
if bytes.Equal(vin.AddrDesc, addrDesc) && vin.ValueSat != nil {
val.Add(&val, (*big.Int)(vin.ValueSat))
}
}
return &val
@ -285,9 +379,8 @@ func (w *Worker) txFromTxAddress(txid string, ta *db.TxAddresses, bi *db.BlockIn
tai := &ta.Inputs[i]
vin := &vins[i]
vin.N = i
vin.ValueSat = tai.ValueSat
vin.Value = w.chainParser.AmountToDecimalString(&vin.ValueSat)
valInSat.Add(&valInSat, &vin.ValueSat)
vin.ValueSat = (*Amount)(&tai.ValueSat)
valInSat.Add(&valInSat, &tai.ValueSat)
vin.Addresses, vin.Searchable, err = tai.Addresses(w.chainParser)
if err != nil {
glog.Errorf("tai.Addresses error %v, tx %v, input %v, tai %+v", err, txid, i, tai)
@ -298,10 +391,9 @@ func (w *Worker) txFromTxAddress(txid string, ta *db.TxAddresses, bi *db.BlockIn
tao := &ta.Outputs[i]
vout := &vouts[i]
vout.N = i
vout.ValueSat = tao.ValueSat
vout.Value = w.chainParser.AmountToDecimalString(&vout.ValueSat)
valOutSat.Add(&valOutSat, &vout.ValueSat)
vout.ScriptPubKey.Addresses, vout.ScriptPubKey.Searchable, err = tao.Addresses(w.chainParser)
vout.ValueSat = (*Amount)(&tao.ValueSat)
valOutSat.Add(&valOutSat, &tao.ValueSat)
vout.Addresses, vout.Searchable, err = tao.Addresses(w.chainParser)
if err != nil {
glog.Errorf("tai.Addresses error %v, tx %v, output %v, tao %+v", err, txid, i, tao)
}
@ -317,11 +409,11 @@ func (w *Worker) txFromTxAddress(txid string, ta *db.TxAddresses, bi *db.BlockIn
Blockheight: int(ta.Height),
Blocktime: bi.Time,
Confirmations: bestheight - ta.Height + 1,
Fees: w.chainParser.AmountToDecimalString(&feesSat),
FeesSat: (*Amount)(&feesSat),
Time: bi.Time,
Txid: txid,
ValueIn: w.chainParser.AmountToDecimalString(&valInSat),
ValueOut: w.chainParser.AmountToDecimalString(&valOutSat),
ValueInSat: (*Amount)(&valInSat),
ValueOutSat: (*Amount)(&valOutSat),
Vin: vins,
Vout: vouts,
}
@ -349,8 +441,94 @@ func computePaging(count, page, itemsOnPage int) (Paging, int, int, int) {
}, from, to, page
}
func (w *Worker) getEthereumTypeAddressBalances(addrDesc bchain.AddressDescriptor, option GetAddressOption, filter *AddressFilter) (*db.AddrBalance, []Erc20Token, *bchain.Erc20Contract, uint64, error) {
var (
ba *db.AddrBalance
erc20t []Erc20Token
ci *bchain.Erc20Contract
n uint64
)
ca, err := w.db.GetAddrDescContracts(addrDesc)
if err != nil {
return nil, nil, nil, 0, NewAPIError(fmt.Sprintf("Address not found, %v", err), true)
}
if ca != nil {
ba = &db.AddrBalance{
Txs: uint32(ca.EthTxs),
}
var b *big.Int
b, err = w.chain.EthereumTypeGetBalance(addrDesc)
if err != nil {
return nil, nil, nil, 0, errors.Annotatef(err, "EthereumTypeGetBalance %v", addrDesc)
}
if b != nil {
ba.BalanceSat = *b
}
n, err = w.chain.EthereumTypeGetNonce(addrDesc)
if err != nil {
return nil, nil, nil, 0, errors.Annotatef(err, "EthereumTypeGetNonce %v", addrDesc)
}
var filterDesc bchain.AddressDescriptor
if filter.Contract != "" {
filterDesc, err = w.chainParser.GetAddrDescFromAddress(filter.Contract)
if err != nil {
return nil, nil, nil, 0, NewAPIError(fmt.Sprintf("Invalid contract filter, %v", err), true)
}
}
erc20t = make([]Erc20Token, len(ca.Contracts))
var j int
for i, c := range ca.Contracts {
if len(filterDesc) > 0 {
if !bytes.Equal(filterDesc, c.Contract) {
continue
}
// filter only transactions by this contract
filter.Vout = i + 1
}
ci, err := w.chain.EthereumTypeGetErc20ContractInfo(c.Contract)
if err != nil {
return nil, nil, nil, 0, errors.Annotatef(err, "EthereumTypeGetErc20ContractInfo %v", c.Contract)
}
if ci == nil {
ci = &bchain.Erc20Contract{}
addresses, _, _ := w.chainParser.GetAddressesFromAddrDesc(c.Contract)
if len(addresses) > 0 {
ci.Contract = addresses[0]
ci.Name = addresses[0]
}
}
// do not read contract balances etc in case of Basic option
if option != Basic {
b, err = w.chain.EthereumTypeGetErc20ContractBalance(addrDesc, c.Contract)
if err != nil {
// return nil, nil, nil, errors.Annotatef(err, "EthereumTypeGetErc20ContractBalance %v %v", addrDesc, c.Contract)
glog.Warningf("EthereumTypeGetErc20ContractBalance addr %v, contract %v, %v", addrDesc, c.Contract, err)
}
} else {
b = nil
}
erc20t[j] = Erc20Token{
BalanceSat: (*Amount)(b),
Contract: ci.Contract,
Name: ci.Name,
Symbol: ci.Symbol,
Transfers: int(c.Txs),
Decimals: ci.Decimals,
ContractIndex: strconv.Itoa(i + 1),
}
j++
}
erc20t = erc20t[:j]
ci, err = w.chain.EthereumTypeGetErc20ContractInfo(addrDesc)
if err != nil {
return nil, nil, nil, 0, err
}
}
return ba, erc20t, ci, n, nil
}
// GetAddress computes address value and gets transactions for given address
func (w *Worker) GetAddress(address string, page int, txsOnPage int, onlyTxids bool) (*Address, error) {
func (w *Worker) GetAddress(address string, page int, txsOnPage int, option GetAddressOption, filter *AddressFilter) (*Address, error) {
start := time.Now()
page--
if page < 0 {
@ -360,118 +538,148 @@ func (w *Worker) GetAddress(address string, page int, txsOnPage int, onlyTxids b
if err != nil {
return nil, NewAPIError(fmt.Sprintf("Invalid address, %v", err), true)
}
// ba can be nil if the address is only in mempool!
ba, err := w.db.GetAddrDescBalance(addrDesc)
if err != nil {
return nil, NewAPIError(fmt.Sprintf("Address not found, %v", err), true)
}
// convert the address to the format defined by the parser
addresses, _, err := w.chainParser.GetAddressesFromAddrDesc(addrDesc)
if err != nil {
glog.V(2).Infof("GetAddressesFromAddrDesc error %v, %v", err, addrDesc)
}
if len(addresses) == 1 {
address = addresses[0]
}
txc, err := w.getAddressTxids(addrDesc, false)
if err != nil {
return nil, errors.Annotatef(err, "getAddressTxids %v false", address)
}
txc = UniqueTxidsInReverse(txc)
var txm []string
// if there are only unconfirmed transactions, ba is nil
if ba == nil {
ba = &db.AddrBalance{}
page = 0
}
txm, err = w.getAddressTxids(addrDesc, true)
if err != nil {
return nil, errors.Annotatef(err, "getAddressTxids %v true", address)
}
txm = UniqueTxidsInReverse(txm)
// check if the address exist
if len(txc)+len(txm) == 0 {
return &Address{
AddrStr: address,
}, nil
}
bestheight, _, err := w.db.GetBestBlock()
if err != nil {
return nil, errors.Annotatef(err, "GetBestBlock")
}
pg, from, to, page := computePaging(len(txc), page, txsOnPage)
var txs []*Tx
var txids []string
if onlyTxids {
txids = make([]string, len(txm)+to-from)
} else {
txs = make([]*Tx, len(txm)+to-from)
}
txi := 0
// load mempool transactions
var uBalSat big.Int
for _, tx := range txm {
tx, err := w.GetTransaction(tx, false)
// mempool transaction may fail
var (
ba *db.AddrBalance
erc20t []Erc20Token
erc20c *bchain.Erc20Contract
txm []string
txs []*Tx
txids []string
pg Paging
uBalSat big.Int
totalReceived, totalSent *big.Int
nonce string
)
if w.chainType == bchain.ChainEthereumType {
var n uint64
ba, erc20t, erc20c, n, err = w.getEthereumTypeAddressBalances(addrDesc, option, filter)
if err != nil {
glog.Error("GetTransaction in mempool ", tx, ": ", err)
} else {
uBalSat.Add(&uBalSat, tx.getAddrVoutValue(addrDesc))
uBalSat.Sub(&uBalSat, tx.getAddrVinValue(addrDesc))
if page == 0 {
if onlyTxids {
txids[txi] = tx.Txid
return nil, err
}
nonce = strconv.Itoa(int(n))
} else {
// ba can be nil if the address is only in mempool!
ba, err = w.db.GetAddrDescBalance(addrDesc)
if err != nil {
return nil, NewAPIError(fmt.Sprintf("Address not found, %v", err), true)
}
}
// get tx history if requested by option or check mempool if there are some transactions for a new address
if option >= TxidHistory || ba == nil {
// convert the address to the format defined by the parser
addresses, _, err := w.chainParser.GetAddressesFromAddrDesc(addrDesc)
if err != nil {
glog.V(2).Infof("GetAddressesFromAddrDesc error %v, %v", err, addrDesc)
}
if len(addresses) == 1 {
address = addresses[0]
}
txm, err = w.getAddressTxids(addrDesc, true, filter)
if err != nil {
return nil, errors.Annotatef(err, "getAddressTxids %v true", addrDesc)
}
txm = UniqueTxidsInReverse(txm)
// if there are only unconfirmed transactions, there is no paging
if ba == nil {
ba = &db.AddrBalance{}
page = 0
}
if option >= TxidHistory {
txc, err := w.getAddressTxids(addrDesc, false, filter)
if err != nil {
return nil, errors.Annotatef(err, "getAddressTxids %v false", addrDesc)
}
txc = UniqueTxidsInReverse(txc)
bestheight, _, err := w.db.GetBestBlock()
if err != nil {
return nil, errors.Annotatef(err, "GetBestBlock")
}
var from, to int
pg, from, to, page = computePaging(len(txc), page, txsOnPage)
if option == TxidHistory {
txids = make([]string, len(txm)+to-from)
} else {
txs = make([]*Tx, len(txm)+to-from)
}
txi := 0
// get mempool transactions
for _, txid := range txm {
tx, err := w.GetTransaction(txid, false, false)
// mempool transaction may fail
if err != nil {
glog.Error("GetTransaction in mempool ", tx, ": ", err)
} else {
txs[txi] = tx
uBalSat.Add(&uBalSat, tx.getAddrVoutValue(addrDesc))
uBalSat.Sub(&uBalSat, tx.getAddrVinValue(addrDesc))
if page == 0 {
if option == TxidHistory {
txids[txi] = tx.Txid
} else {
txs[txi] = tx
}
txi++
}
}
}
// get confirmed transactions
for i := from; i < to; i++ {
txid := txc[i]
if option == TxidHistory {
txids[txi] = txid
} else {
// only ChainBitcoinType supports TxHistoryLight
if option == TxHistoryLight && w.chainType == bchain.ChainBitcoinType {
ta, err := w.db.GetTxAddresses(txid)
if err != nil {
return nil, errors.Annotatef(err, "GetTxAddresses %v", txid)
}
if ta == nil {
glog.Warning("DB inconsistency: tx ", txid, ": not found in txAddresses")
continue
}
bi, err := w.db.GetBlockInfo(ta.Height)
if err != nil {
return nil, errors.Annotatef(err, "GetBlockInfo %v", ta.Height)
}
if bi == nil {
glog.Warning("DB inconsistency: block height ", ta.Height, ": not found in db")
continue
}
txs[txi] = w.txFromTxAddress(txid, ta, bi, bestheight)
} else {
txs[txi], err = w.GetTransaction(txid, false, true)
if err != nil {
return nil, errors.Annotatef(err, "GetTransaction %v", txid)
}
}
}
txi++
}
if option == TxidHistory {
txids = txids[:txi]
} else if option >= TxHistoryLight {
txs = txs[:txi]
}
}
}
if len(txc) != int(ba.Txs) {
glog.Warning("DB inconsistency for address ", address, ": number of txs from column addresses ", len(txc), ", from addressBalance ", ba.Txs)
}
for i := from; i < to; i++ {
txid := txc[i]
if onlyTxids {
txids[txi] = txid
} else {
ta, err := w.db.GetTxAddresses(txid)
if err != nil {
return nil, errors.Annotatef(err, "GetTxAddresses %v", txid)
}
if ta == nil {
glog.Warning("DB inconsistency: tx ", txid, ": not found in txAddresses")
continue
}
bi, err := w.db.GetBlockInfo(ta.Height)
if err != nil {
return nil, errors.Annotatef(err, "GetBlockInfo %v", ta.Height)
}
if bi == nil {
glog.Warning("DB inconsistency: block height ", ta.Height, ": not found in db")
continue
}
txs[txi] = w.txFromTxAddress(txid, ta, bi, bestheight)
}
txi++
}
if onlyTxids {
txids = txids[:txi]
} else {
txs = txs[:txi]
if w.chainType == bchain.ChainBitcoinType {
totalReceived = ba.ReceivedSat()
totalSent = &ba.SentSat
}
r := &Address{
Paging: pg,
AddrStr: address,
Balance: w.chainParser.AmountToDecimalString(&ba.BalanceSat),
TotalReceived: w.chainParser.AmountToDecimalString(ba.ReceivedSat()),
TotalSent: w.chainParser.AmountToDecimalString(&ba.SentSat),
TxApperances: len(txc),
UnconfirmedBalance: w.chainParser.AmountToDecimalString(&uBalSat),
BalanceSat: (*Amount)(&ba.BalanceSat),
TotalReceivedSat: (*Amount)(totalReceived),
TotalSentSat: (*Amount)(totalSent),
TxApperances: int(ba.Txs),
UnconfirmedBalanceSat: (*Amount)(&uBalSat),
UnconfirmedTxApperances: len(txm),
Transactions: txs,
Txids: txids,
Erc20Contract: erc20c,
Erc20Tokens: erc20t,
Nonce: nonce,
}
glog.Info("GetAddress ", address, " finished in ", time.Since(start))
return r, nil
@ -488,7 +696,7 @@ func (w *Worker) GetAddressUtxo(address string, onlyConfirmed bool) ([]AddressUt
r := make([]AddressUtxo, 0, 8)
if !onlyConfirmed {
// get utxo from mempool
txm, err := w.getAddressTxids(addrDesc, true)
txm, err := w.getAddressTxids(addrDesc, true, &AddressFilter{Vout: AddressFilterVoutOff})
if err != nil {
return nil, errors.Annotatef(err, "getAddressTxids %v true", address)
}
@ -520,9 +728,8 @@ func (w *Worker) GetAddressUtxo(address string, onlyConfirmed bool) ([]AddressUt
if !e {
r = append(r, AddressUtxo{
Txid: bchainTx.Txid,
Vout: uint32(i),
AmountSat: vout.ValueSat,
Amount: w.chainParser.AmountToDecimalString(&vout.ValueSat),
Vout: int32(i),
AmountSat: (*Amount)(&vout.ValueSat),
})
}
}
@ -538,14 +745,10 @@ func (w *Worker) GetAddressUtxo(address string, onlyConfirmed bool) ([]AddressUt
var checksum big.Int
// ba can be nil if the address is only in mempool!
if ba != nil && ba.BalanceSat.Uint64() > 0 {
type outpoint struct {
txid string
vout uint32
}
outpoints := make([]outpoint, 0, 8)
err = w.db.GetAddrDescTransactions(addrDesc, 0, ^uint32(0), func(txid string, vout uint32, isOutput bool) error {
outpoints := make([]bchain.Outpoint, 0, 8)
err = w.db.GetAddrDescTransactions(addrDesc, 0, ^uint32(0), func(txid string, vout int32, isOutput bool) error {
if isOutput {
outpoints = append(outpoints, outpoint{txid, vout})
outpoints = append(outpoints, bchain.Outpoint{Txid: txid, Vout: vout})
}
return nil
})
@ -562,29 +765,28 @@ func (w *Worker) GetAddressUtxo(address string, onlyConfirmed bool) ([]AddressUt
bestheight := int(b)
for i := len(outpoints) - 1; i >= 0 && checksum.Int64() > 0; i-- {
o := outpoints[i]
if lastTxid != o.txid {
ta, err = w.db.GetTxAddresses(o.txid)
if lastTxid != o.Txid {
ta, err = w.db.GetTxAddresses(o.Txid)
if err != nil {
return nil, err
}
lastTxid = o.txid
lastTxid = o.Txid
}
if ta == nil {
glog.Warning("DB inconsistency: tx ", o.txid, ": not found in txAddresses")
glog.Warning("DB inconsistency: tx ", o.Txid, ": not found in txAddresses")
} else {
if len(ta.Outputs) <= int(o.vout) {
glog.Warning("DB inconsistency: txAddresses ", o.txid, " does not have enough outputs")
if len(ta.Outputs) <= int(o.Vout) {
glog.Warning("DB inconsistency: txAddresses ", o.Txid, " does not have enough outputs")
} else {
if !ta.Outputs[o.vout].Spent {
v := ta.Outputs[o.vout].ValueSat
if !ta.Outputs[o.Vout].Spent {
v := ta.Outputs[o.Vout].ValueSat
// report only outpoints that are not spent in mempool
_, e := spentInMempool[o.txid+strconv.Itoa(int(o.vout))]
_, e := spentInMempool[o.Txid+strconv.Itoa(int(o.Vout))]
if !e {
r = append(r, AddressUtxo{
Txid: o.txid,
Vout: o.vout,
AmountSat: v,
Amount: w.chainParser.AmountToDecimalString(&v),
Txid: o.Txid,
Vout: o.Vout,
AmountSat: (*Amount)(&v),
Height: int(ta.Height),
Confirmations: bestheight - int(ta.Height) + 1,
})
@ -669,24 +871,37 @@ func (w *Worker) GetBlock(bid string, page int, txsOnPage int) (*Block, error) {
return nil, errors.Annotatef(err, "GetBestBlock")
}
pg, from, to, page := computePaging(txCount, page, txsOnPage)
glog.Info("GetBlock ", bid, ", page ", page, " finished in ", time.Since(start))
txs := make([]*Tx, to-from)
txi := 0
for i := from; i < to; i++ {
txid := bi.Txids[i]
ta, err := w.db.GetTxAddresses(txid)
if err != nil {
return nil, errors.Annotatef(err, "GetTxAddresses %v", txid)
if w.chainType == bchain.ChainBitcoinType {
ta, err := w.db.GetTxAddresses(txid)
if err != nil {
return nil, errors.Annotatef(err, "GetTxAddresses %v", txid)
}
if ta == nil {
glog.Warning("DB inconsistency: tx ", txid, ": not found in txAddresses")
continue
}
txs[txi] = w.txFromTxAddress(txid, ta, dbi, bestheight)
} else {
txs[txi], err = w.GetTransaction(txid, false, false)
if err != nil {
return nil, err
}
}
if ta == nil {
glog.Warning("DB inconsistency: tx ", txid, ": not found in txAddresses")
continue
}
txs[txi] = w.txFromTxAddress(txid, ta, dbi, bestheight)
txi++
}
if bi.Prev == "" && bi.Height != 0 {
bi.Prev, _ = w.db.GetBlockHash(bi.Height - 1)
}
if bi.Next == "" && bi.Height != bestheight {
bi.Next, _ = w.db.GetBlockHash(bi.Height + 1)
}
txs = txs[:txi]
bi.Txids = nil
glog.Info("GetBlock ", bid, ", page ", page, " finished in ", time.Since(start))
return &Block{
Paging: pg,
BlockInfo: *bi,
@ -725,6 +940,7 @@ func (w *Worker) GetSystemInfo(internal bool) (*SystemInfo, error) {
InSyncMempool: ms,
LastMempoolTime: mt,
MempoolSize: msz,
Decimals: w.chainParser.AmountDecimals(),
DbSize: w.db.DatabaseSizeOnDisk(),
DbSizeFromColumns: dbs,
DbColumns: dbc,

55
bchain/basechain.go Normal file
View File

@ -0,0 +1,55 @@
package bchain
import (
"errors"
"math/big"
)
// BaseChain is base type for bchain.BlockChain
type BaseChain struct {
Parser BlockChainParser
Testnet bool
Network string
}
// TODO more bchain.BlockChain methods
// GetChainParser returns BlockChainParser
func (b *BaseChain) GetChainParser() BlockChainParser {
return b.Parser
}
// IsTestnet returns true if the blockchain is testnet
func (b *BaseChain) IsTestnet() bool {
return b.Testnet
}
// GetNetworkName returns network name
func (b *BaseChain) GetNetworkName() string {
return b.Network
}
// EthereumTypeGetBalance is not supported
func (b *BaseChain) EthereumTypeGetBalance(addrDesc AddressDescriptor) (*big.Int, error) {
return nil, errors.New("Not supported")
}
// EthereumTypeGetNonce is not supported
func (b *BaseChain) EthereumTypeGetNonce(addrDesc AddressDescriptor) (uint64, error) {
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")
}
// EthereumTypeGetErc20ContractBalance is not supported
func (b *BaseChain) EthereumTypeGetErc20ContractBalance(addrDesc, contractDesc AddressDescriptor) (*big.Int, error) {
return nil, errors.New("Not supported")
}

View File

@ -34,10 +34,14 @@ func (p *BaseParser) AmountToBigInt(n json.Number) (big.Int, error) {
var r big.Int
s := string(n)
i := strings.IndexByte(s, '.')
d := p.AmountDecimalPoint
if d > len(zeros) {
d = len(zeros)
}
if i == -1 {
s = s + zeros[:p.AmountDecimalPoint]
s = s + zeros[:d]
} else {
z := p.AmountDecimalPoint - len(s) + i + 1
z := d - len(s) + i + 1
if z > 0 {
s = s[:i] + s[i+1:] + zeros[:z]
} else {
@ -50,18 +54,24 @@ func (p *BaseParser) AmountToBigInt(n json.Number) (big.Int, error) {
return r, nil
}
// AmountToDecimalString converts amount in big.Int to string with decimal point in the correct place
func (p *BaseParser) AmountToDecimalString(a *big.Int) string {
// AmountToDecimalString converts amount in big.Int to string with decimal point in the place defined by the parameter d
func AmountToDecimalString(a *big.Int, d int) string {
if a == nil {
return ""
}
n := a.String()
var s string
if n[0] == '-' {
n = n[1:]
s = "-"
}
if len(n) <= p.AmountDecimalPoint {
n = zeros[:p.AmountDecimalPoint-len(n)+1] + n
if d > len(zeros) {
d = len(zeros)
}
i := len(n) - p.AmountDecimalPoint
if len(n) <= d {
n = zeros[:d-len(n)+1] + n
}
i := len(n) - d
ad := strings.TrimRight(n[i:], "0")
if len(ad) > 0 {
n = n[:i] + "." + ad
@ -71,6 +81,16 @@ func (p *BaseParser) AmountToDecimalString(a *big.Int) string {
return s + n
}
// AmountToDecimalString converts amount in big.Int to string with decimal point in the correct place
func (p *BaseParser) AmountToDecimalString(a *big.Int) string {
return AmountToDecimalString(a, p.AmountDecimalPoint)
}
// AmountDecimals returns number of decimal places in amounts
func (p *BaseParser) AmountDecimals() int {
return p.AmountDecimalPoint
}
// ParseTxFromJson parses JSON message containing transaction and returns Tx struct
func (p *BaseParser) ParseTxFromJson(msg json.RawMessage) (*Tx, error) {
var tx Tx
@ -125,9 +145,9 @@ func (p *BaseParser) UnpackBlockHash(buf []byte) (string, error) {
return hex.EncodeToString(buf), nil
}
// IsUTXOChain returns true if the block chain is UTXO type, otherwise false
func (p *BaseParser) IsUTXOChain() bool {
return true
// GetChainType is type of the blockchain, default is ChainBitcoinType
func (p *BaseParser) GetChainType() ChainType {
return ChainBitcoinType
}
// PackTx packs transaction to byte array using protobuf
@ -171,6 +191,7 @@ func (p *BaseParser) PackTx(tx *Tx, height uint32, blockTime int64) ([]byte, err
Locktime: tx.LockTime,
Vin: pti,
Vout: pto,
Version: tx.Version,
}
if pt.Hex, err = hex.DecodeString(tx.Hex); err != nil {
return nil, errors.Annotatef(err, "Hex %v", tx.Hex)
@ -230,6 +251,12 @@ func (p *BaseParser) UnpackTx(buf []byte) (*Tx, uint32, error) {
Txid: txid,
Vin: vin,
Vout: vout,
Version: pt.Version,
}
return &tx, pt.Height, nil
}
// EthereumTypeGetErc20FromTx is unsupported
func (p *BaseParser) EthereumTypeGetErc20FromTx(tx *Tx) ([]Erc20Transfer, error) {
return nil, errors.New("Not supported")
}

View File

@ -27,7 +27,8 @@ var amounts = []struct {
{big.NewInt(-8), "-0.00000008", 8, "!"},
{big.NewInt(-89012345678), "-890.12345678", 8, "!"},
{big.NewInt(-12345), "-0.00012345", 8, "!"},
{big.NewInt(12345678), "0.123456789012", 8, "0.12345678"}, // test of truncation of too many decimal places
{big.NewInt(12345678), "0.123456789012", 8, "0.12345678"}, // test of truncation of too many decimal places
{big.NewInt(12345678), "0.0000000000000000000000000000000012345678", 1234, "!"}, // test of too big number decimal places
}
func TestBaseParser_AmountToDecimalString(t *testing.T) {

View File

@ -11,6 +11,7 @@ import (
"blockbook/bchain/coins/eth"
"blockbook/bchain/coins/gamecredits"
"blockbook/bchain/coins/grs"
"blockbook/bchain/coins/liquid"
"blockbook/bchain/coins/litecoin"
"blockbook/bchain/coins/monacoin"
"blockbook/bchain/coins/myriad"
@ -57,6 +58,7 @@ func init() {
BlockChainFactories["Monacoin Testnet"] = monacoin.NewMonacoinRPC
BlockChainFactories["DigiByte"] = digibyte.NewDigiByteRPC
BlockChainFactories["Myriad"] = myriad.NewMyriadRPC
BlockChainFactories["Liquid"] = liquid.NewLiquidRPC
BlockChainFactories["Groestlcoin"] = grs.NewGroestlcoinRPC
BlockChainFactories["Groestlcoin Testnet"] = grs.NewGroestlcoinRPC
}
@ -187,9 +189,9 @@ func (c *blockChainWithMetrics) GetTransaction(txid string) (v *bchain.Tx, err e
return c.b.GetTransaction(txid)
}
func (c *blockChainWithMetrics) GetTransactionSpecific(txid string) (v json.RawMessage, err error) {
func (c *blockChainWithMetrics) GetTransactionSpecific(tx *bchain.Tx) (v json.RawMessage, err error) {
defer func(s time.Time) { c.observeRPCLatency("GetTransactionSpecific", s, err) }(time.Now())
return c.b.GetTransactionSpecific(txid)
return c.b.GetTransactionSpecific(tx)
}
func (c *blockChainWithMetrics) GetTransactionForMempool(txid string) (v *bchain.Tx, err error) {
@ -221,12 +223,12 @@ func (c *blockChainWithMetrics) ResyncMempool(onNewTxAddr bchain.OnNewTxAddrFunc
return count, err
}
func (c *blockChainWithMetrics) GetMempoolTransactions(address string) (v []string, err error) {
func (c *blockChainWithMetrics) GetMempoolTransactions(address string) (v []bchain.Outpoint, err error) {
defer func(s time.Time) { c.observeRPCLatency("GetMempoolTransactions", s, err) }(time.Now())
return c.b.GetMempoolTransactions(address)
}
func (c *blockChainWithMetrics) GetMempoolTransactionsForAddrDesc(addrDesc bchain.AddressDescriptor) (v []string, err error) {
func (c *blockChainWithMetrics) GetMempoolTransactionsForAddrDesc(addrDesc bchain.AddressDescriptor) (v []bchain.Outpoint, err error) {
defer func(s time.Time) { c.observeRPCLatency("GetMempoolTransactionsForAddrDesc", s, err) }(time.Now())
return c.b.GetMempoolTransactionsForAddrDesc(addrDesc)
}
@ -239,3 +241,28 @@ func (c *blockChainWithMetrics) GetMempoolEntry(txid string) (v *bchain.MempoolE
func (c *blockChainWithMetrics) GetChainParser() bchain.BlockChainParser {
return c.b.GetChainParser()
}
func (c *blockChainWithMetrics) EthereumTypeGetBalance(addrDesc bchain.AddressDescriptor) (v *big.Int, err error) {
defer func(s time.Time) { c.observeRPCLatency("EthereumTypeGetBalance", s, err) }(time.Now())
return c.b.EthereumTypeGetBalance(addrDesc)
}
func (c *blockChainWithMetrics) EthereumTypeGetNonce(addrDesc bchain.AddressDescriptor) (v uint64, err error) {
defer func(s time.Time) { c.observeRPCLatency("EthereumTypeGetNonce", s, err) }(time.Now())
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)
}
func (c *blockChainWithMetrics) EthereumTypeGetErc20ContractBalance(addrDesc, contractDesc bchain.AddressDescriptor) (v *big.Int, err error) {
defer func(s time.Time) { c.observeRPCLatency("EthereumTypeGetErc20ContractInfo", s, err) }(time.Now())
return c.b.EthereumTypeGetErc20ContractBalance(addrDesc, contractDesc)
}

View File

@ -11,24 +11,22 @@ import (
"math/big"
"net"
"net/http"
"runtime/debug"
"time"
"github.com/btcsuite/btcd/wire"
"github.com/golang/glog"
"github.com/juju/errors"
)
// BitcoinRPC is an interface to JSON-RPC bitcoind service.
type BitcoinRPC struct {
*bchain.BaseChain
client http.Client
rpcURL string
user string
password string
Parser bchain.BlockChainParser
Testnet bool
Network string
Mempool *bchain.UTXOMempool
Mempool *bchain.MempoolBitcoinType
ParseBlocks bool
pushHandler func(bchain.NotificationType)
mq *bchain.MQ
@ -36,6 +34,7 @@ type BitcoinRPC struct {
RPCMarshaler RPCMarshaler
}
// Configuration represents json config file
type Configuration struct {
CoinName string `json:"coin_name"`
CoinShortcut string `json:"coin_shortcut"`
@ -84,6 +83,7 @@ func NewBitcoinRPC(config json.RawMessage, pushHandler func(bchain.NotificationT
}
s := &BitcoinRPC{
BaseChain: &bchain.BaseChain{},
client: http.Client{Timeout: time.Duration(c.RPCTimeout) * time.Second, Transport: transport},
rpcURL: c.RPCURL,
user: c.RPCUser,
@ -115,7 +115,7 @@ func (b *BitcoinRPC) GetChainInfoAndInitializeMempool(bc bchain.BlockChain) (str
}
b.mq = mq
b.Mempool = bchain.NewUTXOMempool(bc, b.ChainConfig.MempoolWorkers, b.ChainConfig.MempoolSubWorkers)
b.Mempool = bchain.NewMempoolBitcoinType(bc, b.ChainConfig.MempoolWorkers, b.ChainConfig.MempoolSubWorkers)
return chainName, nil
}
@ -158,14 +158,6 @@ func (b *BitcoinRPC) Shutdown(ctx context.Context) error {
return nil
}
func (b *BitcoinRPC) IsTestnet() bool {
return b.Testnet
}
func (b *BitcoinRPC) GetNetworkName() string {
return b.Network
}
func (b *BitcoinRPC) GetCoinName() string {
return b.ChainConfig.CoinName
}
@ -580,7 +572,7 @@ func (b *BitcoinRPC) GetBlockWithoutHeader(hash string, height uint32) (*bchain.
return block, nil
}
// GetBlockRaw returns block with given hash as bytes.
// GetBlockRaw returns block with given hash as bytes
func (b *BitcoinRPC) GetBlockRaw(hash string) ([]byte, error) {
glog.V(1).Info("rpc: getblock (verbosity=0) ", hash)
@ -602,7 +594,7 @@ func (b *BitcoinRPC) GetBlockRaw(hash string) ([]byte, error) {
return hex.DecodeString(res.Result)
}
// GetBlockFull returns block with given hash.
// GetBlockFull returns block with given hash
func (b *BitcoinRPC) GetBlockFull(hash string) (*bchain.Block, error) {
glog.V(1).Info("rpc: getblock (verbosity=2) ", hash)
@ -624,7 +616,7 @@ func (b *BitcoinRPC) GetBlockFull(hash string) (*bchain.Block, error) {
return &res.Result, nil
}
// GetMempool returns transactions in mempool.
// GetMempool returns transactions in mempool
func (b *BitcoinRPC) GetMempool() ([]string, error) {
glog.V(1).Info("rpc: getrawmempool")
@ -641,7 +633,7 @@ func (b *BitcoinRPC) GetMempool() ([]string, error) {
return res.Result, nil
}
// GetTransactionForMempool returns a transaction by the transaction ID.
// GetTransactionForMempool returns a transaction by the transaction ID
// It could be optimized for mempool, i.e. without block time and confirmations
func (b *BitcoinRPC) GetTransactionForMempool(txid string) (*bchain.Tx, error) {
glog.V(1).Info("rpc: getrawtransaction nonverbose ", txid)
@ -668,13 +660,14 @@ func (b *BitcoinRPC) GetTransactionForMempool(txid string) (*bchain.Tx, error) {
return tx, nil
}
// GetTransaction returns a transaction by the transaction ID.
// GetTransaction returns a transaction by the transaction ID
func (b *BitcoinRPC) GetTransaction(txid string) (*bchain.Tx, error) {
r, err := b.GetTransactionSpecific(txid)
r, err := b.getRawTransaction(txid)
if err != nil {
return nil, err
}
tx, err := b.Parser.ParseTxFromJson(r)
tx.CoinSpecificData = r
if err != nil {
return nil, errors.Annotatef(err, "txid %v", txid)
}
@ -682,7 +675,15 @@ func (b *BitcoinRPC) GetTransaction(txid string) (*bchain.Tx, error) {
}
// GetTransactionSpecific returns json as returned by backend, with all coin specific data
func (b *BitcoinRPC) GetTransactionSpecific(txid string) (json.RawMessage, error) {
func (b *BitcoinRPC) GetTransactionSpecific(tx *bchain.Tx) (json.RawMessage, error) {
if csd, ok := tx.CoinSpecificData.(json.RawMessage); ok {
return csd, nil
}
return b.getRawTransaction(tx.Txid)
}
// getRawTransaction returns json as returned by backend, with all coin specific data
func (b *BitcoinRPC) getRawTransaction(txid string) (json.RawMessage, error) {
glog.V(1).Info("rpc: getrawtransaction ", txid)
res := ResGetRawTransaction{}
@ -702,18 +703,18 @@ func (b *BitcoinRPC) GetTransactionSpecific(txid string) (json.RawMessage, error
// ResyncMempool gets mempool transactions and maps output scripts to transactions.
// ResyncMempool is not reentrant, it should be called from a single thread.
// It returns number of transactions in mempool
// Return value is number of transactions in mempool
func (b *BitcoinRPC) ResyncMempool(onNewTxAddr bchain.OnNewTxAddrFunc) (int, error) {
return b.Mempool.Resync(onNewTxAddr)
}
// GetMempoolTransactions returns slice of mempool transactions for given address
func (b *BitcoinRPC) GetMempoolTransactions(address string) ([]string, error) {
func (b *BitcoinRPC) GetMempoolTransactions(address string) ([]bchain.Outpoint, error) {
return b.Mempool.GetTransactions(address)
}
// GetMempoolTransactionsForAddrDesc returns slice of mempool transactions for given address descriptor
func (b *BitcoinRPC) GetMempoolTransactionsForAddrDesc(addrDesc bchain.AddressDescriptor) ([]string, error) {
func (b *BitcoinRPC) GetMempoolTransactionsForAddrDesc(addrDesc bchain.AddressDescriptor) ([]bchain.Outpoint, error) {
return b.Mempool.GetAddrDescTransactions(addrDesc)
}
@ -828,6 +829,7 @@ func safeDecodeResponse(body io.ReadCloser, res interface{}) (err error) {
defer func() {
if r := recover(); r != nil {
glog.Error("unmarshal json recovered from panic: ", r, "; data: ", string(data))
debug.PrintStack()
if len(data) > 0 && len(data) < 2048 {
err = errors.Errorf("Error: %v", string(data))
} else {
@ -872,8 +874,3 @@ func (b *BitcoinRPC) Call(req interface{}, res interface{}) error {
}
return safeDecodeResponse(httpRes.Body, &res)
}
// GetChainParser returns BlockChainParser
func (b *BitcoinRPC) GetChainParser() bchain.BlockChainParser {
return b.Parser
}

218
bchain/coins/eth/erc20.go Normal file
View File

@ -0,0 +1,218 @@
package eth
import (
"blockbook/bchain"
"context"
"encoding/hex"
"math/big"
"strings"
"sync"
ethcommon "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/golang/glog"
"github.com/juju/errors"
)
var erc20abi = `[{"constant":true,"inputs":[],"name":"name","outputs":[{"name":"","type":"string"}],"payable":false,"type":"function","signature":"0x06fdde03"},
{"constant":true,"inputs":[],"name":"symbol","outputs":[{"name":"","type":"string"}],"payable":false,"type":"function","signature":"0x95d89b41"},
{"constant":true,"inputs":[],"name":"decimals","outputs":[{"name":"","type":"uint8"}],"payable":false,"type":"function","signature":"0x313ce567"},
{"constant":true,"inputs":[],"name":"totalSupply","outputs":[{"name":"","type":"uint256"}],"payable":false,"type":"function","signature":"0x18160ddd"},
{"constant":true,"inputs":[{"name":"_owner","type":"address"}],"name":"balanceOf","outputs":[{"name":"balance","type":"uint256"}],"payable":false,"type":"function","signature":"0x70a08231"},
{"constant":false,"inputs":[{"name":"_to","type":"address"},{"name":"_value","type":"uint256"}],"name":"transfer","outputs":[{"name":"success","type":"bool"}],"payable":false,"type":"function","signature":"0xa9059cbb"},
{"constant":false,"inputs":[{"name":"_from","type":"address"},{"name":"_to","type":"address"},{"name":"_value","type":"uint256"}],"name":"transferFrom","outputs":[{"name":"success","type":"bool"}],"payable":false,"type":"function","signature":"0x23b872dd"},
{"constant":false,"inputs":[{"name":"_spender","type":"address"},{"name":"_value","type":"uint256"}],"name":"approve","outputs":[{"name":"success","type":"bool"}],"payable":false,"type":"function","signature":"0x095ea7b3"},
{"constant":true,"inputs":[{"name":"_owner","type":"address"},{"name":"_spender","type":"address"}],"name":"allowance","outputs":[{"name":"remaining","type":"uint256"}],"payable":false,"type":"function","signature":"0xdd62ed3e"},
{"anonymous":false,"inputs":[{"indexed":true,"name":"_from","type":"address"},{"indexed":true,"name":"_to","type":"address"},{"indexed":false,"name":"_value","type":"uint256"}],"name":"Transfer","type":"event","signature":"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"},
{"anonymous":false,"inputs":[{"indexed":true,"name":"_owner","type":"address"},{"indexed":true,"name":"_spender","type":"address"},{"indexed":false,"name":"_value","type":"uint256"}],"name":"Approval","type":"event","signature":"0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925"},
{"inputs":[{"name":"_initialAmount","type":"uint256"},{"name":"_tokenName","type":"string"},{"name":"_decimalUnits","type":"uint8"},{"name":"_tokenSymbol","type":"string"}],"payable":false,"type":"constructor"},
{"constant":false,"inputs":[{"name":"_spender","type":"address"},{"name":"_value","type":"uint256"},{"name":"_extraData","type":"bytes"}],"name":"approveAndCall","outputs":[{"name":"success","type":"bool"}],"payable":false,"type":"function","signature":"0xcae9ca51"},
{"constant":true,"inputs":[],"name":"version","outputs":[{"name":"","type":"string"}],"payable":false,"type":"function","signature":"0x54fd4d50"}]`
// doing the parsing/processing without using go-ethereum/accounts/abi library, it is simple to get data from Transfer event
const erc20TransferMethodSignature = "0xa9059cbb"
const erc20TransferEventSignature = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"
const erc20NameSignature = "0x06fdde03"
const erc20SymbolSignature = "0x95d89b41"
const erc20DecimalsSignature = "0x313ce567"
const erc20BalanceOf = "0x70a08231"
var cachedContracts = make(map[string]*bchain.Erc20Contract)
var cachedContractsMux sync.Mutex
func addressFromPaddedHex(s string) (string, error) {
var t big.Int
var ok bool
if has0xPrefix(s) {
_, ok = t.SetString(s[2:], 16)
} else {
_, ok = t.SetString(s, 16)
}
if !ok {
return "", errors.New("Data is not a number")
}
a := ethcommon.BigToAddress(&t)
return a.String(), nil
}
func erc20GetTransfersFromLog(logs []*rpcLog) ([]bchain.Erc20Transfer, error) {
var r []bchain.Erc20Transfer
for _, l := range logs {
if len(l.Topics) == 3 && l.Topics[0] == erc20TransferEventSignature {
var t big.Int
_, ok := t.SetString(l.Data, 0)
if !ok {
return nil, errors.New("Data is not a number")
}
from, err := addressFromPaddedHex(l.Topics[1])
if err != nil {
return nil, err
}
to, err := addressFromPaddedHex(l.Topics[2])
if err != nil {
return nil, err
}
r = append(r, bchain.Erc20Transfer{
Contract: strings.ToLower(l.Address),
From: strings.ToLower(from),
To: strings.ToLower(to),
Tokens: t,
})
}
}
return r, nil
}
func erc20GetTransfersFromTx(tx *rpcTransaction) ([]bchain.Erc20Transfer, error) {
var r []bchain.Erc20Transfer
if len(tx.Payload) == 128+len(erc20TransferMethodSignature) && strings.HasPrefix(tx.Payload, erc20TransferMethodSignature) {
to, err := addressFromPaddedHex(tx.Payload[len(erc20TransferMethodSignature) : 64+len(erc20TransferMethodSignature)])
if err != nil {
return nil, err
}
var t big.Int
_, ok := t.SetString(tx.Payload[len(erc20TransferMethodSignature)+64:], 16)
if !ok {
return nil, errors.New("Data is not a number")
}
r = append(r, bchain.Erc20Transfer{
Contract: strings.ToLower(tx.To),
From: strings.ToLower(tx.From),
To: strings.ToLower(to),
Tokens: t,
})
}
return r, nil
}
func (b *EthereumRPC) ethCall(data, to 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{}{
"data": data,
"to": to,
}, "latest")
if err != nil {
return "", err
}
return r, nil
}
func parseErc20NumericProperty(contractDesc bchain.AddressDescriptor, data string) *big.Int {
if has0xPrefix(data) {
data = data[2:]
}
if len(data) == 64 {
var n big.Int
_, ok := n.SetString(data, 16)
if ok {
return &n
}
}
glog.Warning("Cannot parse '", data, "' for contract ", contractDesc)
return nil
}
func parseErc20StringProperty(contractDesc bchain.AddressDescriptor, data string) string {
if has0xPrefix(data) {
data = data[2:]
}
if len(data) > 128 {
n := parseErc20NumericProperty(contractDesc, data[64:128])
if n != nil {
l := n.Uint64()
if 2*int(l) <= len(data)-128 {
b, err := hex.DecodeString(data[128 : 128+2*l])
if err == nil {
return string(b)
}
}
}
}
glog.Warning("Cannot parse '", data, "' for contract ", contractDesc)
return ""
}
// EthereumTypeGetErc20ContractInfo returns information about ERC20 contract
func (b *EthereumRPC) EthereumTypeGetErc20ContractInfo(contractDesc bchain.AddressDescriptor) (*bchain.Erc20Contract, error) {
cds := string(contractDesc)
cachedContractsMux.Lock()
contract, found := cachedContracts[cds]
cachedContractsMux.Unlock()
if !found {
address := hexutil.Encode(contractDesc)
data, err := b.ethCall(erc20NameSignature, address)
if err != nil {
return nil, err
}
name := parseErc20StringProperty(contractDesc, data)
if name != "" {
data, err = b.ethCall(erc20SymbolSignature, address)
if err != nil {
return nil, err
}
symbol := parseErc20StringProperty(contractDesc, data)
data, err = b.ethCall(erc20DecimalsSignature, address)
if err != nil {
return nil, err
}
if name == "" {
name = address
}
contract = &bchain.Erc20Contract{
Contract: address,
Name: name,
Symbol: symbol,
}
d := parseErc20NumericProperty(contractDesc, data)
if d != nil {
contract.Decimals = int(uint8(d.Uint64()))
} else {
contract.Decimals = EtherAmountDecimalPoint
}
} else {
contract = nil
}
cachedContractsMux.Lock()
cachedContracts[cds] = contract
cachedContractsMux.Unlock()
}
return contract, nil
}
// EthereumTypeGetErc20ContractBalance returns balance of ERC20 contract for given address
func (b *EthereumRPC) EthereumTypeGetErc20ContractBalance(addrDesc, contractDesc bchain.AddressDescriptor) (*big.Int, error) {
addr := hexutil.Encode(addrDesc)
contract := hexutil.Encode(contractDesc)
req := erc20BalanceOf + "0000000000000000000000000000000000000000000000000000000000000000"[len(addr)-2:] + addr[2:]
data, err := b.ethCall(req, contract)
if err != nil {
return nil, err
}
r := parseErc20NumericProperty(contractDesc, data)
if r == nil {
return nil, errors.New("Invalid balance")
}
return r, nil
}

View File

@ -0,0 +1,183 @@
// +build unittest
package eth
import (
"blockbook/bchain"
"blockbook/tests/dbtestdata"
fmt "fmt"
"math/big"
"strings"
"testing"
)
func TestErc20_erc20GetTransfersFromLog(t *testing.T) {
tests := []struct {
name string
args []*rpcLog
want []bchain.Erc20Transfer
wantErr bool
}{
{
name: "1",
args: []*rpcLog{
&rpcLog{
Address: "0x76a45e8976499ab9ae223cc584019341d5a84e96",
Topics: []string{
"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
"0x0000000000000000000000002aacf811ac1a60081ea39f7783c0d26c500871a8",
"0x000000000000000000000000e9a5216ff992cfa01594d43501a56e12769eb9d2",
},
Data: "0x0000000000000000000000000000000000000000000000000000000000000123",
},
},
want: []bchain.Erc20Transfer{
{
Contract: "0x76a45e8976499ab9ae223cc584019341d5a84e96",
From: "0x2aacf811ac1a60081ea39f7783c0d26c500871a8",
To: "0xe9a5216ff992cfa01594d43501a56e12769eb9d2",
Tokens: *big.NewInt(0x123),
},
},
},
{
name: "2",
args: []*rpcLog{
&rpcLog{ // Transfer
Address: "0x0d0f936ee4c93e25944694d6c121de94d9760f11",
Topics: []string{
"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
"0x0000000000000000000000006f44cceb49b4a5812d54b6f494fc2febf25511ed",
"0x0000000000000000000000004bda106325c335df99eab7fe363cac8a0ba2a24d",
},
Data: "0x0000000000000000000000000000000000000000000000006a8313d60b1f606b",
},
&rpcLog{ // Transfer
Address: "0xc778417e063141139fce010982780140aa0cd5ab",
Topics: []string{
"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
"0x0000000000000000000000004bda106325c335df99eab7fe363cac8a0ba2a24d",
"0x0000000000000000000000006f44cceb49b4a5812d54b6f494fc2febf25511ed",
},
Data: "0x000000000000000000000000000000000000000000000000000308fd0e798ac0",
},
&rpcLog{ // not Transfer
Address: "0x479cc461fecd078f766ecc58533d6f69580cf3ac",
Topics: []string{
"0x0d0b9391970d9a25552f37d436d2aae2925e2bfe1b2a923754bada030c498cb3",
"0x0000000000000000000000006f44cceb49b4a5812d54b6f494fc2febf25511ed",
"0x0000000000000000000000000000000000000000000000000000000000000000",
"0x5af266c0a89a07c1917deaa024414577e6c3c31c8907d079e13eb448c082594f",
},
Data: "0x0000000000000000000000004bda106325c335df99eab7fe363cac8a0ba2a24d0000000000000",
},
&rpcLog{ // not Transfer
Address: "0x0d0f936ee4c93e25944694d6c121de94d9760f11",
Topics: []string{
"0x0d0b9391970d9a25552f37d436d2aae2925e2bfe1b2a923754bada030c498cb3",
"0x0000000000000000000000007b62eb7fe80350dc7ec945c0b73242cb9877fb1b",
"0xb0b69dad58df6032c3b266e19b1045b19c87acd2c06fb0c598090f44b8e263aa",
},
Data: "0x0000000000000000000000004bda106325c335df99eab7fe363cac8a0ba2a24d000000000000000000000000c778417e063141139fce010982780140aa0cd5ab0000000000000000000000000d0f936ee4c93e25944694d6c121de94d9760f1100000000000000000000000000000000000000000000000000031855667df7a80000000000000000000000000000000000000000000000006a8313d60b1f800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
},
},
want: []bchain.Erc20Transfer{
{
Contract: "0x0d0f936ee4c93e25944694d6c121de94d9760f11",
From: "0x6f44cceb49b4a5812d54b6f494fc2febf25511ed",
To: "0x4bda106325c335df99eab7fe363cac8a0ba2a24d",
Tokens: *big.NewInt(0x6a8313d60b1f606b),
},
{
Contract: "0xc778417e063141139fce010982780140aa0cd5ab",
From: "0x4bda106325c335df99eab7fe363cac8a0ba2a24d",
To: "0x6f44cceb49b4a5812d54b6f494fc2febf25511ed",
Tokens: *big.NewInt(0x308fd0e798ac0),
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := erc20GetTransfersFromLog(tt.args)
if (err != nil) != tt.wantErr {
t.Errorf("erc20GetTransfersFromLog error = %v, wantErr %v", err, tt.wantErr)
return
}
// the addresses could have different case
if strings.ToLower(fmt.Sprint(got)) != strings.ToLower(fmt.Sprint(tt.want)) {
t.Errorf("erc20GetTransfersFromLog = %+v, want %+v", got, tt.want)
}
})
}
}
func TestErc20_parseErc20StringProperty(t *testing.T) {
tests := []struct {
name string
args string
want string
}{
{
name: "1",
args: "0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000758504c4f44444500000000000000000000000000000000000000000000000000",
want: "XPLODDE",
},
{
name: "2",
args: "0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000022426974436c617665202d20436f6e73756d657220416374697669747920546f6b656e00000000000000",
want: "BitClave - Consumer Activity Token",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := parseErc20StringProperty(nil, tt.args)
// the addresses could have different case
if got != tt.want {
t.Errorf("parseErc20StringProperty = %v, want %v", got, tt.want)
}
})
}
}
func TestErc20_erc20GetTransfersFromTx(t *testing.T) {
p := NewEthereumParser(1)
b := dbtestdata.GetTestEthereumTypeBlock1(p)
bn, _ := new(big.Int).SetString("21e19e0c9bab2400000", 16)
tests := []struct {
name string
args *rpcTransaction
want []bchain.Erc20Transfer
}{
{
name: "0",
args: (b.Txs[0].CoinSpecificData.(completeTransaction)).Tx,
want: []bchain.Erc20Transfer{},
},
{
name: "1",
args: (b.Txs[1].CoinSpecificData.(completeTransaction)).Tx,
want: []bchain.Erc20Transfer{
{
Contract: "0x4af4114f73d1c1c903ac9e0361b379d1291808a2",
From: "0x20cd153de35d469ba46127a0c8f18626b59a256a",
To: "0x555ee11fbddc0e49a9bab358a8941ad95ffdb48f",
Tokens: *bn,
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := erc20GetTransfersFromTx(tt.args)
if err != nil {
t.Errorf("erc20GetTransfersFromTx error = %v", err)
return
}
// the addresses could have different case
if strings.ToLower(fmt.Sprint(got)) != strings.ToLower(fmt.Sprint(tt.want)) {
t.Errorf("erc20GetTransfersFromTx = %+v, want %+v", got, tt.want)
}
})
}
}

View File

@ -3,57 +3,95 @@ package eth
import (
"blockbook/bchain"
"encoding/hex"
"encoding/json"
"math/big"
"strconv"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/golang/protobuf/proto"
"github.com/juju/errors"
ethcommon "github.com/ethereum/go-ethereum/common"
)
// EthereumTypeAddressDescriptorLen - in case of EthereumType, the AddressDescriptor has fixed length
const EthereumTypeAddressDescriptorLen = 20
// EtherAmountDecimalPoint defines number of decimal points in Ether amounts
const EtherAmountDecimalPoint = 18
// EthereumParser handle
type EthereumParser struct {
*bchain.BaseParser
}
// NewEthereumParser returns new EthereumParser instance
func NewEthereumParser() *EthereumParser {
func NewEthereumParser(b int) *EthereumParser {
return &EthereumParser{&bchain.BaseParser{
BlockAddressesToKeep: 0,
AmountDecimalPoint: 18,
BlockAddressesToKeep: b,
AmountDecimalPoint: EtherAmountDecimalPoint,
}}
}
type rpcHeader struct {
Hash string `json:"hash"`
ParentHash string `json:"parentHash"`
Difficulty string `json:"difficulty"`
Number string `json:"number"`
Time string `json:"timestamp"`
Size string `json:"size"`
Nonce string `json:"nonce"`
}
type rpcTransaction struct {
AccountNonce string `json:"nonce" gencodec:"required"`
Price string `json:"gasPrice" gencodec:"required"`
GasLimit string `json:"gas" gencodec:"required"`
To string `json:"to" rlp:"nil"` // nil means contract creation
Value string `json:"value" gencodec:"required"`
Payload string `json:"input" gencodec:"required"`
Hash ethcommon.Hash `json:"hash" rlp:"-"`
BlockNumber string `json:"blockNumber"`
BlockHash *ethcommon.Hash `json:"blockHash,omitempty"`
From string `json:"from"`
TransactionIndex string `json:"transactionIndex"`
// Signature values
V string `json:"v" gencodec:"required"`
R string `json:"r" gencodec:"required"`
S string `json:"s" gencodec:"required"`
AccountNonce string `json:"nonce"`
GasPrice string `json:"gasPrice"`
GasLimit string `json:"gas"`
To string `json:"to"` // nil means contract creation
Value string `json:"value"`
Payload string `json:"input"`
Hash string `json:"hash"`
BlockNumber string `json:"blockNumber"`
BlockHash string `json:"blockHash,omitempty"`
From string `json:"from"`
TransactionIndex string `json:"transactionIndex"`
// Signature values - ignored
// V string `json:"v"`
// R string `json:"r"`
// S string `json:"s"`
}
type rpcBlock struct {
Hash ethcommon.Hash `json:"hash"`
type rpcLog struct {
Address string `json:"address"`
Topics []string `json:"topics"`
Data string `json:"data"`
}
type rpcLogWithTxHash struct {
rpcLog
Hash string `json:"transactionHash"`
}
type rpcReceipt struct {
GasUsed string `json:"gasUsed"`
Status string `json:"status"`
Logs []*rpcLog `json:"logs"`
}
type rpcEtcReceipt struct {
GasUsed string `json:"gasUsed"`
Status int `json:"status"`
Logs []*rpcLog `json:"logs"`
}
type completeTransaction struct {
Tx *rpcTransaction `json:"tx"`
Receipt *rpcReceipt `json:"receipt,omitempty"`
}
type rpcBlockTransactions struct {
Transactions []rpcTransaction `json:"transactions"`
UncleHashes []ethcommon.Hash `json:"uncles"`
}
func ethHashToHash(h ethcommon.Hash) string {
return h.Hex()
type rpcBlockTxids struct {
Transactions []string `json:"transactions"`
}
func ethNumber(n string) (int64, error) {
@ -63,8 +101,8 @@ func ethNumber(n string) (int64, error) {
return 0, errors.Errorf("Not a number: '%v'", n)
}
func (p *EthereumParser) ethTxToTx(tx *rpcTransaction, blocktime int64, confirmations uint32) (*bchain.Tx, error) {
txid := ethHashToHash(tx.Hash)
func (p *EthereumParser) ethTxToTx(tx *rpcTransaction, receipt *rpcReceipt, blocktime int64, confirmations uint32) (*bchain.Tx, error) {
txid := tx.Hash
var (
fa, ta []string
err error
@ -75,15 +113,10 @@ func (p *EthereumParser) ethTxToTx(tx *rpcTransaction, blocktime int64, confirma
if len(tx.To) > 2 {
ta = []string{tx.To}
}
// temporarily, the complete rpcTransaction without BlockHash is marshalled and hex encoded to bchain.Tx.Hex
bh := tx.BlockHash
tx.BlockHash = nil
b, err := json.Marshal(tx)
if err != nil {
return nil, err
ct := completeTransaction{
Tx: tx,
Receipt: receipt,
}
tx.BlockHash = bh
h := hex.EncodeToString(b)
vs, err := hexutil.DecodeBig(tx.Value)
if err != nil {
return nil, err
@ -91,7 +124,7 @@ func (p *EthereumParser) ethTxToTx(tx *rpcTransaction, blocktime int64, confirma
return &bchain.Tx{
Blocktime: blocktime,
Confirmations: confirmations,
Hex: h,
// Hex
// LockTime
Time: blocktime,
Txid: txid,
@ -115,6 +148,7 @@ func (p *EthereumParser) ethTxToTx(tx *rpcTransaction, blocktime int64, confirma
},
},
},
CoinSpecificData: ct,
}, nil
}
@ -136,12 +170,9 @@ func (p *EthereumParser) GetAddrDescFromAddress(address string) (bchain.AddressD
if has0xPrefix(address) {
address = address[2:]
}
if len(address) == 0 {
if len(address) != EthereumTypeAddressDescriptorLen*2 {
return nil, bchain.ErrAddressMissing
}
if len(address)&1 == 1 {
address = "0" + address
}
return hex.DecodeString(address)
}
@ -179,83 +210,137 @@ func hexEncodeBig(b []byte) string {
// PackTx packs transaction to byte array
func (p *EthereumParser) PackTx(tx *bchain.Tx, height uint32, blockTime int64) ([]byte, error) {
b, err := hex.DecodeString(tx.Hex)
if err != nil {
return nil, err
}
var r rpcTransaction
var err error
var n uint64
err = json.Unmarshal(b, &r)
if err != nil {
return nil, err
r, ok := tx.CoinSpecificData.(completeTransaction)
if !ok {
return nil, errors.New("Missing CoinSpecificData")
}
pt := &ProtoTransaction{}
if pt.AccountNonce, err = hexutil.DecodeUint64(r.AccountNonce); err != nil {
return nil, errors.Annotatef(err, "AccountNonce %v", r.AccountNonce)
pt := &ProtoCompleteTransaction{}
pt.Tx = &ProtoCompleteTransaction_TxType{}
if pt.Tx.AccountNonce, err = hexutil.DecodeUint64(r.Tx.AccountNonce); err != nil {
return nil, errors.Annotatef(err, "AccountNonce %v", r.Tx.AccountNonce)
}
if n, err = hexutil.DecodeUint64(r.BlockNumber); err != nil {
return nil, errors.Annotatef(err, "BlockNumber %v", r.BlockNumber)
// pt.BlockNumber = height
if n, err = hexutil.DecodeUint64(r.Tx.BlockNumber); err != nil {
return nil, errors.Annotatef(err, "BlockNumber %v", r.Tx.BlockNumber)
}
pt.BlockNumber = uint32(n)
pt.BlockTime = uint64(blockTime)
if pt.From, err = hexDecode(r.From); err != nil {
return nil, errors.Annotatef(err, "From %v", r.From)
if pt.Tx.From, err = hexDecode(r.Tx.From); err != nil {
return nil, errors.Annotatef(err, "From %v", r.Tx.From)
}
if pt.GasLimit, err = hexutil.DecodeUint64(r.GasLimit); err != nil {
return nil, errors.Annotatef(err, "GasLimit %v", r.GasLimit)
if pt.Tx.GasLimit, err = hexutil.DecodeUint64(r.Tx.GasLimit); err != nil {
return nil, errors.Annotatef(err, "GasLimit %v", r.Tx.GasLimit)
}
pt.Hash = r.Hash.Bytes()
if pt.Payload, err = hexDecode(r.Payload); err != nil {
return nil, errors.Annotatef(err, "Payload %v", r.Payload)
if pt.Tx.Hash, err = hexDecode(r.Tx.Hash); err != nil {
return nil, errors.Annotatef(err, "Hash %v", r.Tx.Hash)
}
if pt.Price, err = hexDecodeBig(r.Price); err != nil {
return nil, errors.Annotatef(err, "Price %v", r.Price)
if pt.Tx.Payload, err = hexDecode(r.Tx.Payload); err != nil {
return nil, errors.Annotatef(err, "Payload %v", r.Tx.Payload)
}
if pt.R, err = hexDecodeBig(r.R); err != nil {
return nil, errors.Annotatef(err, "R %v", r.R)
if pt.Tx.GasPrice, err = hexDecodeBig(r.Tx.GasPrice); err != nil {
return nil, errors.Annotatef(err, "Price %v", r.Tx.GasPrice)
}
if pt.S, err = hexDecodeBig(r.S); err != nil {
return nil, errors.Annotatef(err, "S %v", r.S)
// if pt.R, err = hexDecodeBig(r.R); err != nil {
// return nil, errors.Annotatef(err, "R %v", r.R)
// }
// if pt.S, err = hexDecodeBig(r.S); err != nil {
// return nil, errors.Annotatef(err, "S %v", r.S)
// }
// if pt.V, err = hexDecodeBig(r.V); err != nil {
// return nil, errors.Annotatef(err, "V %v", r.V)
// }
if pt.Tx.To, err = hexDecode(r.Tx.To); err != nil {
return nil, errors.Annotatef(err, "To %v", r.Tx.To)
}
if pt.V, err = hexDecodeBig(r.V); err != nil {
return nil, errors.Annotatef(err, "V %v", r.V)
if n, err = hexutil.DecodeUint64(r.Tx.TransactionIndex); err != nil {
return nil, errors.Annotatef(err, "TransactionIndex %v", r.Tx.TransactionIndex)
}
if pt.To, err = hexDecode(r.To); err != nil {
return nil, errors.Annotatef(err, "To %v", r.To)
pt.Tx.TransactionIndex = uint32(n)
if pt.Tx.Value, err = hexDecodeBig(r.Tx.Value); err != nil {
return nil, errors.Annotatef(err, "Value %v", r.Tx.Value)
}
if n, err = hexutil.DecodeUint64(r.TransactionIndex); err != nil {
return nil, errors.Annotatef(err, "TransactionIndex %v", r.TransactionIndex)
}
pt.TransactionIndex = uint32(n)
if pt.Value, err = hexDecodeBig(r.Value); err != nil {
return nil, errors.Annotatef(err, "Value %v", r.Value)
if r.Receipt != nil {
pt.Receipt = &ProtoCompleteTransaction_ReceiptType{}
if pt.Receipt.GasUsed, err = hexDecodeBig(r.Receipt.GasUsed); err != nil {
return nil, errors.Annotatef(err, "GasUsed %v", r.Receipt.GasUsed)
}
if pt.Receipt.Status, err = hexDecodeBig(r.Receipt.Status); err != nil {
return nil, errors.Annotatef(err, "Status %v", r.Receipt.Status)
}
ptLogs := make([]*ProtoCompleteTransaction_ReceiptType_LogType, len(r.Receipt.Logs))
for i, l := range r.Receipt.Logs {
a, err := hexutil.Decode(l.Address)
if err != nil {
return nil, errors.Annotatef(err, "Address cannot be decoded %v", l)
}
d, err := hexutil.Decode(l.Data)
if err != nil {
return nil, errors.Annotatef(err, "Data cannot be decoded %v", l)
}
t := make([][]byte, len(l.Topics))
for j, s := range l.Topics {
t[j], err = hexutil.Decode(s)
if err != nil {
return nil, errors.Annotatef(err, "Topic cannot be decoded %v", l)
}
}
ptLogs[i] = &ProtoCompleteTransaction_ReceiptType_LogType{
Address: a,
Data: d,
Topics: t,
}
}
pt.Receipt.Log = ptLogs
}
return proto.Marshal(pt)
}
// UnpackTx unpacks transaction from byte array
func (p *EthereumParser) UnpackTx(buf []byte) (*bchain.Tx, uint32, error) {
var pt ProtoTransaction
var pt ProtoCompleteTransaction
err := proto.Unmarshal(buf, &pt)
if err != nil {
return nil, 0, err
}
r := rpcTransaction{
AccountNonce: hexutil.EncodeUint64(pt.AccountNonce),
BlockNumber: hexutil.EncodeUint64(uint64(pt.BlockNumber)),
From: hexutil.Encode(pt.From),
GasLimit: hexutil.EncodeUint64(pt.GasLimit),
Hash: ethcommon.BytesToHash(pt.Hash),
Payload: hexutil.Encode(pt.Payload),
Price: hexEncodeBig(pt.Price),
R: hexEncodeBig(pt.R),
S: hexEncodeBig(pt.S),
V: hexEncodeBig(pt.V),
To: hexutil.Encode(pt.To),
TransactionIndex: hexutil.EncodeUint64(uint64(pt.TransactionIndex)),
Value: hexEncodeBig(pt.Value),
rt := rpcTransaction{
AccountNonce: hexutil.EncodeUint64(pt.Tx.AccountNonce),
BlockNumber: hexutil.EncodeUint64(uint64(pt.BlockNumber)),
From: hexutil.Encode(pt.Tx.From),
GasLimit: hexutil.EncodeUint64(pt.Tx.GasLimit),
Hash: hexutil.Encode(pt.Tx.Hash),
Payload: hexutil.Encode(pt.Tx.Payload),
GasPrice: hexEncodeBig(pt.Tx.GasPrice),
// R: hexEncodeBig(pt.R),
// S: hexEncodeBig(pt.S),
// V: hexEncodeBig(pt.V),
To: hexutil.Encode(pt.Tx.To),
TransactionIndex: hexutil.EncodeUint64(uint64(pt.Tx.TransactionIndex)),
Value: hexEncodeBig(pt.Tx.Value),
}
tx, err := p.ethTxToTx(&r, int64(pt.BlockTime), 0)
var rr *rpcReceipt
if pt.Receipt != nil {
logs := make([]*rpcLog, len(pt.Receipt.Log))
for i, l := range pt.Receipt.Log {
topics := make([]string, len(l.Topics))
for j, t := range l.Topics {
topics[j] = hexutil.Encode(t)
}
logs[i] = &rpcLog{
Address: hexutil.Encode(l.Address),
Data: hexutil.Encode(l.Data),
Topics: topics,
}
}
rr = &rpcReceipt{
GasUsed: hexEncodeBig(pt.Receipt.GasUsed),
Status: hexEncodeBig(pt.Receipt.Status),
Logs: logs,
}
}
tx, err := p.ethTxToTx(&rt, rr, int64(pt.BlockTime), 0)
if err != nil {
return nil, 0, err
}
@ -293,7 +378,81 @@ func (p *EthereumParser) UnpackBlockHash(buf []byte) (string, error) {
return hexutil.Encode(buf), nil
}
// IsUTXOChain returns true if the block chain is UTXO type, otherwise false
func (p *EthereumParser) IsUTXOChain() bool {
return false
// GetChainType returns EthereumType
func (p *EthereumParser) GetChainType() bchain.ChainType {
return bchain.ChainEthereumType
}
// GetHeightFromTx returns ethereum specific data from bchain.Tx
func GetHeightFromTx(tx *bchain.Tx) (uint32, error) {
var bn string
csd, ok := tx.CoinSpecificData.(completeTransaction)
if !ok {
return 0, errors.New("Missing CoinSpecificData")
}
bn = csd.Tx.BlockNumber
n, err := hexutil.DecodeUint64(bn)
if err != nil {
return 0, errors.Annotatef(err, "BlockNumber %v", bn)
}
return uint32(n), nil
}
// EthereumTypeGetErc20FromTx returns Erc20 data from bchain.Tx
func (p *EthereumParser) EthereumTypeGetErc20FromTx(tx *bchain.Tx) ([]bchain.Erc20Transfer, error) {
var r []bchain.Erc20Transfer
var err error
csd, ok := tx.CoinSpecificData.(completeTransaction)
if ok {
if csd.Receipt != nil {
r, err = erc20GetTransfersFromLog(csd.Receipt.Logs)
} else {
r, err = erc20GetTransfersFromTx(csd.Tx)
}
if err != nil {
return nil, err
}
}
return r, nil
}
const (
txStatusUnknown = iota - 2
txStatusPending
txStatusFailure
txStatusOK
)
// EthereumTxData contains ethereum specific transaction data
type EthereumTxData struct {
Status int `json:"status"` // 1 OK, 0 Fail, -1 pending, -2 unknown
Nonce uint64 `json:"nonce"`
GasLimit *big.Int `json:"gaslimit"`
GasUsed *big.Int `json:"gasused"`
GasPrice *big.Int `json:"gasprice"`
}
// GetEthereumTxData returns EthereumTxData from bchain.Tx
func GetEthereumTxData(tx *bchain.Tx) *EthereumTxData {
etd := EthereumTxData{Status: txStatusPending}
csd, ok := tx.CoinSpecificData.(completeTransaction)
if ok {
if csd.Tx != nil {
etd.Nonce, _ = hexutil.DecodeUint64(csd.Tx.AccountNonce)
etd.GasLimit, _ = hexutil.DecodeBig(csd.Tx.GasLimit)
etd.GasPrice, _ = hexutil.DecodeBig(csd.Tx.GasPrice)
}
if csd.Receipt != nil {
switch csd.Receipt.Status {
case "0x1":
etd.Status = txStatusOK
case "": // old transactions did not set status
etd.Status = txStatusUnknown
default:
etd.Status = txStatusFailure
}
etd.GasUsed, _ = hexutil.DecodeBig(csd.Receipt.GasUsed)
}
}
return &etd
}

View File

@ -4,7 +4,9 @@ package eth
import (
"blockbook/bchain"
"blockbook/tests/dbtestdata"
"encoding/hex"
"fmt"
"math/big"
"reflect"
"testing"
@ -31,9 +33,10 @@ func TestEthParser_GetAddrDescFromAddress(t *testing.T) {
want: "47526228d673e9f079630d6cdaff5a2ed13e0e60",
},
{
name: "odd address",
args: args{address: "7526228d673e9f079630d6cdaff5a2ed13e0e60"},
want: "07526228d673e9f079630d6cdaff5a2ed13e0e60",
name: "address of wrong length",
args: args{address: "7526228d673e9f079630d6cdaff5a2ed13e0e60"},
want: "",
wantErr: true,
},
{
name: "ErrAddressMissing",
@ -50,7 +53,7 @@ func TestEthParser_GetAddrDescFromAddress(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
p := NewEthereumParser()
p := NewEthereumParser(1)
got, err := p.GetAddrDescFromAddress(tt.args.address)
if (err != nil) != tt.wantErr {
t.Errorf("EthParser.GetAddrDescFromAddress() error = %v, wantErr %v", err, tt.wantErr)
@ -64,38 +67,13 @@ func TestEthParser_GetAddrDescFromAddress(t *testing.T) {
}
}
var (
testTx1, testTx2 bchain.Tx
testTxPacked1 = "08aebf0a1205012a05f20018a0f73622081234567890abcdef2a24f025caaf00000000000000000000000000000000000000000000000000000000000002253220e6b168d6bb3d8ed78e03dbf828b6bfd1fb613f6e129cba624964984553724c5d38f095af014092f4c1d5054a14682b7903a11098cf770c7aef4aa02a85b3f3601a5214dacc9c61754a0c4616fc5323dc946e89eb272302580162011b6a201bd40a31122c03918df6d166d740a6a3a22f08a25934ceb1688c62977661c80c7220607fbc15c1f7995a4258f5a9bccc63b040362d1991d5efe1361c56222e4ca89f"
testTxPacked2 = "08ece40212050430e234001888a4012201213220cd647151552b5132b2aef7c9be00dc6f73afc5901dde157aab131335baaa853b38889eaf0140fa83c3d5054a14555ee11fbddc0e49a9bab358a8941ad95ffdb48f52143e3a3d69dc66ba10737f531ed088954a9ec89d97580a6201296a20f7161c170d43573ad9c8d701cdaf714ff2a548a562b0dc639230d17889fcd40572203c4977fc90385a27efa0032e17b49fd575b2826cb56e3d1ecf21524f2a94f915"
)
var testTx1, testTx2 bchain.Tx
func init() {
testTx1 = bchain.Tx{
Blocktime: 1521515026,
Hex: "7b226e6f6e6365223a2230783239666165222c226761735072696365223a223078313261303566323030222c22676173223a2230786462626130222c22746f223a22307836383262373930336131313039386366373730633761656634616130326138356233663336303161222c2276616c7565223a22307831323334353637383930616263646566222c22696e707574223a223078663032356361616630303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030323235222c2268617368223a22307865366231363864366262336438656437386530336462663832386236626664316662363133663665313239636261363234393634393834353533373234633564222c22626c6f636b4e756d626572223a223078326263616630222c2266726f6d223a22307864616363396336313735346130633436313666633533323364633934366538396562323732333032222c227472616e73616374696f6e496e646578223a22307831222c2276223a2230783162222c2272223a22307831626434306133313132326330333931386466366431363664373430613661336132326630386132353933346365623136383863363239373736363163383063222c2273223a22307836303766626331356331663739393561343235386635613962636363363362303430333632643139393164356566653133363163353632323265346361383966227d",
Time: 1521515026,
Txid: "0xe6b168d6bb3d8ed78e03dbf828b6bfd1fb613f6e129cba624964984553724c5d",
Vin: []bchain.Vin{
{
Addresses: []string{"0xdacc9c61754a0c4616fc5323dc946e89eb272302"},
},
},
Vout: []bchain.Vout{
{
ValueSat: *big.NewInt(1311768467294899695),
ScriptPubKey: bchain.ScriptPubKey{
Addresses: []string{"0x682b7903a11098cf770c7aef4aa02a85b3f3601a"},
},
},
},
}
testTx2 = bchain.Tx{
Blocktime: 1521533434,
Hex: "7b226e6f6e6365223a22307862323663222c226761735072696365223a223078343330653233343030222c22676173223a22307835323038222c22746f223a22307835353565653131666264646330653439613962616233353861383934316164393566666462343866222c2276616c7565223a2230783231222c22696e707574223a223078222c2268617368223a22307863643634373135313535326235313332623261656637633962653030646336663733616663353930316464653135376161623133313333356261616138353362222c22626c6f636b4e756d626572223a223078326263663038222c2266726f6d223a22307833653361336436396463363662613130373337663533316564303838393534613965633839643937222c227472616e73616374696f6e496e646578223a22307861222c2276223a2230783239222c2272223a22307866373136316331373064343335373361643963386437303163646166373134666632613534386135363262306463363339323330643137383839666364343035222c2273223a22307833633439373766633930333835613237656661303033326531376234396664353735623238323663623536653364316563663231353234663261393466393135227d",
Time: 1521533434,
Blocktime: 1534858022,
Time: 1534858022,
Txid: "0xcd647151552b5132b2aef7c9be00dc6f73afc5901dde157aab131335baaa853b",
Vin: []bchain.Vin{
{
@ -104,12 +82,78 @@ func init() {
},
Vout: []bchain.Vout{
{
ValueSat: *big.NewInt(33),
ValueSat: *big.NewInt(1999622000000000000),
ScriptPubKey: bchain.ScriptPubKey{
Addresses: []string{"0x555ee11fbddc0e49a9bab358a8941ad95ffdb48f"},
},
},
},
CoinSpecificData: completeTransaction{
Tx: &rpcTransaction{
AccountNonce: "0xb26c",
GasPrice: "0x430e23400",
GasLimit: "0x5208",
To: "0x555ee11fbddc0e49a9bab358a8941ad95ffdb48f",
Value: "0x1bc0159d530e6000",
Payload: "0x",
Hash: "0xcd647151552b5132b2aef7c9be00dc6f73afc5901dde157aab131335baaa853b",
BlockNumber: "0x41eee8",
From: "0x3e3a3d69dc66ba10737f531ed088954a9ec89d97",
TransactionIndex: "0xa",
},
Receipt: &rpcReceipt{
GasUsed: "0x5208",
Status: "0x1",
Logs: []*rpcLog{},
},
},
}
testTx2 = bchain.Tx{
Blocktime: 1534858022,
Time: 1534858022,
Txid: "0xa9cd088aba2131000da6f38a33c20169baee476218deea6b78720700b895b101",
Vin: []bchain.Vin{
{
Addresses: []string{"0x20cd153de35d469ba46127a0c8f18626b59a256a"},
},
},
Vout: []bchain.Vout{
{
ValueSat: *big.NewInt(0),
ScriptPubKey: bchain.ScriptPubKey{
Addresses: []string{"0x4af4114f73d1c1c903ac9e0361b379d1291808a2"},
},
},
},
CoinSpecificData: completeTransaction{
Tx: &rpcTransaction{
AccountNonce: "0xd0",
GasPrice: "0x9502f9000",
GasLimit: "0x130d5",
To: "0x4af4114f73d1c1c903ac9e0361b379d1291808a2",
Value: "0x0",
Payload: "0xa9059cbb000000000000000000000000555ee11fbddc0e49a9bab358a8941ad95ffdb48f00000000000000000000000000000000000000000000021e19e0c9bab2400000",
Hash: "0xa9cd088aba2131000da6f38a33c20169baee476218deea6b78720700b895b101",
BlockNumber: "0x41eee8",
From: "0x20cd153de35d469ba46127a0c8f18626b59a256a",
TransactionIndex: "0x0"},
Receipt: &rpcReceipt{
GasUsed: "0xcb39",
Status: "0x1",
Logs: []*rpcLog{
&rpcLog{
Address: "0x4af4114f73d1c1c903ac9e0361b379d1291808a2",
Data: "0x00000000000000000000000000000000000000000000021e19e0c9bab2400000",
Topics: []string{
"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
"0x00000000000000000000000020cd153de35d469ba46127a0c8f18626b59a256a",
"0x000000000000000000000000555ee11fbddc0e49a9bab358a8941ad95ffdb48f",
},
},
},
},
},
}
}
@ -130,22 +174,22 @@ func TestEthereumParser_PackTx(t *testing.T) {
name: "1",
args: args{
tx: &testTx1,
height: 2870000,
blockTime: 1521515026,
height: 4321000,
blockTime: 1534858022,
},
want: testTxPacked1,
want: dbtestdata.EthTx1Packed,
},
{
name: "2",
args: args{
tx: &testTx2,
height: 2871048,
blockTime: 1521533434,
height: 4321000,
blockTime: 1534858022,
},
want: testTxPacked2,
want: dbtestdata.EthTx2Packed,
},
}
p := NewEthereumParser()
p := NewEthereumParser(1)
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := p.PackTx(tt.args.tx, tt.args.height, tt.args.blockTime)
@ -175,18 +219,18 @@ func TestEthereumParser_UnpackTx(t *testing.T) {
}{
{
name: "1",
args: args{hex: testTxPacked1},
args: args{hex: dbtestdata.EthTx1Packed},
want: &testTx1,
want1: 2870000,
want1: 4321000,
},
{
name: "2",
args: args{hex: testTxPacked2},
args: args{hex: dbtestdata.EthTx2Packed},
want: &testTx2,
want1: 2871048,
want1: 4321000,
},
}
p := NewEthereumParser()
p := NewEthereumParser(1)
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
b, err := hex.DecodeString(tt.args.hex)
@ -198,8 +242,22 @@ func TestEthereumParser_UnpackTx(t *testing.T) {
t.Errorf("EthereumParser.UnpackTx() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("EthereumParser.UnpackTx() got = %v, want %v", got, tt.want)
// DeepEqual has problems with pointers in completeTransaction
gs := got.CoinSpecificData.(completeTransaction)
ws := tt.want.CoinSpecificData.(completeTransaction)
gc := *got
wc := *tt.want
gc.CoinSpecificData = nil
wc.CoinSpecificData = nil
if fmt.Sprint(gc) != fmt.Sprint(wc) {
// if !reflect.DeepEqual(gc, wc) {
t.Errorf("EthereumParser.UnpackTx() gc got = %+v, want %+v", gc, wc)
}
if !reflect.DeepEqual(gs.Tx, ws.Tx) {
t.Errorf("EthereumParser.UnpackTx() gs.Tx got = %+v, want %+v", gs.Tx, ws.Tx)
}
if !reflect.DeepEqual(gs.Receipt, ws.Receipt) {
t.Errorf("EthereumParser.UnpackTx() gs.Receipt got = %+v, want %+v", gs.Receipt, ws.Receipt)
}
if got1 != tt.want1 {
t.Errorf("EthereumParser.UnpackTx() got1 = %v, want %v", got1, tt.want1)

View File

@ -10,14 +10,13 @@ import (
"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"
"github.com/golang/glog"
"github.com/juju/errors"
)
// EthereumNet type specifies the type of ethereum network
@ -30,31 +29,34 @@ const (
TestNet EthereumNet = 3
)
// Configuration represents json config file
type Configuration struct {
CoinName string `json:"coin_name"`
CoinShortcut string `json:"coin_shortcut"`
RPCURL string `json:"rpc_url"`
RPCTimeout int `json:"rpc_timeout"`
CoinName string `json:"coin_name"`
CoinShortcut string `json:"coin_shortcut"`
RPCURL string `json:"rpc_url"`
RPCTimeout int `json:"rpc_timeout"`
BlockAddressesToKeep int `json:"block_addresses_to_keep"`
}
// EthereumRPC is an interface to JSON-RPC eth service.
type EthereumRPC struct {
client *ethclient.Client
rpc *rpc.Client
timeout time.Duration
Parser *EthereumParser
Testnet bool
Network string
Mempool *bchain.NonUTXOMempool
bestHeaderMu sync.Mutex
bestHeader *ethtypes.Header
bestHeaderTime time.Time
chanNewBlock chan *ethtypes.Header
newBlockSubscription *rpc.ClientSubscription
chanNewTx chan ethcommon.Hash
newTxSubscription *rpc.ClientSubscription
ChainConfig *Configuration
isETC bool
*bchain.BaseChain
client *ethclient.Client
rpc *rpc.Client
timeout time.Duration
Parser *EthereumParser
Mempool *bchain.MempoolEthereumType
bestHeaderLock sync.Mutex
bestHeader *ethtypes.Header
bestHeaderTime time.Time
chanNewBlock chan *ethtypes.Header
newBlockSubscription *rpc.ClientSubscription
chanNewTx chan ethcommon.Hash
newTxSubscription *rpc.ClientSubscription
pendingTransactions map[string]struct{}
pendingTransactionsLock sync.Mutex
ChainConfig *Configuration
isETC bool
}
// NewEthereumRPC returns new EthRPC instance.
@ -65,6 +67,10 @@ func NewEthereumRPC(config json.RawMessage, pushHandler func(bchain.Notification
if err != nil {
return nil, errors.Annotatef(err, "Invalid configuration file")
}
// keep at least 100 mappings block->addresses to allow rollback
if c.BlockAddressesToKeep < 100 {
c.BlockAddressesToKeep = 100
}
rc, err := rpc.Dial(c.RPCURL)
if err != nil {
return nil, err
@ -72,13 +78,15 @@ func NewEthereumRPC(config json.RawMessage, pushHandler func(bchain.Notification
ec := ethclient.NewClient(rc)
s := &EthereumRPC{
client: ec,
rpc: rc,
ChainConfig: &c,
BaseChain: &bchain.BaseChain{},
client: ec,
rpc: rc,
ChainConfig: &c,
pendingTransactions: make(map[string]struct{}),
}
// always create parser
s.Parser = NewEthereumParser()
s.Parser = NewEthereumParser(c.BlockAddressesToKeep)
s.timeout = time.Duration(c.RPCTimeout) * time.Second
// detect ethereum classic
@ -95,10 +103,10 @@ func NewEthereumRPC(config json.RawMessage, pushHandler func(bchain.Notification
}
glog.V(2).Info("rpc: new block header ", h.Number)
// update best header to the new header
s.bestHeaderMu.Lock()
s.bestHeaderLock.Lock()
s.bestHeader = h
s.bestHeaderTime = time.Now()
s.bestHeaderMu.Unlock()
s.bestHeaderLock.Unlock()
// notify blockbook
pushHandler(bchain.NotificationNewBlock)
}
@ -113,9 +121,13 @@ func NewEthereumRPC(config json.RawMessage, pushHandler func(bchain.Notification
if !ok {
break
}
hex := t.Hex()
if glog.V(2) {
glog.Info("rpc: new tx ", t.Hex())
glog.Info("rpc: new tx ", hex)
}
s.pendingTransactionsLock.Lock()
s.pendingTransactions[hex] = struct{}{}
s.pendingTransactionsLock.Unlock()
pushHandler(bchain.NotificationNewTx)
}
}()
@ -185,7 +197,7 @@ func (b *EthereumRPC) Initialize() error {
}
// create mempool
b.Mempool = bchain.NewNonUTXOMempool(b)
b.Mempool = bchain.NewMempoolEthereumType(b)
return nil
}
@ -245,18 +257,12 @@ func (b *EthereumRPC) Shutdown(ctx context.Context) error {
return nil
}
func (b *EthereumRPC) IsTestnet() bool {
return b.Testnet
}
func (b *EthereumRPC) GetNetworkName() string {
return b.Network
}
// GetCoinName returns coin name
func (b *EthereumRPC) GetCoinName() string {
return b.ChainConfig.CoinName
}
// GetSubversion returns empty string, ethereum does not have subversion
func (b *EthereumRPC) GetSubversion() string {
return ""
}
@ -269,20 +275,36 @@ func (b *EthereumRPC) GetChainInfo() (*bchain.ChainInfo, error) {
if err != nil {
return nil, err
}
rv := &bchain.ChainInfo{}
h, err := b.getBestHeader()
if err != nil {
return nil, err
}
var ver, protocol string
if err := b.rpc.CallContext(ctx, &ver, "web3_clientVersion"); err != nil {
return nil, err
}
if err := b.rpc.CallContext(ctx, &protocol, "eth_protocolVersion"); err != nil {
return nil, err
}
rv := &bchain.ChainInfo{
Blocks: int(h.Number.Int64()),
Bestblockhash: h.Hash().Hex(),
Difficulty: h.Difficulty.String(),
Version: ver,
ProtocolVersion: protocol,
}
idi := int(id.Uint64())
if idi == 1 {
rv.Chain = "mainnet"
} else {
rv.Chain = "testnet " + strconv.Itoa(idi)
}
// TODO - return more information about the chain
return rv, nil
}
func (b *EthereumRPC) getBestHeader() (*ethtypes.Header, error) {
b.bestHeaderMu.Lock()
defer b.bestHeaderMu.Unlock()
b.bestHeaderLock.Lock()
defer b.bestHeaderLock.Unlock()
// ETC does not have newBlocks subscription, bestHeader must be updated very often (each 1 second)
if b.isETC {
if b.bestHeaderTime.Add(1 * time.Second).Before(time.Now()) {
@ -302,23 +324,25 @@ func (b *EthereumRPC) getBestHeader() (*ethtypes.Header, error) {
return b.bestHeader, nil
}
// GetBestBlockHash returns hash of the tip of the best-block-chain
func (b *EthereumRPC) GetBestBlockHash() (string, error) {
h, err := b.getBestHeader()
if err != nil {
return "", err
}
return ethHashToHash(h.Hash()), nil
return h.Hash().Hex(), nil
}
// GetBestBlockHeight returns height of the tip of the best-block-chain
func (b *EthereumRPC) GetBestBlockHeight() (uint32, error) {
h, err := b.getBestHeader()
if err != nil {
return 0, err
}
// TODO - can it grow over 2^32 ?
return uint32(h.Number.Uint64()), nil
}
// GetBlockHash returns hash of block in best-block-chain at given height
func (b *EthereumRPC) GetBlockHash(height uint32) (string, error) {
var n big.Int
n.SetUint64(uint64(height))
@ -331,36 +355,47 @@ func (b *EthereumRPC) GetBlockHash(height uint32) (string, error) {
}
return "", errors.Annotatef(err, "height %v", height)
}
return ethHashToHash(h.Hash()), nil
return h.Hash().Hex(), nil
}
func (b *EthereumRPC) ethHeaderToBlockHeader(h *ethtypes.Header) (*bchain.BlockHeader, error) {
hn := h.Number.Uint64()
c, err := b.computeConfirmations(hn)
func (b *EthereumRPC) ethHeaderToBlockHeader(h *rpcHeader) (*bchain.BlockHeader, error) {
height, err := ethNumber(h.Number)
if err != nil {
return nil, err
}
c, err := b.computeConfirmations(uint64(height))
if err != nil {
return nil, err
}
time, err := ethNumber(h.Time)
if err != nil {
return nil, err
}
size, err := ethNumber(h.Size)
if err != nil {
return nil, err
}
return &bchain.BlockHeader{
Hash: ethHashToHash(h.Hash()),
Height: uint32(hn),
Hash: h.Hash,
Prev: h.ParentHash,
Height: uint32(height),
Confirmations: int(c),
Time: int64(h.Time.Uint64()),
// Next
// Prev
Time: time,
Size: int(size),
}, nil
}
// GetBlockHeader returns header of block with given hash
func (b *EthereumRPC) GetBlockHeader(hash string) (*bchain.BlockHeader, error) {
ctx, cancel := context.WithTimeout(context.Background(), b.timeout)
defer cancel()
h, err := b.client.HeaderByHash(ctx, ethcommon.HexToHash(hash))
raw, err := b.getBlockRaw(hash, 0, false)
if err != nil {
if err == ethereum.NotFound {
return nil, bchain.ErrBlockNotFound
}
return nil, err
}
var h rpcHeader
if err := json.Unmarshal(raw, &h); err != nil {
return nil, errors.Annotatef(err, "hash %v", hash)
}
return b.ethHeaderToBlockHeader(h)
return b.ethHeaderToBlockHeader(&h)
}
func (b *EthereumRPC) computeConfirmations(n uint64) (uint32, error) {
@ -373,61 +408,82 @@ func (b *EthereumRPC) computeConfirmations(n uint64) (uint32, error) {
return uint32(bn - n + 1), nil
}
// GetBlock returns block with given hash or height, hash has precedence if both passed
func (b *EthereumRPC) GetBlock(hash string, height uint32) (*bchain.Block, error) {
func (b *EthereumRPC) getBlockRaw(hash string, height uint32, fullTxs bool) (json.RawMessage, error) {
ctx, cancel := context.WithTimeout(context.Background(), b.timeout)
defer cancel()
var raw json.RawMessage
var err error
if hash != "" {
err = b.rpc.CallContext(ctx, &raw, "eth_getBlockByHash", ethcommon.HexToHash(hash), true)
if hash == "pending" {
err = b.rpc.CallContext(ctx, &raw, "eth_getBlockByNumber", hash, fullTxs)
} else {
err = b.rpc.CallContext(ctx, &raw, "eth_getBlockByHash", ethcommon.HexToHash(hash), fullTxs)
}
} else {
err = b.rpc.CallContext(ctx, &raw, "eth_getBlockByNumber", fmt.Sprintf("%#x", height), true)
err = b.rpc.CallContext(ctx, &raw, "eth_getBlockByNumber", fmt.Sprintf("%#x", height), fullTxs)
}
if err != nil {
return nil, errors.Annotatef(err, "hash %v, height %v", hash, height)
} else if len(raw) == 0 {
return nil, bchain.ErrBlockNotFound
}
// Decode header and transactions.
var head *ethtypes.Header
var body rpcBlock
return raw, nil
}
func (b *EthereumRPC) getERC20EventsForBlock(blockNumber string) (map[string][]*rpcLog, error) {
ctx, cancel := context.WithTimeout(context.Background(), b.timeout)
defer cancel()
var logs []rpcLogWithTxHash
err := b.rpc.CallContext(ctx, &logs, "eth_getLogs", map[string]interface{}{
"fromBlock": blockNumber,
"toBlock": blockNumber,
"topics": []string{erc20TransferEventSignature},
})
if err != nil {
return nil, errors.Annotatef(err, "blockNumber %v", blockNumber)
}
r := make(map[string][]*rpcLog)
for i := range logs {
l := &logs[i]
r[l.Hash] = append(r[l.Hash], &l.rpcLog)
}
return r, nil
}
// GetBlock returns block with given hash or height, hash has precedence if both passed
func (b *EthereumRPC) GetBlock(hash string, height uint32) (*bchain.Block, error) {
raw, err := b.getBlockRaw(hash, height, true)
if err != nil {
return nil, err
}
var head rpcHeader
if err := json.Unmarshal(raw, &head); err != nil {
return nil, errors.Annotatef(err, "hash %v, height %v", hash, height)
}
if head == nil {
return nil, bchain.ErrBlockNotFound
}
var body rpcBlockTransactions
if err := json.Unmarshal(raw, &body); err != nil {
return nil, errors.Annotatef(err, "hash %v, height %v", hash, height)
}
// Quick-verify transaction and uncle lists. This mostly helps with debugging the server.
if head.UncleHash == ethtypes.EmptyUncleHash && len(body.UncleHashes) > 0 {
return nil, errors.Annotatef(fmt.Errorf("server returned non-empty uncle list but block header indicates no uncles"), "hash %v, height %v", hash, height)
}
if head.UncleHash != ethtypes.EmptyUncleHash && len(body.UncleHashes) == 0 {
return nil, errors.Annotatef(fmt.Errorf("server returned empty uncle list but block header indicates uncles"), "hash %v, height %v", hash, height)
}
if head.TxHash == ethtypes.EmptyRootHash && len(body.Transactions) > 0 {
return nil, errors.Annotatef(fmt.Errorf("server returned non-empty transaction list but block header indicates no transactions"), "hash %v, height %v", hash, height)
}
if head.TxHash != ethtypes.EmptyRootHash && len(body.Transactions) == 0 {
return nil, errors.Annotatef(fmt.Errorf("server returned empty transaction list but block header indicates transactions"), "hash %v, height %v", hash, height)
}
bbh, err := b.ethHeaderToBlockHeader(head)
bbh, err := b.ethHeaderToBlockHeader(&head)
if err != nil {
return nil, errors.Annotatef(err, "hash %v, height %v", hash, height)
}
// TODO - this is probably not the correct size
bbh.Size = len(raw)
// get ERC20 events
logs, err := b.getERC20EventsForBlock(head.Number)
if err != nil {
return nil, err
}
btxs := make([]bchain.Tx, len(body.Transactions))
for i, tx := range body.Transactions {
btx, err := b.Parser.ethTxToTx(&tx, int64(head.Time.Uint64()), uint32(bbh.Confirmations))
for i := range body.Transactions {
tx := &body.Transactions[i]
btx, err := b.Parser.ethTxToTx(tx, &rpcReceipt{Logs: logs[tx.Hash]}, bbh.Time, uint32(bbh.Confirmations))
if err != nil {
return nil, errors.Annotatef(err, "hash %v, height %v, txid %v", hash, height, tx.Hash.String())
return nil, errors.Annotatef(err, "hash %v, height %v, txid %v", hash, height, tx.Hash)
}
btxs[i] = *btx
b.pendingTransactionsLock.Lock()
delete(b.pendingTransactions, tx.Hash)
b.pendingTransactionsLock.Unlock()
}
bbk := bchain.Block{
BlockHeader: *bbh,
@ -438,14 +494,38 @@ func (b *EthereumRPC) GetBlock(hash string, height uint32) (*bchain.Block, error
// GetBlockInfo returns extended header (more info than in bchain.BlockHeader) with a list of txids
func (b *EthereumRPC) GetBlockInfo(hash string) (*bchain.BlockInfo, error) {
// TODO - implement
return nil, errors.New("Not implemented yet")
raw, err := b.getBlockRaw(hash, 0, false)
if err != nil {
return nil, err
}
var head rpcHeader
var txs rpcBlockTxids
if err := json.Unmarshal(raw, &head); err != nil {
return nil, errors.Annotatef(err, "hash %v", hash)
}
if err = json.Unmarshal(raw, &txs); err != nil {
return nil, err
}
bch, err := b.ethHeaderToBlockHeader(&head)
return &bchain.BlockInfo{
BlockHeader: *bch,
Difficulty: json.Number(head.Difficulty),
Nonce: json.Number(head.Nonce),
Txids: txs.Transactions,
}, nil
}
// GetTransactionForMempool returns a transaction by the transaction ID.
// It could be optimized for mempool, i.e. without block time and confirmations
func (b *EthereumRPC) GetTransactionForMempool(txid string) (*bchain.Tx, error) {
return b.GetTransaction(txid)
tx, err := b.GetTransaction(txid)
// it there is an error getting the tx or the tx is confirmed, remove it from pending transactions
if err != nil || (tx != nil && tx.Confirmations > 0) {
b.pendingTransactionsLock.Lock()
delete(b.pendingTransactions, txid)
b.pendingTransactionsLock.Unlock()
}
return tx, err
}
// GetTransaction returns a transaction by the transaction ID.
@ -453,32 +533,66 @@ func (b *EthereumRPC) GetTransaction(txid string) (*bchain.Tx, error) {
ctx, cancel := context.WithTimeout(context.Background(), b.timeout)
defer cancel()
var tx *rpcTransaction
err := b.rpc.CallContext(ctx, &tx, "eth_getTransactionByHash", ethcommon.HexToHash(txid))
hash := ethcommon.HexToHash(txid)
err := b.rpc.CallContext(ctx, &tx, "eth_getTransactionByHash", hash)
if err != nil {
return nil, err
} else if tx == nil {
return nil, ethereum.NotFound
} else if tx.R == "" {
if !b.isETC {
return nil, errors.Annotatef(fmt.Errorf("server returned transaction without signature"), "txid %v", txid)
} else {
glog.Warning("server returned transaction without signature, txid ", txid)
}
}
var btx *bchain.Tx
if tx.BlockNumber == "" {
// mempool tx
btx, err = b.Parser.ethTxToTx(tx, 0, 0)
btx, err = b.Parser.ethTxToTx(tx, nil, 0, 0)
if err != nil {
return nil, errors.Annotatef(err, "txid %v", txid)
}
} else {
// non mempool tx - we must read the block header to get the block time
n, err := ethNumber(tx.BlockNumber)
// non mempool tx - read the block header to get the block time
raw, err := b.getBlockRaw(tx.BlockHash, 0, false)
if err != nil {
return nil, err
}
var ht struct {
Time string `json:"timestamp"`
}
if err := json.Unmarshal(raw, &ht); err != nil {
return nil, errors.Annotatef(err, "hash %v", hash)
}
var time int64
if time, err = ethNumber(ht.Time); err != nil {
return nil, errors.Annotatef(err, "txid %v", txid)
}
h, err := b.client.HeaderByHash(ctx, *tx.BlockHash)
var receipt rpcReceipt
if b.isETC {
var rawReceipt json.RawMessage
var etcReceipt rpcEtcReceipt
err = b.rpc.CallContext(ctx, &rawReceipt, "eth_getTransactionReceipt", hash)
if err != nil {
return nil, errors.Annotatef(err, "txid %v", txid)
}
err = json.Unmarshal(rawReceipt, &etcReceipt)
if err == nil {
receipt.GasUsed = etcReceipt.GasUsed
receipt.Logs = etcReceipt.Logs
if etcReceipt.Status == 0 {
receipt.Status = "0x0"
} else {
receipt.Status = "0x1"
}
} else {
err = json.Unmarshal(rawReceipt, &receipt)
if err != nil {
return nil, errors.Annotatef(err, "unmarshal receipt for txid %v, %v", txid, string(rawReceipt))
}
}
} else {
err = b.rpc.CallContext(ctx, &receipt, "eth_getTransactionReceipt", hash)
if err != nil {
return nil, errors.Annotatef(err, "txid %v", txid)
}
}
n, err := ethNumber(tx.BlockNumber)
if err != nil {
return nil, errors.Annotatef(err, "txid %v", txid)
}
@ -486,7 +600,7 @@ func (b *EthereumRPC) GetTransaction(txid string) (*bchain.Tx, error) {
if err != nil {
return nil, errors.Annotatef(err, "txid %v", txid)
}
btx, err = b.Parser.ethTxToTx(tx, h.Time.Int64(), confirmations)
btx, err = b.Parser.ethTxToTx(tx, &receipt, time, confirmations)
if err != nil {
return nil, errors.Annotatef(err, "txid %v", txid)
}
@ -495,39 +609,47 @@ func (b *EthereumRPC) GetTransaction(txid string) (*bchain.Tx, error) {
}
// GetTransactionSpecific returns json as returned by backend, with all coin specific data
func (b *EthereumRPC) GetTransactionSpecific(txid string) (json.RawMessage, error) {
ctx, cancel := context.WithTimeout(context.Background(), b.timeout)
defer cancel()
var tx json.RawMessage
err := b.rpc.CallContext(ctx, &tx, "eth_getTransactionByHash", ethcommon.HexToHash(txid))
if err != nil {
return nil, err
} else if tx == nil {
return nil, ethereum.NotFound
func (b *EthereumRPC) GetTransactionSpecific(tx *bchain.Tx) (json.RawMessage, error) {
csd, ok := tx.CoinSpecificData.(completeTransaction)
if !ok {
ntx, err := b.GetTransaction(tx.Txid)
if err != nil {
return nil, err
}
csd, ok = ntx.CoinSpecificData.(completeTransaction)
if !ok {
return nil, errors.New("Cannot get CoinSpecificData")
}
}
return tx, nil
}
type rpcMempoolBlock struct {
Transactions []string `json:"transactions"`
m, err := json.Marshal(&csd)
return json.RawMessage(m), err
}
// GetMempool returns transactions in mempool
func (b *EthereumRPC) GetMempool() ([]string, error) {
ctx, cancel := context.WithTimeout(context.Background(), b.timeout)
defer cancel()
var raw json.RawMessage
var err error
err = b.rpc.CallContext(ctx, &raw, "eth_getBlockByNumber", "pending", false)
raw, err := b.getBlockRaw("pending", 0, false)
if err != nil {
return nil, err
} else if len(raw) == 0 {
return nil, bchain.ErrBlockNotFound
}
var body rpcMempoolBlock
if err := json.Unmarshal(raw, &body); err != nil {
return nil, err
var body rpcBlockTxids
if len(raw) > 0 {
if err := json.Unmarshal(raw, &body); err != nil {
return nil, err
}
}
return body.Transactions, nil
b.pendingTransactionsLock.Lock()
// join transactions returned by getBlockRaw with pendingTransactions from subscription
for _, txid := range body.Transactions {
b.pendingTransactions[txid] = struct{}{}
}
txids := make([]string, len(b.pendingTransactions))
i := 0
for txid := range b.pendingTransactions {
txids[i] = txid
i++
}
b.pendingTransactionsLock.Unlock()
return txids, nil
}
// EstimateFee returns fee estimation
@ -539,21 +661,45 @@ 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
}
// SendRawTransaction sends raw transaction.
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
func (b *EthereumRPC) SendRawTransaction(hex string) (string, error) {
ctx, cancel := context.WithTimeout(context.Background(), b.timeout)
defer cancel()
@ -574,24 +720,43 @@ func (b *EthereumRPC) SendRawTransaction(hex string) (string, error) {
return result, nil
}
// EthereumTypeGetBalance returns current balance of an address
func (b *EthereumRPC) EthereumTypeGetBalance(addrDesc bchain.AddressDescriptor) (*big.Int, error) {
ctx, cancel := context.WithTimeout(context.Background(), b.timeout)
defer cancel()
return b.client.BalanceAt(ctx, ethcommon.BytesToAddress(addrDesc), nil)
}
// EthereumTypeGetNonce returns current balance of an address
func (b *EthereumRPC) EthereumTypeGetNonce(addrDesc bchain.AddressDescriptor) (uint64, error) {
ctx, cancel := context.WithTimeout(context.Background(), b.timeout)
defer cancel()
return b.client.NonceAt(ctx, ethcommon.BytesToAddress(addrDesc), nil)
}
// ResyncMempool gets mempool transactions and maps output scripts to transactions.
// ResyncMempool is not reentrant, it should be called from a single thread.
// Return value is number of transactions in mempool
func (b *EthereumRPC) ResyncMempool(onNewTxAddr bchain.OnNewTxAddrFunc) (int, error) {
return b.Mempool.Resync(onNewTxAddr)
}
// GetMempoolTransactions returns slice of mempool transactions for given address
func (b *EthereumRPC) GetMempoolTransactions(address string) ([]string, error) {
func (b *EthereumRPC) GetMempoolTransactions(address string) ([]bchain.Outpoint, error) {
return b.Mempool.GetTransactions(address)
}
// GetMempoolTransactionsForAddrDesc returns slice of mempool transactions for given address descriptor
func (b *EthereumRPC) GetMempoolTransactionsForAddrDesc(addrDesc bchain.AddressDescriptor) ([]string, error) {
func (b *EthereumRPC) GetMempoolTransactionsForAddrDesc(addrDesc bchain.AddressDescriptor) ([]bchain.Outpoint, error) {
return b.Mempool.GetAddrDescTransactions(addrDesc)
}
// GetMempoolEntry is not supported by etherem
func (b *EthereumRPC) GetMempoolEntry(txid string) (*bchain.MempoolEntry, error) {
return nil, errors.New("GetMempoolEntry: not implemented")
return nil, errors.New("GetMempoolEntry: not supported")
}
// GetChainParser returns ethereum BlockChainParser
func (b *EthereumRPC) GetChainParser() bchain.BlockChainParser {
return b.Parser
}

View File

@ -8,7 +8,7 @@ It is generated from these files:
tx.proto
It has these top-level messages:
ProtoTransaction
ProtoCompleteTransaction
*/
package eth
@ -27,149 +27,234 @@ var _ = math.Inf
// proto package needs to be updated.
const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package
type ProtoTransaction struct {
AccountNonce uint64 `protobuf:"varint,1,opt,name=AccountNonce" json:"AccountNonce,omitempty"`
Price []byte `protobuf:"bytes,2,opt,name=Price,proto3" json:"Price,omitempty"`
GasLimit uint64 `protobuf:"varint,3,opt,name=GasLimit" json:"GasLimit,omitempty"`
Value []byte `protobuf:"bytes,4,opt,name=Value,proto3" json:"Value,omitempty"`
Payload []byte `protobuf:"bytes,5,opt,name=Payload,proto3" json:"Payload,omitempty"`
Hash []byte `protobuf:"bytes,6,opt,name=Hash,proto3" json:"Hash,omitempty"`
BlockNumber uint32 `protobuf:"varint,7,opt,name=BlockNumber" json:"BlockNumber,omitempty"`
BlockTime uint64 `protobuf:"varint,8,opt,name=BlockTime" json:"BlockTime,omitempty"`
To []byte `protobuf:"bytes,9,opt,name=To,proto3" json:"To,omitempty"`
From []byte `protobuf:"bytes,10,opt,name=From,proto3" json:"From,omitempty"`
TransactionIndex uint32 `protobuf:"varint,11,opt,name=TransactionIndex" json:"TransactionIndex,omitempty"`
V []byte `protobuf:"bytes,12,opt,name=V,proto3" json:"V,omitempty"`
R []byte `protobuf:"bytes,13,opt,name=R,proto3" json:"R,omitempty"`
S []byte `protobuf:"bytes,14,opt,name=S,proto3" json:"S,omitempty"`
type ProtoCompleteTransaction struct {
BlockNumber uint32 `protobuf:"varint,1,opt,name=BlockNumber" json:"BlockNumber,omitempty"`
BlockTime uint64 `protobuf:"varint,2,opt,name=BlockTime" json:"BlockTime,omitempty"`
Tx *ProtoCompleteTransaction_TxType `protobuf:"bytes,3,opt,name=Tx" json:"Tx,omitempty"`
Receipt *ProtoCompleteTransaction_ReceiptType `protobuf:"bytes,4,opt,name=Receipt" json:"Receipt,omitempty"`
}
func (m *ProtoTransaction) Reset() { *m = ProtoTransaction{} }
func (m *ProtoTransaction) String() string { return proto.CompactTextString(m) }
func (*ProtoTransaction) ProtoMessage() {}
func (*ProtoTransaction) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{0} }
func (m *ProtoCompleteTransaction) Reset() { *m = ProtoCompleteTransaction{} }
func (m *ProtoCompleteTransaction) String() string { return proto.CompactTextString(m) }
func (*ProtoCompleteTransaction) ProtoMessage() {}
func (*ProtoCompleteTransaction) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{0} }
func (m *ProtoTransaction) GetAccountNonce() uint64 {
if m != nil {
return m.AccountNonce
}
return 0
}
func (m *ProtoTransaction) GetPrice() []byte {
if m != nil {
return m.Price
}
return nil
}
func (m *ProtoTransaction) GetGasLimit() uint64 {
if m != nil {
return m.GasLimit
}
return 0
}
func (m *ProtoTransaction) GetValue() []byte {
if m != nil {
return m.Value
}
return nil
}
func (m *ProtoTransaction) GetPayload() []byte {
if m != nil {
return m.Payload
}
return nil
}
func (m *ProtoTransaction) GetHash() []byte {
if m != nil {
return m.Hash
}
return nil
}
func (m *ProtoTransaction) GetBlockNumber() uint32 {
func (m *ProtoCompleteTransaction) GetBlockNumber() uint32 {
if m != nil {
return m.BlockNumber
}
return 0
}
func (m *ProtoTransaction) GetBlockTime() uint64 {
func (m *ProtoCompleteTransaction) GetBlockTime() uint64 {
if m != nil {
return m.BlockTime
}
return 0
}
func (m *ProtoTransaction) GetTo() []byte {
func (m *ProtoCompleteTransaction) GetTx() *ProtoCompleteTransaction_TxType {
if m != nil {
return m.Tx
}
return nil
}
func (m *ProtoCompleteTransaction) GetReceipt() *ProtoCompleteTransaction_ReceiptType {
if m != nil {
return m.Receipt
}
return nil
}
type ProtoCompleteTransaction_TxType struct {
AccountNonce uint64 `protobuf:"varint,1,opt,name=AccountNonce" json:"AccountNonce,omitempty"`
GasPrice []byte `protobuf:"bytes,2,opt,name=GasPrice,proto3" json:"GasPrice,omitempty"`
GasLimit uint64 `protobuf:"varint,3,opt,name=GasLimit" json:"GasLimit,omitempty"`
Value []byte `protobuf:"bytes,4,opt,name=Value,proto3" json:"Value,omitempty"`
Payload []byte `protobuf:"bytes,5,opt,name=Payload,proto3" json:"Payload,omitempty"`
Hash []byte `protobuf:"bytes,6,opt,name=Hash,proto3" json:"Hash,omitempty"`
To []byte `protobuf:"bytes,7,opt,name=To,proto3" json:"To,omitempty"`
From []byte `protobuf:"bytes,8,opt,name=From,proto3" json:"From,omitempty"`
TransactionIndex uint32 `protobuf:"varint,9,opt,name=TransactionIndex" json:"TransactionIndex,omitempty"`
}
func (m *ProtoCompleteTransaction_TxType) Reset() { *m = ProtoCompleteTransaction_TxType{} }
func (m *ProtoCompleteTransaction_TxType) String() string { return proto.CompactTextString(m) }
func (*ProtoCompleteTransaction_TxType) ProtoMessage() {}
func (*ProtoCompleteTransaction_TxType) Descriptor() ([]byte, []int) {
return fileDescriptor0, []int{0, 0}
}
func (m *ProtoCompleteTransaction_TxType) GetAccountNonce() uint64 {
if m != nil {
return m.AccountNonce
}
return 0
}
func (m *ProtoCompleteTransaction_TxType) GetGasPrice() []byte {
if m != nil {
return m.GasPrice
}
return nil
}
func (m *ProtoCompleteTransaction_TxType) GetGasLimit() uint64 {
if m != nil {
return m.GasLimit
}
return 0
}
func (m *ProtoCompleteTransaction_TxType) GetValue() []byte {
if m != nil {
return m.Value
}
return nil
}
func (m *ProtoCompleteTransaction_TxType) GetPayload() []byte {
if m != nil {
return m.Payload
}
return nil
}
func (m *ProtoCompleteTransaction_TxType) GetHash() []byte {
if m != nil {
return m.Hash
}
return nil
}
func (m *ProtoCompleteTransaction_TxType) GetTo() []byte {
if m != nil {
return m.To
}
return nil
}
func (m *ProtoTransaction) GetFrom() []byte {
func (m *ProtoCompleteTransaction_TxType) GetFrom() []byte {
if m != nil {
return m.From
}
return nil
}
func (m *ProtoTransaction) GetTransactionIndex() uint32 {
func (m *ProtoCompleteTransaction_TxType) GetTransactionIndex() uint32 {
if m != nil {
return m.TransactionIndex
}
return 0
}
func (m *ProtoTransaction) GetV() []byte {
type ProtoCompleteTransaction_ReceiptType struct {
GasUsed []byte `protobuf:"bytes,1,opt,name=GasUsed,proto3" json:"GasUsed,omitempty"`
Status []byte `protobuf:"bytes,2,opt,name=Status,proto3" json:"Status,omitempty"`
Log []*ProtoCompleteTransaction_ReceiptType_LogType `protobuf:"bytes,3,rep,name=Log" json:"Log,omitempty"`
}
func (m *ProtoCompleteTransaction_ReceiptType) Reset() { *m = ProtoCompleteTransaction_ReceiptType{} }
func (m *ProtoCompleteTransaction_ReceiptType) String() string { return proto.CompactTextString(m) }
func (*ProtoCompleteTransaction_ReceiptType) ProtoMessage() {}
func (*ProtoCompleteTransaction_ReceiptType) Descriptor() ([]byte, []int) {
return fileDescriptor0, []int{0, 1}
}
func (m *ProtoCompleteTransaction_ReceiptType) GetGasUsed() []byte {
if m != nil {
return m.V
return m.GasUsed
}
return nil
}
func (m *ProtoTransaction) GetR() []byte {
func (m *ProtoCompleteTransaction_ReceiptType) GetStatus() []byte {
if m != nil {
return m.R
return m.Status
}
return nil
}
func (m *ProtoTransaction) GetS() []byte {
func (m *ProtoCompleteTransaction_ReceiptType) GetLog() []*ProtoCompleteTransaction_ReceiptType_LogType {
if m != nil {
return m.S
return m.Log
}
return nil
}
type ProtoCompleteTransaction_ReceiptType_LogType struct {
Address []byte `protobuf:"bytes,1,opt,name=Address,proto3" json:"Address,omitempty"`
Data []byte `protobuf:"bytes,2,opt,name=Data,proto3" json:"Data,omitempty"`
Topics [][]byte `protobuf:"bytes,3,rep,name=Topics,proto3" json:"Topics,omitempty"`
}
func (m *ProtoCompleteTransaction_ReceiptType_LogType) Reset() {
*m = ProtoCompleteTransaction_ReceiptType_LogType{}
}
func (m *ProtoCompleteTransaction_ReceiptType_LogType) String() string {
return proto.CompactTextString(m)
}
func (*ProtoCompleteTransaction_ReceiptType_LogType) ProtoMessage() {}
func (*ProtoCompleteTransaction_ReceiptType_LogType) Descriptor() ([]byte, []int) {
return fileDescriptor0, []int{0, 1, 0}
}
func (m *ProtoCompleteTransaction_ReceiptType_LogType) GetAddress() []byte {
if m != nil {
return m.Address
}
return nil
}
func (m *ProtoCompleteTransaction_ReceiptType_LogType) GetData() []byte {
if m != nil {
return m.Data
}
return nil
}
func (m *ProtoCompleteTransaction_ReceiptType_LogType) GetTopics() [][]byte {
if m != nil {
return m.Topics
}
return nil
}
func init() {
proto.RegisterType((*ProtoTransaction)(nil), "eth.ProtoTransaction")
proto.RegisterType((*ProtoCompleteTransaction)(nil), "eth.ProtoCompleteTransaction")
proto.RegisterType((*ProtoCompleteTransaction_TxType)(nil), "eth.ProtoCompleteTransaction.TxType")
proto.RegisterType((*ProtoCompleteTransaction_ReceiptType)(nil), "eth.ProtoCompleteTransaction.ReceiptType")
proto.RegisterType((*ProtoCompleteTransaction_ReceiptType_LogType)(nil), "eth.ProtoCompleteTransaction.ReceiptType.LogType")
}
func init() { proto.RegisterFile("tx.proto", fileDescriptor0) }
var fileDescriptor0 = []byte{
// 262 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x64, 0x90, 0xbd, 0x6a, 0xeb, 0x40,
0x10, 0x85, 0x59, 0x59, 0xb6, 0xe5, 0xb1, 0x6c, 0xcc, 0x70, 0x8b, 0xe1, 0x92, 0x42, 0xb8, 0x12,
0x29, 0xd2, 0xe4, 0x09, 0x92, 0x22, 0x3f, 0x10, 0x8c, 0x90, 0x85, 0xfa, 0xf5, 0x7a, 0xc1, 0x22,
0x92, 0x26, 0x48, 0x2b, 0x70, 0x5e, 0x38, 0xcf, 0x11, 0x76, 0x44, 0x12, 0x87, 0x74, 0xf3, 0x7d,
0x70, 0xf6, 0x2c, 0x07, 0x22, 0x77, 0xbe, 0x79, 0xeb, 0xd8, 0x31, 0x4e, 0xac, 0x3b, 0x6d, 0x3f,
0x02, 0xd8, 0x64, 0x1e, 0x8b, 0x4e, 0xb7, 0xbd, 0x36, 0xae, 0xe2, 0x16, 0xb7, 0x10, 0xdf, 0x19,
0xc3, 0x43, 0xeb, 0x76, 0xdc, 0x1a, 0x4b, 0x2a, 0x51, 0x69, 0x98, 0xff, 0x72, 0xf8, 0x0f, 0xa6,
0x59, 0x57, 0x19, 0x4b, 0x41, 0xa2, 0xd2, 0x38, 0x1f, 0x01, 0xff, 0x43, 0xf4, 0xa8, 0xfb, 0x97,
0xaa, 0xa9, 0x1c, 0x4d, 0x24, 0xf5, 0xcd, 0x3e, 0x51, 0xea, 0x7a, 0xb0, 0x14, 0x8e, 0x09, 0x01,
0x24, 0x98, 0x67, 0xfa, 0xbd, 0x66, 0x7d, 0xa4, 0xa9, 0xf8, 0x2f, 0x44, 0x84, 0xf0, 0x49, 0xf7,
0x27, 0x9a, 0x89, 0x96, 0x1b, 0x13, 0x58, 0xde, 0xd7, 0x6c, 0x5e, 0x77, 0x43, 0x73, 0xb0, 0x1d,
0xcd, 0x13, 0x95, 0xae, 0xf2, 0x4b, 0x85, 0x57, 0xb0, 0x10, 0x2c, 0xaa, 0xc6, 0x52, 0x24, 0x5f,
0xf8, 0x11, 0xb8, 0x86, 0xa0, 0x60, 0x5a, 0xc8, 0x8b, 0x41, 0xc1, 0xbe, 0xe3, 0xa1, 0xe3, 0x86,
0x60, 0xec, 0xf0, 0x37, 0x5e, 0xc3, 0xe6, 0x62, 0x8c, 0xe7, 0xf6, 0x68, 0xcf, 0xb4, 0x94, 0xa2,
0x3f, 0x1e, 0x63, 0x50, 0x25, 0xc5, 0x12, 0x56, 0xa5, 0xa7, 0x9c, 0x56, 0x23, 0xe5, 0x9e, 0xf6,
0xb4, 0x1e, 0x69, 0x7f, 0x98, 0xc9, 0xe8, 0xb7, 0x9f, 0x01, 0x00, 0x00, 0xff, 0xff, 0x15, 0xc8,
0xe4, 0x30, 0x80, 0x01, 0x00, 0x00,
// 393 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x8c, 0x92, 0xdf, 0x8a, 0xd4, 0x30,
0x14, 0xc6, 0xe9, 0x9f, 0xf9, 0xb3, 0xa7, 0x55, 0x24, 0x88, 0x84, 0xe2, 0x45, 0x59, 0xbc, 0xa8,
0x5e, 0x14, 0x5c, 0x7d, 0x81, 0x75, 0xc4, 0x55, 0x18, 0xd6, 0x21, 0x46, 0xef, 0xb3, 0x69, 0xd8,
0x29, 0xb6, 0x4d, 0x69, 0x52, 0xe8, 0xbe, 0x91, 0x2f, 0xe4, 0xbb, 0x78, 0x29, 0x39, 0x4d, 0xd7,
0x11, 0x51, 0xbc, 0x3b, 0xbf, 0x6f, 0xce, 0x37, 0xf9, 0xbe, 0xa4, 0xb0, 0xb5, 0x53, 0xd9, 0x0f,
0xda, 0x6a, 0x12, 0x29, 0x7b, 0x3c, 0xff, 0xb6, 0x02, 0x7a, 0x70, 0xb8, 0xd3, 0x6d, 0xdf, 0x28,
0xab, 0xf8, 0x20, 0x3a, 0x23, 0xa4, 0xad, 0x75, 0x47, 0x72, 0x48, 0xde, 0x34, 0x5a, 0x7e, 0xbd,
0x1e, 0xdb, 0x1b, 0x35, 0xd0, 0x20, 0x0f, 0x8a, 0x07, 0xec, 0x54, 0x22, 0x4f, 0xe1, 0x0c, 0x91,
0xd7, 0xad, 0xa2, 0x61, 0x1e, 0x14, 0x31, 0xfb, 0x25, 0x90, 0xd7, 0x10, 0xf2, 0x89, 0x46, 0x79,
0x50, 0x24, 0x17, 0xcf, 0x4a, 0x65, 0x8f, 0xe5, 0xdf, 0x8e, 0x2a, 0xf9, 0xc4, 0xef, 0x7a, 0xc5,
0x42, 0x3e, 0x91, 0x1d, 0x6c, 0x98, 0x92, 0xaa, 0xee, 0x2d, 0x8d, 0xd1, 0xfa, 0xfc, 0xdf, 0x56,
0xbf, 0x8c, 0xfe, 0xc5, 0x99, 0xfd, 0x08, 0x60, 0x3d, 0xff, 0x27, 0x39, 0x87, 0xf4, 0x52, 0x4a,
0x3d, 0x76, 0xf6, 0x5a, 0x77, 0x52, 0x61, 0x8d, 0x98, 0xfd, 0xa6, 0x91, 0x0c, 0xb6, 0x57, 0xc2,
0x1c, 0x86, 0x5a, 0xce, 0x35, 0x52, 0x76, 0xcf, 0xfe, 0xb7, 0x7d, 0xdd, 0xd6, 0x16, 0xbb, 0xc4,
0xec, 0x9e, 0xc9, 0x63, 0x58, 0x7d, 0x11, 0xcd, 0xa8, 0x30, 0x69, 0xca, 0x66, 0x20, 0x14, 0x36,
0x07, 0x71, 0xd7, 0x68, 0x51, 0xd1, 0x15, 0xea, 0x0b, 0x12, 0x02, 0xf1, 0x7b, 0x61, 0x8e, 0x74,
0x8d, 0x32, 0xce, 0xe4, 0x21, 0x84, 0x5c, 0xd3, 0x0d, 0x2a, 0x21, 0xd7, 0x6e, 0xe7, 0xdd, 0xa0,
0x5b, 0xba, 0x9d, 0x77, 0xdc, 0x4c, 0x5e, 0xc0, 0xa3, 0x93, 0xca, 0x1f, 0xba, 0x4a, 0x4d, 0xf4,
0x0c, 0x9f, 0xe3, 0x0f, 0x3d, 0xfb, 0x1e, 0x40, 0x72, 0x72, 0x27, 0x2e, 0xcd, 0x95, 0x30, 0x9f,
0x8d, 0xaa, 0xb0, 0x7a, 0xca, 0x16, 0x24, 0x4f, 0x60, 0xfd, 0xc9, 0x0a, 0x3b, 0x1a, 0xdf, 0xd9,
0x13, 0xd9, 0x41, 0xb4, 0xd7, 0xb7, 0x34, 0xca, 0xa3, 0x22, 0xb9, 0x78, 0xf9, 0xdf, 0xb7, 0x5f,
0xee, 0xf5, 0x2d, 0xbe, 0x82, 0x73, 0x67, 0x1f, 0x61, 0xe3, 0xd9, 0x25, 0xb8, 0xac, 0xaa, 0x41,
0x19, 0xb3, 0x24, 0xf0, 0xe8, 0xba, 0xbe, 0x15, 0x56, 0xf8, 0xf3, 0x71, 0x76, 0xa9, 0xb8, 0xee,
0x6b, 0x69, 0x30, 0x40, 0xca, 0x3c, 0xdd, 0xac, 0xf1, 0xb3, 0x7d, 0xf5, 0x33, 0x00, 0x00, 0xff,
0xff, 0xde, 0xd5, 0x28, 0xa3, 0xc2, 0x02, 0x00, 0x00,
}

View File

@ -1,19 +1,30 @@
syntax = "proto3";
package eth;
message ProtoTransaction {
uint64 AccountNonce = 1;
bytes Price = 2;
uint64 GasLimit = 3;
bytes Value = 4;
bytes Payload = 5;
bytes Hash = 6;
uint32 BlockNumber = 7;
uint64 BlockTime = 8;
bytes To = 9;
bytes From = 10;
uint32 TransactionIndex = 11;
bytes V = 12;
bytes R = 13;
bytes S = 14;
message ProtoCompleteTransaction {
message TxType {
uint64 AccountNonce = 1;
bytes GasPrice = 2;
uint64 GasLimit = 3;
bytes Value = 4;
bytes Payload = 5;
bytes Hash = 6;
bytes To = 7;
bytes From = 8;
uint32 TransactionIndex = 9;
}
message ReceiptType {
message LogType {
bytes Address = 1;
bytes Data = 2;
repeated bytes Topics = 3;
}
bytes GasUsed = 1;
bytes Status = 2;
repeated LogType Log = 3;
}
uint32 BlockNumber = 1;
uint64 BlockTime = 2;
TxType Tx = 3;
ReceiptType Receipt = 4;
}

View File

@ -18,8 +18,8 @@ import (
var (
testTx1, testTx2 bchain.Tx
testTxPacked1 = "0a20f56521b17b828897f72b30dd21b0192fd942342e89acbb06abf1d446282c30f512bf0101000000014a9d1fdba915e0907ab02f04f88898863112a2b4fdcf872c7414588c47c874cb000000006a47304402201fb96d20d0778f54520ab59afe70d5fb20e500ecc9f02281cf57934e8029e8e10220383d5a3e80f2e1eb92765b6da0f23d454aecbd8236f083d483e9a7430236876101210331693756f749180aeed0a65a0fab0625a2250bd9abca502282a4cf0723152e67ffffffff01a0330300000000001976a914fe40329c95c5598ac60752a5310b320cb52d18e688ac0000000018ffff87da05200028a6f383013298010a001220cb74c8478c5814742c87cffdb4a21231869888f8042fb07a90e015a9db1f9d4a1800226a47304402201fb96d20d0778f54520ab59afe70d5fb20e500ecc9f02281cf57934e8029e8e10220383d5a3e80f2e1eb92765b6da0f23d454aecbd8236f083d483e9a7430236876101210331693756f749180aeed0a65a0fab0625a2250bd9abca502282a4cf0723152e6728ffffffff0f3a460a030333a010001a1976a914fe40329c95c5598ac60752a5310b320cb52d18e688ac222246744d347a416e39615659674867786d616d5742675750795a7362365268766b4139"
testTxPacked2 = "0a209b5c4859a8a31e69788cb4402812bb28f14ad71cbd8c60b09903478bc56f79a312e00101000000000101d1613f483f2086d076c82fe34674385a86beb08f052d5405fe1aed397f852f4f0000000000feffffff02404b4c000000000017a9147a55d61848e77ca266e79a39bfc85c580a6426c987a8386f0000000000160014cc8067093f6f843d6d3e22004a4290cd0c0f336b02483045022100ea8780bc1e60e14e945a80654a41748bbf1aa7d6f2e40a88d91dfc2de1f34bd10220181a474a3420444bd188501d8d270736e1e9fe379da9970de992ff445b0972e3012103adc58245cf28406af0ef5cc24b8afba7f1be6c72f279b642d85c48798685f862d9ed090018caa384da0520d9db2728dadb27322c0a0012204f2f857f39ed1afe05542d058fb0be865a387446e32fc876d086203f483f61d1180028feffffff0f3a450a034c4b4010001a17a9147a55d61848e77ca266e79a39bfc85c580a6426c9872223324e345135466855323439374272794666556762716b414a453837614b4476335633653a4d0a036f38a810011a160014cc8067093f6f843d6d3e22004a4290cd0c0f336b222c746772733171656a7178777a666c64377a72366d663779677179357335736535787137766d74396c6b643537"
testTxPacked1 = "0a20f56521b17b828897f72b30dd21b0192fd942342e89acbb06abf1d446282c30f512bf0101000000014a9d1fdba915e0907ab02f04f88898863112a2b4fdcf872c7414588c47c874cb000000006a47304402201fb96d20d0778f54520ab59afe70d5fb20e500ecc9f02281cf57934e8029e8e10220383d5a3e80f2e1eb92765b6da0f23d454aecbd8236f083d483e9a7430236876101210331693756f749180aeed0a65a0fab0625a2250bd9abca502282a4cf0723152e67ffffffff01a0330300000000001976a914fe40329c95c5598ac60752a5310b320cb52d18e688ac0000000018ffff87da05200028a6f383013298010a001220cb74c8478c5814742c87cffdb4a21231869888f8042fb07a90e015a9db1f9d4a1800226a47304402201fb96d20d0778f54520ab59afe70d5fb20e500ecc9f02281cf57934e8029e8e10220383d5a3e80f2e1eb92765b6da0f23d454aecbd8236f083d483e9a7430236876101210331693756f749180aeed0a65a0fab0625a2250bd9abca502282a4cf0723152e6728ffffffff0f3a460a030333a010001a1976a914fe40329c95c5598ac60752a5310b320cb52d18e688ac222246744d347a416e39615659674867786d616d5742675750795a7362365268766b41394000"
testTxPacked2 = "0a209b5c4859a8a31e69788cb4402812bb28f14ad71cbd8c60b09903478bc56f79a312e00101000000000101d1613f483f2086d076c82fe34674385a86beb08f052d5405fe1aed397f852f4f0000000000feffffff02404b4c000000000017a9147a55d61848e77ca266e79a39bfc85c580a6426c987a8386f0000000000160014cc8067093f6f843d6d3e22004a4290cd0c0f336b02483045022100ea8780bc1e60e14e945a80654a41748bbf1aa7d6f2e40a88d91dfc2de1f34bd10220181a474a3420444bd188501d8d270736e1e9fe379da9970de992ff445b0972e3012103adc58245cf28406af0ef5cc24b8afba7f1be6c72f279b642d85c48798685f862d9ed090018caa384da0520d9db2728dadb27322c0a0012204f2f857f39ed1afe05542d058fb0be865a387446e32fc876d086203f483f61d1180028feffffff0f3a450a034c4b4010001a17a9147a55d61848e77ca266e79a39bfc85c580a6426c9872223324e345135466855323439374272794666556762716b414a453837614b4476335633653a4d0a036f38a810011a160014cc8067093f6f843d6d3e22004a4290cd0c0f336b222c746772733171656a7178777a666c64377a72366d663779677179357335736535787137766d74396c6b6435374000"
)
func init() {

View File

@ -0,0 +1,64 @@
package liquid
import (
"blockbook/bchain"
"blockbook/bchain/coins/btc"
"github.com/btcsuite/btcd/wire"
"github.com/jakm/btcutil/chaincfg"
)
const (
MainnetMagic wire.BitcoinNet = 0xdab5bffa
)
var (
MainNetParams chaincfg.Params
)
func init() {
MainNetParams = chaincfg.MainNetParams
MainNetParams.Net = MainnetMagic
MainNetParams.PubKeyHashAddrID = []byte{57}
MainNetParams.ScriptHashAddrID = []byte{39}
// BLINDED_ADDRESS 12
}
// LiquidParser handle
type LiquidParser struct {
*btc.BitcoinParser
baseparser *bchain.BaseParser
}
// NewLiquidParser returns new LiquidParser instance
func NewLiquidParser(params *chaincfg.Params, c *btc.Configuration) *LiquidParser {
return &LiquidParser{
BitcoinParser: btc.NewBitcoinParser(params, c),
baseparser: &bchain.BaseParser{},
}
}
// GetChainParams contains network parameters for the main GameCredits network,
// and the test GameCredits network
func GetChainParams(chain string) *chaincfg.Params {
if !chaincfg.IsRegistered(&MainNetParams) {
err := chaincfg.Register(&MainNetParams)
if err != nil {
panic(err)
}
}
switch chain {
default:
return &MainNetParams
}
}
// PackTx packs transaction to byte array using protobuf
func (p *LiquidParser) PackTx(tx *bchain.Tx, height uint32, blockTime int64) ([]byte, error) {
return p.baseparser.PackTx(tx, height, blockTime)
}
// UnpackTx unpacks transaction from protobuf byte array
func (p *LiquidParser) UnpackTx(buf []byte) (*bchain.Tx, uint32, error) {
return p.baseparser.UnpackTx(buf)
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,108 @@
package liquid
import (
"blockbook/bchain"
"blockbook/bchain/coins/btc"
"encoding/json"
"github.com/golang/glog"
"github.com/juju/errors"
)
// LiquidRPC is an interface to JSON-RPC bitcoind service.
type LiquidRPC struct {
*btc.BitcoinRPC
}
// NewLiquidRPC returns new LiquidRPC instance.
func NewLiquidRPC(config json.RawMessage, pushHandler func(bchain.NotificationType)) (bchain.BlockChain, error) {
b, err := btc.NewBitcoinRPC(config, pushHandler)
if err != nil {
return nil, err
}
s := &LiquidRPC{
b.(*btc.BitcoinRPC),
}
s.RPCMarshaler = btc.JSONMarshalerV2{}
return s, nil
}
// Initialize initializes GameCreditsRPC instance.
func (b *LiquidRPC) Initialize() error {
chainName, err := b.GetChainInfoAndInitializeMempool(b)
if err != nil {
return err
}
glog.Info("Chain name ", chainName)
params := GetChainParams(chainName)
// always create parser
b.Parser = NewLiquidParser(params, b.ChainConfig)
// parameters for getInfo request
if params.Net == MainnetMagic {
b.Testnet = false
b.Network = "livenet"
} else {
b.Testnet = true
b.Network = "testnet"
}
glog.Info("rpc: block chain ", params.Name)
return nil
}
// GetBlock returns block with given hash.
func (b *LiquidRPC) GetBlock(hash string, height uint32) (*bchain.Block, error) {
var err error
if hash == "" {
hash, err = b.GetBlockHash(height)
if err != nil {
return nil, err
}
}
glog.V(1).Info("rpc: getblock (verbosity=1) ", hash)
res := btc.ResGetBlockThin{}
req := btc.CmdGetBlock{Method: "getblock"}
req.Params.BlockHash = hash
req.Params.Verbosity = 1
err = b.Call(&req, &res)
if err != nil {
return nil, errors.Annotatef(err, "hash %v", hash)
}
if res.Error != nil {
return nil, errors.Annotatef(res.Error, "hash %v", hash)
}
txs := make([]bchain.Tx, 0, len(res.Result.Txids))
for _, txid := range res.Result.Txids {
tx, err := b.GetTransaction(txid)
if err != nil {
return nil, err
}
txs = append(txs, *tx)
}
block := &bchain.Block{
BlockHeader: res.Result.BlockHeader,
Txs: txs,
}
return block, nil
}
// GetTransactionForMempool returns a transaction by the transaction ID.
// It could be optimized for mempool, i.e. without block time and confirmations
func (b *LiquidRPC) GetTransactionForMempool(txid string) (*bchain.Tx, error) {
return b.GetTransaction(txid)
}
// GetMempoolEntry returns mempool data for given transaction
func (b *LiquidRPC) GetMempoolEntry(txid string) (*bchain.MempoolEntry, error) {
return nil, errors.New("GetMempoolEntry: not implemented")
}

View File

@ -18,8 +18,8 @@ import (
var (
testTx1, testTx2 bchain.Tx
testTxPacked1 = "0a20e64aac0c211ad210c90934f06b1cc932327329e41a9f70c6eb76f79ef798b7b812ab1002000000019c012650c99d0ef761e863dbb966babf2cb7a7a2b5d90b1461c09521c473d23d000000006b483045022100f220f48c5267ef92a1e7a4d3b44fe9d97cce76eeba2785d45a0e2620b70e8d7302205640bc39e197ce19d95a98a3239af0f208ca289c067f80c97d8e411e61da5dee0121021721e83315fb5282f1d9d2a11892322df589bccd9cef45517b5fb3cfd3055c83ffffffff018eec1a3c040000001976a9149bb8229741305d8316ba3ca6a8d20740ce33c24188ac000000000162b4fc6b0000000000000000000000006ffa88c89b74f0f82e24744296845a0d0113b132ff5dfc2af34e6418eb15206af53078c4dd475cf143cd9a427983f5993622464b53e3a37d2519a946492c3977e30f0866550b9097222993a439a39260ac5e7d36aef38c7fdd1df3035a2d5817a9c20526e38f52f822d4db9d2f0156c4119d786d6e3a060ca871df7fae9a5c3a9c921b38ddc6414b13d16aa807389c68016e54bd6a9eb3b23a6bc7bf152e6dba15e9ec36f95dab15ad8f4a92a9d0309bbd930ef24bb7247bf534065c1e2f5b42e2c80eb59f48b4da6ec522319e065f8c4e463f95cc7fcad8d7ee91608e3c0ffcaa44129ba2d2da45d9a413919eca41af29faaf806a3eeb823e5a6c51afb1ec709505d812c0306bd76061a0a62d207355ad44d1ffce2b9e1dfd0818f79bd0f8e4031116b71fee2488484f17818b80532865773166cd389929e8409bb94e3948bd2e0215ef96d4e29d094590fda0de50715c11ff47c03380bb1d31b14e5b4ad8a372ca0b03364ef85f086b8a8eb5c56c3b1aee33e2cfbf1b2be1a3fb41b14b2c432b5d04d54c058fa87a96ae1d65d61b79360d09acc1e25a883fd7ae9a2a734a03362903021401c243173e1050b5cdb459b9ffc07c95e920f026618952d3a800b2e47e03b902084aed7ee8466a65d34abdbbd292781564dcd9b7440029d48c2640ebc196d4b40217f2872c1d0c1c9c2abf1147d6a5a9501895bc92960bfa182ceeb76a658224f1022bc53c4c1cd6888d72a152dc1aec5ba8a1d750fb7e498bee844d3481e4b4cd210227f94f775744185c9f24571b7df0c1c694cb2d3e4e9b955ed0b1caad2b02b5702139c4fbba03f0e422b2f3e4fc822b4f58baf32e7cd217cdbdec8540cb13d6496f271959b72a05e130eeffbe5b9a7fcd2793347cd9c0ea695265669844c363190f690c52a600cf413c3f00bdc5e9d1539e0cc63f4ec2945e0d86e6304a6deb5651e73eac21add5a641dfc95ab56200ed40d81f76755aee4659334c17ed3841ca5a5ab22f923956be1d264be2b485a0de55404510ece5c73d6626798be688f9dc18b69846acfe897a357cc4afe31f57fea32896717f124290e68f36f849fa6ecf76e02087f8c19dbc566135d7fa2daca2d843b9cc5bc3897d35f1de7d174f6407658f4a3706c12cea53d880b4d8c4d45b3f0d210214f815be49a664021a4a44b4a63e06a41d76b46f9aa6bad248e8d1a974ae7bbae5ea8ac269447db91637a19346729083cad5aebd5ff43ea13d04783068e9136da321b1152c666d2995d0ca06b26541deac62f4ef91f0e4af445b18a5c2a17c96eada0b27f85bb26dfb8f16515114c6b9f88037e2b85b3b84b65822eb99c992d99d12dcf9c71e5b46a586016faf5758483a716566db95b42187c101df68ca0554824e1c23cf0302bea03ad0a146af57e91794a268b8c82d78211718c8b5fea286f5de72fc7dfffecddcc02413525c472cb26022641d4bec2b8b7e71a7beb9ee18b82632799498eeee9a351cb9431a8d1906d5164acdf351bd538c3e9d1da8a211fe1cd18c44e72d8cdf16ce3fc9551552c05d52846ea7ef619232102588395cc2bcce509a4e7f150262a76c15475496c923dfce6bfc05871467ee7c213b39ea365c010083e0b1ba8926d3a9e586d8b11c9bab2a47d888bc7cb1a226c0086a1530e295d0047547006f4c8f1c24cdd8e16bb3845749895dec95f03fcda97d3224f6875b1b7b1c819d2fd35dd30968a3c82bc480d10082caf9d9dda8f9ec649c136c7fa07978099d97eaf4abfdc9854c266979d3cfc868f60689b6e3098b6c52a21796fe7c259d9a0dadf1b6efa59297d4c8c902febe7acf826eed30d40d2ac5119be91b51f4839d94599872c9a93c3e2691294914034001d3a278cb4a84d4ae048c0201a97e4cf1341ee663a162f5b586355018b9e5e30624ccdbeacf7d0382afacaf45f08e84d30c50bcd4e55c3138377261deb4e8c2931cd3c51cee94a048ae4839517b6e6537a5c0148d3830a33fea719ef9b4fa437e4d5fecdb646397c19ee56a0973c362a81803895cdc67246352dc566689cb203f9ebda900a5537bbb75aa25ddf3d4ab87b88737a58d760e1d271f08265daae1fe056e71971a8b826e5b215a05b71f99315b167dd2ec78874189657acafac2b5eeb9a901913f55f7ab69e1f9b203504448d414e71098b932a2309db57257eb3fef9de2f2a5a69aa46747d7b827df838345d38b95772bdab8c178c45777b92e8773864964b8e12ae29dbc1b21bf6527589f6bec71ff1cbb9928477409811c2e8150c79c3f21027ee954863b716875d3e9adfc6fdb18cd57a49bb395ca5c42da56f3beb78aad3a7a487de34a870bca61f3cdec422061328c83c910ab32ea7403c354915b7ebee29e1fea5a75158197e4a68e103f017fd7de5a70148ee7ce59356b1a74f83492e14faaa6cd4870bcc004e6eb0114d3429b74ea98fe2851b4553467a7660074e69b040aa31220d0e405d9166dbaf15e3ae2d8ec3b049ed99d17e0743bb6a1a7c3890bbdb7117f7374ad7a59aa1ab47d10445b28f4bc033794a71f88a8bf024189e9d27f9dc5859a4296437585b215656f807aca9dad35747494a43b8a1cf38be2b18a13de32a262ab29f9ba271c4fbce1a470a8243ebf9e7fd37b09262314afbb9a7e180218a0f1c9d505200028b0eb113299010a0012203dd273c42195c061140bd9b5a2a7b72cbfba66b9db63e861f70e9dc95026019c1800226b483045022100f220f48c5267ef92a1e7a4d3b44fe9d97cce76eeba2785d45a0e2620b70e8d7302205640bc39e197ce19d95a98a3239af0f208ca289c067f80c97d8e411e61da5dee0121021721e83315fb5282f1d9d2a11892322df589bccd9cef45517b5fb3cfd3055c8328ffffffff0f3a490a05043c1aec8e10001a1976a9149bb8229741305d8316ba3ca6a8d20740ce33c24188ac222374315934794c31344143486141626a656d6b647057376e594e48576e76317951624441"
testTxPacked2 = "0a20bb47a9dd926de63e9d4f8dac58c3f63f4a079569ed3b80e932274a80f60e58b512e20101000000019cafb5c287980e6e5afb47339f6c1c81136d8255f5bd5226b36b01288494c46f000000006b483045022100c92b2f3c54918fa26288530c63a58197ea4974e5b6d92db792dd9717e6d9183c02204e577254213675466a6adad3ae6e9384cf8269fb2dd9943b86fac0c0ad8e3f98012102c99dab469e63b232488b3e7acb9cfcab7e5755f61aad318d9e06b38e5ea22880feffffff0223a7a784010000001976a914826f87806ddd4643730be99b41c98acc379e83db88ac80969800000000001976a914e395634b7684289285926d4c64db395b783720ec88ac6e75040018e4b1c9d50520eeea1128f9ea113299010a0012206fc4948428016bb32652bdf555826d13811c6c9f3347fb5a6e0e9887c2b5af9c1800226b483045022100c92b2f3c54918fa26288530c63a58197ea4974e5b6d92db792dd9717e6d9183c02204e577254213675466a6adad3ae6e9384cf8269fb2dd9943b86fac0c0ad8e3f98012102c99dab469e63b232488b3e7acb9cfcab7e5755f61aad318d9e06b38e5ea2288028feffffff0f3a490a050184a7a72310001a1976a914826f87806ddd4643730be99b41c98acc379e83db88ac22237431566d4854547770457477766f6a786f644e32435351714c596931687a59336341713a470a0398968010011a1976a914e395634b7684289285926d4c64db395b783720ec88ac222374316563784d587070685554525158474c586e56684a367563714433445a6970646467"
testTxPacked1 = "0a20e64aac0c211ad210c90934f06b1cc932327329e41a9f70c6eb76f79ef798b7b812ab1002000000019c012650c99d0ef761e863dbb966babf2cb7a7a2b5d90b1461c09521c473d23d000000006b483045022100f220f48c5267ef92a1e7a4d3b44fe9d97cce76eeba2785d45a0e2620b70e8d7302205640bc39e197ce19d95a98a3239af0f208ca289c067f80c97d8e411e61da5dee0121021721e83315fb5282f1d9d2a11892322df589bccd9cef45517b5fb3cfd3055c83ffffffff018eec1a3c040000001976a9149bb8229741305d8316ba3ca6a8d20740ce33c24188ac000000000162b4fc6b0000000000000000000000006ffa88c89b74f0f82e24744296845a0d0113b132ff5dfc2af34e6418eb15206af53078c4dd475cf143cd9a427983f5993622464b53e3a37d2519a946492c3977e30f0866550b9097222993a439a39260ac5e7d36aef38c7fdd1df3035a2d5817a9c20526e38f52f822d4db9d2f0156c4119d786d6e3a060ca871df7fae9a5c3a9c921b38ddc6414b13d16aa807389c68016e54bd6a9eb3b23a6bc7bf152e6dba15e9ec36f95dab15ad8f4a92a9d0309bbd930ef24bb7247bf534065c1e2f5b42e2c80eb59f48b4da6ec522319e065f8c4e463f95cc7fcad8d7ee91608e3c0ffcaa44129ba2d2da45d9a413919eca41af29faaf806a3eeb823e5a6c51afb1ec709505d812c0306bd76061a0a62d207355ad44d1ffce2b9e1dfd0818f79bd0f8e4031116b71fee2488484f17818b80532865773166cd389929e8409bb94e3948bd2e0215ef96d4e29d094590fda0de50715c11ff47c03380bb1d31b14e5b4ad8a372ca0b03364ef85f086b8a8eb5c56c3b1aee33e2cfbf1b2be1a3fb41b14b2c432b5d04d54c058fa87a96ae1d65d61b79360d09acc1e25a883fd7ae9a2a734a03362903021401c243173e1050b5cdb459b9ffc07c95e920f026618952d3a800b2e47e03b902084aed7ee8466a65d34abdbbd292781564dcd9b7440029d48c2640ebc196d4b40217f2872c1d0c1c9c2abf1147d6a5a9501895bc92960bfa182ceeb76a658224f1022bc53c4c1cd6888d72a152dc1aec5ba8a1d750fb7e498bee844d3481e4b4cd210227f94f775744185c9f24571b7df0c1c694cb2d3e4e9b955ed0b1caad2b02b5702139c4fbba03f0e422b2f3e4fc822b4f58baf32e7cd217cdbdec8540cb13d6496f271959b72a05e130eeffbe5b9a7fcd2793347cd9c0ea695265669844c363190f690c52a600cf413c3f00bdc5e9d1539e0cc63f4ec2945e0d86e6304a6deb5651e73eac21add5a641dfc95ab56200ed40d81f76755aee4659334c17ed3841ca5a5ab22f923956be1d264be2b485a0de55404510ece5c73d6626798be688f9dc18b69846acfe897a357cc4afe31f57fea32896717f124290e68f36f849fa6ecf76e02087f8c19dbc566135d7fa2daca2d843b9cc5bc3897d35f1de7d174f6407658f4a3706c12cea53d880b4d8c4d45b3f0d210214f815be49a664021a4a44b4a63e06a41d76b46f9aa6bad248e8d1a974ae7bbae5ea8ac269447db91637a19346729083cad5aebd5ff43ea13d04783068e9136da321b1152c666d2995d0ca06b26541deac62f4ef91f0e4af445b18a5c2a17c96eada0b27f85bb26dfb8f16515114c6b9f88037e2b85b3b84b65822eb99c992d99d12dcf9c71e5b46a586016faf5758483a716566db95b42187c101df68ca0554824e1c23cf0302bea03ad0a146af57e91794a268b8c82d78211718c8b5fea286f5de72fc7dfffecddcc02413525c472cb26022641d4bec2b8b7e71a7beb9ee18b82632799498eeee9a351cb9431a8d1906d5164acdf351bd538c3e9d1da8a211fe1cd18c44e72d8cdf16ce3fc9551552c05d52846ea7ef619232102588395cc2bcce509a4e7f150262a76c15475496c923dfce6bfc05871467ee7c213b39ea365c010083e0b1ba8926d3a9e586d8b11c9bab2a47d888bc7cb1a226c0086a1530e295d0047547006f4c8f1c24cdd8e16bb3845749895dec95f03fcda97d3224f6875b1b7b1c819d2fd35dd30968a3c82bc480d10082caf9d9dda8f9ec649c136c7fa07978099d97eaf4abfdc9854c266979d3cfc868f60689b6e3098b6c52a21796fe7c259d9a0dadf1b6efa59297d4c8c902febe7acf826eed30d40d2ac5119be91b51f4839d94599872c9a93c3e2691294914034001d3a278cb4a84d4ae048c0201a97e4cf1341ee663a162f5b586355018b9e5e30624ccdbeacf7d0382afacaf45f08e84d30c50bcd4e55c3138377261deb4e8c2931cd3c51cee94a048ae4839517b6e6537a5c0148d3830a33fea719ef9b4fa437e4d5fecdb646397c19ee56a0973c362a81803895cdc67246352dc566689cb203f9ebda900a5537bbb75aa25ddf3d4ab87b88737a58d760e1d271f08265daae1fe056e71971a8b826e5b215a05b71f99315b167dd2ec78874189657acafac2b5eeb9a901913f55f7ab69e1f9b203504448d414e71098b932a2309db57257eb3fef9de2f2a5a69aa46747d7b827df838345d38b95772bdab8c178c45777b92e8773864964b8e12ae29dbc1b21bf6527589f6bec71ff1cbb9928477409811c2e8150c79c3f21027ee954863b716875d3e9adfc6fdb18cd57a49bb395ca5c42da56f3beb78aad3a7a487de34a870bca61f3cdec422061328c83c910ab32ea7403c354915b7ebee29e1fea5a75158197e4a68e103f017fd7de5a70148ee7ce59356b1a74f83492e14faaa6cd4870bcc004e6eb0114d3429b74ea98fe2851b4553467a7660074e69b040aa31220d0e405d9166dbaf15e3ae2d8ec3b049ed99d17e0743bb6a1a7c3890bbdb7117f7374ad7a59aa1ab47d10445b28f4bc033794a71f88a8bf024189e9d27f9dc5859a4296437585b215656f807aca9dad35747494a43b8a1cf38be2b18a13de32a262ab29f9ba271c4fbce1a470a8243ebf9e7fd37b09262314afbb9a7e180218a0f1c9d505200028b0eb113299010a0012203dd273c42195c061140bd9b5a2a7b72cbfba66b9db63e861f70e9dc95026019c1800226b483045022100f220f48c5267ef92a1e7a4d3b44fe9d97cce76eeba2785d45a0e2620b70e8d7302205640bc39e197ce19d95a98a3239af0f208ca289c067f80c97d8e411e61da5dee0121021721e83315fb5282f1d9d2a11892322df589bccd9cef45517b5fb3cfd3055c8328ffffffff0f3a490a05043c1aec8e10001a1976a9149bb8229741305d8316ba3ca6a8d20740ce33c24188ac222374315934794c31344143486141626a656d6b647057376e594e48576e763179516244414000"
testTxPacked2 = "0a20bb47a9dd926de63e9d4f8dac58c3f63f4a079569ed3b80e932274a80f60e58b512e20101000000019cafb5c287980e6e5afb47339f6c1c81136d8255f5bd5226b36b01288494c46f000000006b483045022100c92b2f3c54918fa26288530c63a58197ea4974e5b6d92db792dd9717e6d9183c02204e577254213675466a6adad3ae6e9384cf8269fb2dd9943b86fac0c0ad8e3f98012102c99dab469e63b232488b3e7acb9cfcab7e5755f61aad318d9e06b38e5ea22880feffffff0223a7a784010000001976a914826f87806ddd4643730be99b41c98acc379e83db88ac80969800000000001976a914e395634b7684289285926d4c64db395b783720ec88ac6e75040018e4b1c9d50520eeea1128f9ea113299010a0012206fc4948428016bb32652bdf555826d13811c6c9f3347fb5a6e0e9887c2b5af9c1800226b483045022100c92b2f3c54918fa26288530c63a58197ea4974e5b6d92db792dd9717e6d9183c02204e577254213675466a6adad3ae6e9384cf8269fb2dd9943b86fac0c0ad8e3f98012102c99dab469e63b232488b3e7acb9cfcab7e5755f61aad318d9e06b38e5ea2288028feffffff0f3a490a050184a7a72310001a1976a914826f87806ddd4643730be99b41c98acc379e83db88ac22237431566d4854547770457477766f6a786f644e32435351714c596931687a59336341713a470a0398968010011a1976a914e395634b7684289285926d4c64db395b783720ec88ac222374316563784d587070685554525158474c586e56684a367563714433445a69706464674000"
)
func init() {

View File

@ -7,44 +7,38 @@ import (
"github.com/golang/glog"
)
// addrIndex and outpoint are used also in non utxo mempool
type addrIndex struct {
addrDesc string
n int32
}
type outpoint struct {
txid string
vout int32
}
type txidio struct {
txid string
io []addrIndex
}
// UTXOMempool is mempool handle.
type UTXOMempool struct {
// MempoolBitcoinType is mempool handle.
type MempoolBitcoinType struct {
chain BlockChain
mux sync.Mutex
txToInputOutput map[string][]addrIndex
addrDescToTx map[string][]outpoint
addrDescToTx map[string][]Outpoint
chanTxid chan string
chanAddrIndex chan txidio
onNewTxAddr OnNewTxAddrFunc
}
// NewUTXOMempool creates new mempool handler.
// NewMempoolBitcoinType creates new mempool handler.
// For now there is no cleanup of sync routines, the expectation is that the mempool is created only once per process
func NewUTXOMempool(chain BlockChain, workers int, subworkers int) *UTXOMempool {
m := &UTXOMempool{
func NewMempoolBitcoinType(chain BlockChain, workers int, subworkers int) *MempoolBitcoinType {
m := &MempoolBitcoinType{
chain: chain,
chanTxid: make(chan string, 1),
chanAddrIndex: make(chan txidio, 1),
}
for i := 0; i < workers; i++ {
go func(i int) {
chanInput := make(chan outpoint, 1)
chanInput := make(chan Outpoint, 1)
chanResult := make(chan *addrIndex, 1)
for j := 0; j < subworkers; j++ {
go func(j int) {
@ -68,7 +62,7 @@ func NewUTXOMempool(chain BlockChain, workers int, subworkers int) *UTXOMempool
}
// GetTransactions returns slice of mempool transactions for given address
func (m *UTXOMempool) GetTransactions(address string) ([]string, error) {
func (m *MempoolBitcoinType) GetTransactions(address string) ([]Outpoint, error) {
parser := m.chain.GetChainParser()
addrDesc, err := parser.GetAddrDescFromAddress(address)
if err != nil {
@ -78,44 +72,39 @@ func (m *UTXOMempool) GetTransactions(address string) ([]string, error) {
}
// GetAddrDescTransactions returns slice of mempool transactions for given address descriptor
func (m *UTXOMempool) GetAddrDescTransactions(addrDesc AddressDescriptor) ([]string, error) {
func (m *MempoolBitcoinType) GetAddrDescTransactions(addrDesc AddressDescriptor) ([]Outpoint, error) {
m.mux.Lock()
defer m.mux.Unlock()
outpoints := m.addrDescToTx[string(addrDesc)]
txs := make([]string, 0, len(outpoints))
for _, o := range outpoints {
txs = append(txs, o.txid)
}
return txs, nil
return append([]Outpoint(nil), m.addrDescToTx[string(addrDesc)]...), nil
}
func (m *UTXOMempool) updateMappings(newTxToInputOutput map[string][]addrIndex, newAddrDescToTx map[string][]outpoint) {
func (m *MempoolBitcoinType) updateMappings(newTxToInputOutput map[string][]addrIndex, newAddrDescToTx map[string][]Outpoint) {
m.mux.Lock()
defer m.mux.Unlock()
m.txToInputOutput = newTxToInputOutput
m.addrDescToTx = newAddrDescToTx
}
func (m *UTXOMempool) getInputAddress(input outpoint) *addrIndex {
itx, err := m.chain.GetTransactionForMempool(input.txid)
func (m *MempoolBitcoinType) getInputAddress(input Outpoint) *addrIndex {
itx, err := m.chain.GetTransactionForMempool(input.Txid)
if err != nil {
glog.Error("cannot get transaction ", input.txid, ": ", err)
glog.Error("cannot get transaction ", input.Txid, ": ", err)
return nil
}
if int(input.vout) >= len(itx.Vout) {
glog.Error("Vout len in transaction ", input.txid, " ", len(itx.Vout), " input.Vout=", input.vout)
if int(input.Vout) >= len(itx.Vout) {
glog.Error("Vout len in transaction ", input.Txid, " ", len(itx.Vout), " input.Vout=", input.Vout)
return nil
}
addrDesc, err := m.chain.GetChainParser().GetAddrDescFromVout(&itx.Vout[input.vout])
addrDesc, err := m.chain.GetChainParser().GetAddrDescFromVout(&itx.Vout[input.Vout])
if err != nil {
glog.Error("error in addrDesc in ", input.txid, " ", input.vout, ": ", err)
glog.Error("error in addrDesc in ", input.Txid, " ", input.Vout, ": ", err)
return nil
}
return &addrIndex{string(addrDesc), ^input.vout}
return &addrIndex{string(addrDesc), ^input.Vout}
}
func (m *UTXOMempool) getTxAddrs(txid string, chanInput chan outpoint, chanResult chan *addrIndex) ([]addrIndex, bool) {
func (m *MempoolBitcoinType) getTxAddrs(txid string, chanInput chan Outpoint, chanResult chan *addrIndex) ([]addrIndex, bool) {
tx, err := m.chain.GetTransactionForMempool(txid)
if err != nil {
glog.Error("cannot get transaction ", txid, ": ", err)
@ -133,7 +122,7 @@ func (m *UTXOMempool) getTxAddrs(txid string, chanInput chan outpoint, chanResul
io = append(io, addrIndex{string(addrDesc), int32(output.N)})
}
if m.onNewTxAddr != nil {
m.onNewTxAddr(tx.Txid, addrDesc, true)
m.onNewTxAddr(tx, addrDesc)
}
}
dispatched := 0
@ -141,7 +130,7 @@ func (m *UTXOMempool) getTxAddrs(txid string, chanInput chan outpoint, chanResul
if input.Coinbase != "" {
continue
}
o := outpoint{input.Txid, int32(input.Vout)}
o := Outpoint{input.Txid, int32(input.Vout)}
loop:
for {
select {
@ -170,7 +159,7 @@ func (m *UTXOMempool) getTxAddrs(txid string, chanInput chan outpoint, chanResul
// Resync gets mempool transactions and maps outputs to transactions.
// Resync is not reentrant, it should be called from a single thread.
// Read operations (GetTransactions) are safe.
func (m *UTXOMempool) Resync(onNewTxAddr OnNewTxAddrFunc) (int, error) {
func (m *MempoolBitcoinType) Resync(onNewTxAddr OnNewTxAddrFunc) (int, error) {
start := time.Now()
glog.V(1).Info("mempool: resync")
m.onNewTxAddr = onNewTxAddr
@ -181,13 +170,13 @@ func (m *UTXOMempool) Resync(onNewTxAddr OnNewTxAddrFunc) (int, error) {
glog.V(2).Info("mempool: resync ", len(txs), " txs")
// allocate slightly larger capacity of the maps
newTxToInputOutput := make(map[string][]addrIndex, len(m.txToInputOutput)+5)
newAddrDescToTx := make(map[string][]outpoint, len(m.addrDescToTx)+5)
newAddrDescToTx := make(map[string][]Outpoint, len(m.addrDescToTx)+5)
dispatched := 0
onNewData := func(txid string, io []addrIndex) {
if len(io) > 0 {
newTxToInputOutput[txid] = io
for _, si := range io {
newAddrDescToTx[si.addrDesc] = append(newAddrDescToTx[si.addrDesc], outpoint{txid, si.n})
newAddrDescToTx[si.addrDesc] = append(newAddrDescToTx[si.addrDesc], Outpoint{txid, si.n})
}
}
}

View File

@ -7,21 +7,21 @@ import (
"github.com/golang/glog"
)
// NonUTXOMempool is mempool handle of non UTXO chains
type NonUTXOMempool struct {
// MempoolEthereumType is mempool handle of EthereumType chains
type MempoolEthereumType struct {
chain BlockChain
mux sync.Mutex
txToInputOutput map[string][]addrIndex
addrDescToTx map[string][]outpoint
addrDescToTx map[string][]Outpoint
}
// NewNonUTXOMempool creates new mempool handler.
func NewNonUTXOMempool(chain BlockChain) *NonUTXOMempool {
return &NonUTXOMempool{chain: chain}
// NewMempoolEthereumType creates new mempool handler.
func NewMempoolEthereumType(chain BlockChain) *MempoolEthereumType {
return &MempoolEthereumType{chain: chain}
}
// GetTransactions returns slice of mempool transactions for given address
func (m *NonUTXOMempool) GetTransactions(address string) ([]string, error) {
func (m *MempoolEthereumType) GetTransactions(address string) ([]Outpoint, error) {
parser := m.chain.GetChainParser()
addrDesc, err := parser.GetAddrDescFromAddress(address)
if err != nil {
@ -31,28 +31,35 @@ func (m *NonUTXOMempool) GetTransactions(address string) ([]string, error) {
}
// GetAddrDescTransactions returns slice of mempool transactions for given address descriptor
func (m *NonUTXOMempool) GetAddrDescTransactions(addrDesc AddressDescriptor) ([]string, error) {
func (m *MempoolEthereumType) GetAddrDescTransactions(addrDesc AddressDescriptor) ([]Outpoint, error) {
m.mux.Lock()
defer m.mux.Unlock()
outpoints := m.addrDescToTx[string(addrDesc)]
txs := make([]string, 0, len(outpoints))
for _, o := range outpoints {
txs = append(txs, o.txid)
}
return txs, nil
return append([]Outpoint(nil), m.addrDescToTx[string(addrDesc)]...), nil
}
func (m *NonUTXOMempool) updateMappings(newTxToInputOutput map[string][]addrIndex, newAddrDescToTx map[string][]outpoint) {
func (m *MempoolEthereumType) updateMappings(newTxToInputOutput map[string][]addrIndex, newAddrDescToTx map[string][]Outpoint) {
m.mux.Lock()
defer m.mux.Unlock()
m.txToInputOutput = newTxToInputOutput
m.addrDescToTx = newAddrDescToTx
}
func appendAddress(io []addrIndex, i int32, a string, parser BlockChainParser) []addrIndex {
if len(a) > 0 {
addrDesc, err := parser.GetAddrDescFromAddress(a)
if err != nil {
glog.Error("error in input addrDesc in ", a, ": ", err)
return io
}
io = append(io, addrIndex{string(addrDesc), i})
}
return io
}
// Resync gets mempool transactions and maps outputs to transactions.
// Resync is not reentrant, it should be called from a single thread.
// Read operations (GetTransactions) are safe.
func (m *NonUTXOMempool) Resync(onNewTxAddr OnNewTxAddrFunc) (int, error) {
func (m *MempoolEthereumType) Resync(onNewTxAddr OnNewTxAddrFunc) (int, error) {
start := time.Now()
glog.V(1).Info("Mempool: resync")
txs, err := m.chain.GetMempool()
@ -62,13 +69,13 @@ func (m *NonUTXOMempool) Resync(onNewTxAddr OnNewTxAddrFunc) (int, error) {
parser := m.chain.GetChainParser()
// allocate slightly larger capacity of the maps
newTxToInputOutput := make(map[string][]addrIndex, len(m.txToInputOutput)+5)
newAddrDescToTx := make(map[string][]outpoint, len(m.addrDescToTx)+5)
newAddrDescToTx := make(map[string][]Outpoint, len(m.addrDescToTx)+5)
for _, txid := range txs {
io, exists := m.txToInputOutput[txid]
if !exists {
tx, err := m.chain.GetTransactionForMempool(txid)
if err != nil {
glog.Error("cannot get transaction ", txid, ": ", err)
glog.Warning("cannot get transaction ", txid, ": ", err)
continue
}
io = make([]addrIndex, 0, len(tx.Vout)+len(tx.Vin))
@ -83,29 +90,34 @@ func (m *NonUTXOMempool) Resync(onNewTxAddr OnNewTxAddrFunc) (int, error) {
if len(addrDesc) > 0 {
io = append(io, addrIndex{string(addrDesc), int32(output.N)})
}
if onNewTxAddr != nil {
onNewTxAddr(tx.Txid, addrDesc, true)
}
}
for _, input := range tx.Vin {
for i, a := range input.Addresses {
if len(a) > 0 {
addrDesc, err := parser.GetAddrDescFromAddress(a)
if err != nil {
glog.Error("error in input addrDesc in ", txid, " ", a, ": ", err)
continue
}
io = append(io, addrIndex{string(addrDesc), int32(^i)})
if onNewTxAddr != nil {
onNewTxAddr(tx.Txid, addrDesc, false)
}
appendAddress(io, ^int32(i), a, parser)
}
}
t, err := parser.EthereumTypeGetErc20FromTx(tx)
if err != nil {
glog.Error("GetErc20FromTx for tx ", txid, ", ", err)
} else {
for i := range t {
io = appendAddress(io, ^int32(i+1), t[i].From, parser)
io = appendAddress(io, int32(i+1), t[i].To, parser)
}
}
if onNewTxAddr != nil {
sent := make(map[string]struct{})
for _, si := range io {
if _, found := sent[si.addrDesc]; !found {
onNewTxAddr(tx, AddressDescriptor(si.addrDesc))
sent[si.addrDesc] = struct{}{}
}
}
}
}
newTxToInputOutput[txid] = io
for _, si := range io {
newAddrDescToTx[si.addrDesc] = append(newAddrDescToTx[si.addrDesc], outpoint{txid, si.n})
newAddrDescToTx[si.addrDesc] = append(newAddrDescToTx[si.addrDesc], Outpoint{txid, si.n})
}
}
m.updateMappings(newTxToInputOutput, newAddrDescToTx)

View File

@ -35,6 +35,7 @@ type ProtoTransaction struct {
Height uint32 `protobuf:"varint,5,opt,name=Height" json:"Height,omitempty"`
Vin []*ProtoTransaction_VinType `protobuf:"bytes,6,rep,name=Vin" json:"Vin,omitempty"`
Vout []*ProtoTransaction_VoutType `protobuf:"bytes,7,rep,name=Vout" json:"Vout,omitempty"`
Version int32 `protobuf:"varint,8,opt,name=Version" json:"Version,omitempty"`
}
func (m *ProtoTransaction) Reset() { *m = ProtoTransaction{} }
@ -91,6 +92,13 @@ func (m *ProtoTransaction) GetVout() []*ProtoTransaction_VoutType {
return nil
}
func (m *ProtoTransaction) GetVersion() int32 {
if m != nil {
return m.Version
}
return 0
}
type ProtoTransaction_VinType struct {
Coinbase string `protobuf:"bytes,1,opt,name=Coinbase" json:"Coinbase,omitempty"`
Txid []byte `protobuf:"bytes,2,opt,name=Txid,proto3" json:"Txid,omitempty"`
@ -196,26 +204,27 @@ func init() {
func init() { proto.RegisterFile("tx.proto", fileDescriptor0) }
var fileDescriptor0 = []byte{
// 331 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x74, 0x52, 0xc1, 0x6e, 0xea, 0x30,
0x10, 0x94, 0x89, 0x5f, 0x80, 0x7d, 0xa0, 0x87, 0xf6, 0xf0, 0x14, 0xa1, 0x1e, 0x52, 0x4e, 0x39,
0xe5, 0x40, 0xd5, 0x0f, 0x68, 0x7b, 0x41, 0x6a, 0x85, 0x90, 0x83, 0x72, 0x4f, 0x82, 0x05, 0x56,
0xa9, 0x4d, 0x13, 0x47, 0x02, 0xa9, 0x3f, 0xd3, 0x73, 0x7f, 0xb2, 0xf2, 0x12, 0x42, 0x41, 0xea,
0x6d, 0x67, 0xbc, 0xe3, 0x19, 0x4f, 0x02, 0x3d, 0xbb, 0x8f, 0x77, 0xa5, 0xb1, 0x06, 0xfd, 0xbc,
0xd8, 0x64, 0x4a, 0x4f, 0x3e, 0x39, 0x8c, 0x16, 0x8e, 0x59, 0x96, 0x99, 0xae, 0xb2, 0xc2, 0x2a,
0xa3, 0x11, 0x81, 0x2f, 0xf7, 0x6a, 0x15, 0xb0, 0x90, 0x45, 0x03, 0x41, 0x33, 0x8e, 0xc0, 0x9b,
0xc9, 0x7d, 0xd0, 0x21, 0xca, 0x8d, 0x78, 0x03, 0xfd, 0xc7, 0xad, 0x29, 0x5e, 0xad, 0x7a, 0x93,
0x81, 0x17, 0xb2, 0x88, 0x8b, 0x33, 0x81, 0x63, 0xe8, 0xbd, 0x9c, 0x0e, 0x79, 0xc8, 0xa2, 0xa1,
0x68, 0x31, 0xfe, 0x07, 0x7f, 0x26, 0xd5, 0x7a, 0x63, 0x83, 0x3f, 0x74, 0xd2, 0x20, 0x9c, 0x82,
0x97, 0x2a, 0x1d, 0xf8, 0xa1, 0x17, 0xfd, 0x9d, 0x86, 0xf1, 0x31, 0x62, 0x7c, 0x1d, 0x2f, 0x4e,
0x95, 0x5e, 0x1e, 0x76, 0x52, 0xb8, 0x65, 0xbc, 0x07, 0x9e, 0x9a, 0xda, 0x06, 0x5d, 0x12, 0xdd,
0xfe, 0x2e, 0x32, 0xb5, 0x25, 0x15, 0xad, 0x8f, 0xbf, 0x18, 0x74, 0x9b, 0x7b, 0x5c, 0xd4, 0x27,
0xa3, 0x74, 0x9e, 0x55, 0x92, 0x9e, 0xdc, 0x17, 0x2d, 0x6e, 0xab, 0xe8, 0xfc, 0xa8, 0x02, 0x1b,
0x4b, 0x8f, 0xc2, 0xd3, 0x8c, 0x13, 0x18, 0x24, 0x45, 0xa9, 0x76, 0x36, 0x51, 0x6b, 0xd7, 0x13,
0xa7, 0xfd, 0x0b, 0xce, 0xf9, 0x24, 0xf2, 0xbd, 0x96, 0xba, 0x90, 0xcd, 0xc3, 0x5b, 0xec, 0xca,
0x7c, 0x58, 0xad, 0x4a, 0x59, 0x55, 0xb2, 0xa2, 0x02, 0xfa, 0xe2, 0x4c, 0x8c, 0x3f, 0xa0, 0x77,
0xca, 0xef, 0x6e, 0x49, 0xb3, 0x6d, 0x2d, 0x93, 0xcc, 0x36, 0x1f, 0xa8, 0xc5, 0x38, 0x00, 0x36,
0xa7, 0xa8, 0x43, 0xc1, 0xe6, 0x18, 0xc1, 0xbf, 0xa3, 0xff, 0xa2, 0xce, 0x9f, 0xe5, 0xc1, 0xc5,
0xf2, 0x48, 0x70, 0x4d, 0x5f, 0xba, 0xf3, 0x2b, 0xf7, 0xdc, 0xa7, 0x5f, 0xe6, 0xee, 0x3b, 0x00,
0x00, 0xff, 0xff, 0x95, 0x9e, 0x00, 0x7a, 0x3e, 0x02, 0x00, 0x00,
// 346 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x74, 0x52, 0x41, 0x6e, 0xea, 0x30,
0x14, 0x94, 0x89, 0x09, 0xe1, 0x7d, 0xd0, 0x47, 0x5e, 0x54, 0x16, 0xea, 0x22, 0x65, 0x95, 0x55,
0x16, 0x54, 0x3d, 0x40, 0xdb, 0x0d, 0x52, 0x2b, 0x84, 0x1c, 0x94, 0x7d, 0x12, 0x2c, 0xb0, 0x4a,
0x6d, 0x9a, 0x38, 0x12, 0x48, 0xbd, 0x51, 0x8f, 0xd0, 0xcb, 0x55, 0x7e, 0x84, 0x50, 0x90, 0xba,
0xf3, 0x8c, 0xdf, 0x64, 0xe6, 0x4d, 0x0c, 0x81, 0xdd, 0xc7, 0xbb, 0xd2, 0x58, 0xc3, 0xfc, 0xbc,
0xd8, 0x64, 0x4a, 0x4f, 0xbe, 0x29, 0x8c, 0x16, 0x8e, 0x59, 0x96, 0x99, 0xae, 0xb2, 0xc2, 0x2a,
0xa3, 0x19, 0x03, 0xba, 0xdc, 0xab, 0x15, 0x27, 0x21, 0x89, 0x06, 0x02, 0xcf, 0x6c, 0x04, 0xde,
0x4c, 0xee, 0x79, 0x07, 0x29, 0x77, 0x64, 0xb7, 0xd0, 0x7f, 0xda, 0x9a, 0xe2, 0xcd, 0xaa, 0x77,
0xc9, 0xbd, 0x90, 0x44, 0x54, 0x9c, 0x09, 0x36, 0x86, 0xe0, 0xf5, 0x74, 0x49, 0x43, 0x12, 0x0d,
0x45, 0x8b, 0xd9, 0x0d, 0xf8, 0x33, 0xa9, 0xd6, 0x1b, 0xcb, 0xbb, 0x78, 0xd3, 0x20, 0x36, 0x05,
0x2f, 0x55, 0x9a, 0xfb, 0xa1, 0x17, 0xfd, 0x9b, 0x86, 0xf1, 0x31, 0x62, 0x7c, 0x1d, 0x2f, 0x4e,
0x95, 0x5e, 0x1e, 0x76, 0x52, 0xb8, 0x61, 0xf6, 0x00, 0x34, 0x35, 0xb5, 0xe5, 0x3d, 0x14, 0xdd,
0xfd, 0x2d, 0x32, 0xb5, 0x45, 0x15, 0x8e, 0x33, 0x0e, 0xbd, 0x54, 0x96, 0x95, 0x32, 0x9a, 0x07,
0x21, 0x89, 0xba, 0xe2, 0x04, 0xc7, 0x5f, 0x04, 0x7a, 0x8d, 0x83, 0x5b, 0xe2, 0xd9, 0x28, 0x9d,
0x67, 0x95, 0xc4, 0x32, 0xfa, 0xa2, 0xc5, 0x6d, 0x49, 0x9d, 0x5f, 0x25, 0xb1, 0x26, 0x8c, 0x87,
0x6b, 0x1d, 0x9d, 0x26, 0x30, 0x48, 0x8a, 0x52, 0xed, 0x6c, 0xa2, 0xd6, 0xae, 0x41, 0x8a, 0xf3,
0x17, 0x9c, 0xf3, 0x49, 0xe4, 0x47, 0x2d, 0x75, 0x21, 0x9b, 0x4a, 0x5a, 0xec, 0x6a, 0x7e, 0x5c,
0xad, 0x4a, 0x59, 0x55, 0xb2, 0xc2, 0x6a, 0xfa, 0xe2, 0x4c, 0x8c, 0x3f, 0x21, 0x38, 0x6d, 0xe6,
0xbe, 0x92, 0x66, 0xdb, 0x5a, 0x26, 0x99, 0x6d, 0x7e, 0x5d, 0x8b, 0xd9, 0x00, 0xc8, 0x1c, 0xa3,
0x0e, 0x05, 0x99, 0xb3, 0x08, 0xfe, 0x1f, 0xfd, 0x17, 0x75, 0xfe, 0x22, 0x0f, 0x2e, 0x96, 0x87,
0x82, 0x6b, 0xfa, 0xd2, 0x9d, 0x5e, 0xb9, 0xe7, 0x3e, 0x3e, 0xa6, 0xfb, 0x9f, 0x00, 0x00, 0x00,
0xff, 0xff, 0xa1, 0x51, 0x2e, 0xba, 0x58, 0x02, 0x00, 0x00,
}

View File

@ -23,4 +23,5 @@ syntax = "proto3";
uint32 Height = 5;
repeated VinType Vin = 6;
repeated VoutType Vout = 7;
int32 Version = 8;
}

View File

@ -9,6 +9,16 @@ import (
"math/big"
)
// ChainType is type of the blockchain
type ChainType int
const (
// ChainBitcoinType is blockchain derived from bitcoin
ChainBitcoinType = ChainType(iota)
// ChainEthereumType is blockchain derived from ethereum
ChainEthereumType
)
// errors with specific meaning returned by blockchain rpc
var (
// ErrBlockNotFound is returned when block is not found
@ -23,11 +33,19 @@ var (
ErrTxidMissing = errors.New("Txid missing")
)
// Outpoint is txid together with output (or input) index
type Outpoint struct {
Txid string
Vout int32
}
// ScriptSig contains data about input script
type ScriptSig struct {
// Asm string `json:"asm"`
Hex string `json:"hex"`
}
// Vin contains data about tx output
type Vin struct {
Coinbase string `json:"coinbase"`
Txid string `json:"txid"`
@ -37,6 +55,7 @@ type Vin struct {
Addresses []string `json:"addresses"`
}
// ScriptPubKey contains data about output script
type ScriptPubKey struct {
// Asm string `json:"asm"`
Hex string `json:"hex,omitempty"`
@ -44,6 +63,7 @@ type ScriptPubKey struct {
Addresses []string `json:"addresses"`
}
// Vout contains data about tx output
type Vout struct {
ValueSat big.Int
JsonValue json.Number `json:"value"`
@ -61,11 +81,13 @@ type Tx struct {
Vin []Vin `json:"vin"`
Vout []Vout `json:"vout"`
// BlockHash string `json:"blockhash,omitempty"`
Confirmations uint32 `json:"confirmations,omitempty"`
Time int64 `json:"time,omitempty"`
Blocktime int64 `json:"blocktime,omitempty"`
Confirmations uint32 `json:"confirmations,omitempty"`
Time int64 `json:"time,omitempty"`
Blocktime int64 `json:"blocktime,omitempty"`
CoinSpecificData interface{} `json:"-"`
}
// Block is block header and list of transactions
type Block struct {
BlockHeader
Txs []Tx `json:"tx"`
@ -93,6 +115,7 @@ type BlockInfo struct {
Txids []string `json:"tx,omitempty"`
}
// MempoolEntry is used to get data about mempool entry
type MempoolEntry struct {
Size uint32 `json:"size"`
FeeSat big.Int
@ -110,6 +133,7 @@ type MempoolEntry struct {
Depends []string `json:"depends"`
}
// ChainInfo is used to get information about blockchain
type ChainInfo struct {
Chain string `json:"chain"`
Blocks int `json:"blocks"`
@ -124,6 +148,7 @@ type ChainInfo struct {
Warnings string `json:"warnings"`
}
// RPCError defines rpc error returned by backend
type RPCError struct {
Code int `json:"code"`
Message string `json:"message"`
@ -140,11 +165,29 @@ func (ad AddressDescriptor) String() string {
return "ad:" + hex.EncodeToString(ad)
}
// EthereumType specific
// Erc20Contract contains info about ERC20 contract
type Erc20Contract struct {
Contract string `json:"contract"`
Name string `json:"name"`
Symbol string `json:"symbol"`
Decimals int `json:"decimals"`
}
// Erc20Transfer contains a single ERC20 token transfer
type Erc20Transfer struct {
Contract string
From string
To string
Tokens big.Int
}
// OnNewBlockFunc is used to send notification about a new block
type OnNewBlockFunc func(hash string, height uint32)
// OnNewTxAddrFunc is used to send notification about a new transaction/address
type OnNewTxAddrFunc func(txid string, desc AddressDescriptor, isOutput bool)
type OnNewTxAddrFunc func(tx *Tx, desc AddressDescriptor)
// BlockChain defines common interface to block chain daemon
type BlockChain interface {
@ -167,29 +210,34 @@ type BlockChain interface {
GetMempool() ([]string, error)
GetTransaction(txid string) (*Tx, error)
GetTransactionForMempool(txid string) (*Tx, error)
GetTransactionSpecific(txid string) (json.RawMessage, error)
GetTransactionSpecific(tx *Tx) (json.RawMessage, error)
EstimateSmartFee(blocks int, conservative bool) (big.Int, error)
EstimateFee(blocks int) (big.Int, error)
SendRawTransaction(tx string) (string, error)
// mempool
ResyncMempool(onNewTxAddr OnNewTxAddrFunc) (int, error)
GetMempoolTransactions(address string) ([]string, error)
GetMempoolTransactionsForAddrDesc(addrDesc AddressDescriptor) ([]string, error)
GetMempoolTransactions(address string) ([]Outpoint, error)
GetMempoolTransactionsForAddrDesc(addrDesc AddressDescriptor) ([]Outpoint, error)
GetMempoolEntry(txid string) (*MempoolEntry, error)
// parser
GetChainParser() BlockChainParser
// 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)
}
// BlockChainParser defines common interface to parsing and conversions of block chain data
type BlockChainParser interface {
// chain configuration description
// UTXO chains need "inputs" column in db, that map transactions to transactions that spend them
// non UTXO chains have mapping of address to input and output transactions directly in "outputs" column in db
IsUTXOChain() bool
// KeepBlockAddresses returns number of blocks which are to be kept in blockaddresses column
// and used in case of fork
// if 0 the blockaddresses column is not used at all (usually non UTXO chains)
// type of the blockchain
GetChainType() ChainType
// KeepBlockAddresses returns number of blocks which are to be kept in blockTxs column
// to be used for rollbacks
KeepBlockAddresses() int
// AmountDecimals returns number of decimal places in coin amounts
AmountDecimals() int
// AmountToDecimalString converts amount in big.Int to string with decimal point in the correct place
AmountToDecimalString(a *big.Int) string
// AmountToBigInt converts amount in json.Number (string) to big.Int
@ -212,4 +260,6 @@ type BlockChainParser interface {
PackBlockHash(hash string) ([]byte, error)
UnpackBlockHash(buf []byte) (string, error)
ParseBlock(b []byte) (*Block, error)
// EthereumType specific
EthereumTypeGetErc20FromTx(tx *Tx) ([]Erc20Transfer, error)
}

View File

@ -498,17 +498,17 @@ func storeInternalStateLoop() {
glog.Info("storeInternalStateLoop stopped")
}
func onNewTxAddr(txid string, desc bchain.AddressDescriptor, isOutput bool) {
func onNewTxAddr(tx *bchain.Tx, desc bchain.AddressDescriptor) {
for _, c := range callbacksOnNewTxAddr {
c(txid, desc, isOutput)
c(tx, desc)
}
}
func pushSynchronizationHandler(nt bchain.NotificationType) {
glog.V(1).Info("MQ: notification ", nt)
if atomic.LoadInt32(&inShutdown) != 0 {
return
}
glog.V(1).Info("MQ: notification ", nt)
if nt == bchain.NotificationNewBlock {
chanSyncIndex <- struct{}{}
} else if nt == bchain.NotificationNewTx {
@ -545,7 +545,7 @@ func waitForSignalAndShutdown(internal *server.InternalServer, public *server.Pu
}
}
func printResult(txid string, vout uint32, isOutput bool) error {
func printResult(txid string, vout int32, isOutput bool) error {
glog.Info(txid, vout, isOutput)
return nil
}

View File

@ -150,9 +150,7 @@ func (is *InternalState) GetDBColumnStatValues(c int) (int64, int64, int64) {
func (is *InternalState) GetAllDBColumnStats() []InternalStateColumn {
is.mux.Lock()
defer is.mux.Unlock()
rv := make([]InternalStateColumn, len(is.DbColumns))
copy(rv, is.DbColumns)
return rv
return append(is.DbColumns[:0:0], is.DbColumns...)
}
// DBSizeTotal sums the computed sizes of all columns

View File

@ -11,6 +11,10 @@ type Metrics struct {
SocketIOSubscribes *prometheus.CounterVec
SocketIOClients prometheus.Gauge
SocketIOReqDuration *prometheus.HistogramVec
WebsocketRequests *prometheus.CounterVec
WebsocketSubscribes *prometheus.CounterVec
WebsocketClients prometheus.Gauge
WebsocketReqDuration *prometheus.HistogramVec
IndexResyncDuration prometheus.Histogram
MempoolResyncDuration prometheus.Histogram
TxCacheEfficiency *prometheus.CounterVec
@ -48,7 +52,7 @@ func GetMetrics(coin string) (*Metrics, error) {
metrics.SocketIOClients = prometheus.NewGauge(
prometheus.GaugeOpts{
Name: "blockbook_socketio_clients",
Help: "Number of currently connected clients",
Help: "Number of currently connected socketio clients",
ConstLabels: Labels{"coin": coin},
},
)
@ -61,6 +65,38 @@ func GetMetrics(coin string) (*Metrics, error) {
},
[]string{"method"},
)
metrics.WebsocketRequests = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "blockbook_websocket_requests",
Help: "Total number of websocket requests by method and status",
ConstLabels: Labels{"coin": coin},
},
[]string{"method", "status"},
)
metrics.WebsocketSubscribes = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "blockbook_websocket_subscribes",
Help: "Total number of websocket subscribes by channel and status",
ConstLabels: Labels{"coin": coin},
},
[]string{"channel", "status"},
)
metrics.WebsocketClients = prometheus.NewGauge(
prometheus.GaugeOpts{
Name: "blockbook_websocket_clients",
Help: "Number of currently connected websocket clients",
ConstLabels: Labels{"coin": coin},
},
)
metrics.WebsocketReqDuration = prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Name: "blockbook_websocket_req_duration",
Help: "Websocket request duration by method (in microseconds)",
Buckets: []float64{1, 5, 10, 25, 50, 75, 100, 250},
ConstLabels: Labels{"coin": coin},
},
[]string{"method"},
)
metrics.IndexResyncDuration = prometheus.NewHistogram(
prometheus.HistogramOpts{
Name: "blockbook_index_resync_duration",

View File

@ -40,7 +40,7 @@
"system_user": "blockbook-ethereum-classic",
"internal_binding_template": ":{{.Ports.BlockbookInternal}}",
"public_binding_template": ":{{.Ports.BlockbookPublic}}",
"explorer_url": "https://gastracker.io/",
"explorer_url": "",
"additional_params": "-resyncindexperiod=4441",
"block_chain": {
"parse": true,

View File

@ -42,7 +42,7 @@
"system_user": "blockbook-ethereum",
"internal_binding_template": ":{{.Ports.BlockbookInternal}}",
"public_binding_template": ":{{.Ports.BlockbookPublic}}",
"explorer_url": "https://etherscan.io/",
"explorer_url": "",
"additional_params": "",
"block_chain": {
"parse": true,

View File

@ -1,8 +1,8 @@
{
"coin": {
"name": "Ethereum Testnet Ropsten",
"shortcut": "tETH",
"label": "Ethereum Testnet Ropsten",
"shortcut": "tROP",
"label": "Ethereum Ropsten",
"alias": "ethereum_testnet_ropsten"
},
"ports": {
@ -41,7 +41,7 @@
"system_user": "blockbook-ethereum",
"internal_binding_template": ":{{.Ports.BlockbookInternal}}",
"public_binding_template": ":{{.Ports.BlockbookPublic}}",
"explorer_url": "https://ropsten.etherscan.io/",
"explorer_url": "",
"additional_params": "",
"block_chain": {
"parse": true,

68
configs/coins/liquid.json Normal file
View File

@ -0,0 +1,68 @@
{
"coin": {
"name": "Liquid",
"shortcut": "L-BTC",
"label": "Liquid",
"alias": "liquid"
},
"ports": {
"backend_rpc": 8046,
"backend_message_queue": 38346,
"blockbook_internal": 9046,
"blockbook_public": 9146
},
"ipc": {
"rpc_url_template": "http://127.0.0.1:{{.Ports.BackendRPC}}",
"rpc_user": "rpc",
"rpc_pass": "rpc",
"rpc_timeout": 25,
"message_queue_binding_template": "tcp://127.0.0.1:{{.Ports.BackendMessageQueue}}"
},
"backend": {
"package_name": "backend-liquid",
"package_revision": "satoshilabs-1",
"system_user": "liquid",
"version": "3.14.1.21",
"binary_url": "https://github.com/Blockstream/liquid/releases/download/liquid.3.14.1.21/liquid-3.14.1.21-x86_64-linux-gnu.tar.gz",
"verification_type": "sha256",
"verification_source": "ea2836aa267b32b29e890acdd5e724b4be225c34891fd26426ce741c12c1e166",
"extract_command": "tar -C backend --strip 1 -xf",
"exclude_files": [
"bin/test_elements"
],
"exec_command_template": "{{.Env.BackendInstallPath}}/{{.Coin.Alias}}/bin/liquidd -datadir={{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend -conf={{.Env.BackendInstallPath}}/{{.Coin.Alias}}/{{.Coin.Alias}}.conf -pid=/run/{{.Coin.Alias}}/{{.Coin.Alias}}.pid",
"logrotate_files_template": "{{.Env.BackendDataPath}}/{{.Coin.Alias}}/backend/liquidv1/*.log",
"postinst_script_template": "",
"service_type": "forking",
"service_additional_params_template": "",
"protect_memory": true,
"mainnet": true,
"server_config_file": "bitcoin_like.conf",
"client_config_file": "bitcoin_like_client.conf",
"additional_params": {
"deprecatedrpc": "estimatefee",
"mainchainrpcport": "8030",
"mainchainrpcuser": "rpc",
"mainchainrpcpassword": "rpc"
}
},
"blockbook": {
"package_name": "blockbook-liquid",
"system_user": "blockbook-liquid",
"internal_binding_template": ":{{.Ports.BlockbookInternal}}",
"public_binding_template": ":{{.Ports.BlockbookPublic}}",
"explorer_url": "",
"additional_params": "",
"block_chain": {
"parse": true,
"mempool_workers": 8,
"mempool_sub_workers": 2,
"block_addresses_to_keep": 300,
"additional_params": {}
}
},
"meta": {
"package_maintainer": "Martin Bohm",
"package_maintainer_email": "martin.bohm@satoshilabs.com"
}
}

View File

@ -22,7 +22,7 @@ type bulkAddresses struct {
// BulkConnect is used to connect blocks in bulk, faster but if interrupted inconsistent way
type BulkConnect struct {
d *RocksDB
isUTXO bool
chainType bchain.ChainType
bulkAddresses []bulkAddresses
bulkAddressesCount int
txAddressesMap map[string]*TxAddresses
@ -42,7 +42,7 @@ const (
func (d *RocksDB) InitBulkConnect() (*BulkConnect, error) {
bc := &BulkConnect{
d: d,
isUTXO: d.chainParser.IsUTXOChain(),
chainType: d.chainParser.GetChainType(),
txAddressesMap: make(map[string]*TxAddresses),
balances: make(map[string]*AddrBalance),
}
@ -168,11 +168,12 @@ func (b *BulkConnect) storeBulkAddresses(wb *gorocksdb.WriteBatch) error {
// ConnectBlock connects block in bulk mode
func (b *BulkConnect) ConnectBlock(block *bchain.Block, storeBlockTxs bool) error {
b.height = block.Height
if !b.isUTXO {
// for non bitcoin types connect blocks in non bulk mode
if b.chainType != bchain.ChainBitcoinType {
return b.d.ConnectBlock(block)
}
addresses := make(map[string][]outpoint)
if err := b.d.processAddressesUTXO(block, addresses, b.txAddressesMap, b.balances); err != nil {
if err := b.d.processAddressesBitcoinType(block, addresses, b.txAddressesMap, b.balances); err != nil {
return err
}
var storeAddressesChan, storeBalancesChan chan error

View File

@ -59,13 +59,21 @@ const (
cfDefault = iota
cfHeight
cfAddresses
cfTxAddresses
cfAddressBalance
cfBlockTxs
cfTransactions
// BitcoinType
cfAddressBalance
cfTxAddresses
// EthereumType
cfAddressContracts = cfAddressBalance
)
var cfNames = []string{"default", "height", "addresses", "txAddresses", "addressBalance", "blockTxs", "transactions"}
// common columns
var cfNames = []string{"default", "height", "addresses", "blockTxs", "transactions"}
// type specific columns
var cfNamesBitcoinType = []string{"addressBalance", "txAddresses"}
var cfNamesEthereumType = []string{"addressContracts"}
func openDB(path string, c *gorocksdb.Cache, openFiles int) (*gorocksdb.DB, []*gorocksdb.ColumnFamilyHandle, error) {
// opts with bloom filter
@ -73,9 +81,14 @@ func openDB(path string, c *gorocksdb.Cache, openFiles int) (*gorocksdb.DB, []*g
// opts for addresses without bloom filter
// from documentation: if most of your queries are executed using iterators, you shouldn't set bloom filter
optsAddresses := createAndSetDBOptions(0, c, openFiles)
// default, height, addresses, txAddresses, addressBalance, blockTxids, transactions
fcOptions := []*gorocksdb.Options{opts, opts, optsAddresses, opts, opts, opts, opts}
db, cfh, err := gorocksdb.OpenDbColumnFamilies(opts, path, cfNames, fcOptions)
// default, height, addresses, blockTxids, transactions
cfOptions := []*gorocksdb.Options{opts, opts, optsAddresses, opts, opts}
// append type specific options
count := len(cfNames) - len(cfOptions)
for i := 0; i < count; i++ {
cfOptions = append(cfOptions, opts)
}
db, cfh, err := gorocksdb.OpenDbColumnFamilies(opts, path, cfNames, cfOptions)
if err != nil {
return nil, nil, err
}
@ -86,6 +99,16 @@ func openDB(path string, c *gorocksdb.Cache, openFiles int) (*gorocksdb.DB, []*g
// needs to be called to release it.
func NewRocksDB(path string, cacheSize, maxOpenFiles int, parser bchain.BlockChainParser, metrics *common.Metrics) (d *RocksDB, err error) {
glog.Infof("rocksdb: opening %s, required data version %v, cache size %v, max open files %v", path, dbVersion, cacheSize, maxOpenFiles)
chainType := parser.GetChainType()
if chainType == bchain.ChainBitcoinType {
cfNames = append(cfNames, cfNamesBitcoinType...)
} else if chainType == bchain.ChainEthereumType {
cfNames = append(cfNames, cfNamesEthereumType...)
} else {
return nil, errors.New("Unknown chain type")
}
c := gorocksdb.NewLRUCache(cacheSize)
db, cfh, err := openDB(path, c, maxOpenFiles)
if err != nil {
@ -185,7 +208,7 @@ func (e *StopIteration) Error() string {
// GetTransactions finds all input/output transactions for address
// Transaction are passed to callback function.
func (d *RocksDB) GetTransactions(address string, lower uint32, higher uint32, fn func(txid string, vout uint32, isOutput bool) error) (err error) {
func (d *RocksDB) GetTransactions(address string, lower uint32, higher uint32, fn func(txid string, vout int32, isOutput bool) error) (err error) {
if glog.V(1) {
glog.Infof("rocksdb: address get %s %d-%d ", address, lower, higher)
}
@ -198,7 +221,7 @@ func (d *RocksDB) GetTransactions(address string, lower uint32, higher uint32, f
// GetAddrDescTransactions finds all input/output transactions for address descriptor
// Transaction are passed to callback function.
func (d *RocksDB) GetAddrDescTransactions(addrDesc bchain.AddressDescriptor, lower uint32, higher uint32, fn func(txid string, vout uint32, isOutput bool) error) (err error) {
func (d *RocksDB) GetAddrDescTransactions(addrDesc bchain.AddressDescriptor, lower uint32, higher uint32, fn func(txid string, vout int32, isOutput bool) error) (err error) {
kstart := packAddressKey(addrDesc, lower)
kstop := packAddressKey(addrDesc, higher)
@ -219,13 +242,13 @@ func (d *RocksDB) GetAddrDescTransactions(addrDesc bchain.AddressDescriptor, low
glog.Infof("rocksdb: output %s: %s", hex.EncodeToString(key), hex.EncodeToString(val))
}
for _, o := range outpoints {
var vout uint32
var vout int32
var isOutput bool
if o.index < 0 {
vout = uint32(^o.index)
vout = int32(^o.index)
isOutput = false
} else {
vout = uint32(o.index)
vout = int32(o.index)
isOutput = true
}
tx, err := d.chainParser.UnpackTxid(o.btxID)
@ -250,45 +273,23 @@ const (
// ConnectBlock indexes addresses in the block and stores them in db
func (d *RocksDB) ConnectBlock(block *bchain.Block) error {
return d.writeBlock(block, opInsert)
}
// DisconnectBlock removes addresses in the block from the db
func (d *RocksDB) DisconnectBlock(block *bchain.Block) error {
return d.writeBlock(block, opDelete)
}
func (d *RocksDB) writeBlock(block *bchain.Block, op int) error {
wb := gorocksdb.NewWriteBatch()
defer wb.Destroy()
if glog.V(2) {
switch op {
case opInsert:
glog.Infof("rocksdb: insert %d %s", block.Height, block.Hash)
case opDelete:
glog.Infof("rocksdb: delete %d %s", block.Height, block.Hash)
}
glog.Infof("rocksdb: insert %d %s", block.Height, block.Hash)
}
isUTXO := d.chainParser.IsUTXOChain()
chainType := d.chainParser.GetChainType()
if err := d.writeHeightFromBlock(wb, block, op); err != nil {
if err := d.writeHeightFromBlock(wb, block, opInsert); err != nil {
return err
}
if isUTXO {
if op == opDelete {
// block does not contain mapping tx-> input address, which is necessary to recreate
// unspentTxs; therefore it is not possible to DisconnectBlocks this way
return errors.New("DisconnectBlock is not supported for UTXO chains")
}
addresses := make(map[string][]outpoint)
addresses := make(map[string][]outpoint)
if chainType == bchain.ChainBitcoinType {
txAddressesMap := make(map[string]*TxAddresses)
balances := make(map[string]*AddrBalance)
if err := d.processAddressesUTXO(block, addresses, txAddressesMap, balances); err != nil {
return err
}
if err := d.storeAddresses(wb, block.Height, addresses); err != nil {
if err := d.processAddressesBitcoinType(block, addresses, txAddressesMap, balances); err != nil {
return err
}
if err := d.storeTxAddresses(wb, txAddressesMap); err != nil {
@ -300,10 +301,23 @@ func (d *RocksDB) writeBlock(block *bchain.Block, op int) error {
if err := d.storeAndCleanupBlockTxs(wb, block); err != nil {
return err
}
} else {
if err := d.writeAddressesNonUTXO(wb, block, op); err != nil {
} else if chainType == bchain.ChainEthereumType {
addressContracts := make(map[string]*AddrContracts)
blockTxs, err := d.processAddressesEthereumType(block, addresses, addressContracts)
if err != nil {
return err
}
if err := d.storeAddressContracts(wb, addressContracts); err != nil {
return err
}
if err := d.storeAndCleanupBlockTxsEthereumType(wb, block, blockTxs); err != nil {
return err
}
} else {
return errors.New("Unknown chain type")
}
if err := d.storeAddresses(wb, block.Height, addresses); err != nil {
return err
}
return d.db.Write(d.wo, wb)
@ -375,7 +389,7 @@ func (d *RocksDB) GetAndResetConnectBlockStats() string {
return s
}
func (d *RocksDB) processAddressesUTXO(block *bchain.Block, addresses map[string][]outpoint, txAddressesMap map[string]*TxAddresses, balances map[string]*AddrBalance) error {
func (d *RocksDB) processAddressesBitcoinType(block *bchain.Block, addresses map[string][]outpoint, txAddressesMap map[string]*TxAddresses, balances map[string]*AddrBalance) error {
blockTxIDs := make([][]byte, len(block.Txs))
blockTxAddresses := make([]*TxAddresses, len(block.Txs))
// first process all outputs so that inputs can point to txs in this block
@ -577,6 +591,27 @@ func (d *RocksDB) storeBalances(wb *gorocksdb.WriteBatch, abm map[string]*AddrBa
return nil
}
func (d *RocksDB) cleanupBlockTxs(wb *gorocksdb.WriteBatch, block *bchain.Block) error {
keep := d.chainParser.KeepBlockAddresses()
// cleanup old block address
if block.Height > uint32(keep) {
for rh := block.Height - uint32(keep); rh > 0; rh-- {
key := packUint(rh)
val, err := d.db.GetCF(d.ro, d.cfh[cfBlockTxs], key)
if err != nil {
return err
}
// nil data means the key was not found in DB
if val.Data() == nil {
break
}
val.Free()
d.db.DeleteCF(d.wo, d.cfh[cfBlockTxs], key)
}
}
return nil
}
func (d *RocksDB) storeAndCleanupBlockTxs(wb *gorocksdb.WriteBatch, block *bchain.Block) error {
pl := d.chainParser.PackedTxidLen()
buf := make([]byte, 0, pl*len(block.Txs))
@ -610,23 +645,7 @@ func (d *RocksDB) storeAndCleanupBlockTxs(wb *gorocksdb.WriteBatch, block *bchai
}
key := packUint(block.Height)
wb.PutCF(d.cfh[cfBlockTxs], key, buf)
keep := d.chainParser.KeepBlockAddresses()
// cleanup old block address
if block.Height > uint32(keep) {
for rh := block.Height - uint32(keep); rh < block.Height; rh-- {
key = packUint(rh)
val, err := d.db.GetCF(d.ro, d.cfh[cfBlockTxs], key)
if err != nil {
return err
}
if val.Size() == 0 {
break
}
val.Free()
d.db.DeleteCF(d.wo, d.cfh[cfBlockTxs], key)
}
}
return nil
return d.cleanupBlockTxs(wb, block)
}
func (d *RocksDB) getBlockTxs(height uint32) ([]blockTxs, error) {
@ -643,8 +662,7 @@ func (d *RocksDB) getBlockTxs(height uint32) ([]blockTxs, error) {
glog.Error("rocksdb: Inconsistent data in blockTxs ", hex.EncodeToString(buf))
return nil, errors.New("Inconsistent data in blockTxs")
}
txid := make([]byte, pl)
copy(txid, buf[i:])
txid := append([]byte(nil), buf[i:i+pl]...)
i += pl
o, ol, err := d.unpackNOutpoints(buf[i:])
if err != nil {
@ -775,8 +793,7 @@ func unpackTxAddresses(buf []byte) (*TxAddresses, error) {
func unpackTxInput(ti *TxInput, buf []byte) int {
al, l := unpackVaruint(buf)
ti.AddrDesc = make([]byte, al)
copy(ti.AddrDesc, buf[l:l+int(al)])
ti.AddrDesc = append([]byte(nil), buf[l:l+int(al)]...)
al += uint(l)
ti.ValueSat, l = unpackBigint(buf[al:])
return l + int(al)
@ -788,8 +805,7 @@ func unpackTxOutput(to *TxOutput, buf []byte) int {
to.Spent = true
al = ^al
}
to.AddrDesc = make([]byte, al)
copy(to.AddrDesc, buf[l:l+al])
to.AddrDesc = append([]byte(nil), buf[l:l+al]...)
al += l
to.ValueSat, l = unpackBigint(buf[al:])
return l + al
@ -842,74 +858,6 @@ func (d *RocksDB) unpackNOutpoints(buf []byte) ([]outpoint, int, error) {
return outpoints, p, nil
}
func (d *RocksDB) addAddrDescToRecords(op int, wb *gorocksdb.WriteBatch, records map[string][]outpoint, addrDesc bchain.AddressDescriptor, btxid []byte, vout int32, bh uint32) error {
if len(addrDesc) > 0 {
if len(addrDesc) > maxAddrDescLen {
glog.Infof("rocksdb: block %d, skipping addrDesc of length %d", bh, len(addrDesc))
} else {
strAddrDesc := string(addrDesc)
records[strAddrDesc] = append(records[strAddrDesc], outpoint{
btxID: btxid,
index: vout,
})
if op == opDelete {
// remove transactions from cache
d.internalDeleteTx(wb, btxid)
}
}
}
return nil
}
func (d *RocksDB) writeAddressesNonUTXO(wb *gorocksdb.WriteBatch, block *bchain.Block, op int) error {
addresses := make(map[string][]outpoint)
for _, tx := range block.Txs {
btxID, err := d.chainParser.PackTxid(tx.Txid)
if err != nil {
return err
}
for _, output := range tx.Vout {
addrDesc, err := d.chainParser.GetAddrDescFromVout(&output)
if err != nil {
// do not log ErrAddressMissing, transactions can be without to address (for example eth contracts)
if err != bchain.ErrAddressMissing {
glog.Warningf("rocksdb: addrDesc: %v - height %d, tx %v, output %v", err, block.Height, tx.Txid, output)
}
continue
}
err = d.addAddrDescToRecords(op, wb, addresses, addrDesc, btxID, int32(output.N), block.Height)
if err != nil {
return err
}
}
// store inputs in format txid ^index
for _, input := range tx.Vin {
for i, a := range input.Addresses {
addrDesc, err := d.chainParser.GetAddrDescFromAddress(a)
if err != nil {
glog.Warningf("rocksdb: addrDesc: %v - %d %s", err, block.Height, addrDesc)
continue
}
err = d.addAddrDescToRecords(op, wb, addresses, addrDesc, btxID, int32(^i), block.Height)
if err != nil {
return err
}
}
}
}
for addrDesc, outpoints := range addresses {
key := packAddressKey(bchain.AddressDescriptor(addrDesc), block.Height)
switch op {
case opInsert:
val := d.packOutpoints(outpoints)
wb.PutCF(d.cfh[cfAddresses], key, val)
case opDelete:
wb.DeleteCF(d.cfh[cfAddresses], key)
}
}
return nil
}
// Block index
// BlockInfo holds information about blocks kept in column height
@ -1035,51 +983,6 @@ func (d *RocksDB) writeHeight(wb *gorocksdb.WriteBatch, height uint32, bi *Block
// Disconnect blocks
func (d *RocksDB) allAddressesScan(lower uint32, higher uint32) ([][]byte, [][]byte, error) {
glog.Infof("db: doing full scan of addresses column")
addrKeys := [][]byte{}
addrValues := [][]byte{}
var totalOutputs, count uint64
var seekKey []byte
for {
var key []byte
it := d.db.NewIteratorCF(d.ro, d.cfh[cfAddresses])
if totalOutputs == 0 {
it.SeekToFirst()
} else {
it.Seek(seekKey)
it.Next()
}
for count = 0; it.Valid() && count < refreshIterator; it.Next() {
totalOutputs++
count++
key = it.Key().Data()
l := len(key)
if l > packedHeightBytes {
height := unpackUint(key[l-packedHeightBytes : l])
if height >= lower && height <= higher {
addrKey := make([]byte, len(key))
copy(addrKey, key)
addrKeys = append(addrKeys, addrKey)
value := it.Value().Data()
addrValue := make([]byte, len(value))
copy(addrValue, value)
addrValues = append(addrValues, addrValue)
}
}
}
seekKey = make([]byte, len(key))
copy(seekKey, key)
valid := it.Valid()
it.Close()
if !valid {
break
}
}
glog.Infof("rocksdb: scanned %d addresses, found %d to disconnect", totalOutputs, len(addrKeys))
return addrKeys, addrValues, nil
}
func (d *RocksDB) disconnectTxAddresses(wb *gorocksdb.WriteBatch, height uint32, txid string, inputs []outpoint, txa *TxAddresses,
txAddressesToUpdate map[string]*TxAddresses, balances map[string]*AddrBalance) error {
addresses := make(map[string]struct{})
@ -1166,10 +1069,9 @@ func (d *RocksDB) disconnectTxAddresses(wb *gorocksdb.WriteBatch, height uint32,
return nil
}
// DisconnectBlockRangeUTXO removes all data belonging to blocks in range lower-higher
// if they are in the range kept in the cfBlockTxids column
func (d *RocksDB) DisconnectBlockRangeUTXO(lower uint32, higher uint32) error {
glog.Infof("db: disconnecting blocks %d-%d", lower, higher)
// DisconnectBlockRangeBitcoinType removes all data belonging to blocks in range lower-higher
// it is able to disconnect only blocks for which there are data in the blockTxs column
func (d *RocksDB) DisconnectBlockRangeBitcoinType(lower uint32, higher uint32) error {
blocks := make([][]blockTxs, higher-lower+1)
for height := lower; height <= higher; height++ {
blockTxs, err := d.getBlockTxs(height)
@ -1227,37 +1129,6 @@ func (d *RocksDB) DisconnectBlockRangeUTXO(lower uint32, higher uint32) error {
return err
}
// DisconnectBlockRangeNonUTXO performs full range scan to remove a range of blocks
// it is very slow operation
func (d *RocksDB) DisconnectBlockRangeNonUTXO(lower uint32, higher uint32) error {
glog.Infof("db: disconnecting blocks %d-%d", lower, higher)
addrKeys, _, err := d.allAddressesScan(lower, higher)
if err != nil {
return err
}
glog.Infof("rocksdb: about to disconnect %d addresses ", len(addrKeys))
wb := gorocksdb.NewWriteBatch()
defer wb.Destroy()
for _, addrKey := range addrKeys {
if glog.V(2) {
glog.Info("address ", hex.EncodeToString(addrKey))
}
// delete address:height from the index
wb.DeleteCF(d.cfh[cfAddresses], addrKey)
}
for height := lower; height <= higher; height++ {
if glog.V(2) {
glog.Info("height ", height)
}
wb.DeleteCF(d.cfh[cfHeight], packUint(height))
}
err = d.db.Write(d.wo, wb)
if err == nil {
glog.Infof("rocksdb: blocks %d-%d disconnected", lower, higher)
}
return err
}
func dirSize(path string) (int64, error) {
var size int64
err := filepath.Walk(path, func(_ string, info os.FileInfo, err error) error {

409
db/rocksdb_ethereumtype.go Normal file
View File

@ -0,0 +1,409 @@
package db
import (
"blockbook/bchain"
"blockbook/bchain/coins/eth"
"bytes"
"encoding/hex"
"github.com/bsm/go-vlq"
"github.com/golang/glog"
"github.com/juju/errors"
"github.com/tecbot/gorocksdb"
)
// AddrContract is Contract address with number of transactions done by given address
type AddrContract struct {
Contract bchain.AddressDescriptor
Txs uint
}
// AddrContracts is array of contracts with
type AddrContracts struct {
EthTxs uint
Contracts []AddrContract
}
func (d *RocksDB) storeAddressContracts(wb *gorocksdb.WriteBatch, acm map[string]*AddrContracts) error {
buf := make([]byte, 64)
varBuf := make([]byte, vlq.MaxLen64)
for addrDesc, acs := range acm {
// address with 0 contracts is removed from db - happens on disconnect
if acs == nil || (acs.EthTxs == 0 && len(acs.Contracts) == 0) {
wb.DeleteCF(d.cfh[cfAddressContracts], bchain.AddressDescriptor(addrDesc))
} else {
buf = buf[:0]
l := packVaruint(acs.EthTxs, varBuf)
buf = append(buf, varBuf[:l]...)
for _, ac := range acs.Contracts {
buf = append(buf, ac.Contract...)
l = packVaruint(ac.Txs, varBuf)
buf = append(buf, varBuf[:l]...)
}
wb.PutCF(d.cfh[cfAddressContracts], bchain.AddressDescriptor(addrDesc), buf)
}
}
return nil
}
// GetAddrDescContracts returns AddrContracts for given addrDesc
func (d *RocksDB) GetAddrDescContracts(addrDesc bchain.AddressDescriptor) (*AddrContracts, error) {
val, err := d.db.GetCF(d.ro, d.cfh[cfAddressContracts], addrDesc)
if err != nil {
return nil, err
}
defer val.Free()
buf := val.Data()
if len(buf) == 0 {
return nil, nil
}
c := make([]AddrContract, 0, 4)
et, l := unpackVaruint(buf)
buf = buf[l:]
for len(buf) > 0 {
if len(buf) < eth.EthereumTypeAddressDescriptorLen {
return nil, errors.New("Invalid data stored in cfAddressContracts for AddrDesc " + addrDesc.String())
}
txs, l := unpackVaruint(buf[eth.EthereumTypeAddressDescriptorLen:])
contract := append(bchain.AddressDescriptor(nil), buf[:eth.EthereumTypeAddressDescriptorLen]...)
c = append(c, AddrContract{
Contract: contract,
Txs: txs,
})
buf = buf[eth.EthereumTypeAddressDescriptorLen+l:]
}
return &AddrContracts{EthTxs: et, Contracts: c}, nil
}
func findContractInAddressContracts(contract bchain.AddressDescriptor, contracts []AddrContract) (int, bool) {
for i := range contracts {
if bytes.Equal(contract, contracts[i].Contract) {
return i, true
}
}
return 0, false
}
func (d *RocksDB) addToAddressesAndContractsEthereumType(addrDesc bchain.AddressDescriptor, btxID []byte, index int32, contract bchain.AddressDescriptor, addresses map[string][]outpoint, addressContracts map[string]*AddrContracts) error {
var err error
strAddrDesc := string(addrDesc)
ac, e := addressContracts[strAddrDesc]
if !e {
ac, err = d.GetAddrDescContracts(addrDesc)
if err != nil {
return err
}
if ac == nil {
ac = &AddrContracts{}
}
addressContracts[strAddrDesc] = ac
d.cbs.balancesMiss++
} else {
d.cbs.balancesHit++
}
if contract == nil {
ac.EthTxs++
} else {
// locate the contract and set i to the index in the array of contracts
i, found := findContractInAddressContracts(contract, ac.Contracts)
if !found {
i = len(ac.Contracts)
ac.Contracts = append(ac.Contracts, AddrContract{Contract: contract})
}
// index 0 is for ETH transfers, contract indexes start with 1
if index < 0 {
index = ^int32(i + 1)
} else {
index = int32(i + 1)
}
ac.Contracts[i].Txs++
}
addresses[strAddrDesc] = append(addresses[strAddrDesc], outpoint{
btxID: btxID,
index: index,
})
return nil
}
type ethBlockTxContract struct {
addr, contract bchain.AddressDescriptor
}
type ethBlockTx struct {
btxID []byte
from, to bchain.AddressDescriptor
contracts []ethBlockTxContract
}
func (d *RocksDB) processAddressesEthereumType(block *bchain.Block, addresses map[string][]outpoint, addressContracts map[string]*AddrContracts) ([]ethBlockTx, error) {
blockTxs := make([]ethBlockTx, len(block.Txs))
for txi, tx := range block.Txs {
btxID, err := d.chainParser.PackTxid(tx.Txid)
if err != nil {
return nil, err
}
blockTx := &blockTxs[txi]
blockTx.btxID = btxID
// there is only one output address in EthereumType transaction, store it in format txid 0
if len(tx.Vout) == 1 && len(tx.Vout[0].ScriptPubKey.Addresses) == 1 {
addrDesc, err := d.chainParser.GetAddrDescFromAddress(tx.Vout[0].ScriptPubKey.Addresses[0])
if err != nil {
// do not log ErrAddressMissing, transactions can be without to address (for example eth contracts)
if err != bchain.ErrAddressMissing {
glog.Warningf("rocksdb: addrDesc: %v - height %d, tx %v, output", err, block.Height, tx.Txid)
}
continue
}
if err = d.addToAddressesAndContractsEthereumType(addrDesc, btxID, 0, nil, addresses, addressContracts); err != nil {
return nil, err
}
blockTx.to = addrDesc
}
// there is only one input address in EthereumType transaction, store it in format txid ^0
if len(tx.Vin) == 1 && len(tx.Vin[0].Addresses) == 1 {
addrDesc, err := d.chainParser.GetAddrDescFromAddress(tx.Vin[0].Addresses[0])
if err != nil {
if err != bchain.ErrAddressMissing {
glog.Warningf("rocksdb: addrDesc: %v - height %d, tx %v, input", err, block.Height, tx.Txid)
}
continue
}
if err = d.addToAddressesAndContractsEthereumType(addrDesc, btxID, ^int32(0), nil, addresses, addressContracts); err != nil {
return nil, err
}
blockTx.from = addrDesc
}
// store erc20 transfers
erc20, err := d.chainParser.EthereumTypeGetErc20FromTx(&tx)
if err != nil {
glog.Warningf("rocksdb: GetErc20FromTx %v - height %d, tx %v", err, block.Height, tx.Txid)
}
blockTx.contracts = make([]ethBlockTxContract, len(erc20)*2)
for i, t := range erc20 {
var contract, from, to bchain.AddressDescriptor
contract, err = d.chainParser.GetAddrDescFromAddress(t.Contract)
if err == nil {
from, err = d.chainParser.GetAddrDescFromAddress(t.From)
if err == nil {
to, err = d.chainParser.GetAddrDescFromAddress(t.To)
}
}
if err != nil {
glog.Warningf("rocksdb: GetErc20FromTx %v - height %d, tx %v, transfer %v", err, block.Height, tx.Txid, t)
continue
}
if err = d.addToAddressesAndContractsEthereumType(from, btxID, ^int32(i), contract, addresses, addressContracts); err != nil {
return nil, err
}
bc := &blockTx.contracts[i*2]
bc.addr = from
bc.contract = contract
if err = d.addToAddressesAndContractsEthereumType(to, btxID, int32(i), contract, addresses, addressContracts); err != nil {
return nil, err
}
bc = &blockTx.contracts[i*2+1]
bc.addr = to
bc.contract = contract
}
}
return blockTxs, nil
}
func (d *RocksDB) storeAndCleanupBlockTxsEthereumType(wb *gorocksdb.WriteBatch, block *bchain.Block, blockTxs []ethBlockTx) error {
pl := d.chainParser.PackedTxidLen()
buf := make([]byte, 0, (pl+2*eth.EthereumTypeAddressDescriptorLen)*len(blockTxs))
varBuf := make([]byte, vlq.MaxLen64)
zeroAddress := make([]byte, eth.EthereumTypeAddressDescriptorLen)
appendAddress := func(a bchain.AddressDescriptor) {
if len(a) != eth.EthereumTypeAddressDescriptorLen {
buf = append(buf, zeroAddress...)
} else {
buf = append(buf, a...)
}
}
for i := range blockTxs {
blockTx := &blockTxs[i]
buf = append(buf, blockTx.btxID...)
appendAddress(blockTx.from)
appendAddress(blockTx.to)
l := packVaruint(uint(len(blockTx.contracts)), varBuf)
buf = append(buf, varBuf[:l]...)
for j := range blockTx.contracts {
c := &blockTx.contracts[j]
appendAddress(c.addr)
appendAddress(c.contract)
}
}
key := packUint(block.Height)
wb.PutCF(d.cfh[cfBlockTxs], key, buf)
return d.cleanupBlockTxs(wb, block)
}
func (d *RocksDB) getBlockTxsEthereumType(height uint32) ([]ethBlockTx, error) {
pl := d.chainParser.PackedTxidLen()
val, err := d.db.GetCF(d.ro, d.cfh[cfBlockTxs], packUint(height))
if err != nil {
return nil, err
}
defer val.Free()
buf := val.Data()
// nil data means the key was not found in DB
if buf == nil {
return nil, nil
}
// buf can be empty slice, this means the block did not contain any transactions
bt := make([]ethBlockTx, 0, 8)
getAddress := func(i int) (bchain.AddressDescriptor, int, error) {
if len(buf)-i < eth.EthereumTypeAddressDescriptorLen {
glog.Error("rocksdb: Inconsistent data in blockTxs ", hex.EncodeToString(buf))
return nil, 0, errors.New("Inconsistent data in blockTxs")
}
a := append(bchain.AddressDescriptor(nil), buf[i:i+eth.EthereumTypeAddressDescriptorLen]...)
// return null addresses as nil
for _, b := range a {
if b != 0 {
return a, i + eth.EthereumTypeAddressDescriptorLen, nil
}
}
return nil, i + eth.EthereumTypeAddressDescriptorLen, nil
}
var from, to bchain.AddressDescriptor
for i := 0; i < len(buf); {
if len(buf)-i < pl {
glog.Error("rocksdb: Inconsistent data in blockTxs ", hex.EncodeToString(buf))
return nil, errors.New("Inconsistent data in blockTxs")
}
txid := append([]byte(nil), buf[i:i+pl]...)
i += pl
from, i, err = getAddress(i)
if err != nil {
return nil, err
}
to, i, err = getAddress(i)
if err != nil {
return nil, err
}
cc, l := unpackVaruint(buf[i:])
i += l
contracts := make([]ethBlockTxContract, cc)
for j := range contracts {
contracts[j].addr, i, err = getAddress(i)
if err != nil {
return nil, err
}
contracts[j].contract, i, err = getAddress(i)
if err != nil {
return nil, err
}
}
bt = append(bt, ethBlockTx{
btxID: txid,
from: from,
to: to,
contracts: contracts,
})
}
return bt, nil
}
func (d *RocksDB) disconnectBlockTxsEthereumType(wb *gorocksdb.WriteBatch, height uint32, blockTxs []ethBlockTx, contracts map[string]*AddrContracts) error {
glog.Info("Disconnecting block ", height, " containing ", len(blockTxs), " transactions")
addresses := make(map[string]struct{})
disconnectAddress := func(btxID []byte, addrDesc, contract bchain.AddressDescriptor) error {
var err error
// do not process empty address
if len(addrDesc) == 0 {
return nil
}
s := string(addrDesc)
addresses[s] = struct{}{}
c, fc := contracts[s]
if !fc {
c, err = d.GetAddrDescContracts(addrDesc)
if err != nil {
return err
}
contracts[s] = c
}
if c != nil {
if contract == nil {
if c.EthTxs > 0 {
c.EthTxs--
} else {
glog.Warning("AddressContracts ", addrDesc, ", EthTxs would be negative, tx ", hex.EncodeToString(btxID))
}
} else {
i, found := findContractInAddressContracts(contract, c.Contracts)
if found {
if c.Contracts[i].Txs > 0 {
c.Contracts[i].Txs--
if c.Contracts[i].Txs == 0 {
c.Contracts = append(c.Contracts[:i], c.Contracts[i+1:]...)
}
} else {
glog.Warning("AddressContracts ", addrDesc, ", contract ", i, " Txs would be negative, tx ", hex.EncodeToString(btxID))
}
} else {
glog.Warning("AddressContracts ", addrDesc, ", contract ", contract, " not found, tx ", hex.EncodeToString(btxID))
}
}
} else {
glog.Warning("AddressContracts ", addrDesc, " not found, tx ", hex.EncodeToString(btxID))
}
return nil
}
for i := range blockTxs {
blockTx := &blockTxs[i]
if err := disconnectAddress(blockTx.btxID, blockTx.from, nil); err != nil {
return err
}
if err := disconnectAddress(blockTx.btxID, blockTx.to, nil); err != nil {
return err
}
for _, c := range blockTx.contracts {
if err := disconnectAddress(blockTx.btxID, c.addr, c.contract); err != nil {
return err
}
}
wb.DeleteCF(d.cfh[cfTransactions], blockTx.btxID)
}
for a := range addresses {
key := packAddressKey([]byte(a), height)
wb.DeleteCF(d.cfh[cfAddresses], key)
}
return nil
}
// DisconnectBlockRangeEthereumType removes all data belonging to blocks in range lower-higher
// it is able to disconnect only blocks for which there are data in the blockTxs column
func (d *RocksDB) DisconnectBlockRangeEthereumType(lower uint32, higher uint32) error {
blocks := make([][]ethBlockTx, higher-lower+1)
for height := lower; height <= higher; height++ {
blockTxs, err := d.getBlockTxsEthereumType(height)
if err != nil {
return err
}
// nil blockTxs means blockTxs were not found in db
if blockTxs == nil {
return errors.Errorf("Cannot disconnect blocks with height %v and lower. It is necessary to rebuild index.", height)
}
blocks[height-lower] = blockTxs
}
wb := gorocksdb.NewWriteBatch()
defer wb.Destroy()
contracts := make(map[string]*AddrContracts)
for height := higher; height >= lower; height-- {
if err := d.disconnectBlockTxsEthereumType(wb, height, blocks[height-lower], contracts); err != nil {
return err
}
key := packUint(height)
wb.DeleteCF(d.cfh[cfBlockTxs], key)
wb.DeleteCF(d.cfh[cfHeight], key)
}
d.storeAddressContracts(wb, contracts)
err := d.db.Write(d.wo, wb)
if err == nil {
glog.Infof("rocksdb: blocks %d-%d disconnected", lower, higher)
}
return err
}

View File

@ -0,0 +1,286 @@
// build unittest
package db
import (
"blockbook/bchain/coins/eth"
"blockbook/tests/dbtestdata"
"encoding/hex"
"reflect"
"testing"
"github.com/juju/errors"
)
type testEthereumParser struct {
*eth.EthereumParser
}
func ethereumTestnetParser() *eth.EthereumParser {
return eth.NewEthereumParser(1)
}
func verifyAfterEthereumTypeBlock1(t *testing.T, d *RocksDB, afterDisconnect bool) {
if err := checkColumn(d, cfHeight, []keyPair{
keyPair{
"0041eee8",
"c7b98df95acfd11c51ba25611a39e004fe56c8fdfc1582af99354fcd09c17b11" + uintToHex(1534858022) + varuintToHex(2) + varuintToHex(31839),
nil,
},
}); err != nil {
{
t.Fatal(err)
}
}
// the vout is encoded as signed varint, i.e. value * 2 for positive values, abs(value)*2 + 1 for negative values
if err := checkColumn(d, cfAddresses, []keyPair{
keyPair{dbtestdata.AddressToPubKeyHex(dbtestdata.EthAddr3e, d.chainParser) + "0041eee8", dbtestdata.EthTxidB1T1 + "01", nil},
keyPair{dbtestdata.AddressToPubKeyHex(dbtestdata.EthAddr55, d.chainParser) + "0041eee8", dbtestdata.EthTxidB1T1 + "00" + dbtestdata.EthTxidB1T2 + "02", nil},
keyPair{dbtestdata.AddressToPubKeyHex(dbtestdata.EthAddr20, d.chainParser) + "0041eee8", dbtestdata.EthTxidB1T2 + "01" + dbtestdata.EthTxidB1T2 + "03", nil},
keyPair{dbtestdata.AddressToPubKeyHex(dbtestdata.EthAddrContract4a, d.chainParser) + "0041eee8", dbtestdata.EthTxidB1T2 + "00", nil},
}); err != nil {
{
t.Fatal(err)
}
}
if err := checkColumn(d, cfAddressContracts, []keyPair{
keyPair{dbtestdata.AddressToPubKeyHex(dbtestdata.EthAddr3e, d.chainParser), "01", nil},
keyPair{dbtestdata.AddressToPubKeyHex(dbtestdata.EthAddr55, d.chainParser), "01" + dbtestdata.AddressToPubKeyHex(dbtestdata.EthAddrContract4a, d.chainParser) + "01", nil},
keyPair{dbtestdata.AddressToPubKeyHex(dbtestdata.EthAddr20, d.chainParser), "01" + dbtestdata.AddressToPubKeyHex(dbtestdata.EthAddrContract4a, d.chainParser) + "01", nil},
keyPair{dbtestdata.AddressToPubKeyHex(dbtestdata.EthAddrContract4a, d.chainParser), "01", nil},
}); err != nil {
{
t.Fatal(err)
}
}
var blockTxsKp []keyPair
if afterDisconnect {
blockTxsKp = []keyPair{}
} else {
blockTxsKp = []keyPair{
keyPair{
"0041eee8",
dbtestdata.EthTxidB1T1 +
dbtestdata.AddressToPubKeyHex(dbtestdata.EthAddr3e, d.chainParser) + dbtestdata.AddressToPubKeyHex(dbtestdata.EthAddr55, d.chainParser) + "00" +
dbtestdata.EthTxidB1T2 +
dbtestdata.AddressToPubKeyHex(dbtestdata.EthAddr20, d.chainParser) + dbtestdata.AddressToPubKeyHex(dbtestdata.EthAddrContract4a, d.chainParser) +
"02" +
dbtestdata.AddressToPubKeyHex(dbtestdata.EthAddr20, d.chainParser) + dbtestdata.AddressToPubKeyHex(dbtestdata.EthAddrContract4a, d.chainParser) +
dbtestdata.AddressToPubKeyHex(dbtestdata.EthAddr55, d.chainParser) + dbtestdata.AddressToPubKeyHex(dbtestdata.EthAddrContract4a, d.chainParser),
nil,
},
}
}
if err := checkColumn(d, cfBlockTxs, blockTxsKp); err != nil {
{
t.Fatal(err)
}
}
}
func verifyAfterEthereumTypeBlock2(t *testing.T, d *RocksDB) {
if err := checkColumn(d, cfHeight, []keyPair{
keyPair{
"0041eee8",
"c7b98df95acfd11c51ba25611a39e004fe56c8fdfc1582af99354fcd09c17b11" + uintToHex(1534858022) + varuintToHex(2) + varuintToHex(31839),
nil,
},
keyPair{
"0041eee9",
"2b57e15e93a0ed197417a34c2498b7187df79099572c04a6b6e6ff418f74e6ee" + uintToHex(1534859988) + varuintToHex(2) + varuintToHex(2345678),
nil,
},
}); err != nil {
{
t.Fatal(err)
}
}
// the vout is encoded as signed varint, i.e. value * 2 for positive values, abs(value)*2 + 1 for negative values
if err := checkColumn(d, cfAddresses, []keyPair{
keyPair{dbtestdata.AddressToPubKeyHex(dbtestdata.EthAddr3e, d.chainParser) + "0041eee8", dbtestdata.EthTxidB1T1 + "01", nil},
keyPair{dbtestdata.AddressToPubKeyHex(dbtestdata.EthAddr55, d.chainParser) + "0041eee8", dbtestdata.EthTxidB1T1 + "00" + dbtestdata.EthTxidB1T2 + "02", nil},
keyPair{dbtestdata.AddressToPubKeyHex(dbtestdata.EthAddr20, d.chainParser) + "0041eee8", dbtestdata.EthTxidB1T2 + "01" + dbtestdata.EthTxidB1T2 + "03", nil},
keyPair{dbtestdata.AddressToPubKeyHex(dbtestdata.EthAddrContract4a, d.chainParser) + "0041eee8", dbtestdata.EthTxidB1T2 + "00", nil},
keyPair{dbtestdata.AddressToPubKeyHex(dbtestdata.EthAddr55, d.chainParser) + "0041eee9", dbtestdata.EthTxidB2T1 + "01" + dbtestdata.EthTxidB2T2 + "05" + dbtestdata.EthTxidB2T2 + "02", nil},
keyPair{dbtestdata.AddressToPubKeyHex(dbtestdata.EthAddr9f, d.chainParser) + "0041eee9", dbtestdata.EthTxidB2T1 + "00", nil},
keyPair{dbtestdata.AddressToPubKeyHex(dbtestdata.EthAddr4b, d.chainParser) + "0041eee9", dbtestdata.EthTxidB2T2 + "01" + dbtestdata.EthTxidB2T2 + "02" + dbtestdata.EthTxidB2T2 + "05" + dbtestdata.EthTxidB2T2 + "04" + dbtestdata.EthTxidB2T2 + "03", nil},
keyPair{dbtestdata.AddressToPubKeyHex(dbtestdata.EthAddr7b, d.chainParser) + "0041eee9", dbtestdata.EthTxidB2T2 + "03" + dbtestdata.EthTxidB2T2 + "04", nil},
keyPair{dbtestdata.AddressToPubKeyHex(dbtestdata.EthAddrContract47, d.chainParser) + "0041eee9", dbtestdata.EthTxidB2T2 + "00", nil},
}); err != nil {
{
t.Fatal(err)
}
}
if err := checkColumn(d, cfAddressContracts, []keyPair{
keyPair{dbtestdata.AddressToPubKeyHex(dbtestdata.EthAddr3e, d.chainParser), "01", nil},
keyPair{dbtestdata.AddressToPubKeyHex(dbtestdata.EthAddr55, d.chainParser), "02" + dbtestdata.AddressToPubKeyHex(dbtestdata.EthAddrContract4a, d.chainParser) + "02" + dbtestdata.AddressToPubKeyHex(dbtestdata.EthAddrContract0d, d.chainParser) + "01", nil},
keyPair{dbtestdata.AddressToPubKeyHex(dbtestdata.EthAddr20, d.chainParser), "01" + dbtestdata.AddressToPubKeyHex(dbtestdata.EthAddrContract4a, d.chainParser) + "01", nil},
keyPair{dbtestdata.AddressToPubKeyHex(dbtestdata.EthAddrContract4a, d.chainParser), "01", nil},
keyPair{dbtestdata.AddressToPubKeyHex(dbtestdata.EthAddr9f, d.chainParser), "01", nil},
keyPair{dbtestdata.AddressToPubKeyHex(dbtestdata.EthAddr4b, d.chainParser), "01" + dbtestdata.AddressToPubKeyHex(dbtestdata.EthAddrContract0d, d.chainParser) + "02" + dbtestdata.AddressToPubKeyHex(dbtestdata.EthAddrContract4a, d.chainParser) + "02", nil},
keyPair{dbtestdata.AddressToPubKeyHex(dbtestdata.EthAddr7b, d.chainParser), "00" + dbtestdata.AddressToPubKeyHex(dbtestdata.EthAddrContract4a, d.chainParser) + "01" + dbtestdata.AddressToPubKeyHex(dbtestdata.EthAddrContract0d, d.chainParser) + "01", nil},
keyPair{dbtestdata.AddressToPubKeyHex(dbtestdata.EthAddrContract47, d.chainParser), "01", nil},
}); err != nil {
{
t.Fatal(err)
}
}
if err := checkColumn(d, cfBlockTxs, []keyPair{
keyPair{
"0041eee9",
dbtestdata.EthTxidB2T1 +
dbtestdata.AddressToPubKeyHex(dbtestdata.EthAddr55, d.chainParser) + dbtestdata.AddressToPubKeyHex(dbtestdata.EthAddr9f, d.chainParser) + "00" +
dbtestdata.EthTxidB2T2 +
dbtestdata.AddressToPubKeyHex(dbtestdata.EthAddr4b, d.chainParser) + dbtestdata.AddressToPubKeyHex(dbtestdata.EthAddrContract47, d.chainParser) +
"08" +
dbtestdata.AddressToPubKeyHex(dbtestdata.EthAddr55, d.chainParser) + dbtestdata.AddressToPubKeyHex(dbtestdata.EthAddrContract0d, d.chainParser) +
dbtestdata.AddressToPubKeyHex(dbtestdata.EthAddr4b, d.chainParser) + dbtestdata.AddressToPubKeyHex(dbtestdata.EthAddrContract0d, d.chainParser) +
dbtestdata.AddressToPubKeyHex(dbtestdata.EthAddr4b, d.chainParser) + dbtestdata.AddressToPubKeyHex(dbtestdata.EthAddrContract4a, d.chainParser) +
dbtestdata.AddressToPubKeyHex(dbtestdata.EthAddr55, d.chainParser) + dbtestdata.AddressToPubKeyHex(dbtestdata.EthAddrContract4a, d.chainParser) +
dbtestdata.AddressToPubKeyHex(dbtestdata.EthAddr7b, d.chainParser) + dbtestdata.AddressToPubKeyHex(dbtestdata.EthAddrContract4a, d.chainParser) +
dbtestdata.AddressToPubKeyHex(dbtestdata.EthAddr4b, d.chainParser) + dbtestdata.AddressToPubKeyHex(dbtestdata.EthAddrContract4a, d.chainParser) +
dbtestdata.AddressToPubKeyHex(dbtestdata.EthAddr4b, d.chainParser) + dbtestdata.AddressToPubKeyHex(dbtestdata.EthAddrContract0d, d.chainParser) +
dbtestdata.AddressToPubKeyHex(dbtestdata.EthAddr7b, d.chainParser) + dbtestdata.AddressToPubKeyHex(dbtestdata.EthAddrContract0d, d.chainParser),
nil,
},
}); err != nil {
{
t.Fatal(err)
}
}
}
// TestRocksDB_Index_EthereumType is an integration test probing the whole indexing functionality for EthereumType chains
// It does the following:
// 1) Connect two blocks (inputs from 2nd block are spending some outputs from the 1st block)
// 2) GetTransactions for various addresses / low-high ranges
// 3) GetBestBlock, GetBlockHash
// 4) Test tx caching functionality
// 5) Disconnect the block 2 using BlockTxs column
// 6) Reconnect block 2 and check
// After each step, the content of DB is examined and any difference against expected state is regarded as failure
func TestRocksDB_Index_EthereumType(t *testing.T) {
d := setupRocksDB(t, &testEthereumParser{
EthereumParser: ethereumTestnetParser(),
})
defer closeAndDestroyRocksDB(t, d)
// connect 1st block
block1 := dbtestdata.GetTestEthereumTypeBlock1(d.chainParser)
if err := d.ConnectBlock(block1); err != nil {
t.Fatal(err)
}
verifyAfterEthereumTypeBlock1(t, d, false)
// connect 2nd block
block2 := dbtestdata.GetTestEthereumTypeBlock2(d.chainParser)
if err := d.ConnectBlock(block2); err != nil {
t.Fatal(err)
}
verifyAfterEthereumTypeBlock2(t, d)
// get transactions for various addresses / low-high ranges
verifyGetTransactions(t, d, "0x"+dbtestdata.EthAddr55, 0, 10000000, []txidVoutOutput{
txidVoutOutput{"0x" + dbtestdata.EthTxidB1T1, 0, true},
txidVoutOutput{"0x" + dbtestdata.EthTxidB1T2, 1, true},
txidVoutOutput{"0x" + dbtestdata.EthTxidB2T1, 0, false},
txidVoutOutput{"0x" + dbtestdata.EthTxidB2T2, 2, false},
txidVoutOutput{"0x" + dbtestdata.EthTxidB2T2, 1, true},
}, nil)
verifyGetTransactions(t, d, "mtGXQvBowMkBpnhLckhxhbwYK44Gs9eBad", 500000, 1000000, []txidVoutOutput{}, errors.New("Address missing"))
// GetBestBlock
height, hash, err := d.GetBestBlock()
if err != nil {
t.Fatal(err)
}
if height != 4321001 {
t.Fatalf("GetBestBlock: got height %v, expected %v", height, 4321001)
}
if hash != "0x2b57e15e93a0ed197417a34c2498b7187df79099572c04a6b6e6ff418f74e6ee" {
t.Fatalf("GetBestBlock: got hash %v, expected %v", hash, "0x2b57e15e93a0ed197417a34c2498b7187df79099572c04a6b6e6ff418f74e6ee")
}
// GetBlockHash
hash, err = d.GetBlockHash(4321000)
if err != nil {
t.Fatal(err)
}
if hash != "0xc7b98df95acfd11c51ba25611a39e004fe56c8fdfc1582af99354fcd09c17b11" {
t.Fatalf("GetBlockHash: got hash %v, expected %v", hash, "0xc7b98df95acfd11c51ba25611a39e004fe56c8fdfc1582af99354fcd09c17b11")
}
// Not connected block
hash, err = d.GetBlockHash(4321002)
if err != nil {
t.Fatal(err)
}
if hash != "" {
t.Fatalf("GetBlockHash: got hash '%v', expected ''", hash)
}
// GetBlockHash
info, err := d.GetBlockInfo(4321001)
if err != nil {
t.Fatal(err)
}
iw := &BlockInfo{
Hash: "0x2b57e15e93a0ed197417a34c2498b7187df79099572c04a6b6e6ff418f74e6ee",
Txs: 2,
Size: 2345678,
Time: 1534859988,
Height: 4321001,
}
if !reflect.DeepEqual(info, iw) {
t.Errorf("GetBlockInfo() = %+v, want %+v", info, iw)
}
// Test tx caching functionality, leave one tx in db to test cleanup in DisconnectBlock
testTxCache(t, d, block1, &block1.Txs[0])
testTxCache(t, d, block2, &block2.Txs[0])
if err = d.PutTx(&block2.Txs[1], block2.Height, block2.Txs[1].Blocktime); err != nil {
t.Fatal(err)
}
if err = d.PutTx(&block2.Txs[1], block2.Height, block2.Txs[1].Blocktime); err != nil {
t.Fatal(err)
}
// check that there is only the last tx in the cache
packedTx, err := d.chainParser.PackTx(&block2.Txs[1], block2.Height, block2.Txs[1].Blocktime)
if err := checkColumn(d, cfTransactions, []keyPair{
keyPair{dbtestdata.EthTxidB2T2, hex.EncodeToString(packedTx), nil},
}); err != nil {
{
t.Fatal(err)
}
}
// try to disconnect both blocks, however only the last one is kept, it is not possible
err = d.DisconnectBlockRangeEthereumType(4321000, 4321001)
if err == nil || err.Error() != "Cannot disconnect blocks with height 4321000 and lower. It is necessary to rebuild index." {
t.Fatal(err)
}
verifyAfterEthereumTypeBlock2(t, d)
// disconnect the 2nd block, verify that the db contains only data from the 1st block with restored unspentTxs
// and that the cached tx is removed
err = d.DisconnectBlockRangeEthereumType(4321001, 4321001)
if err != nil {
t.Fatal(err)
}
verifyAfterEthereumTypeBlock1(t, d, true)
if err := checkColumn(d, cfTransactions, []keyPair{}); err != nil {
{
t.Fatal(err)
}
}
// connect block again and verify the state of db
if err := d.ConnectBlock(block2); err != nil {
t.Fatal(err)
}
verifyAfterEthereumTypeBlock2(t, d)
}

View File

@ -1,4 +1,4 @@
// +build unittest
// build unittest
package db
@ -9,7 +9,6 @@ import (
"blockbook/tests/dbtestdata"
"encoding/binary"
"encoding/hex"
"fmt"
"io/ioutil"
"math/big"
"os"
@ -33,6 +32,10 @@ func TestMain(m *testing.M) {
os.Exit(c)
}
type testBitcoinParser struct {
*btc.BitcoinParser
}
func bitcoinTestnetParser() *btc.BitcoinParser {
return btc.NewBitcoinParser(
btc.GetChainParams("test"),
@ -48,7 +51,7 @@ func setupRocksDB(t *testing.T, p bchain.BlockChainParser) *RocksDB {
if err != nil {
t.Fatal(err)
}
is, err := d.LoadInternalState("btc-testnet")
is, err := d.LoadInternalState("coin-unittest")
if err != nil {
t.Fatal(err)
}
@ -151,7 +154,7 @@ func checkColumn(d *RocksDB, col int, kp []keyPair) error {
return nil
}
func verifyAfterUTXOBlock1(t *testing.T, d *RocksDB, afterDisconnect bool) {
func verifyAfterBitcoinTypeBlock1(t *testing.T, d *RocksDB, afterDisconnect bool) {
if err := checkColumn(d, cfHeight, []keyPair{
keyPair{
"000370d5",
@ -232,7 +235,7 @@ func verifyAfterUTXOBlock1(t *testing.T, d *RocksDB, afterDisconnect bool) {
}
}
func verifyAfterUTXOBlock2(t *testing.T, d *RocksDB) {
func verifyAfterBitcoinTypeBlock2(t *testing.T, d *RocksDB) {
if err := checkColumn(d, cfHeight, []keyPair{
keyPair{
"000370d5",
@ -368,13 +371,13 @@ func verifyAfterUTXOBlock2(t *testing.T, d *RocksDB) {
type txidVoutOutput struct {
txid string
vout uint32
vout int32
isOutput bool
}
func verifyGetTransactions(t *testing.T, d *RocksDB, addr string, low, high uint32, wantTxids []txidVoutOutput, wantErr error) {
gotTxids := make([]txidVoutOutput, 0)
addToTxids := func(txid string, vout uint32, isOutput bool) error {
addToTxids := func(txid string, vout int32, isOutput bool) error {
gotTxids = append(gotTxids, txidVoutOutput{txid, vout, isOutput})
return nil
}
@ -388,10 +391,6 @@ func verifyGetTransactions(t *testing.T, d *RocksDB, addr string, low, high uint
}
}
type testBitcoinParser struct {
*btc.BitcoinParser
}
// override PackTx and UnpackTx to default BaseParser functionality
// BitcoinParser uses tx hex which is not available for the test transactions
func (p *testBitcoinParser) PackTx(tx *bchain.Tx, height uint32, blockTime int64) ([]byte, error) {
@ -415,7 +414,7 @@ func testTxCache(t *testing.T, d *RocksDB, b *bchain.Block, tx *bchain.Tx) {
}
// Confirmations are not stored in the DB, set them from input tx
gtx.Confirmations = tx.Confirmations
if fmt.Sprint(gtx) != fmt.Sprint(tx) {
if !reflect.DeepEqual(gtx, tx) {
t.Errorf("GetTx: %v, want %v", gtx, tx)
}
if err := d.DeleteTx(tx.Txid); err != nil {
@ -423,35 +422,34 @@ func testTxCache(t *testing.T, d *RocksDB, b *bchain.Block, tx *bchain.Tx) {
}
}
// TestRocksDB_Index_UTXO is an integration test probing the whole indexing functionality for UTXO chains
// TestRocksDB_Index_BitcoinType is an integration test probing the whole indexing functionality for BitcoinType chains
// It does the following:
// 1) Connect two blocks (inputs from 2nd block are spending some outputs from the 1st block)
// 2) GetTransactions for various addresses / low-high ranges
// 3) GetBestBlock, GetBlockHash
// 4) Test tx caching functionality
// 5) Disconnect block 2 - expect error
// 6) Disconnect the block 2 using BlockTxs column
// 7) Reconnect block 2 and check
// 5) Disconnect the block 2 using BlockTxs column
// 6) Reconnect block 2 and check
// After each step, the content of DB is examined and any difference against expected state is regarded as failure
func TestRocksDB_Index_UTXO(t *testing.T) {
func TestRocksDB_Index_BitcoinType(t *testing.T) {
d := setupRocksDB(t, &testBitcoinParser{
BitcoinParser: bitcoinTestnetParser(),
})
defer closeAndDestroyRocksDB(t, d)
// connect 1st block - will log warnings about missing UTXO transactions in txAddresses column
block1 := dbtestdata.GetTestUTXOBlock1(d.chainParser)
block1 := dbtestdata.GetTestBitcoinTypeBlock1(d.chainParser)
if err := d.ConnectBlock(block1); err != nil {
t.Fatal(err)
}
verifyAfterUTXOBlock1(t, d, false)
verifyAfterBitcoinTypeBlock1(t, d, false)
// connect 2nd block - use some outputs from the 1st block as the inputs and 1 input uses tx from the same block
block2 := dbtestdata.GetTestUTXOBlock2(d.chainParser)
block2 := dbtestdata.GetTestBitcoinTypeBlock2(d.chainParser)
if err := d.ConnectBlock(block2); err != nil {
t.Fatal(err)
}
verifyAfterUTXOBlock2(t, d)
verifyAfterBitcoinTypeBlock2(t, d)
// get transactions for various addresses / low-high ranges
verifyGetTransactions(t, d, dbtestdata.Addr2, 0, 1000000, []txidVoutOutput{
@ -513,7 +511,7 @@ func TestRocksDB_Index_UTXO(t *testing.T) {
Height: 225494,
}
if !reflect.DeepEqual(info, iw) {
t.Errorf("GetAddressBalance() = %+v, want %+v", info, iw)
t.Errorf("GetBlockInfo() = %+v, want %+v", info, iw)
}
// Test tx caching functionality, leave one tx in db to test cleanup in DisconnectBlock
@ -532,27 +530,20 @@ func TestRocksDB_Index_UTXO(t *testing.T) {
}
}
// DisconnectBlock for UTXO chains is not possible
err = d.DisconnectBlock(block2)
if err == nil || err.Error() != "DisconnectBlock is not supported for UTXO chains" {
t.Fatal(err)
}
verifyAfterUTXOBlock2(t, d)
// try to disconnect both blocks, however only the last one is kept, it is not possible
err = d.DisconnectBlockRangeUTXO(225493, 225494)
err = d.DisconnectBlockRangeBitcoinType(225493, 225494)
if err == nil || err.Error() != "Cannot disconnect blocks with height 225493 and lower. It is necessary to rebuild index." {
t.Fatal(err)
}
verifyAfterUTXOBlock2(t, d)
verifyAfterBitcoinTypeBlock2(t, d)
// disconnect the 2nd block, verify that the db contains only data from the 1st block with restored unspentTxs
// and that the cached tx is removed
err = d.DisconnectBlockRangeUTXO(225494, 225494)
err = d.DisconnectBlockRangeBitcoinType(225494, 225494)
if err != nil {
t.Fatal(err)
}
verifyAfterUTXOBlock1(t, d, true)
verifyAfterBitcoinTypeBlock1(t, d, true)
if err := checkColumn(d, cfTransactions, []keyPair{}); err != nil {
{
t.Fatal(err)
@ -563,10 +554,9 @@ func TestRocksDB_Index_UTXO(t *testing.T) {
if err := d.ConnectBlock(block2); err != nil {
t.Fatal(err)
}
verifyAfterUTXOBlock2(t, d)
verifyAfterBitcoinTypeBlock2(t, d)
// test public methods for address balance and tx addresses
ab, err := d.GetAddressBalance(dbtestdata.Addr5)
if err != nil {
t.Fatal(err)
@ -627,7 +617,7 @@ func TestRocksDB_Index_UTXO(t *testing.T) {
}
func Test_BulkConnect_UTXO(t *testing.T) {
func Test_BulkConnect_BitcoinType(t *testing.T) {
d := setupRocksDB(t, &testBitcoinParser{
BitcoinParser: bitcoinTestnetParser(),
})
@ -642,7 +632,7 @@ func Test_BulkConnect_UTXO(t *testing.T) {
t.Fatal("DB not in DbStateInconsistent")
}
if err := bc.ConnectBlock(dbtestdata.GetTestUTXOBlock1(d.chainParser), false); err != nil {
if err := bc.ConnectBlock(dbtestdata.GetTestBitcoinTypeBlock1(d.chainParser), false); err != nil {
t.Fatal(err)
}
if err := checkColumn(d, cfBlockTxs, []keyPair{}); err != nil {
@ -651,7 +641,7 @@ func Test_BulkConnect_UTXO(t *testing.T) {
}
}
if err := bc.ConnectBlock(dbtestdata.GetTestUTXOBlock2(d.chainParser), true); err != nil {
if err := bc.ConnectBlock(dbtestdata.GetTestBitcoinTypeBlock2(d.chainParser), true); err != nil {
t.Fatal(err)
}
@ -663,7 +653,7 @@ func Test_BulkConnect_UTXO(t *testing.T) {
t.Fatal("DB not in DbStateOpen")
}
verifyAfterUTXOBlock2(t, d)
verifyAfterBitcoinTypeBlock2(t, d)
}
func Test_packBigint_unpackBigint(t *testing.T) {
@ -848,17 +838,17 @@ func Test_packTxAddresses_unpackTxAddresses(t *testing.T) {
Height: 123456789,
Inputs: []TxInput{
{
AddrDesc: []byte{},
AddrDesc: []byte(nil),
ValueSat: *big.NewInt(1234),
},
},
Outputs: []TxOutput{
{
AddrDesc: []byte{},
AddrDesc: []byte(nil),
ValueSat: *big.NewInt(5678),
},
{
AddrDesc: []byte{},
AddrDesc: []byte(nil),
ValueSat: *big.NewInt(98),
Spent: true,
},

View File

@ -393,26 +393,11 @@ func (w *SyncWorker) getBlockChain(out chan blockResult, done chan struct{}) {
// DisconnectBlocks removes all data belonging to blocks in range lower-higher,
func (w *SyncWorker) DisconnectBlocks(lower uint32, higher uint32, hashes []string) error {
glog.Infof("sync: disconnecting blocks %d-%d", lower, higher)
// if the chain is UTXO, always use DisconnectBlockRange
if w.chain.GetChainParser().IsUTXOChain() {
return w.db.DisconnectBlockRangeUTXO(lower, higher)
ct := w.chain.GetChainParser().GetChainType()
if ct == bchain.ChainBitcoinType {
return w.db.DisconnectBlockRangeBitcoinType(lower, higher)
} else if ct == bchain.ChainEthereumType {
return w.db.DisconnectBlockRangeEthereumType(lower, higher)
}
blocks := make([]*bchain.Block, len(hashes))
var err error
// try to get all blocks first to see if we can avoid full scan
for i, hash := range hashes {
blocks[i], err = w.chain.GetBlock(hash, 0)
if err != nil {
// cannot get a block, we must do full range scan
return w.db.DisconnectBlockRangeNonUTXO(lower, higher)
}
}
// got all blocks to be disconnected, disconnect them one after another
for i, block := range blocks {
glog.Info("Disconnecting block ", (int(higher) - i), " ", block.Hash)
if err = w.db.DisconnectBlock(block); err != nil {
return err
}
}
return nil
return errors.New("Unknown chain type")
}

View File

@ -2,18 +2,21 @@ package db
import (
"blockbook/bchain"
"blockbook/bchain/coins/eth"
"blockbook/common"
"github.com/golang/glog"
"github.com/juju/errors"
)
// TxCache is handle to TxCacheServer
type TxCache struct {
db *RocksDB
chain bchain.BlockChain
metrics *common.Metrics
is *common.InternalState
enabled bool
db *RocksDB
chain bchain.BlockChain
metrics *common.Metrics
is *common.InternalState
enabled bool
chainType bchain.ChainType
}
// NewTxCache creates new TxCache interface and returns its handle
@ -22,11 +25,12 @@ func NewTxCache(db *RocksDB, chain bchain.BlockChain, metrics *common.Metrics, i
glog.Info("txcache: disabled")
}
return &TxCache{
db: db,
chain: chain,
metrics: metrics,
is: is,
enabled: enabled,
db: db,
chain: chain,
metrics: metrics,
is: is,
enabled: enabled,
chainType: chain.GetChainParser().GetChainType(),
}, nil
}
@ -56,18 +60,27 @@ func (c *TxCache) GetTransaction(txid string) (*bchain.Tx, uint32, error) {
c.metrics.TxCacheEfficiency.With(common.Labels{"status": "miss"}).Inc()
// cache only confirmed transactions
if tx.Confirmations > 0 {
ta, err := c.db.GetTxAddresses(txid)
if err != nil {
return nil, 0, err
}
// the transaction may me not yet indexed, in that case get the height from the backend
if ta == nil {
h, err = c.chain.GetBestBlockHeight()
if c.chainType == bchain.ChainBitcoinType {
ta, err := c.db.GetTxAddresses(txid)
if err != nil {
return nil, 0, err
}
// the transaction may me not yet indexed, in that case get the height from the backend
if ta == nil {
h, err = c.chain.GetBestBlockHeight()
if err != nil {
return nil, 0, err
}
} else {
h = ta.Height
}
} else if c.chainType == bchain.ChainEthereumType {
h, err = eth.GetHeightFromTx(tx)
if err != nil {
return nil, 0, err
}
} else {
h = ta.Height
return nil, 0, errors.New("Unknown chain type")
}
if c.enabled {
err = c.db.PutTx(tx, h, tx.Blocktime)

View File

@ -86,8 +86,8 @@ Good examples of coin configuration are
* `parse` Use binary parser for block decoding if *true* else call verbose back-end RPC method that returns
JSON. Note that verbose method is slow and not every coin support it. However there are coin implementations
that don't support binary parsing (e.g. ZCash).
* `mempool_workers` Number of workers for UTXO mempool.
* `mempool_sub_workers` Number of subworkers for UTXO mempool.
* `mempool_workers` Number of workers for BitcoinType mempool.
* `mempool_sub_workers` Number of subworkers for BitcoinType mempool.
* `block_addresses_to_keep` Number of blocks that are to be kept in blockaddresses column.
* `additional_params` Object of coin-specific params.

View File

@ -14,6 +14,7 @@ import (
"net/http"
"reflect"
"runtime"
"runtime/debug"
"strconv"
"strings"
"time"
@ -25,11 +26,18 @@ const txsOnPage = 25
const blocksOnPage = 50
const txsInAPI = 1000
const (
_ = iota
apiV1
apiV2
)
// PublicServer is a handle to public http server
type PublicServer struct {
binding string
certFiles string
socketio *SocketIoServer
websocket *WebsocketServer
https *http.Server
db *db.RocksDB
txCache *db.TxCache
@ -58,6 +66,11 @@ func NewPublicServer(binding string, certFiles string, db *db.RocksDB, chain bch
return nil, err
}
websocket, err := NewWebsocketServer(db, chain, txCache, metrics, is)
if err != nil {
return nil, err
}
addr, path := splitBinding(binding)
serveMux := http.NewServeMux()
https := &http.Server{
@ -71,6 +84,7 @@ func NewPublicServer(binding string, certFiles string, db *db.RocksDB, chain bch
https: https,
api: api,
socketio: socketio,
websocket: websocket,
db: db,
txCache: txCache,
chain: chain,
@ -81,7 +95,7 @@ func NewPublicServer(binding string, certFiles string, db *db.RocksDB, chain bch
is: is,
debug: debugMode,
}
s.templates = parseTemplates()
s.templates = s.parseTemplates()
// map only basic functions, the rest is enabled by method MapFullPublicInterface
serveMux.Handle(path+"favicon.ico", http.FileServer(http.Dir("./static/")))
@ -89,7 +103,7 @@ func NewPublicServer(binding string, certFiles string, db *db.RocksDB, chain bch
// default handler
serveMux.HandleFunc(path, s.htmlTemplateHandler(s.explorerIndex))
// default API handler
serveMux.HandleFunc(path+"api/", s.jsonHandler(s.apiIndex))
serveMux.HandleFunc(path+"api/", s.jsonHandler(s.apiIndex, apiV2))
return s, nil
}
@ -108,8 +122,9 @@ func (s *PublicServer) Run() error {
func (s *PublicServer) ConnectFullPublicInterface() {
serveMux := s.https.Handler.(*http.ServeMux)
_, path := splitBinding(s.binding)
// support for tests of socket.io interface
serveMux.Handle(path+"test.html", http.FileServer(http.Dir("./static/")))
// support for test pages
serveMux.Handle(path+"test-socketio.html", http.FileServer(http.Dir("./static/")))
serveMux.Handle(path+"test-websocket.html", http.FileServer(http.Dir("./static/")))
if s.internalExplorer {
// internal explorer handlers
serveMux.HandleFunc(path+"tx/", s.htmlTemplateHandler(s.explorerTx))
@ -125,16 +140,46 @@ func (s *PublicServer) ConnectFullPublicInterface() {
serveMux.HandleFunc(path+"address/", s.addressRedirect)
}
// API calls
serveMux.HandleFunc(path+"api/block-index/", s.jsonHandler(s.apiBlockIndex))
serveMux.HandleFunc(path+"api/tx/", s.jsonHandler(s.apiTx))
serveMux.HandleFunc(path+"api/tx-specific/", s.jsonHandler(s.apiTxSpecific))
serveMux.HandleFunc(path+"api/address/", s.jsonHandler(s.apiAddress))
serveMux.HandleFunc(path+"api/utxo/", s.jsonHandler(s.apiAddressUtxo))
serveMux.HandleFunc(path+"api/block/", s.jsonHandler(s.apiBlock))
serveMux.HandleFunc(path+"api/sendtx/", s.jsonHandler(s.apiSendTx))
serveMux.HandleFunc(path+"api/estimatefee/", s.jsonHandler(s.apiEstimateFee))
// default api without version can be changed to different version at any time
// use versioned api for stability
var apiDefault int
// ethereum supports only api V2
if s.chainParser.GetChainType() == bchain.ChainEthereumType {
apiDefault = apiV2
} else {
apiDefault = apiV1
// legacy v1 format
serveMux.HandleFunc(path+"api/v1/block-index/", s.jsonHandler(s.apiBlockIndex, apiV1))
serveMux.HandleFunc(path+"api/v1/tx-specific/", s.jsonHandler(s.apiTxSpecific, apiV1))
serveMux.HandleFunc(path+"api/v1/tx/", s.jsonHandler(s.apiTx, apiV1))
serveMux.HandleFunc(path+"api/v1/address/", s.jsonHandler(s.apiAddress, apiV1))
serveMux.HandleFunc(path+"api/v1/utxo/", s.jsonHandler(s.apiAddressUtxo, apiV1))
serveMux.HandleFunc(path+"api/v1/block/", s.jsonHandler(s.apiBlock, apiV1))
serveMux.HandleFunc(path+"api/v1/sendtx/", s.jsonHandler(s.apiSendTx, apiV1))
serveMux.HandleFunc(path+"api/v1/estimatefee/", s.jsonHandler(s.apiEstimateFee, apiV1))
}
serveMux.HandleFunc(path+"api/block-index/", s.jsonHandler(s.apiBlockIndex, apiDefault))
serveMux.HandleFunc(path+"api/tx-specific/", s.jsonHandler(s.apiTxSpecific, apiDefault))
serveMux.HandleFunc(path+"api/tx/", s.jsonHandler(s.apiTx, apiDefault))
serveMux.HandleFunc(path+"api/address/", s.jsonHandler(s.apiAddress, apiDefault))
serveMux.HandleFunc(path+"api/utxo/", s.jsonHandler(s.apiAddressUtxo, apiDefault))
serveMux.HandleFunc(path+"api/block/", s.jsonHandler(s.apiBlock, apiDefault))
serveMux.HandleFunc(path+"api/sendtx/", s.jsonHandler(s.apiSendTx, apiDefault))
serveMux.HandleFunc(path+"api/estimatefee/", s.jsonHandler(s.apiEstimateFee, apiDefault))
// v2 format
serveMux.HandleFunc(path+"api/v2/block-index/", s.jsonHandler(s.apiBlockIndex, apiV2))
serveMux.HandleFunc(path+"api/v2/tx-specific/", s.jsonHandler(s.apiTxSpecific, apiV2))
serveMux.HandleFunc(path+"api/v2/tx/", s.jsonHandler(s.apiTx, apiV2))
serveMux.HandleFunc(path+"api/v2/address/", s.jsonHandler(s.apiAddress, apiV2))
serveMux.HandleFunc(path+"api/v2/utxo/", s.jsonHandler(s.apiAddressUtxo, apiV2))
serveMux.HandleFunc(path+"api/v2/block/", s.jsonHandler(s.apiBlock, apiV2))
serveMux.HandleFunc(path+"api/v2/sendtx/", s.jsonHandler(s.apiSendTx, apiV2))
serveMux.HandleFunc(path+"api/v2/estimatefee/", s.jsonHandler(s.apiEstimateFee, apiV2))
// socket.io interface
serveMux.Handle(path+"socket.io/", s.socketio.GetHandler())
// websocket interface
serveMux.Handle(path+"websocket", s.websocket.GetHandler())
}
// Close closes the server
@ -152,11 +197,13 @@ func (s *PublicServer) Shutdown(ctx context.Context) error {
// OnNewBlock notifies users subscribed to bitcoind/hashblock about new block
func (s *PublicServer) OnNewBlock(hash string, height uint32) {
s.socketio.OnNewBlockHash(hash)
s.websocket.OnNewBlock(hash, height)
}
// OnNewTxAddr notifies users subscribed to bitcoind/addresstxid about new block
func (s *PublicServer) OnNewTxAddr(txid string, desc bchain.AddressDescriptor, isOutput bool) {
s.socketio.OnNewTxAddr(txid, desc, isOutput)
func (s *PublicServer) OnNewTxAddr(tx *bchain.Tx, desc bchain.AddressDescriptor) {
s.socketio.OnNewTxAddr(tx.Txid, desc)
s.websocket.OnNewTxAddr(tx, desc)
}
func (s *PublicServer) txRedirect(w http.ResponseWriter, r *http.Request) {
@ -191,7 +238,7 @@ func getFunctionName(i interface{}) string {
return runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name()
}
func (s *PublicServer) jsonHandler(handler func(r *http.Request) (interface{}, error)) func(w http.ResponseWriter, r *http.Request) {
func (s *PublicServer) jsonHandler(handler func(r *http.Request, apiVersion int) (interface{}, error), apiVersion int) func(w http.ResponseWriter, r *http.Request) {
type jsonError struct {
Text string `json:"error"`
HTTPStatus int `json:"-"`
@ -202,6 +249,7 @@ func (s *PublicServer) jsonHandler(handler func(r *http.Request) (interface{}, e
defer func() {
if e := recover(); e != nil {
glog.Error(getFunctionName(handler), " recovered from panic: ", e)
debug.PrintStack()
if s.debug {
data = jsonError{fmt.Sprint("Internal server error: recovered from panic ", e), http.StatusInternalServerError}
} else {
@ -214,7 +262,7 @@ func (s *PublicServer) jsonHandler(handler func(r *http.Request) (interface{}, e
}
json.NewEncoder(w).Encode(data)
}()
data, err = handler(r)
data, err = handler(r, apiVersion)
if err != nil || data == nil {
if apiErr, ok := err.(*api.APIError); ok {
if apiErr.Public {
@ -245,6 +293,7 @@ func (s *PublicServer) newTemplateData() *TemplateData {
CoinName: s.is.Coin,
CoinShortcut: s.is.CoinShortcut,
CoinLabel: s.is.CoinLabel,
ChainType: s.chainParser.GetChainType(),
InternalExplorer: s.internalExplorer && !s.is.InitialSync,
TOSLink: api.Text.TOSLink,
}
@ -264,6 +313,7 @@ func (s *PublicServer) htmlTemplateHandler(handler func(w http.ResponseWriter, r
defer func() {
if e := recover(); e != nil {
glog.Error(getFunctionName(handler), " recovered from panic: ", e)
debug.PrintStack()
t = errorInternalTpl
if s.debug {
data = s.newTemplateDataWithError(fmt.Sprint("Internal server error: recovered from panic ", e))
@ -286,7 +336,7 @@ func (s *PublicServer) htmlTemplateHandler(handler func(w http.ResponseWriter, r
if s.debug {
// reload templates on each request
// to reflect changes during development
s.templates = parseTemplates()
s.templates = s.parseTemplates()
}
t, data, err = handler(w, r)
if err != nil || (data == nil && t != noTpl) {
@ -333,10 +383,10 @@ type TemplateData struct {
CoinShortcut string
CoinLabel string
InternalExplorer bool
ChainType bchain.ChainType
Address *api.Address
AddrStr string
Tx *api.Tx
TxSpecific json.RawMessage
Error *api.APIError
Blocks *api.Blocks
Block *api.Block
@ -345,28 +395,36 @@ type TemplateData struct {
PrevPage int
NextPage int
PagingRange []int
PageParams template.URL
TOSLink string
SendTxHex string
Status string
}
func parseTemplates() []*template.Template {
func (s *PublicServer) parseTemplates() []*template.Template {
templateFuncMap := template.FuncMap{
"formatTime": formatTime,
"formatUnixTime": formatUnixTime,
"formatAmount": formatAmount,
"setTxToTemplateData": setTxToTemplateData,
"stringInSlice": stringInSlice,
"formatTime": formatTime,
"formatUnixTime": formatUnixTime,
"formatAmount": s.formatAmount,
"formatAmountWithDecimals": formatAmountWithDecimals,
"setTxToTemplateData": setTxToTemplateData,
"stringInSlice": stringInSlice,
}
t := make([]*template.Template, tplCount)
t[errorTpl] = template.Must(template.New("error").Funcs(templateFuncMap).ParseFiles("./static/templates/error.html", "./static/templates/base.html"))
t[errorInternalTpl] = template.Must(template.New("error").Funcs(templateFuncMap).ParseFiles("./static/templates/error.html", "./static/templates/base.html"))
t[indexTpl] = template.Must(template.New("index").Funcs(templateFuncMap).ParseFiles("./static/templates/index.html", "./static/templates/base.html"))
t[txTpl] = template.Must(template.New("tx").Funcs(templateFuncMap).ParseFiles("./static/templates/tx.html", "./static/templates/txdetail.html", "./static/templates/base.html"))
t[addressTpl] = template.Must(template.New("address").Funcs(templateFuncMap).ParseFiles("./static/templates/address.html", "./static/templates/txdetail.html", "./static/templates/paging.html", "./static/templates/base.html"))
t[blocksTpl] = template.Must(template.New("blocks").Funcs(templateFuncMap).ParseFiles("./static/templates/blocks.html", "./static/templates/paging.html", "./static/templates/base.html"))
t[blockTpl] = template.Must(template.New("block").Funcs(templateFuncMap).ParseFiles("./static/templates/block.html", "./static/templates/txdetail.html", "./static/templates/paging.html", "./static/templates/base.html"))
t[sendTransactionTpl] = template.Must(template.New("block").Funcs(templateFuncMap).ParseFiles("./static/templates/sendtx.html", "./static/templates/base.html"))
if s.chainParser.GetChainType() == bchain.ChainEthereumType {
t[txTpl] = template.Must(template.New("tx").Funcs(templateFuncMap).ParseFiles("./static/templates/tx.html", "./static/templates/txdetail_ethereumtype.html", "./static/templates/base.html"))
t[addressTpl] = template.Must(template.New("address").Funcs(templateFuncMap).ParseFiles("./static/templates/address.html", "./static/templates/txdetail_ethereumtype.html", "./static/templates/paging.html", "./static/templates/base.html"))
t[blockTpl] = template.Must(template.New("block").Funcs(templateFuncMap).ParseFiles("./static/templates/block.html", "./static/templates/txdetail_ethereumtype.html", "./static/templates/paging.html", "./static/templates/base.html"))
} else {
t[txTpl] = template.Must(template.New("tx").Funcs(templateFuncMap).ParseFiles("./static/templates/tx.html", "./static/templates/txdetail.html", "./static/templates/base.html"))
t[addressTpl] = template.Must(template.New("address").Funcs(templateFuncMap).ParseFiles("./static/templates/address.html", "./static/templates/txdetail.html", "./static/templates/paging.html", "./static/templates/base.html"))
t[blockTpl] = template.Must(template.New("block").Funcs(templateFuncMap).ParseFiles("./static/templates/block.html", "./static/templates/txdetail.html", "./static/templates/paging.html", "./static/templates/base.html"))
}
return t
}
@ -380,11 +438,18 @@ func formatTime(t time.Time) string {
// for now return the string as it is
// in future could be used to do coin specific formatting
func formatAmount(a string) string {
if a == "" {
func (s *PublicServer) formatAmount(a *api.Amount) string {
if a == nil {
return "0"
}
return a
return s.chainParser.AmountToDecimalString((*big.Int)(a))
}
func formatAmountWithDecimals(a *api.Amount, d int) string {
if a == nil {
return "0"
}
return a.DecimalString(d)
}
// called from template to support txdetail.html functionality
@ -395,23 +460,17 @@ func setTxToTemplateData(td *TemplateData, tx *api.Tx) *TemplateData {
func (s *PublicServer) explorerTx(w http.ResponseWriter, r *http.Request) (tpl, *TemplateData, error) {
var tx *api.Tx
var txSpecific json.RawMessage
var err error
s.metrics.ExplorerViews.With(common.Labels{"action": "tx"}).Inc()
if i := strings.LastIndexByte(r.URL.Path, '/'); i > 0 {
txid := r.URL.Path[i+1:]
tx, err = s.api.GetTransaction(txid, false)
if err != nil {
return errorTpl, nil, err
}
txSpecific, err = s.chain.GetTransactionSpecific(txid)
tx, err = s.api.GetTransaction(txid, false, true)
if err != nil {
return errorTpl, nil, err
}
}
data := s.newTemplateData()
data.Tx = tx
data.TxSpecific = txSpecific
return txTpl, data, nil
}
@ -438,6 +497,8 @@ func (s *PublicServer) explorerSpendingTx(w http.ResponseWriter, r *http.Request
func (s *PublicServer) explorerAddress(w http.ResponseWriter, r *http.Request) (tpl, *TemplateData, error) {
var address *api.Address
var filter string
var fn = api.AddressFilterVoutOff
var err error
s.metrics.ExplorerViews.With(common.Labels{"action": "address"}).Inc()
if i := strings.LastIndexByte(r.URL.Path, '/'); i > 0 {
@ -445,7 +506,21 @@ func (s *PublicServer) explorerAddress(w http.ResponseWriter, r *http.Request) (
if ec != nil {
page = 0
}
address, err = s.api.GetAddress(r.URL.Path[i+1:], page, txsOnPage, false)
filter = r.URL.Query().Get("filter")
if len(filter) > 0 {
if filter == "inputs" {
fn = api.AddressFilterVoutInputs
} else if filter == "outputs" {
fn = api.AddressFilterVoutOutputs
} else {
fn, ec = strconv.Atoi(filter)
if ec != nil || fn < 0 {
filter = ""
fn = api.AddressFilterVoutOff
}
}
}
address, err = s.api.GetAddress(r.URL.Path[i+1:], page, txsOnPage, api.TxHistoryLight, &api.AddressFilter{Vout: fn})
if err != nil {
return errorTpl, nil, err
}
@ -455,6 +530,10 @@ func (s *PublicServer) explorerAddress(w http.ResponseWriter, r *http.Request) (
data.Address = address
data.Page = address.Page
data.PagingRange, data.PrevPage, data.NextPage = getPagingRange(address.Page, address.TotalPages)
if filter != "" {
data.PageParams = template.URL("&filter=" + filter)
data.Address.Filter = filter
}
return addressTpl, data, nil
}
@ -524,12 +603,12 @@ func (s *PublicServer) explorerSearch(w http.ResponseWriter, r *http.Request) (t
http.Redirect(w, r, joinURL("/block/", block.Hash), 302)
return noTpl, nil, nil
}
tx, err = s.api.GetTransaction(q, false)
tx, err = s.api.GetTransaction(q, false, false)
if err == nil {
http.Redirect(w, r, joinURL("/tx/", tx.Txid), 302)
return noTpl, nil, nil
}
address, err = s.api.GetAddress(q, 0, 1, true)
address, err = s.api.GetAddress(q, 0, 1, api.Basic, &api.AddressFilter{Vout: api.AddressFilterVoutOff})
if err == nil {
http.Redirect(w, r, joinURL("/address/", address.AddrStr), 302)
return noTpl, nil, nil
@ -614,12 +693,12 @@ func getPagingRange(page int, total int) ([]int, int, int) {
return r, pp, np
}
func (s *PublicServer) apiIndex(r *http.Request) (interface{}, error) {
func (s *PublicServer) apiIndex(r *http.Request, apiVersion int) (interface{}, error) {
s.metrics.ExplorerViews.With(common.Labels{"action": "api-index"}).Inc()
return s.api.GetSystemInfo(false)
}
func (s *PublicServer) apiBlockIndex(r *http.Request) (interface{}, error) {
func (s *PublicServer) apiBlockIndex(r *http.Request, apiVersion int) (interface{}, error) {
type resBlockIndex struct {
BlockHash string `json:"blockHash"`
}
@ -645,7 +724,7 @@ func (s *PublicServer) apiBlockIndex(r *http.Request) (interface{}, error) {
}, nil
}
func (s *PublicServer) apiTx(r *http.Request) (interface{}, error) {
func (s *PublicServer) apiTx(r *http.Request, apiVersion int) (interface{}, error) {
var tx *api.Tx
var err error
s.metrics.ExplorerViews.With(common.Labels{"action": "api-tx"}).Inc()
@ -659,23 +738,26 @@ func (s *PublicServer) apiTx(r *http.Request) (interface{}, error) {
return nil, api.NewAPIError("Parameter 'spending' cannot be converted to boolean", true)
}
}
tx, err = s.api.GetTransaction(txid, spendingTxs)
tx, err = s.api.GetTransaction(txid, spendingTxs, false)
if err == nil && apiVersion == apiV1 {
return s.api.TxToV1(tx), nil
}
}
return tx, err
}
func (s *PublicServer) apiTxSpecific(r *http.Request) (interface{}, error) {
func (s *PublicServer) apiTxSpecific(r *http.Request, apiVersion int) (interface{}, error) {
var tx json.RawMessage
var err error
s.metrics.ExplorerViews.With(common.Labels{"action": "api-tx-specific"}).Inc()
if i := strings.LastIndexByte(r.URL.Path, '/'); i > 0 {
txid := r.URL.Path[i+1:]
tx, err = s.chain.GetTransactionSpecific(txid)
tx, err = s.chain.GetTransactionSpecific(&bchain.Tx{Txid: txid})
}
return tx, err
}
func (s *PublicServer) apiAddress(r *http.Request) (interface{}, error) {
func (s *PublicServer) apiAddress(r *http.Request, apiVersion int) (interface{}, error) {
var address *api.Address
var err error
s.metrics.ExplorerViews.With(common.Labels{"action": "api-address"}).Inc()
@ -684,12 +766,15 @@ func (s *PublicServer) apiAddress(r *http.Request) (interface{}, error) {
if ec != nil {
page = 0
}
address, err = s.api.GetAddress(r.URL.Path[i+1:], page, txsInAPI, true)
address, err = s.api.GetAddress(r.URL.Path[i+1:], page, txsInAPI, api.TxidHistory, &api.AddressFilter{Vout: api.AddressFilterVoutOff})
if err == nil && apiVersion == apiV1 {
return s.api.AddressToV1(address), nil
}
}
return address, err
}
func (s *PublicServer) apiAddressUtxo(r *http.Request) (interface{}, error) {
func (s *PublicServer) apiAddressUtxo(r *http.Request, apiVersion int) (interface{}, error) {
var utxo []api.AddressUtxo
var err error
s.metrics.ExplorerViews.With(common.Labels{"action": "api-address"}).Inc()
@ -703,11 +788,14 @@ func (s *PublicServer) apiAddressUtxo(r *http.Request) (interface{}, error) {
}
}
utxo, err = s.api.GetAddressUtxo(r.URL.Path[i+1:], onlyConfirmed)
if err == nil && apiVersion == apiV1 {
return s.api.AddressUtxoToV1(utxo), nil
}
}
return utxo, err
}
func (s *PublicServer) apiBlock(r *http.Request) (interface{}, error) {
func (s *PublicServer) apiBlock(r *http.Request, apiVersion int) (interface{}, error) {
var block *api.Block
var err error
s.metrics.ExplorerViews.With(common.Labels{"action": "api-block"}).Inc()
@ -717,6 +805,9 @@ func (s *PublicServer) apiBlock(r *http.Request) (interface{}, error) {
page = 0
}
block, err = s.api.GetBlock(r.URL.Path[i+1:], page, txsInAPI)
if err == nil && apiVersion == apiV1 {
return s.api.BlockToV1(block), nil
}
}
return block, err
}
@ -725,7 +816,7 @@ type resultSendTransaction struct {
Result string `json:"result"`
}
func (s *PublicServer) apiSendTx(r *http.Request) (interface{}, error) {
func (s *PublicServer) apiSendTx(r *http.Request, apiVersion int) (interface{}, error) {
var err error
var res resultSendTransaction
var hex string
@ -755,7 +846,7 @@ type resultEstimateFeeAsString struct {
Result string `json:"result"`
}
func (s *PublicServer) apiEstimateFee(r *http.Request) (interface{}, error) {
func (s *PublicServer) apiEstimateFee(r *http.Request, apiVersion int) (interface{}, error) {
var res resultEstimateFeeAsString
s.metrics.ExplorerViews.With(common.Labels{"action": "api-estimatefee"}).Inc()
if i := strings.LastIndexByte(r.URL.Path, '/'); i > 0 {

View File

@ -48,10 +48,10 @@ func setupRocksDB(t *testing.T, parser bchain.BlockChainParser) (*db.RocksDB, *c
}
d.SetInternalState(is)
// import data
if err := d.ConnectBlock(dbtestdata.GetTestUTXOBlock1(parser)); err != nil {
if err := d.ConnectBlock(dbtestdata.GetTestBitcoinTypeBlock1(parser)); err != nil {
t.Fatal(err)
}
if err := d.ConnectBlock(dbtestdata.GetTestUTXOBlock2(parser)); err != nil {
if err := d.ConnectBlock(dbtestdata.GetTestBitcoinTypeBlock2(parser)); err != nil {
t.Fatal(err)
}
return d, is, tmp
@ -131,7 +131,7 @@ func newPostRequest(u string, body string) *http.Request {
return r
}
func httpTests(t *testing.T, ts *httptest.Server) {
func httpTests_BitcoinType(t *testing.T, ts *httptest.Server) {
tests := []struct {
name string
r *http.Request
@ -359,6 +359,7 @@ func httpTests(t *testing.T, ts *httptest.Server) {
body: []string{
`{"blockbook":{"coin":"Fakecoin"`,
`"bestHeight":225494`,
`"decimals":8`,
`"backend":{"chain":"fakecoin","blocks":2,"headers":2,"bestblockhash":"00000000eb0443fd7dc4a1ed5c686a8e995057805f9a161d9a5a77a95e72b7b6"`,
`"version":"001001","subversion":"/Fakecoin:0.0.1/"`,
},
@ -373,17 +374,35 @@ func httpTests(t *testing.T, ts *httptest.Server) {
},
},
{
name: "apiTx",
r: newGetRequest(ts.URL + "/api/tx/05e2e48aeabdd9b75def7b48d756ba304713c2aba7b522bf9dbc893fc4231b07"),
name: "apiTx v1",
r: newGetRequest(ts.URL + "/api/v1/tx/05e2e48aeabdd9b75def7b48d756ba304713c2aba7b522bf9dbc893fc4231b07"),
status: http.StatusOK,
contentType: "application/json; charset=utf-8",
body: []string{
`{"txid":"05e2e48aeabdd9b75def7b48d756ba304713c2aba7b522bf9dbc893fc4231b07","vin":[{"txid":"effd9ef509383d536b1c8af5bf434c8efbf521a4f2befd4022bbd68694b4ac75","vout":2,"n":0,"scriptSig":{"hex":""},"addresses":["2NEVv9LJmAnY99W1pFoc5UJjVdypBqdnvu1"],"value":"0.00009876"}],"vout":[{"value":"0.00009","n":0,"scriptPubKey":{"hex":"a914e921fc4912a315078f370d959f2c4f7b6d2a683c87","addresses":["2NEVv9LJmAnY99W1pFoc5UJjVdypBqdnvu1"]},"spent":false}],"blockhash":"00000000eb0443fd7dc4a1ed5c686a8e995057805f9a161d9a5a77a95e72b7b6","blockheight":225494,"confirmations":1,"time":22549400002,"blocktime":22549400002,"valueOut":"0.00009","valueIn":"0.00009876","fees":"0.00000876","hex":""}`,
`{"txid":"05e2e48aeabdd9b75def7b48d756ba304713c2aba7b522bf9dbc893fc4231b07","vin":[{"txid":"effd9ef509383d536b1c8af5bf434c8efbf521a4f2befd4022bbd68694b4ac75","vout":2,"n":0,"scriptSig":{},"addresses":["2NEVv9LJmAnY99W1pFoc5UJjVdypBqdnvu1"],"value":"0.00009876"}],"vout":[{"value":"0.00009","n":0,"scriptPubKey":{"hex":"a914e921fc4912a315078f370d959f2c4f7b6d2a683c87","addresses":["2NEVv9LJmAnY99W1pFoc5UJjVdypBqdnvu1"]},"spent":false}],"blockhash":"00000000eb0443fd7dc4a1ed5c686a8e995057805f9a161d9a5a77a95e72b7b6","blockheight":225494,"confirmations":1,"time":22549400002,"blocktime":22549400002,"valueOut":"0.00009","valueIn":"0.00009876","fees":"0.00000876","hex":""}`,
},
},
{
name: "apiTx - not found",
r: newGetRequest(ts.URL + "/api/tx/1232e48aeabdd9b75def7b48d756ba304713c2aba7b522bf9dbc893fc4231b07"),
name: "apiTx - not found v1",
r: newGetRequest(ts.URL + "/api/v1/tx/1232e48aeabdd9b75def7b48d756ba304713c2aba7b522bf9dbc893fc4231b07"),
status: http.StatusBadRequest,
contentType: "application/json; charset=utf-8",
body: []string{
`{"error":"Tx not found, Not found"}`,
},
},
{
name: "apiTx v2",
r: newGetRequest(ts.URL + "/api/v2/tx/05e2e48aeabdd9b75def7b48d756ba304713c2aba7b522bf9dbc893fc4231b07"),
status: http.StatusOK,
contentType: "application/json; charset=utf-8",
body: []string{
`{"txid":"05e2e48aeabdd9b75def7b48d756ba304713c2aba7b522bf9dbc893fc4231b07","vin":[{"txid":"effd9ef509383d536b1c8af5bf434c8efbf521a4f2befd4022bbd68694b4ac75","vout":2,"n":0,"addresses":["2NEVv9LJmAnY99W1pFoc5UJjVdypBqdnvu1"],"value":"9876"}],"vout":[{"value":"9000","n":0,"hex":"a914e921fc4912a315078f370d959f2c4f7b6d2a683c87","addresses":["2NEVv9LJmAnY99W1pFoc5UJjVdypBqdnvu1"]}],"blockhash":"00000000eb0443fd7dc4a1ed5c686a8e995057805f9a161d9a5a77a95e72b7b6","blockheight":225494,"confirmations":1,"time":22549400002,"blocktime":22549400002,"value":"9000","valueIn":"9876","fees":"876"}`,
},
},
{
name: "apiTx - not found v2",
r: newGetRequest(ts.URL + "/api/v2/tx/1232e48aeabdd9b75def7b48d756ba304713c2aba7b522bf9dbc893fc4231b07"),
status: http.StatusBadRequest,
contentType: "application/json; charset=utf-8",
body: []string{
@ -396,12 +415,12 @@ func httpTests(t *testing.T, ts *httptest.Server) {
status: http.StatusOK,
contentType: "application/json; charset=utf-8",
body: []string{
`{"hex":"","txid":"00b2c06055e5e90e9c82bd4181fde310104391a7fa4f289b1704e5d90caa3840","version":0,"locktime":0,"vin":null,"vout":[{"ValueSat":100000000,"value":0,"n":0,"scriptPubKey":{"hex":"76a914010d39800f86122416e28f485029acf77507169288ac","addresses":null}},{"ValueSat":12345,"value":0,"n":1,"scriptPubKey":{"hex":"76a9148bdf0aa3c567aa5975c2e61321b8bebbe7293df688ac","addresses":null}}],"confirmations":2,"time":22549300000,"blocktime":22549300000}`,
`{"hex":"","txid":"00b2c06055e5e90e9c82bd4181fde310104391a7fa4f289b1704e5d90caa3840","version":0,"locktime":0,"vin":[],"vout":[{"ValueSat":100000000,"value":0,"n":0,"scriptPubKey":{"hex":"76a914010d39800f86122416e28f485029acf77507169288ac","addresses":null}},{"ValueSat":12345,"value":0,"n":1,"scriptPubKey":{"hex":"76a9148bdf0aa3c567aa5975c2e61321b8bebbe7293df688ac","addresses":null}}],"confirmations":2,"time":22549300000,"blocktime":22549300000}`,
},
},
{
name: "apiAddress",
r: newGetRequest(ts.URL + "/api/address/mv9uLThosiEnGRbVPS7Vhyw6VssbVRsiAw"),
name: "apiAddress v1",
r: newGetRequest(ts.URL + "/api/v1/address/mv9uLThosiEnGRbVPS7Vhyw6VssbVRsiAw"),
status: http.StatusOK,
contentType: "application/json; charset=utf-8",
body: []string{
@ -409,14 +428,32 @@ func httpTests(t *testing.T, ts *httptest.Server) {
},
},
{
name: "apiAddressUtxo",
r: newGetRequest(ts.URL + "/api/utxo/mtR97eM2HPWVM6c8FGLGcukgaHHQv7THoL"),
name: "apiAddress v2",
r: newGetRequest(ts.URL + "/api/v2/address/mv9uLThosiEnGRbVPS7Vhyw6VssbVRsiAw"),
status: http.StatusOK,
contentType: "application/json; charset=utf-8",
body: []string{
`{"page":1,"totalPages":1,"itemsOnPage":1000,"addrStr":"mv9uLThosiEnGRbVPS7Vhyw6VssbVRsiAw","balance":"0","totalReceived":"1234567890123","totalSent":"1234567890123","unconfirmedBalance":"0","unconfirmedTxApperances":0,"txApperances":2,"txids":["7c3be24063f268aaa1ed81b64776798f56088757641a34fb156c4f51ed2e9d25","effd9ef509383d536b1c8af5bf434c8efbf521a4f2befd4022bbd68694b4ac75"]}`,
},
},
{
name: "apiAddressUtxo v1",
r: newGetRequest(ts.URL + "/api/v1/utxo/mtR97eM2HPWVM6c8FGLGcukgaHHQv7THoL"),
status: http.StatusOK,
contentType: "application/json; charset=utf-8",
body: []string{
`[{"txid":"7c3be24063f268aaa1ed81b64776798f56088757641a34fb156c4f51ed2e9d25","vout":1,"amount":"9172.83951061","satoshis":917283951061,"height":225494,"confirmations":1}]`,
},
},
{
name: "apiAddressUtxo v2",
r: newGetRequest(ts.URL + "/api/v2/utxo/mtR97eM2HPWVM6c8FGLGcukgaHHQv7THoL"),
status: http.StatusOK,
contentType: "application/json; charset=utf-8",
body: []string{
`[{"txid":"7c3be24063f268aaa1ed81b64776798f56088757641a34fb156c4f51ed2e9d25","vout":1,"value":"917283951061","height":225494,"confirmations":1}]`,
},
},
{
name: "apiSendTx",
r: newGetRequest(ts.URL + "/api/sendtx/1234567890"),
@ -475,7 +512,7 @@ func httpTests(t *testing.T, ts *httptest.Server) {
b := string(bb)
for _, c := range tt.body {
if !strings.Contains(b, c) {
t.Errorf("Page body does not contain %v, body %v", c, b)
t.Errorf("got %v, want to contain %v", b, c)
break
}
}
@ -483,7 +520,7 @@ func httpTests(t *testing.T, ts *httptest.Server) {
}
}
func socketioTests(t *testing.T, ts *httptest.Server) {
func socketioTests_BitcoinType(t *testing.T, ts *httptest.Server) {
type socketioReq struct {
Method string `json:"method"`
Params []interface{} `json:"params"`
@ -578,13 +615,13 @@ func socketioTests(t *testing.T, ts *httptest.Server) {
t.Errorf("Socketio error %v", err)
}
if resp != tt.want {
t.Errorf("Socketio resp %v, want %v", resp, tt.want)
t.Errorf("got %v, want %v", resp, tt.want)
}
})
}
}
func Test_PublicServer_UTXO(t *testing.T) {
func Test_PublicServer_BitcoinType(t *testing.T) {
s, dbpath := setupPublicHTTPServer(t)
defer closeAndDestroyPublicServer(t, s, dbpath)
s.ConnectFullPublicInterface()
@ -592,7 +629,6 @@ func Test_PublicServer_UTXO(t *testing.T) {
ts := httptest.NewServer(s.https.Handler)
defer ts.Close()
httpTests(t, ts)
socketioTests(t, ts)
httpTests_BitcoinType(t, ts)
socketioTests_BitcoinType(t, ts)
}

View File

@ -8,6 +8,7 @@ import (
"encoding/json"
"math/big"
"net/http"
"runtime/debug"
"strconv"
"strings"
"time"
@ -161,6 +162,7 @@ func (s *SocketIoServer) onMessage(c *gosocketio.Channel, req map[string]json.Ra
defer func() {
if r := recover(); r != nil {
glog.Error(c.Id(), " onMessage ", method, " recovered from panic: ", r)
debug.PrintStack()
e := resultError{}
e.Error.Message = "Internal error"
rv = e
@ -214,7 +216,7 @@ func (s *SocketIoServer) getAddressTxids(addr []string, opts *addrOpts) (res res
lower, higher := uint32(opts.End), uint32(opts.Start)
for _, address := range addr {
if !opts.QueryMempoolOnly {
err = s.db.GetTransactions(address, lower, higher, func(txid string, vout uint32, isOutput bool) error {
err = s.db.GetTransactions(address, lower, higher, func(txid string, vout int32, isOutput bool) error {
txids = append(txids, txid)
return nil
})
@ -222,11 +224,13 @@ func (s *SocketIoServer) getAddressTxids(addr []string, opts *addrOpts) (res res
return res, err
}
} else {
m, err := s.chain.GetMempoolTransactions(address)
o, err := s.chain.GetMempoolTransactions(address)
if err != nil {
return res, err
}
txids = append(txids, m...)
for _, m := range o {
txids = append(txids, m.Txid)
}
}
}
res.Result = api.UniqueTxidsInReverse(txids)
@ -302,13 +306,13 @@ func txToResTx(tx *api.Tx) resTx {
for i := range tx.Vin {
vin := &tx.Vin[i]
txid := vin.Txid
script := vin.ScriptSig.Hex
script := vin.Hex
input := txInputs{
Txid: &txid,
Script: &script,
Sequence: int64(vin.Sequence),
OutputIndex: int(vin.Vout),
Satoshis: vin.ValueSat.Int64(),
Satoshis: (*big.Int)(vin.ValueSat).Int64(),
}
if len(vin.Addresses) > 0 {
a := vin.Addresses[0]
@ -319,13 +323,13 @@ func txToResTx(tx *api.Tx) resTx {
outputs := make([]txOutputs, len(tx.Vout))
for i := range tx.Vout {
vout := &tx.Vout[i]
script := vout.ScriptPubKey.Hex
script := vout.Hex
output := txOutputs{
Satoshis: vout.ValueSat.Int64(),
Satoshis: (*big.Int)(vout.ValueSat).Int64(),
Script: &script,
}
if len(vout.ScriptPubKey.Addresses) > 0 {
a := vout.ScriptPubKey.Addresses[0]
if len(vout.Addresses) > 0 {
a := vout.Addresses[0]
output.Address = &a
}
outputs[i] = output
@ -338,15 +342,15 @@ func txToResTx(tx *api.Tx) resTx {
}
return resTx{
BlockTimestamp: tx.Blocktime,
FeeSatoshis: tx.FeesSat.Int64(),
FeeSatoshis: (*big.Int)(tx.FeesSat).Int64(),
Hash: tx.Txid,
Height: h,
Hex: tx.Hex,
Inputs: inputs,
InputSatoshis: tx.ValueInSat.Int64(),
InputSatoshis: (*big.Int)(tx.ValueInSat).Int64(),
Locktime: int(tx.Locktime),
Outputs: outputs,
OutputSatoshis: tx.ValueOutSat.Int64(),
OutputSatoshis: (*big.Int)(tx.ValueOutSat).Int64(),
Version: int(tx.Version),
}
}
@ -387,7 +391,7 @@ func (s *SocketIoServer) getAddressHistory(addr []string, opts *addrOpts) (res r
to = opts.To
}
for txi := opts.From; txi < to; txi++ {
tx, err := s.api.GetTransaction(txids[txi], false)
tx, err := s.api.GetTransaction(txids[txi], false, false)
if err != nil {
return res, err
}
@ -403,12 +407,12 @@ func (s *SocketIoServer) getAddressHistory(addr []string, opts *addrOpts) (res r
ads[a] = hi
}
hi.InputIndexes = append(hi.InputIndexes, int(vin.N))
totalSat.Sub(&totalSat, &vin.ValueSat)
totalSat.Sub(&totalSat, (*big.Int)(vin.ValueSat))
}
}
for i := range tx.Vout {
vout := &tx.Vout[i]
a := addressInSlice(vout.ScriptPubKey.Addresses, addr)
a := addressInSlice(vout.Addresses, addr)
if a != "" {
hi := ads[a]
if hi == nil {
@ -416,7 +420,7 @@ func (s *SocketIoServer) getAddressHistory(addr []string, opts *addrOpts) (res r
ads[a] = hi
}
hi.OutputIndexes = append(hi.OutputIndexes, int(vout.N))
totalSat.Add(&totalSat, &vout.ValueSat)
totalSat.Add(&totalSat, (*big.Int)(vout.ValueSat))
}
}
ahi := addressHistoryItem{}
@ -579,10 +583,7 @@ type resultGetInfo struct {
}
func (s *SocketIoServer) getInfo() (res resultGetInfo, err error) {
height, _, err := s.db.GetBestBlock()
if err != nil {
return
}
_, height, _ := s.is.GetSyncState()
res.Result.Blocks = int(height)
res.Result.Testnet = s.chain.IsTestnet()
res.Result.Network = s.chain.GetNetworkName()
@ -627,7 +628,7 @@ type resultGetDetailedTransaction struct {
}
func (s *SocketIoServer) getDetailedTransaction(txid string) (res resultGetDetailedTransaction, err error) {
tx, err := s.api.GetTransaction(txid, false)
tx, err := s.api.GetTransaction(txid, false, false)
if err != nil {
return res, err
}
@ -664,6 +665,7 @@ func (s *SocketIoServer) onSubscribe(c *gosocketio.Channel, req []byte) interfac
defer func() {
if r := recover(); r != nil {
glog.Error(c.Id(), " onSubscribe recovered from panic: ", r)
debug.PrintStack()
}
}()
@ -720,15 +722,12 @@ func (s *SocketIoServer) OnNewBlockHash(hash string) {
}
// OnNewTxAddr notifies users subscribed to bitcoind/addresstxid about new block
func (s *SocketIoServer) OnNewTxAddr(txid string, desc bchain.AddressDescriptor, isOutput bool) {
func (s *SocketIoServer) OnNewTxAddr(txid string, desc bchain.AddressDescriptor) {
addr, searchable, err := s.chainParser.GetAddressesFromAddrDesc(desc)
if err != nil {
glog.Error("GetAddressesFromAddrDesc error ", err, " for descriptor ", desc)
} else if searchable && len(addr) == 1 {
data := map[string]interface{}{"address": addr[0], "txid": txid}
if !isOutput {
data["input"] = true
}
c := s.server.BroadcastTo("bitcoind/addresstxid-"+string(desc), "bitcoind/addresstxid", data)
if c > 0 {
glog.Info("broadcasting new txid ", txid, " for addr ", addr[0], " to ", c, " channels")

575
server/websocket.go Normal file
View File

@ -0,0 +1,575 @@
package server
import (
"blockbook/api"
"blockbook/bchain"
"blockbook/common"
"blockbook/db"
"encoding/json"
"math/big"
"net/http"
"runtime/debug"
"strconv"
"sync"
"sync/atomic"
"time"
"github.com/golang/glog"
"github.com/gorilla/websocket"
"github.com/juju/errors"
)
const upgradeFailed = "Upgrade failed: "
const outChannelSize = 500
const defaultTimeout = 60 * time.Second
var (
// ErrorMethodNotAllowed is returned when client tries to upgrade method other than GET
ErrorMethodNotAllowed = errors.New("Method not allowed")
connectionCounter uint64
)
type websocketReq struct {
ID string `json:"id"`
Method string `json:"method"`
Params json.RawMessage `json:"params"`
}
type websocketRes struct {
ID string `json:"id"`
Data interface{} `json:"data"`
}
type websocketChannel struct {
id uint64
conn *websocket.Conn
out chan *websocketRes
ip string
requestHeader http.Header
alive bool
aliveLock sync.Mutex
}
// WebsocketServer is a handle to websocket server
type WebsocketServer struct {
socket *websocket.Conn
upgrader *websocket.Upgrader
db *db.RocksDB
txCache *db.TxCache
chain bchain.BlockChain
chainParser bchain.BlockChainParser
metrics *common.Metrics
is *common.InternalState
api *api.Worker
block0hash string
newBlockSubscriptions map[*websocketChannel]string
newBlockSubscriptionsLock sync.Mutex
addressSubscriptions map[string]map[*websocketChannel]string
addressSubscriptionsLock sync.Mutex
}
// NewWebsocketServer creates new websocket interface to blockbook and returns its handle
func NewWebsocketServer(db *db.RocksDB, chain bchain.BlockChain, txCache *db.TxCache, metrics *common.Metrics, is *common.InternalState) (*WebsocketServer, error) {
api, err := api.NewWorker(db, chain, txCache, is)
if err != nil {
return nil, err
}
b0, err := db.GetBlockHash(0)
if err != nil {
return nil, err
}
s := &WebsocketServer{
upgrader: &websocket.Upgrader{
ReadBufferSize: 1024 * 32,
WriteBufferSize: 1024 * 32,
CheckOrigin: checkOrigin,
},
db: db,
txCache: txCache,
chain: chain,
chainParser: chain.GetChainParser(),
metrics: metrics,
is: is,
api: api,
block0hash: b0,
newBlockSubscriptions: make(map[*websocketChannel]string),
addressSubscriptions: make(map[string]map[*websocketChannel]string),
}
return s, nil
}
// allow all origins
func checkOrigin(r *http.Request) bool {
return true
}
// ServeHTTP sets up handler of websocket channel
func (s *WebsocketServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if r.Method != "GET" {
http.Error(w, upgradeFailed+ErrorMethodNotAllowed.Error(), 503)
return
}
conn, err := s.upgrader.Upgrade(w, r, nil)
if err != nil {
http.Error(w, upgradeFailed+err.Error(), 503)
return
}
c := &websocketChannel{
id: atomic.AddUint64(&connectionCounter, 1),
conn: conn,
out: make(chan *websocketRes, outChannelSize),
ip: r.RemoteAddr,
requestHeader: r.Header,
alive: true,
}
go s.inputLoop(c)
go s.outputLoop(c)
s.onConnect(c)
}
// GetHandler returns http handler
func (s *WebsocketServer) GetHandler() http.Handler {
return s
}
func (s *WebsocketServer) closeChannel(c *websocketChannel) {
c.aliveLock.Lock()
defer c.aliveLock.Unlock()
if c.alive {
c.conn.Close()
c.alive = false
//clean out
close(c.out)
for len(c.out) > 0 {
<-c.out
}
s.onDisconnect(c)
}
}
func (c *websocketChannel) IsAlive() bool {
c.aliveLock.Lock()
defer c.aliveLock.Unlock()
return c.alive
}
func (s *WebsocketServer) inputLoop(c *websocketChannel) {
defer func() {
if r := recover(); r != nil {
glog.Error("recovered from panic: ", r, ", ", c.id)
debug.PrintStack()
s.closeChannel(c)
}
}()
for {
t, d, err := c.conn.ReadMessage()
if err != nil {
s.closeChannel(c)
return
}
switch t {
case websocket.TextMessage:
var req websocketReq
err := json.Unmarshal(d, &req)
if err != nil {
glog.Error("Error parsing message from ", c.id, ", ", string(d), ", ", err)
s.closeChannel(c)
return
}
go s.onRequest(c, &req)
case websocket.BinaryMessage:
glog.Error("Binary message received from ", c.id, ", ", c.ip)
s.closeChannel(c)
return
case websocket.PingMessage:
c.conn.WriteControl(websocket.PongMessage, nil, time.Now().Add(defaultTimeout))
break
case websocket.CloseMessage:
s.closeChannel(c)
return
case websocket.PongMessage:
// do nothing
}
}
}
func (s *WebsocketServer) outputLoop(c *websocketChannel) {
for m := range c.out {
err := c.conn.WriteJSON(m)
if err != nil {
glog.Error("Error sending message to ", c.id, ", ", err)
s.closeChannel(c)
}
}
}
func (s *WebsocketServer) onConnect(c *websocketChannel) {
glog.Info("Client connected ", c.id, ", ", c.ip)
s.metrics.WebsocketClients.Inc()
}
func (s *WebsocketServer) onDisconnect(c *websocketChannel) {
s.unsubscribeNewBlock(c)
s.unsubscribeAddresses(c)
glog.Info("Client disconnected ", c.id, ", ", c.ip)
s.metrics.WebsocketClients.Dec()
}
var requestHandlers = map[string]func(*WebsocketServer, *websocketChannel, *websocketReq) (interface{}, error){
"getAccountInfo": func(s *WebsocketServer, c *websocketChannel, req *websocketReq) (rv interface{}, err error) {
r, err := unmarshalGetAccountInfoRequest(req.Params)
if err == nil {
rv, err = s.getAccountInfo(r)
}
return
},
"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"`
}{}
err = json.Unmarshal(req.Params, &r)
if err == nil {
rv, err = s.sendTransaction(r.Hex)
}
return
},
"subscribeNewBlock": func(s *WebsocketServer, c *websocketChannel, req *websocketReq) (rv interface{}, err error) {
return s.subscribeNewBlock(c, req)
},
"unsubscribeNewBlock": func(s *WebsocketServer, c *websocketChannel, req *websocketReq) (rv interface{}, err error) {
return s.unsubscribeNewBlock(c)
},
"subscribeAddresses": func(s *WebsocketServer, c *websocketChannel, req *websocketReq) (rv interface{}, err error) {
ad, err := s.unmarshalAddresses(req.Params)
if err == nil {
rv, err = s.subscribeAddresses(c, ad, req)
}
return
},
"unsubscribeAddresses": func(s *WebsocketServer, c *websocketChannel, req *websocketReq) (rv interface{}, err error) {
return s.unsubscribeAddresses(c)
},
}
func (s *WebsocketServer) onRequest(c *websocketChannel, req *websocketReq) {
var err error
var data interface{}
defer func() {
if r := recover(); r != nil {
glog.Error("Client ", c.id, ", onRequest ", req.Method, " recovered from panic: ", r)
debug.PrintStack()
e := resultError{}
e.Error.Message = "Internal error"
data = e
}
// nil data means no response
if data != nil {
c.out <- &websocketRes{
ID: req.ID,
Data: data,
}
}
}()
t := time.Now()
defer s.metrics.WebsocketReqDuration.With(common.Labels{"method": req.Method}).Observe(float64(time.Since(t)) / 1e3) // in microseconds
f, ok := requestHandlers[req.Method]
if ok {
data, err = f(s, c, req)
} else {
err = errors.New("unknown method")
}
if err == nil {
glog.V(1).Info("Client ", c.id, " onRequest ", req.Method, " success")
s.metrics.SocketIORequests.With(common.Labels{"method": req.Method, "status": "success"}).Inc()
} else {
glog.Error("Client ", c.id, " onMessage ", req.Method, ": ", errors.ErrorStack(err))
s.metrics.SocketIORequests.With(common.Labels{"method": req.Method, "status": err.Error()}).Inc()
e := resultError{}
e.Error.Message = err.Error()
data = e
}
}
type accountInfoReq struct {
Descriptor string `json:"descriptor"`
Details string `json:"details"`
PageSize int `json:"pageSize"`
Page int `json:"page"`
FromHeight int `json:"from"`
ToHeight int `json:"to"`
ContractFilter string `json:"contractFilter"`
}
func unmarshalGetAccountInfoRequest(params []byte) (*accountInfoReq, error) {
var r accountInfoReq
err := json.Unmarshal(params, &r)
if err != nil {
return nil, err
}
return &r, nil
}
func (s *WebsocketServer) getAccountInfo(req *accountInfoReq) (res *api.Address, err error) {
var opt api.GetAddressOption
switch req.Details {
case "balance":
opt = api.Balance
case "txids":
opt = api.TxidHistory
case "txs":
opt = api.TxHistory
default:
opt = api.Basic
}
return s.api.GetAddress(req.Descriptor, req.Page, req.PageSize, opt, &api.AddressFilter{
FromHeight: uint32(req.FromHeight),
ToHeight: uint32(req.ToHeight),
Contract: req.ContractFilter,
Vout: api.AddressFilterVoutOff,
})
}
func (s *WebsocketServer) getInfo() (interface{}, error) {
vi := common.GetVersionInfo()
height, hash, err := s.db.GetBestBlock()
if err != nil {
return nil, err
}
type info struct {
Name string `json:"name"`
Shortcut string `json:"shortcut"`
Decimals int `json:"decimals"`
Version string `json:"version"`
BestHeight int `json:"bestheight"`
BestHash string `json:"besthash"`
Block0Hash string `json:"block0hash"`
Testnet bool `json:"testnet"`
}
return &info{
Name: s.is.Coin,
Shortcut: s.is.CoinShortcut,
Decimals: s.chainParser.AmountDecimals(),
BestHeight: int(height),
BestHash: hash,
Version: vi.Version,
Block0Hash: s.block0hash,
Testnet: s.chain.IsTestnet(),
}, 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 {
return res, err
}
res.Result = txid
return
}
type subscriptionResponse struct {
Subscribed bool `json:"subscribed"`
}
func (s *WebsocketServer) subscribeNewBlock(c *websocketChannel, req *websocketReq) (res interface{}, err error) {
s.newBlockSubscriptionsLock.Lock()
defer s.newBlockSubscriptionsLock.Unlock()
s.newBlockSubscriptions[c] = req.ID
return &subscriptionResponse{true}, nil
}
func (s *WebsocketServer) unsubscribeNewBlock(c *websocketChannel) (res interface{}, err error) {
s.newBlockSubscriptionsLock.Lock()
defer s.newBlockSubscriptionsLock.Unlock()
delete(s.newBlockSubscriptions, c)
return &subscriptionResponse{false}, nil
}
func (s *WebsocketServer) unmarshalAddresses(params []byte) ([]bchain.AddressDescriptor, error) {
r := struct {
Addresses []string `json:"addresses"`
}{}
err := json.Unmarshal(params, &r)
if err != nil {
return nil, err
}
rv := make([]bchain.AddressDescriptor, len(r.Addresses))
for i, a := range r.Addresses {
ad, err := s.chainParser.GetAddrDescFromAddress(a)
if err != nil {
return nil, err
}
rv[i] = ad
}
return rv, nil
}
func (s *WebsocketServer) subscribeAddresses(c *websocketChannel, addrDesc []bchain.AddressDescriptor, req *websocketReq) (res interface{}, err error) {
// unsubscribe all previous subscriptions
s.unsubscribeAddresses(c)
s.addressSubscriptionsLock.Lock()
defer s.addressSubscriptionsLock.Unlock()
for i := range addrDesc {
ads := string(addrDesc[i])
as, ok := s.addressSubscriptions[ads]
if !ok {
as = make(map[*websocketChannel]string)
s.addressSubscriptions[ads] = as
}
as[c] = req.ID
}
return &subscriptionResponse{true}, nil
}
// unsubscribeAddresses unsubscribes all address subscriptions by this channel
func (s *WebsocketServer) unsubscribeAddresses(c *websocketChannel) (res interface{}, err error) {
s.addressSubscriptionsLock.Lock()
defer s.addressSubscriptionsLock.Unlock()
for _, sa := range s.addressSubscriptions {
for sc := range sa {
if sc == c {
delete(sa, c)
}
}
}
return &subscriptionResponse{false}, nil
}
// OnNewBlock is a callback that broadcasts info about new block to subscribed clients
func (s *WebsocketServer) OnNewBlock(hash string, height uint32) {
s.newBlockSubscriptionsLock.Lock()
defer s.newBlockSubscriptionsLock.Unlock()
data := struct {
Height uint32 `json:"height"`
Hash string `json:"hash"`
}{
Height: height,
Hash: hash,
}
for c, id := range s.newBlockSubscriptions {
if c.IsAlive() {
c.out <- &websocketRes{
ID: id,
Data: &data,
}
}
}
glog.Info("broadcasting new block ", height, " ", hash, " to ", len(s.newBlockSubscriptions), " channels")
}
// OnNewTxAddr is a callback that broadcasts info about a tx affecting subscribed address
func (s *WebsocketServer) OnNewTxAddr(tx *bchain.Tx, addrDesc bchain.AddressDescriptor) {
// 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()
if ok && len(as) > 0 {
addr, _, err := s.chainParser.GetAddressesFromAddrDesc(addrDesc)
if err != nil {
glog.Error("GetAddressesFromAddrDesc error ", err, " for ", addrDesc)
return
}
if len(addr) == 1 {
atx, err := s.api.GetTransactionFromBchainTx(tx, 0, false, false)
if err != nil {
glog.Error("GetTransactionFromBchainTx error ", err, " for ", tx.Txid)
return
}
data := struct {
Address string `json:"address"`
Tx *api.Tx `json:"tx"`
}{
Address: addr[0],
Tx: atx,
}
// get the list of subscriptions again, this time keep the lock
s.addressSubscriptionsLock.Lock()
defer s.addressSubscriptionsLock.Unlock()
as, ok = s.addressSubscriptions[string(addrDesc)]
if ok {
for c, id := range as {
if c.IsAlive() {
c.out <- &websocketRes{
ID: id,
Data: &data,
}
}
}
glog.Info("broadcasting new tx ", tx.Txid, " for addr ", addr[0], " to ", len(as), " channels")
}
}
}
}

View File

@ -214,6 +214,11 @@ h3 {
font-weight: bold;
}
table.data-table table.data-table th {
border-top: 0;
font-weight: normal;
}
.alert .data-table {
margin: 0;
}
@ -239,19 +244,7 @@ h3 {
font-size: 14px;
}
.h-container {
display: -webkit-box;
display: -ms-flexbox;
-ms-flex-wrap: wrap;
flex-wrap: wrap;
}
.h-container-6 {
width: 50%;
margin: 0;
}
.h-container ul {
.h-container ul, .h-container h3 {
margin: 0;
}

View File

@ -1,6 +1,6 @@
{{define "specific"}}{{$cs := .CoinShortcut}}{{$addr := .Address}}{{$data := .}}
<h1>Address
<small class="text-muted">{{formatAmount $addr.Balance}} {{$cs}}</small>
<h1>{{if $addr.Erc20Contract}}Contract {{$addr.Erc20Contract.Name}} ({{$addr.Erc20Contract.Symbol}}){{else}}Address{{end}}
<small class="text-muted">{{formatAmount $addr.BalanceSat}} {{$cs}}</small>
</h1>
<div class="alert alert-data ellipsis">
<span class="data">{{$addr.AddrStr}}</span>
@ -10,22 +10,61 @@
<div class="col-md-10">
<table class="table data-table">
<tbody>
{{- if eq .ChainType 1 -}}
<tr>
<td style="width: 25%;">Balance</td>
<td class="data">{{formatAmount $addr.BalanceSat}} {{$cs}}</td>
</tr>
<tr>
<td>Non-contract Transactions</td>
<td class="data">{{$addr.TxApperances}}</td>
</tr>
<tr>
<td>Nonce</td>
<td class="data">{{$addr.Nonce}}</td>
</tr>
{{- if $addr.Erc20Tokens -}}
<tr>
<td>ERC20 Tokens</td>
<td style="padding: 0;">
<table class="table data-table">
<tbody>
<tr>
<th>Contract</th>
<th>Tokens</th>
<th style="width: 15%;">Transfers</th>
</tr>
{{- range $et := $addr.Erc20Tokens -}}
<tr>
<td class="data ellipsis"><a href="/address/{{$et.Contract}}">{{$et.Name}}</a></td>
<td class="data">{{formatAmountWithDecimals $et.BalanceSat $et.Decimals}} {{$et.Symbol}}</td>
<td class="data">{{$et.Transfers}}</td>
</tr>
{{- end -}}
</tbody>
</table>
</td>
</tr>
{{- end -}}
</tr>
{{- else -}}
<tr>
<td style="width: 25%;">Total Received</td>
<td class="data">{{formatAmount $addr.TotalReceived}} {{$cs}}</td>
<td class="data">{{formatAmount $addr.TotalReceivedSat}} {{$cs}}</td>
</tr>
<tr>
<td>Total Sent</td>
<td class="data">{{formatAmount $addr.TotalSent}} {{$cs}}</td>
<td class="data">{{formatAmount $addr.TotalSentSat}} {{$cs}}</td>
</tr>
<tr>
<td>Final Balance</td>
<td class="data">{{formatAmount $addr.Balance}} {{$cs}}</td>
<td class="data">{{formatAmount $addr.BalanceSat}} {{$cs}}</td>
</tr>
<tr>
<td>No. Transactions</td>
<td class="data">{{$addr.TxApperances}}</td>
</tr>
{{- end -}}
</tbody>
</table>
</div>
@ -44,7 +83,7 @@
<tbody>
<tr>
<td style="width: 25%;">Unconfirmed Balance</td>
<td class="data">{{formatAmount $addr.UnconfirmedBalance}} {{$cs}}</td>
<td class="data">{{formatAmount $addr.UnconfirmedBalanceSat}} {{$cs}}</td>
</tr>
<tr>
<td>No. Transactions</td>
@ -53,10 +92,23 @@
</tbody>
</table>
</div>
{{- end}}{{if $addr.Transactions -}}
<div class="h-container">
<h3 class="h-container-6">Transactions</h3>
<nav class="h-container-6">{{template "paging" $data}}</nav>
{{- end}}{{if or $addr.Transactions $addr.Filter -}}
<div class="row h-container">
<h3 class="col-md-3">Transactions</h3>
<select class="col-md-2" style="background-color: #eaeaea;" onchange="self.location='?filter='+options[selectedIndex].value">
<option>All</option>
<option {{if eq $addr.Filter "inputs" -}} selected{{end}} value="inputs">Inputs</option>
<option {{if eq $addr.Filter "outputs" -}} selected{{end}} value="outputs">Outputs</option>
{{- if $addr.Erc20Tokens -}}
<option {{if eq $addr.Filter "0" -}} selected{{end}} value="0">Non-contract</option>
{{- range $et := $addr.Erc20Tokens -}}
<option {{if eq $addr.Filter $et.ContractIndex -}} selected{{end}} value="{{$et.ContractIndex}}">{{$et.Name}}</option>
{{- end -}}
{{- end -}}
</select>
<div class="col-md-7">
<nav>{{template "paging" $data}}</nav>
</div>
</div>
<div class="data-div">
{{- range $tx := $addr.Transactions}}{{$data := setTxToTemplateData $data $tx}}{{template "txdetail" $data}}{{end -}}

View File

@ -3,9 +3,9 @@
<div class="alert alert-data ellipsis">
<span class="data">{{$b.Hash}}</span>
</div>
<div class="h-container">
<h3 class="h-container-6">Summary</h3>
<nav class="h-container-6">
<div class="row h-container">
<h3 class="col-md-6 col-sm-12">Summary</h3>
<nav class="col-md-6 col-sm-12">
<ul class="pagination justify-content-end">
<li class="page-item">{{if $b.Prev}}<a class="page-link" href="/block/{{$b.Prev}}">Previous Block</a>{{else}}<span class="page-link text-muted disabled">Previous Block</span>{{end}}</li>
<li class="page-item">{{if $b.Next}}<a class="page-link" href="/block/{{$b.Next}}">Next Block</a>{{else}}<span class="page-link text-muted disabled">Next Block</span>{{end}}</li>
@ -67,9 +67,9 @@
</div>
</div>
{{- if $b.Transactions -}}
<div class="h-container">
<h3 class="h-container-6">Transactions</h3>
<nav class="h-container-6">{{template "paging" $data}}</nav>
<div class="row h-container">
<h3 class="col-md-6 col-sm-12">Transactions</h3>
<nav class="col-md-6 col-sm-12">{{template "paging" $data}}</nav>
</div>
<div class="data-div">
{{- range $tx := $b.Transactions}}{{$data := setTxToTemplateData $data $tx}}{{template "txdetail" $data}}{{end -}}

View File

@ -78,10 +78,12 @@
<td>Difficulty</td>
<td class="data">{{$be.Difficulty}}</td>
</tr>
{{- if $be.Timeoffset -}}
<tr>
<td>Timeoffset</td>
<td class="data">{{$be.Timeoffset}}</td>
</tr>
{{- end -}}
{{- if $be.SizeOnDisk -}}
<tr>
<td>Size On Disk</td>

View File

@ -1,11 +1,11 @@
{{- define "paging"}}{{$data := . -}}{{if $data.PagingRange -}}
<ul class="pagination justify-content-end">
<li class="page-item"><a class="page-link" href="?page={{$data.PrevPage}}">&lt;</a></li>
<li class="page-item"><a class="page-link" href="?page={{$data.PrevPage}}{{$data.PageParams}}">&lt;</a></li>
{{- range $p := $data.PagingRange -}}
<li class="page-item{{if eq $data.Page $p}} active{{end}}">
{{- if $p}}<a class="page-link" href="?page={{$p}}">{{$p}}</a>
{{- if $p}}<a class="page-link" href="?page={{$p}}{{$data.PageParams}}">{{$p}}</a>
{{- else -}}<span class="page-text">...</span>{{- end -}}
</li>{{- end -}}
<li class="page-item"><a class="page-link" href="?page={{$data.NextPage}}">&gt;</a></li>
<li class="page-item"><a class="page-link" href="?page={{$data.NextPage}}{{$data.PageParams}}">&gt;</a></li>
</ul>
{{- end -}}{{- end -}}

View File

@ -1,4 +1,4 @@
{{define "specific"}}{{$cs := .CoinShortcut}}{{$tx := .Tx}}{{$txSpecific := .TxSpecific}}
{{define "specific"}}{{$cs := .CoinShortcut}}{{$tx := .Tx}}
<h1>Transaction</h1>
<div class="alert alert-data ellipsis">
<span class="data">{{$tx.Txid}}</span>
@ -7,33 +7,64 @@
<div class="data-div">
<table class="table data-table">
<tbody>
{{if $tx.Confirmations}}
{{- if $tx.Confirmations -}}
<tr>
<td style="width: 25%;">Mined Time</td>
<td class="data">{{formatUnixTime $tx.Blocktime}}</td>
</tr>{{end}}
</tr>{{end -}}
<tr>
<td style="width: 25%;">In Block</td>
<td class="ellipsis data">{{if $tx.Confirmations}}{{$tx.Blockhash}}{{else}}Unconfirmed{{end}}</td>
</tr>
{{if $tx.Confirmations}}
{{- if $tx.Confirmations -}}
<tr>
<td>In Block Height</td>
<td class="data"><a href="/block/{{$tx.Blockheight}}">{{$tx.Blockheight}}</a></td>
</tr>{{end}}
{{- if $tx.EthereumSpecific -}}
<tr>
<td>Status</td>
{{- if $tx.EthereumSpecific.Status -}}
{{- if eq $tx.EthereumSpecific.Status 1 -}}
<td class="data text-success">Success</td>
{{- else -}}
{{- if eq $tx.EthereumSpecific.Status -1 -}}
<td class="data">Pending</td>
{{- else -}}
<td class="data">Unknown</td>
{{- end -}}
{{- end -}}
{{- else -}}
<td class="data text-danger">Fail</td>
{{- end -}}
</tr>
<tr>
<td>Value</td>
<td class="data">{{formatAmount $tx.ValueOutSat}} {{$cs}}</td>
</tr>
<tr>
<td>Gas Used / Limit</td>
<td class="data">{{if $tx.EthereumSpecific.GasUsed}}{{$tx.EthereumSpecific.GasUsed}}{{else}}pending{{end}} / {{$tx.EthereumSpecific.GasLimit}}</td>
</tr>
<tr>
<td>Gas Price</td>
<td class="data">{{formatAmount $tx.EthereumSpecific.GasPrice}} {{$cs}}</td>
</tr>
{{- else -}}
<tr>
<td>Total Input</td>
<td class="data">{{formatAmount $tx.ValueIn}} {{$cs}}</td>
<td class="data">{{formatAmount $tx.ValueInSat}} {{$cs}}</td>
</tr>
<tr>
<td>Total Output</td>
<td class="data">{{formatAmount $tx.ValueOut}} {{$cs}}</td>
<td class="data">{{formatAmount $tx.ValueOutSat}} {{$cs}}</td>
</tr>
{{if $tx.Fees}}
{{- end -}}
{{- if $tx.FeesSat -}}
<tr>
<td>Fees</td>
<td class="data">{{formatAmount $tx.Fees}} {{$cs}}</td>
</tr>{{end}}
<td class="data">{{formatAmount $tx.FeesSat}} {{$cs}}</td>
</tr>{{end -}}
</tbody>
</table>
</div>
@ -47,7 +78,7 @@
<pre id="txSpecific"></pre>
</div>
<script type="text/javascript">
txSpecific = {{ $txSpecific }};
txSpecific = {{$tx.CoinSpecificJSON}};
function syntaxHighlight(json) {
json = JSON.stringify(json, undefined, 2);
json = json.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');

View File

@ -24,9 +24,9 @@
{{if and (ne $a $addr) $vin.Searchable}}<a href="/address/{{$a}}">{{$a}}</a>{{else}}{{$a}}{{end}}
</span>
{{- else -}}
<span class="float-left">{{- if $vin.ScriptSig.Hex -}}Unparsed address{{- else -}}No Inputs (Newly Generated Coins){{- end -}}</span>
<span class="float-left">{{- if $vin.Hex -}}Unparsed address{{- else -}}No Inputs (Newly Generated Coins){{- end -}}</span>
{{- end -}}{{- if $vin.Addresses -}}
<span class="float-right{{if stringInSlice $addr $vin.Addresses}} text-danger{{end}}">{{formatAmount $vin.Value}} {{$cs}}</span>
<span class="float-right{{if stringInSlice $addr $vin.Addresses}} text-danger{{end}}">{{formatAmount $vin.ValueSat}} {{$cs}}</span>
{{- end -}}
</td>
</tr>
@ -51,15 +51,15 @@
{{- range $vout := $tx.Vout -}}
<tr>
<td>
{{- range $a := $vout.ScriptPubKey.Addresses -}}
{{- range $a := $vout.Addresses -}}
<span class="ellipsis float-left">
{{- if and (ne $a $addr) $vout.ScriptPubKey.Searchable}}<a href="/address/{{$a}}">{{$a}}</a>{{else}}{{$a}}{{- end -}}
{{- if and (ne $a $addr) $vout.Searchable}}<a href="/address/{{$a}}">{{$a}}</a>{{else}}{{$a}}{{- end -}}
</span>
{{- else -}}
<span class="float-left">Unparsed address</span>
{{- end -}}
<span class="float-right{{if stringInSlice $addr $vout.ScriptPubKey.Addresses}} text-success{{end}}">
{{formatAmount $vout.Value}} {{$cs}} {{if $vout.Spent}}<a class="text-danger" href="{{if $vout.SpentTxID}}/tx/{{$vout.SpentTxID}}{{else}}/spending/{{$tx.Txid}}/{{$vout.N}}{{end}}" title="Spent"></a>{{else -}}
<span class="float-right{{if stringInSlice $addr $vout.Addresses}} text-success{{end}}">
{{formatAmount $vout.ValueSat}} {{$cs}} {{if $vout.Spent}}<a class="text-danger" href="{{if $vout.SpentTxID}}/tx/{{$vout.SpentTxID}}{{else}}/spending/{{$tx.Txid}}/{{$vout.N}}{{end}}" title="Spent"></a>{{else -}}
<span class="text-success" title="Unspent"> <b>×</b></span>
{{- end -}}
</span>
@ -77,8 +77,8 @@
</div>
<div class="row line-top">
<div class="col-xs-6 col-sm-4 col-md-4">
{{- if $tx.Fees -}}
<span class="txvalues txvalues-default">Fee: {{formatAmount $tx.Fees}} {{$cs}}</span>
{{- if $tx.FeesSat -}}
<span class="txvalues txvalues-default">Fee: {{formatAmount $tx.FeesSat}} {{$cs}}</span>
{{- end -}}
</div>
<div class="col-xs-6 col-sm-8 col-md-8 text-right">
@ -87,7 +87,7 @@
{{- else -}}
<span class="txvalues txvalues-danger ng-hide">Unconfirmed Transaction!</span>
{{- end -}}
<span class="txvalues txvalues-primary">{{formatAmount $tx.ValueOut}} {{$cs}}</span>
<span class="txvalues txvalues-primary">{{formatAmount $tx.ValueOutSat}} {{$cs}}</span>
</div>
</div>
</div>

View File

@ -0,0 +1,130 @@
{{define "txdetail"}}{{$cs := .CoinShortcut}}{{$addr := .AddrStr}}{{$tx := .Tx}}
<div class="alert alert-data"{{if eq $tx.EthereumSpecific.Status 0}} style="background-color: #faf2ee;"{{end}}>
<div class="row line-bot">
<div class="col-xs-7 col-md-8 ellipsis">
<a href="/tx/{{$tx.Txid}}">{{$tx.Txid}}</a>
{{if eq $tx.EthereumSpecific.Status 1}}<span class="text-success"></span>{{end}}{{if eq $tx.EthereumSpecific.Status 0}}<span class="text-danger"></span>{{end}}
</div>
{{- if $tx.Confirmations -}}
<div class="col-xs-5 col-md-4 text-muted text-right">mined {{formatUnixTime $tx.Blocktime}}</div>
{{- end -}}
</div>
<div class="row line-mid">
<div class="col-md-4">
<div class="row">
<table class="table data-table">
<tbody>
{{- range $vin := $tx.Vin -}}
<tr>
<td>
{{- range $a := $vin.Addresses -}}
<span class="ellipsis float-left">
{{if and (ne $a $addr) $vin.Searchable}}<a href="/address/{{$a}}">{{$a}}</a>{{else}}{{$a}}{{end}}
</span>
{{- else -}}
<span class="float-left">Unparsed address</span>
{{- end -}}
</td>
</tr>
{{- else -}}
<tr>
<td>No Inputs</td>
</tr>
{{- end -}}
</tbody>
</table>
</div>
</div>
<div class="col-md-1 col-xs-12 text-center">
<svg class="octicon" viewBox="0 0 8 16">
<path fill-rule="evenodd" d="M7.5 8l-5 5L1 11.5 4.75 8 1 4.5 2.5 3l5 5z"></path>
</svg>
</div>
<div class="col-md-4">
<div class="row">
<table class="table data-table">
<tbody>
{{- range $vout := $tx.Vout -}}
<tr>
<td>
{{- range $a := $vout.Addresses -}}
<span class="ellipsis float-left">
{{- if and (ne $a $addr) $vout.Searchable}}<a href="/address/{{$a}}">{{$a}}</a>{{else}}{{$a}}{{- end -}}
</span>
{{- else -}}
<span class="float-left">Unparsed address</span>
{{- end -}}
</td>
</tr>
{{- else -}}
<tr>
<td>No Outputs</td>
</tr>
{{- end -}}
</tbody>
</table>
</div>
</div>
<div class="col-md-3 text-right" style="padding: .4rem 0;">
{{formatAmount $tx.ValueOutSat}} {{$cs}}
</div>
</div>
{{- if $tx.TokenTransfers -}}
<div class="row line-top" style="padding: 15px 0 6px 15px;font-weight: bold;">
ERC20 Token Transfers
</div>
{{- range $erc20 := $tx.TokenTransfers -}}
<div class="row" style="padding: 2px 15px;">
<div class="col-md-4">
<div class="row">
<table class="table data-table">
<tbody>
<tr>
<td>
<span class="ellipsis float-left">{{if ne $erc20.From $addr}}<a href="/address/{{$erc20.From}}">{{$erc20.From}}</a>{{else}}{{$erc20.From}}{{end}}</span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="col-md-1 col-xs-12 text-center">
<svg class="octicon" viewBox="0 0 8 16">
<path fill-rule="evenodd" d="M7.5 8l-5 5L1 11.5 4.75 8 1 4.5 2.5 3l5 5z"></path>
</svg>
</div>
<div class="col-md-4">
<div class="row">
<table class="table data-table">
<tbody>
<tr>
<td>
<span class="ellipsis float-left">{{if ne $erc20.To $addr}}<a href="/address/{{$erc20.To}}">{{$erc20.To}}</a>{{else}}{{$erc20.To}}{{end}}</span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="col-md-3 text-right" style="padding: .4rem 0;">{{formatAmountWithDecimals $erc20.Value $erc20.Decimals}} {{$erc20.Symbol}}</div>
</div>
{{- end -}}
<div class="row" style="padding: 6px 15px;"></div>
{{- end -}}
<div class="row line-top">
<div class="col-xs-6 col-sm-4 col-md-4">
{{- if $tx.FeesSat -}}
<span class="txvalues txvalues-default">Fee: {{formatAmount $tx.FeesSat}} {{$cs}}</span>
{{- end -}}
</div>
<div class="col-xs-6 col-sm-8 col-md-8 text-right">
{{- if $tx.Confirmations -}}
<span class="txvalues txvalues-success">{{$tx.Confirmations}} Confirmations</span>
{{- else -}}
<span class="txvalues txvalues-danger ng-hide">Unconfirmed Transaction!</span>
{{- end -}}
<span class="txvalues txvalues-primary">{{formatAmount $tx.ValueOutSat}} {{$cs}}</span>
</div>
</div>
</div>
{{end}}

View File

@ -11,7 +11,7 @@
}
</style>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/1.7.4/socket.io.js"></script>
<title>Test socket.io</title>
<title>Blockbook Socket.io Test Page</title>
<script>
var socket;
function connect(server) {
@ -251,7 +251,7 @@
<body>
<div class="container">
<div class="row justify-content-center">
<h1>Socket.io tester</h1>
<h1>Blockbook Socket.io Test Page</h1>
</div>
<div class="row">
<div class="col">

349
static/test-websocket.html Normal file
View File

@ -0,0 +1,349 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
<style>
.row {
margin-top: 1%;
}
</style>
<title>Blockbook Websocket Test Page</title>
<script>
var ws;
var messageID;
var pendingMessages;
var subscriptions;
var subscriptionNewBlockId;
var subscriptionAddressesId;
function send(method, params, callback) {
var id = messageID.toString();
messageID++;
pendingMessages[id] = callback;
var req = {
id,
method,
params
}
ws.send(JSON.stringify(req));
return id;
}
function subscribe(method, params, callback) {
var id = messageID.toString();
messageID++;
subscriptions[id] = callback;
var req = {
id,
method,
params
}
ws.send(JSON.stringify(req));
return id;
}
function unsubscribe(method, id, params, callback) {
delete subscriptions[id];
pendingMessages[id] = callback;
var req = {
id,
method,
params
}
ws.send(JSON.stringify(req));
return id;
}
function connect(server) {
messageID = 0;
pendingMessages = {};
subscriptions = {};
subscribeNewBlockId = "";
subscribeAddressesId = "";
if (server.startsWith("http")) {
server = server.replace("http", "ws");
}
if (!server.endsWith("/websocket")) {
server += "/websocket";
}
ws = new WebSocket(server);
ws.onopen = function (e) {
console.log('socket connected', e);
document.getElementById('connectionStatus').innerText = "connected";
};
ws.onclose = function (e) {
console.log('socket closed', e);
document.getElementById('connectionStatus').innerText = "disconnected";
};
ws.onerror = function (e) {
console.log('socket error ', e);
document.getElementById('connectionStatus').innerText = "error";
};
ws.onmessage = function (e) {
console.log('resp ' + e.data);
var resp = JSON.parse(e.data);
var f = pendingMessages[resp.id];
if (f != undefined) {
delete pendingMessages[resp.id];
f(resp.data);
} else {
f = subscriptions[resp.id];
if (f != undefined) {
f(resp.data);
}
else {
console.log("unkown response " + resp.id);
}
}
};
}
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');
const details = selectDetails.options[selectDetails.selectedIndex].value;
const page = parseInt(document.getElementById("getAccountInfoPage").value);
const from = parseInt(document.getElementById("getAccountInfoFrom").value);
const to = parseInt(document.getElementById("getAccountInfoTo").value);
const contractFilter = document.getElementById("getAccountInfoContract").value.trim();
const pageSize = 10;
const method = 'getAccountInfo';
const params = {
descriptor,
details,
page,
pageSize,
from,
to,
contractFilter
};
send(method, params, function (result) {
document.getElementById('getAccountInfoResult').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() {
var hex = document.getElementById('sendTransactionHex').value.trim();
const method = 'sendTransaction';
const params = {
hex,
};
send(method, params, function (result) {
document.getElementById('sendTransactionResult').innerText = JSON.stringify(result).replace(/,/g, ", ");
});
}
function subscribeNewBlock() {
const method = 'subscribeNewBlock';
const params = {
};
if (subscribeNewBlockId) {
delete subscriptions[subscribeNewBlockId];
subscribeNewBlockId = "";
}
subscribeNewBlockId = subscribe(method, params, function (result) {
document.getElementById('subscribeNewBlockResult').innerText += JSON.stringify(result).replace(/,/g, ", ") + "\n";
});
document.getElementById('subscribeNewBlockId').innerText = subscribeNewBlockId;
document.getElementById('unsubscribeNewBlockButton').setAttribute("style", "display: inherit;");
}
function unsubscribeNewBlock() {
const method = 'unsubscribeNewBlock';
const params = {
};
unsubscribe(method, subscribeNewBlockId, params, function (result) {
subscribeNewBlockId = "";
document.getElementById('subscribeNewBlockResult').innerText += JSON.stringify(result).replace(/,/g, ", ") + "\n";
document.getElementById('subscribeNewBlockId').innerText = "";
document.getElementById('unsubscribeNewBlockButton').setAttribute("style", "display: none;");
});
}
function subscribeAddresses() {
const method = 'subscribeAddresses';
var addresses = document.getElementById('subscribeAddressesName').value.split(",");
addresses = addresses.map(s => s.trim());
const params = {
addresses
};
if (subscribeAddressesId) {
delete subscriptions[subscribeAddressesId];
subscribeAddressesId = "";
}
subscribeAddressesId = subscribe(method, params, function (result) {
document.getElementById('subscribeAddressesResult').innerText += JSON.stringify(result).replace(/,/g, ", ") + "\n";
});
document.getElementById('subscribeAddressesIds').innerText = subscribeAddressesId;
document.getElementById('unsubscribeAddressesButton').setAttribute("style", "display: inherit;");
}
function unsubscribeAddresses() {
const method = 'unsubscribeAddresses';
const params = {
};
unsubscribe(method, subscribeAddressesId, params, function (result) {
subscribeAddressesId = "";
document.getElementById('subscribeAddressesResult').innerText += JSON.stringify(result).replace(/,/g, ", ") + "\n";
document.getElementById('subscribeAddressesIds').innerText = "";
document.getElementById('unsubscribeAddressesButton').setAttribute("style", "display: none;");
});
}
</script>
</head>
<body>
<div class="container">
<div class="row justify-content-center">
<h1>Blockbook Websocket Test Page</h1>
</div>
<div class="row">
<div class="col">
<input class="btn btn-secondary" type="button" value="Login" onclick="connect(document.getElementById('serverAddress').value)">
</div>
<div class="col-8">
<input type="text" class="form-control" id="serverAddress" value="">
</div>
<div class="col form-inline">
<label id="connectionStatus">not connected</label>
</div>
</div>
<div class="row">
<div class="col">
<input class="btn btn-secondary" type="button" value="getInfo" onclick="getInfo()">
</div>
<div class="col-10" id="getInfoResult">
</div>
</div>
<div class="row">
<div class="col">
<input class="btn btn-secondary" type="button" value="getAccountInfo" onclick="getAccountInfo()">
</div>
<div class="col-8">
<div class="row" style="margin: 0;">
<input type="text" style="width: 79%" class="form-control" id="getAccountInfoDescriptor" value="0xba98d6a5ac827632e3457de7512d211e4ff7e8bd">
<select id="getAccountInfoDetails" style="width: 20%; margin-left: 5px;">
<option value="basic">Basic</option>
<option value="balance">Balance</option>
<option value="txids">Txids</option>
<option value="txs">Transactions</option>
</select>
</div>
<div class="row" style="margin: 0; margin-top: 5px;">
<input type="text" placeholder="page" style="width: 10%; margin-right: 5px;" class="form-control" id="getAccountInfoPage">
<input type="text" placeholder="from" style="width: 15%;margin-left: 5px;margin-right: 5px;" class="form-control" id="getAccountInfoFrom">
<input type="text" placeholder="to" style="width: 15%; margin-left: 5px; margin-right: 5px;" class="form-control" id="getAccountInfoTo">
<input type="text" placeholder="contract" style="width: 55%; margin-left: 5px; margin-right: 5px;" class="form-control" id="getAccountInfoContract">
</div>
</div>
<div class="col form-inline"></div>
</div>
<div class="row">
<div class="col" id="getAccountInfoResult">
</div>
</div>
<div class="row">
<div class="col">
<input class="btn btn-secondary" type="button" value="estimateFee" onclick="estimateFee()">
</div>
<div class="col-8">
<div class="row" style="margin: 0;">
<input type="text" placeholder="comma separated list of block targets" class="form-control" id="estimateFeeBlocks" value="2,5,10,20">
</div>
<div class="row" style="margin: 0; margin-top: 5px;">
<input type="text" placeholder="tx specific JSON" class="form-control" id="estimateFeeSpecific" value="">
</div>
</div>
<div class="col"></div>
</div>
<div class="row">
<div class="col" id="estimateFeeResult">
</div>
</div>
<div class="row">
<div class="col">
<input class="btn btn-secondary" type="button" value="sendTransaction" onclick="sendTransaction()">
</div>
<div class="col-8">
<input type="text" class="form-control" id="sendTransactionHex" value="010000000001019d64f0c72a0d206001decbffaa722eb1044534c74eee7a5df8318e42a4323ec10000000017160014550da1f5d25a9dae2eafd6902b4194c4c6500af6ffffffff02809698000000000017a914cd668d781ece600efa4b2404dc91fd26b8b8aed8870553d7360000000017a914246655bdbd54c7e477d0ea2375e86e0db2b8f80a8702473044022076aba4ad559616905fa51d4ddd357fc1fdb428d40cb388e042cdd1da4a1b7357022011916f90c712ead9a66d5f058252efd280439ad8956a967e95d437d246710bc9012102a80a5964c5612bb769ef73147b2cf3c149bc0fd4ecb02f8097629c94ab013ffd00000000">
</div>
<div class="col">
</div>
</div>
<div class="row">
<div class="col" id="sendTransactionResult"></div>
</div>
<div class="row">
<div class="col">
<input class="btn btn-secondary" type="button" value="subscribe new block" onclick="subscribeNewBlock()">
</div>
<div class="col-4">
<span id="subscribeNewBlockId"></span>
</div>
<div class="col">
<input class="btn btn-secondary" id="unsubscribeNewBlockButton" style="display: none;" type="button" value="unsubscribe" onclick="unsubscribeNewBlock()">
</div>
</div>
<div class="row">
<div class="col" id="subscribeNewBlockResult"></div>
</div>
<div class="row">
<div class="col">
<input class="btn btn-secondary" type="button" value="subscribe address" onclick="subscribeAddresses()">
</div>
<div class="col-8">
<input type="text" class="form-control" id="subscribeAddressesName" value="0xba98d6a5ac827632e3457de7512d211e4ff7e8bd,0x73d0385f4d8e00c5e6504c6030f47bf6212736a8">
</div>
<div class="col">
<span id="subscribeAddressesIds"></span>
</div>
<div class="col">
<input class="btn btn-secondary" id="unsubscribeAddressesButton" style="display: none;" type="button" value="unsubscribe" onclick="unsubscribeAddresses()">
</div>
</div>
<div class="row">
<div class="col" id="subscribeAddressesResult"></div>
</div>
</div>
</body>
<script>
document.getElementById('serverAddress').value = window.location.protocol + "//" + window.location.host;
</script>
</html>

View File

@ -54,7 +54,7 @@ func AddressToPubKeyHex(addr string, parser bchain.BlockChainParser) string {
return hex.EncodeToString(b)
}
func GetTestUTXOBlock1(parser bchain.BlockChainParser) *bchain.Block {
func GetTestBitcoinTypeBlock1(parser bchain.BlockChainParser) *bchain.Block {
return &bchain.Block{
BlockHeader: bchain.BlockHeader{
Height: 225493,
@ -66,6 +66,7 @@ func GetTestUTXOBlock1(parser bchain.BlockChainParser) *bchain.Block {
Txs: []bchain.Tx{
bchain.Tx{
Txid: TxidB1T1,
Vin: []bchain.Vin{},
Vout: []bchain.Vout{
bchain.Vout{
N: 0,
@ -119,7 +120,7 @@ func GetTestUTXOBlock1(parser bchain.BlockChainParser) *bchain.Block {
}
}
func GetTestUTXOBlock2(parser bchain.BlockChainParser) *bchain.Block {
func GetTestBitcoinTypeBlock2(parser bchain.BlockChainParser) *bchain.Block {
return &bchain.Block{
BlockHeader: bchain.BlockHeader{
Height: 225494,

File diff suppressed because one or more lines are too long

View File

@ -9,11 +9,11 @@ import (
)
type fakeBlockChain struct {
parser bchain.BlockChainParser
*bchain.BaseChain
}
func NewFakeBlockChain(parser bchain.BlockChainParser) (*fakeBlockChain, error) {
return &fakeBlockChain{parser: parser}, nil
return &fakeBlockChain{&bchain.BaseChain{Parser: parser}}, nil
}
func (c *fakeBlockChain) Initialize() error {
@ -45,26 +45,26 @@ func (c *fakeBlockChain) GetChainInfo() (v *bchain.ChainInfo, err error) {
Chain: c.GetNetworkName(),
Blocks: 2,
Headers: 2,
Bestblockhash: GetTestUTXOBlock2(c.parser).BlockHeader.Hash,
Bestblockhash: GetTestBitcoinTypeBlock2(c.Parser).BlockHeader.Hash,
Version: "001001",
Subversion: c.GetSubversion(),
}, nil
}
func (c *fakeBlockChain) GetBestBlockHash() (v string, err error) {
return GetTestUTXOBlock2(c.parser).BlockHeader.Hash, nil
return GetTestBitcoinTypeBlock2(c.Parser).BlockHeader.Hash, nil
}
func (c *fakeBlockChain) GetBestBlockHeight() (v uint32, err error) {
return GetTestUTXOBlock2(c.parser).BlockHeader.Height, nil
return GetTestBitcoinTypeBlock2(c.Parser).BlockHeader.Height, nil
}
func (c *fakeBlockChain) GetBlockHash(height uint32) (v string, err error) {
b1 := GetTestUTXOBlock1(c.parser)
b1 := GetTestBitcoinTypeBlock1(c.Parser)
if height == b1.BlockHeader.Height {
return b1.BlockHeader.Hash, nil
}
b2 := GetTestUTXOBlock2(c.parser)
b2 := GetTestBitcoinTypeBlock2(c.Parser)
if height == b2.BlockHeader.Height {
return b2.BlockHeader.Hash, nil
}
@ -72,11 +72,11 @@ func (c *fakeBlockChain) GetBlockHash(height uint32) (v string, err error) {
}
func (c *fakeBlockChain) GetBlockHeader(hash string) (v *bchain.BlockHeader, err error) {
b1 := GetTestUTXOBlock1(c.parser)
b1 := GetTestBitcoinTypeBlock1(c.Parser)
if hash == b1.BlockHeader.Hash {
return &b1.BlockHeader, nil
}
b2 := GetTestUTXOBlock2(c.parser)
b2 := GetTestBitcoinTypeBlock2(c.Parser)
if hash == b2.BlockHeader.Hash {
return &b2.BlockHeader, nil
}
@ -84,11 +84,11 @@ func (c *fakeBlockChain) GetBlockHeader(hash string) (v *bchain.BlockHeader, err
}
func (c *fakeBlockChain) GetBlock(hash string, height uint32) (v *bchain.Block, err error) {
b1 := GetTestUTXOBlock1(c.parser)
b1 := GetTestBitcoinTypeBlock1(c.Parser)
if hash == b1.BlockHeader.Hash || height == b1.BlockHeader.Height {
return b1, nil
}
b2 := GetTestUTXOBlock2(c.parser)
b2 := GetTestBitcoinTypeBlock2(c.Parser)
if hash == b2.BlockHeader.Hash || height == b2.BlockHeader.Height {
return b2, nil
}
@ -106,11 +106,11 @@ func getBlockInfo(b *bchain.Block) *bchain.BlockInfo {
}
func (c *fakeBlockChain) GetBlockInfo(hash string) (v *bchain.BlockInfo, err error) {
b1 := GetTestUTXOBlock1(c.parser)
b1 := GetTestBitcoinTypeBlock1(c.Parser)
if hash == b1.BlockHeader.Hash {
return getBlockInfo(b1), nil
}
b2 := GetTestUTXOBlock2(c.parser)
b2 := GetTestBitcoinTypeBlock2(c.Parser)
if hash == b2.BlockHeader.Hash {
return getBlockInfo(b2), nil
}
@ -131,9 +131,9 @@ func getTxInBlock(b *bchain.Block, txid string) *bchain.Tx {
}
func (c *fakeBlockChain) GetTransaction(txid string) (v *bchain.Tx, err error) {
v = getTxInBlock(GetTestUTXOBlock1(c.parser), txid)
v = getTxInBlock(GetTestBitcoinTypeBlock1(c.Parser), txid)
if v == nil {
v = getTxInBlock(GetTestUTXOBlock2(c.parser), txid)
v = getTxInBlock(GetTestBitcoinTypeBlock2(c.Parser), txid)
}
if v != nil {
return v, nil
@ -141,8 +141,8 @@ func (c *fakeBlockChain) GetTransaction(txid string) (v *bchain.Tx, err error) {
return nil, errors.New("Not found")
}
func (c *fakeBlockChain) GetTransactionSpecific(txid string) (v json.RawMessage, err error) {
tx, err := c.GetTransaction(txid)
func (c *fakeBlockChain) GetTransactionSpecific(tx *bchain.Tx) (v json.RawMessage, err error) {
tx, err = c.GetTransaction(tx.Txid)
if err != nil {
return nil, err
}
@ -182,12 +182,12 @@ func (c *fakeBlockChain) ResyncMempool(onNewTxAddr bchain.OnNewTxAddrFunc) (coun
return 0, errors.New("Not implemented")
}
func (c *fakeBlockChain) GetMempoolTransactions(address string) (v []string, err error) {
func (c *fakeBlockChain) GetMempoolTransactions(address string) (v []bchain.Outpoint, err error) {
return nil, errors.New("Not implemented")
}
func (c *fakeBlockChain) GetMempoolTransactionsForAddrDesc(addrDesc bchain.AddressDescriptor) (v []string, err error) {
return []string{}, nil
func (c *fakeBlockChain) GetMempoolTransactionsForAddrDesc(addrDesc bchain.AddressDescriptor) (v []bchain.Outpoint, err error) {
return []bchain.Outpoint{}, nil
}
func (c *fakeBlockChain) GetMempoolEntry(txid string) (v *bchain.MempoolEntry, err error) {
@ -195,5 +195,5 @@ func (c *fakeBlockChain) GetMempoolEntry(txid string) (v *bchain.MempoolEntry, e
}
func (c *fakeBlockChain) GetChainParser() bchain.BlockChainParser {
return c.parser
return c.Parser
}

View File

@ -166,6 +166,8 @@ func testGetTransaction(t *testing.T, h *TestHandler) {
continue
}
got.Confirmations = 0
// CoinSpecificData are not specified in the fixtures
got.CoinSpecificData = nil
if !reflect.DeepEqual(got, want) {
t.Errorf("GetTransaction() got %+v, want %+v", got, want)
@ -218,7 +220,7 @@ func testMempoolSync(t *testing.T, h *TestHandler) {
if err != nil {
t.Fatalf("address %q: %s", a, err)
}
if !containsString(got, txid) {
if !containsTx(got, txid) {
t.Errorf("ResyncMempool() - for address %s, transaction %s wasn't found in mempool", a, txid)
return
}
@ -399,9 +401,9 @@ func intersect(a, b []string) []string {
return res
}
func containsString(slice []string, s string) bool {
for i := range slice {
if slice[i] == s {
func containsTx(o []bchain.Outpoint, tx string) bool {
for i := range o {
if o[i].Txid == tx {
return true
}
}

View File

@ -107,7 +107,7 @@ func verifyBlockInfo(t *testing.T, d *db.RocksDB, h *TestHandler, rng Range) {
func verifyTransactions(t *testing.T, d *db.RocksDB, h *TestHandler, rng Range) {
type txInfo struct {
txid string
vout uint32
vout int32
isOutput bool
}
addr2txs := make(map[string][]txInfo)
@ -122,13 +122,13 @@ func verifyTransactions(t *testing.T, d *db.RocksDB, h *TestHandler, rng Range)
for _, tx := range block.TxDetails {
for _, vin := range tx.Vin {
for _, a := range vin.Addresses {
addr2txs[a] = append(addr2txs[a], txInfo{tx.Txid, vin.Vout, false})
addr2txs[a] = append(addr2txs[a], txInfo{tx.Txid, int32(vin.Vout), false})
checkMap[a] = append(checkMap[a], false)
}
}
for _, vout := range tx.Vout {
for _, a := range vout.ScriptPubKey.Addresses {
addr2txs[a] = append(addr2txs[a], txInfo{tx.Txid, vout.N, true})
addr2txs[a] = append(addr2txs[a], txInfo{tx.Txid, int32(vout.N), true})
checkMap[a] = append(checkMap[a], false)
}
}
@ -136,7 +136,7 @@ func verifyTransactions(t *testing.T, d *db.RocksDB, h *TestHandler, rng Range)
}
for addr, txs := range addr2txs {
err := d.GetTransactions(addr, rng.Lower, rng.Upper, func(txid string, vout uint32, isOutput bool) error {
err := d.GetTransactions(addr, rng.Lower, rng.Upper, func(txid string, vout int32, isOutput bool) error {
for i, tx := range txs {
if txid == tx.txid && vout == tx.vout && isOutput == tx.isOutput {
checkMap[addr][i] = true

View File

@ -129,7 +129,7 @@ func verifyTransactions2(t *testing.T, d *db.RocksDB, rng Range, addr2txs map[st
checkMap[txid] = false
}
err := d.GetTransactions(addr, rng.Lower, rng.Upper, func(txid string, vout uint32, isOutput bool) error {
err := d.GetTransactions(addr, rng.Lower, rng.Upper, func(txid string, vout int32, isOutput bool) error {
if isOutput {
checkMap[txid] = true
}