Add ERC20 transfer information to ethereum transactions
This commit is contained in:
parent
1f32a39d16
commit
8ac57a3d56
70
api/types.go
70
api/types.go
@ -2,6 +2,7 @@ package api
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"blockbook/bchain"
|
"blockbook/bchain"
|
||||||
|
"blockbook/bchain/coins/eth"
|
||||||
"blockbook/common"
|
"blockbook/common"
|
||||||
"blockbook/db"
|
"blockbook/db"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
@ -69,28 +70,49 @@ type Vout struct {
|
|||||||
SpentHeight int `json:"spentHeight,omitempty"`
|
SpentHeight int `json:"spentHeight,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Erc20Token contains info about ERC20 token held by an address
|
||||||
|
type Erc20Token struct {
|
||||||
|
Contract string `json:"contract"`
|
||||||
|
Txs int `json:"txs"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Symbol string `json:"symbol"`
|
||||||
|
Balance string `json:"balance"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Erc20Transfer contains info about ERC20 transfer done in a transaction
|
||||||
|
type Erc20Transfer struct {
|
||||||
|
From string `json:"from"`
|
||||||
|
To string `json:"to"`
|
||||||
|
Contract string `json:"contract"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Symbol string `json:"symbol"`
|
||||||
|
Tokens string `json:"tokens"`
|
||||||
|
}
|
||||||
|
|
||||||
// Tx holds information about a transaction
|
// Tx holds information about a transaction
|
||||||
type Tx struct {
|
type Tx struct {
|
||||||
Txid string `json:"txid"`
|
Txid string `json:"txid"`
|
||||||
Version int32 `json:"version,omitempty"`
|
Version int32 `json:"version,omitempty"`
|
||||||
Locktime uint32 `json:"locktime,omitempty"`
|
Locktime uint32 `json:"locktime,omitempty"`
|
||||||
Vin []Vin `json:"vin"`
|
Vin []Vin `json:"vin"`
|
||||||
Vout []Vout `json:"vout"`
|
Vout []Vout `json:"vout"`
|
||||||
Blockhash string `json:"blockhash,omitempty"`
|
Blockhash string `json:"blockhash,omitempty"`
|
||||||
Blockheight int `json:"blockheight"`
|
Blockheight int `json:"blockheight"`
|
||||||
Confirmations uint32 `json:"confirmations"`
|
Confirmations uint32 `json:"confirmations"`
|
||||||
Time int64 `json:"time,omitempty"`
|
Time int64 `json:"time,omitempty"`
|
||||||
Blocktime int64 `json:"blocktime"`
|
Blocktime int64 `json:"blocktime"`
|
||||||
ValueOut string `json:"valueOut"`
|
ValueOut string `json:"valueOut"`
|
||||||
ValueOutSat big.Int `json:"-"`
|
ValueOutSat big.Int `json:"-"`
|
||||||
Size int `json:"size,omitempty"`
|
Size int `json:"size,omitempty"`
|
||||||
ValueIn string `json:"valueIn"`
|
ValueIn string `json:"valueIn"`
|
||||||
ValueInSat big.Int `json:"-"`
|
ValueInSat big.Int `json:"-"`
|
||||||
Fees string `json:"fees"`
|
Fees string `json:"fees"`
|
||||||
FeesSat big.Int `json:"-"`
|
FeesSat big.Int `json:"-"`
|
||||||
Hex string `json:"hex"`
|
Hex string `json:"hex"`
|
||||||
CoinSpecificData interface{} `json:"-"`
|
CoinSpecificData interface{} `json:"-"`
|
||||||
CoinSpecificJSON json.RawMessage `json:"-"`
|
CoinSpecificJSON json.RawMessage `json:"-"`
|
||||||
|
Erc20Transfers []Erc20Transfer `json:"erc20transfers,omitempty"`
|
||||||
|
EthereumSpecific *eth.EthereumTxData `json:"ethereumspecific,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Paging contains information about paging for address, blocks and block
|
// Paging contains information about paging for address, blocks and block
|
||||||
@ -100,14 +122,6 @@ type Paging struct {
|
|||||||
ItemsOnPage int `json:"itemsOnPage"`
|
ItemsOnPage int `json:"itemsOnPage"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Erc20Token struct {
|
|
||||||
Contract string `json:"contract"`
|
|
||||||
Txs int `json:"txs"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Symbol string `json:"symbol"`
|
|
||||||
Value string `json:"value"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Address holds information about address and its transactions
|
// Address holds information about address and its transactions
|
||||||
type Address struct {
|
type Address struct {
|
||||||
Paging
|
Paging
|
||||||
|
|||||||
@ -2,6 +2,7 @@ package api
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"blockbook/bchain"
|
"blockbook/bchain"
|
||||||
|
"blockbook/bchain/coins/eth"
|
||||||
"blockbook/common"
|
"blockbook/common"
|
||||||
"blockbook/db"
|
"blockbook/db"
|
||||||
"bytes"
|
"bytes"
|
||||||
@ -106,6 +107,8 @@ func (w *Worker) GetTransaction(txid string, spendingTxs bool, specificJSON bool
|
|||||||
return nil, NewAPIError(fmt.Sprintf("Tx not found, %v", err), true)
|
return nil, NewAPIError(fmt.Sprintf("Tx not found, %v", err), true)
|
||||||
}
|
}
|
||||||
var ta *db.TxAddresses
|
var ta *db.TxAddresses
|
||||||
|
var erc20t []Erc20Transfer
|
||||||
|
var ethSpecific *eth.EthereumTxData
|
||||||
var blockhash string
|
var blockhash string
|
||||||
if bchainTx.Confirmations > 0 {
|
if bchainTx.Confirmations > 0 {
|
||||||
if w.chainType == bchain.ChainBitcoinType {
|
if w.chainType == bchain.ChainBitcoinType {
|
||||||
@ -204,10 +207,45 @@ func (w *Worker) GetTransaction(txid string, spendingTxs bool, specificJSON bool
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// for coinbase transactions valIn is 0
|
if w.chainType == bchain.ChainBitcoinType {
|
||||||
feesSat.Sub(&valInSat, &valOutSat)
|
// for coinbase transactions valIn is 0
|
||||||
if feesSat.Sign() == -1 {
|
feesSat.Sub(&valInSat, &valOutSat)
|
||||||
feesSat.SetUint64(0)
|
if feesSat.Sign() == -1 {
|
||||||
|
feesSat.SetUint64(0)
|
||||||
|
}
|
||||||
|
} else if w.chainType == bchain.ChainEthereumType {
|
||||||
|
ets, err := eth.GetErc20FromTx(bchainTx)
|
||||||
|
if err != nil {
|
||||||
|
glog.Errorf("GetErc20FromTx error %v, %v", err, bchainTx)
|
||||||
|
}
|
||||||
|
erc20t = make([]Erc20Transfer, 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)
|
||||||
|
erc20c = &bchain.Erc20Contract{}
|
||||||
|
}
|
||||||
|
erc20t[i] = Erc20Transfer{
|
||||||
|
Contract: e.Contract,
|
||||||
|
From: e.From,
|
||||||
|
To: e.To,
|
||||||
|
Tokens: bchain.AmountToDecimalString(&e.Tokens, erc20c.Decimals),
|
||||||
|
Name: erc20c.Name,
|
||||||
|
Symbol: erc20c.Symbol,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ethSpecific = eth.GetEthereumTxData(bchainTx)
|
||||||
|
feesSat.Mul(ethSpecific.GasPriceNum, ethSpecific.GasUsed)
|
||||||
|
if len(bchainTx.Vout) > 0 {
|
||||||
|
valInSat = bchainTx.Vout[0].ValueSat
|
||||||
|
}
|
||||||
|
valOutSat = valInSat
|
||||||
}
|
}
|
||||||
// for now do not return size, we would have to compute vsize of segwit transactions
|
// for now do not return size, we would have to compute vsize of segwit transactions
|
||||||
// size:=len(bchainTx.Hex) / 2
|
// size:=len(bchainTx.Hex) / 2
|
||||||
@ -238,6 +276,8 @@ func (w *Worker) GetTransaction(txid string, spendingTxs bool, specificJSON bool
|
|||||||
Vout: vouts,
|
Vout: vouts,
|
||||||
CoinSpecificData: bchainTx.CoinSpecificData,
|
CoinSpecificData: bchainTx.CoinSpecificData,
|
||||||
CoinSpecificJSON: sj,
|
CoinSpecificJSON: sj,
|
||||||
|
Erc20Transfers: erc20t,
|
||||||
|
EthereumSpecific: ethSpecific,
|
||||||
}
|
}
|
||||||
if spendingTxs {
|
if spendingTxs {
|
||||||
glog.Info("GetTransaction ", txid, " finished in ", time.Since(start))
|
glog.Info("GetTransaction ", txid, " finished in ", time.Since(start))
|
||||||
|
|||||||
45
bchain/basechain.go
Normal file
45
bchain/basechain.go
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
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")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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")
|
||||||
|
}
|
||||||
@ -34,10 +34,14 @@ func (p *BaseParser) AmountToBigInt(n json.Number) (big.Int, error) {
|
|||||||
var r big.Int
|
var r big.Int
|
||||||
s := string(n)
|
s := string(n)
|
||||||
i := strings.IndexByte(s, '.')
|
i := strings.IndexByte(s, '.')
|
||||||
|
d := p.AmountDecimalPoint
|
||||||
|
if d > len(zeros) {
|
||||||
|
d = len(zeros)
|
||||||
|
}
|
||||||
if i == -1 {
|
if i == -1 {
|
||||||
s = s + zeros[:p.AmountDecimalPoint]
|
s = s + zeros[:d]
|
||||||
} else {
|
} else {
|
||||||
z := p.AmountDecimalPoint - len(s) + i + 1
|
z := d - len(s) + i + 1
|
||||||
if z > 0 {
|
if z > 0 {
|
||||||
s = s[:i] + s[i+1:] + zeros[:z]
|
s = s[:i] + s[i+1:] + zeros[:z]
|
||||||
} else {
|
} else {
|
||||||
@ -58,6 +62,9 @@ func AmountToDecimalString(a *big.Int, d int) string {
|
|||||||
n = n[1:]
|
n = n[1:]
|
||||||
s = "-"
|
s = "-"
|
||||||
}
|
}
|
||||||
|
if d > len(zeros) {
|
||||||
|
d = len(zeros)
|
||||||
|
}
|
||||||
if len(n) <= d {
|
if len(n) <= d {
|
||||||
n = zeros[:d-len(n)+1] + n
|
n = zeros[:d-len(n)+1] + n
|
||||||
}
|
}
|
||||||
|
|||||||
@ -27,7 +27,8 @@ var amounts = []struct {
|
|||||||
{big.NewInt(-8), "-0.00000008", 8, "!"},
|
{big.NewInt(-8), "-0.00000008", 8, "!"},
|
||||||
{big.NewInt(-89012345678), "-890.12345678", 8, "!"},
|
{big.NewInt(-89012345678), "-890.12345678", 8, "!"},
|
||||||
{big.NewInt(-12345), "-0.00012345", 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) {
|
func TestBaseParser_AmountToDecimalString(t *testing.T) {
|
||||||
|
|||||||
@ -239,3 +239,18 @@ func (c *blockChainWithMetrics) GetMempoolEntry(txid string) (v *bchain.MempoolE
|
|||||||
func (c *blockChainWithMetrics) GetChainParser() bchain.BlockChainParser {
|
func (c *blockChainWithMetrics) GetChainParser() bchain.BlockChainParser {
|
||||||
return c.b.GetChainParser()
|
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) 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)
|
||||||
|
}
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import (
|
|||||||
"math/big"
|
"math/big"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"runtime/debug"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/btcsuite/btcd/wire"
|
"github.com/btcsuite/btcd/wire"
|
||||||
@ -20,13 +21,11 @@ import (
|
|||||||
|
|
||||||
// BitcoinRPC is an interface to JSON-RPC bitcoind service.
|
// BitcoinRPC is an interface to JSON-RPC bitcoind service.
|
||||||
type BitcoinRPC struct {
|
type BitcoinRPC struct {
|
||||||
|
*bchain.BaseChain
|
||||||
client http.Client
|
client http.Client
|
||||||
rpcURL string
|
rpcURL string
|
||||||
user string
|
user string
|
||||||
password string
|
password string
|
||||||
Parser bchain.BlockChainParser
|
|
||||||
Testnet bool
|
|
||||||
Network string
|
|
||||||
Mempool *bchain.MempoolBitcoinType
|
Mempool *bchain.MempoolBitcoinType
|
||||||
ParseBlocks bool
|
ParseBlocks bool
|
||||||
pushHandler func(bchain.NotificationType)
|
pushHandler func(bchain.NotificationType)
|
||||||
@ -84,6 +83,7 @@ func NewBitcoinRPC(config json.RawMessage, pushHandler func(bchain.NotificationT
|
|||||||
}
|
}
|
||||||
|
|
||||||
s := &BitcoinRPC{
|
s := &BitcoinRPC{
|
||||||
|
BaseChain: &bchain.BaseChain{},
|
||||||
client: http.Client{Timeout: time.Duration(c.RPCTimeout) * time.Second, Transport: transport},
|
client: http.Client{Timeout: time.Duration(c.RPCTimeout) * time.Second, Transport: transport},
|
||||||
rpcURL: c.RPCURL,
|
rpcURL: c.RPCURL,
|
||||||
user: c.RPCUser,
|
user: c.RPCUser,
|
||||||
@ -158,14 +158,6 @@ func (b *BitcoinRPC) Shutdown(ctx context.Context) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *BitcoinRPC) IsTestnet() bool {
|
|
||||||
return b.Testnet
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *BitcoinRPC) GetNetworkName() string {
|
|
||||||
return b.Network
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *BitcoinRPC) GetCoinName() string {
|
func (b *BitcoinRPC) GetCoinName() string {
|
||||||
return b.ChainConfig.CoinName
|
return b.ChainConfig.CoinName
|
||||||
}
|
}
|
||||||
@ -837,6 +829,7 @@ func safeDecodeResponse(body io.ReadCloser, res interface{}) (err error) {
|
|||||||
defer func() {
|
defer func() {
|
||||||
if r := recover(); r != nil {
|
if r := recover(); r != nil {
|
||||||
glog.Error("unmarshal json recovered from panic: ", r, "; data: ", string(data))
|
glog.Error("unmarshal json recovered from panic: ", r, "; data: ", string(data))
|
||||||
|
debug.PrintStack()
|
||||||
if len(data) > 0 && len(data) < 2048 {
|
if len(data) > 0 && len(data) < 2048 {
|
||||||
err = errors.Errorf("Error: %v", string(data))
|
err = errors.Errorf("Error: %v", string(data))
|
||||||
} else {
|
} else {
|
||||||
@ -881,8 +874,3 @@ func (b *BitcoinRPC) Call(req interface{}, res interface{}) error {
|
|||||||
}
|
}
|
||||||
return safeDecodeResponse(httpRes.Body, &res)
|
return safeDecodeResponse(httpRes.Body, &res)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetChainParser returns BlockChainParser
|
|
||||||
func (b *BitcoinRPC) GetChainParser() bchain.BlockChainParser {
|
|
||||||
return b.Parser
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,9 +1,15 @@
|
|||||||
package eth
|
package eth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"blockbook/bchain"
|
||||||
|
"context"
|
||||||
|
"encoding/hex"
|
||||||
"math/big"
|
"math/big"
|
||||||
|
"sync"
|
||||||
|
|
||||||
ethcommon "github.com/ethereum/go-ethereum/common"
|
ethcommon "github.com/ethereum/go-ethereum/common"
|
||||||
|
"github.com/ethereum/go-ethereum/common/hexutil"
|
||||||
|
"github.com/golang/glog"
|
||||||
"github.com/juju/errors"
|
"github.com/juju/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -24,8 +30,12 @@ var erc20abi = `[{"constant":true,"inputs":[],"name":"name","outputs":[{"name":"
|
|||||||
|
|
||||||
// doing the parsing/processing without using go-ethereum/accounts/abi library, it is simple to get data from Transfer event
|
// doing the parsing/processing without using go-ethereum/accounts/abi library, it is simple to get data from Transfer event
|
||||||
const erc20EventTransferSignature = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"
|
const erc20EventTransferSignature = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"
|
||||||
|
const erc20NameSignature = "0x06fdde03"
|
||||||
|
const erc20SymbolSignature = "0x95d89b41"
|
||||||
|
const erc20DecimalsSignature = "0x313ce567"
|
||||||
|
const erc20BalanceOf = "0x70a08231"
|
||||||
|
|
||||||
// Erc20Transfer contains a single Erc20 token transfer
|
// Erc20Transfer contains a single ERC20 token transfer
|
||||||
type Erc20Transfer struct {
|
type Erc20Transfer struct {
|
||||||
Contract string
|
Contract string
|
||||||
From string
|
From string
|
||||||
@ -33,6 +43,9 @@ type Erc20Transfer struct {
|
|||||||
Tokens big.Int
|
Tokens big.Int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var cachedContracts = make(map[string]bchain.Erc20Contract)
|
||||||
|
var cachedContractsMux sync.Mutex
|
||||||
|
|
||||||
func addressFromPaddedHex(s string) (string, error) {
|
func addressFromPaddedHex(s string) (string, error) {
|
||||||
var t big.Int
|
var t big.Int
|
||||||
_, ok := t.SetString(s, 0)
|
_, ok := t.SetString(s, 0)
|
||||||
@ -70,3 +83,107 @@ func erc20GetTransfersFromLog(logs []*rpcLog) ([]Erc20Transfer, error) {
|
|||||||
}
|
}
|
||||||
return r, nil
|
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) == 192 {
|
||||||
|
n := parseErc20NumericProperty(contractDesc, data[64:128])
|
||||||
|
if n != nil {
|
||||||
|
l := n.Uint64()
|
||||||
|
if l <= 32 {
|
||||||
|
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)
|
||||||
|
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
|
||||||
|
}
|
||||||
|
contract = bchain.Erc20Contract{
|
||||||
|
Name: name,
|
||||||
|
Symbol: symbol,
|
||||||
|
}
|
||||||
|
d := parseErc20NumericProperty(contractDesc, data)
|
||||||
|
if d != nil {
|
||||||
|
contract.Decimals = int(uint8(d.Uint64()))
|
||||||
|
} else {
|
||||||
|
contract.Decimals = EtherAmountDecimalPoint
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
// +build unittest
|
// build unittest
|
||||||
|
|
||||||
package eth
|
package eth
|
||||||
|
|
||||||
@ -108,5 +108,32 @@ func TestErc20_erc20GetTransfersFromLog(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestErc20_parseErc20StringProperty(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args string
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "1",
|
||||||
|
args: "0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000758504c4f44444500000000000000000000000000000000000000000000000000",
|
||||||
|
want: "XPLODDE",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "1",
|
||||||
|
args: "0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000758504c4f44444500000000000000000000000000000000000000000000000000",
|
||||||
|
want: "XPLODDE",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,6 +16,9 @@ import (
|
|||||||
// EthereumTypeAddressDescriptorLen - in case of EthereumType, the AddressDescriptor has fixed length
|
// EthereumTypeAddressDescriptorLen - in case of EthereumType, the AddressDescriptor has fixed length
|
||||||
const EthereumTypeAddressDescriptorLen = 20
|
const EthereumTypeAddressDescriptorLen = 20
|
||||||
|
|
||||||
|
// EtherAmountDecimalPoint defines number of decimal points in Ether amounts
|
||||||
|
const EtherAmountDecimalPoint = 18
|
||||||
|
|
||||||
// EthereumParser handle
|
// EthereumParser handle
|
||||||
type EthereumParser struct {
|
type EthereumParser struct {
|
||||||
*bchain.BaseParser
|
*bchain.BaseParser
|
||||||
@ -25,7 +28,7 @@ type EthereumParser struct {
|
|||||||
func NewEthereumParser(b int) *EthereumParser {
|
func NewEthereumParser(b int) *EthereumParser {
|
||||||
return &EthereumParser{&bchain.BaseParser{
|
return &EthereumParser{&bchain.BaseParser{
|
||||||
BlockAddressesToKeep: b,
|
BlockAddressesToKeep: b,
|
||||||
AmountDecimalPoint: 18,
|
AmountDecimalPoint: EtherAmountDecimalPoint,
|
||||||
}}
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -435,3 +438,36 @@ func GetErc20FromTx(tx *bchain.Tx) ([]Erc20Transfer, error) {
|
|||||||
}
|
}
|
||||||
return r, nil
|
return r, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// EthereumTxData contains ethereum specific transaction data
|
||||||
|
type EthereumTxData 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 string `json:"gasprice"`
|
||||||
|
GasPriceNum *big.Int `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetEthereumTxData returns EthereumTxData from bchain.Tx
|
||||||
|
func GetEthereumTxData(tx *bchain.Tx) *EthereumTxData {
|
||||||
|
etd := EthereumTxData{Status: -1}
|
||||||
|
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.GasPriceNum, _ = hexutil.DecodeBig(csd.Tx.GasPrice)
|
||||||
|
etd.GasPrice = bchain.AmountToDecimalString(etd.GasPriceNum, EtherAmountDecimalPoint)
|
||||||
|
}
|
||||||
|
if csd.Receipt != nil {
|
||||||
|
if csd.Receipt.Status == "0x1" {
|
||||||
|
etd.Status = 1
|
||||||
|
} else {
|
||||||
|
etd.Status = 0
|
||||||
|
}
|
||||||
|
etd.GasUsed, _ = hexutil.DecodeBig(csd.Receipt.GasUsed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &etd
|
||||||
|
}
|
||||||
|
|||||||
@ -40,12 +40,11 @@ type Configuration struct {
|
|||||||
|
|
||||||
// EthereumRPC is an interface to JSON-RPC eth service.
|
// EthereumRPC is an interface to JSON-RPC eth service.
|
||||||
type EthereumRPC struct {
|
type EthereumRPC struct {
|
||||||
|
*bchain.BaseChain
|
||||||
client *ethclient.Client
|
client *ethclient.Client
|
||||||
rpc *rpc.Client
|
rpc *rpc.Client
|
||||||
timeout time.Duration
|
timeout time.Duration
|
||||||
Parser *EthereumParser
|
Parser *EthereumParser
|
||||||
Testnet bool
|
|
||||||
Network string
|
|
||||||
Mempool *bchain.MempoolEthereumType
|
Mempool *bchain.MempoolEthereumType
|
||||||
bestHeaderMu sync.Mutex
|
bestHeaderMu sync.Mutex
|
||||||
bestHeader *ethtypes.Header
|
bestHeader *ethtypes.Header
|
||||||
@ -77,6 +76,7 @@ func NewEthereumRPC(config json.RawMessage, pushHandler func(bchain.Notification
|
|||||||
ec := ethclient.NewClient(rc)
|
ec := ethclient.NewClient(rc)
|
||||||
|
|
||||||
s := &EthereumRPC{
|
s := &EthereumRPC{
|
||||||
|
BaseChain: &bchain.BaseChain{},
|
||||||
client: ec,
|
client: ec,
|
||||||
rpc: rc,
|
rpc: rc,
|
||||||
ChainConfig: &c,
|
ChainConfig: &c,
|
||||||
@ -250,16 +250,6 @@ func (b *EthereumRPC) Shutdown(ctx context.Context) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsTestnet returns true if the network is testnet
|
|
||||||
func (b *EthereumRPC) IsTestnet() bool {
|
|
||||||
return b.Testnet
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetNetworkName returns network name
|
|
||||||
func (b *EthereumRPC) GetNetworkName() string {
|
|
||||||
return b.Network
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetCoinName returns coin name
|
// GetCoinName returns coin name
|
||||||
func (b *EthereumRPC) GetCoinName() string {
|
func (b *EthereumRPC) GetCoinName() string {
|
||||||
return b.ChainConfig.CoinName
|
return b.ChainConfig.CoinName
|
||||||
@ -652,6 +642,13 @@ func (b *EthereumRPC) SendRawTransaction(hex string) (string, error) {
|
|||||||
return result, nil
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
// ResyncMempool gets mempool transactions and maps output scripts to transactions.
|
// ResyncMempool gets mempool transactions and maps output scripts to transactions.
|
||||||
// ResyncMempool is not reentrant, it should be called from a single thread.
|
// ResyncMempool is not reentrant, it should be called from a single thread.
|
||||||
// Return value is number of transactions in mempool
|
// Return value is number of transactions in mempool
|
||||||
|
|||||||
@ -68,7 +68,7 @@ func (m *MempoolEthereumType) Resync(onNewTxAddr OnNewTxAddrFunc) (int, error) {
|
|||||||
if !exists {
|
if !exists {
|
||||||
tx, err := m.chain.GetTransactionForMempool(txid)
|
tx, err := m.chain.GetTransactionForMempool(txid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
glog.Error("cannot get transaction ", txid, ": ", err)
|
glog.Warning("cannot get transaction ", txid, ": ", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
io = make([]addrIndex, 0, len(tx.Vout)+len(tx.Vin))
|
io = make([]addrIndex, 0, len(tx.Vout)+len(tx.Vin))
|
||||||
|
|||||||
@ -159,6 +159,16 @@ func (ad AddressDescriptor) String() string {
|
|||||||
return "ad:" + hex.EncodeToString(ad)
|
return "ad:" + hex.EncodeToString(ad)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// EthereumType specific
|
||||||
|
|
||||||
|
// Erc20Contract contains info about ERC20 contract
|
||||||
|
type Erc20Contract struct {
|
||||||
|
Contract string
|
||||||
|
Name string
|
||||||
|
Symbol string
|
||||||
|
Decimals int
|
||||||
|
}
|
||||||
|
|
||||||
// OnNewBlockFunc is used to send notification about a new block
|
// OnNewBlockFunc is used to send notification about a new block
|
||||||
type OnNewBlockFunc func(hash string, height uint32)
|
type OnNewBlockFunc func(hash string, height uint32)
|
||||||
|
|
||||||
@ -197,6 +207,10 @@ type BlockChain interface {
|
|||||||
GetMempoolEntry(txid string) (*MempoolEntry, error)
|
GetMempoolEntry(txid string) (*MempoolEntry, error)
|
||||||
// parser
|
// parser
|
||||||
GetChainParser() BlockChainParser
|
GetChainParser() BlockChainParser
|
||||||
|
// EthereumType specific
|
||||||
|
EthereumTypeGetBalance(addrDesc AddressDescriptor) (*big.Int, 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
|
// BlockChainParser defines common interface to parsing and conversions of block chain data
|
||||||
|
|||||||
@ -9,11 +9,11 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type fakeBlockChain struct {
|
type fakeBlockChain struct {
|
||||||
parser bchain.BlockChainParser
|
*bchain.BaseChain
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewFakeBlockChain(parser bchain.BlockChainParser) (*fakeBlockChain, error) {
|
func NewFakeBlockChain(parser bchain.BlockChainParser) (*fakeBlockChain, error) {
|
||||||
return &fakeBlockChain{parser: parser}, nil
|
return &fakeBlockChain{&bchain.BaseChain{Parser: parser}}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *fakeBlockChain) Initialize() error {
|
func (c *fakeBlockChain) Initialize() error {
|
||||||
@ -45,26 +45,26 @@ func (c *fakeBlockChain) GetChainInfo() (v *bchain.ChainInfo, err error) {
|
|||||||
Chain: c.GetNetworkName(),
|
Chain: c.GetNetworkName(),
|
||||||
Blocks: 2,
|
Blocks: 2,
|
||||||
Headers: 2,
|
Headers: 2,
|
||||||
Bestblockhash: GetTestBitcoinTypeBlock2(c.parser).BlockHeader.Hash,
|
Bestblockhash: GetTestBitcoinTypeBlock2(c.Parser).BlockHeader.Hash,
|
||||||
Version: "001001",
|
Version: "001001",
|
||||||
Subversion: c.GetSubversion(),
|
Subversion: c.GetSubversion(),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *fakeBlockChain) GetBestBlockHash() (v string, err error) {
|
func (c *fakeBlockChain) GetBestBlockHash() (v string, err error) {
|
||||||
return GetTestBitcoinTypeBlock2(c.parser).BlockHeader.Hash, nil
|
return GetTestBitcoinTypeBlock2(c.Parser).BlockHeader.Hash, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *fakeBlockChain) GetBestBlockHeight() (v uint32, err error) {
|
func (c *fakeBlockChain) GetBestBlockHeight() (v uint32, err error) {
|
||||||
return GetTestBitcoinTypeBlock2(c.parser).BlockHeader.Height, nil
|
return GetTestBitcoinTypeBlock2(c.Parser).BlockHeader.Height, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *fakeBlockChain) GetBlockHash(height uint32) (v string, err error) {
|
func (c *fakeBlockChain) GetBlockHash(height uint32) (v string, err error) {
|
||||||
b1 := GetTestBitcoinTypeBlock1(c.parser)
|
b1 := GetTestBitcoinTypeBlock1(c.Parser)
|
||||||
if height == b1.BlockHeader.Height {
|
if height == b1.BlockHeader.Height {
|
||||||
return b1.BlockHeader.Hash, nil
|
return b1.BlockHeader.Hash, nil
|
||||||
}
|
}
|
||||||
b2 := GetTestBitcoinTypeBlock2(c.parser)
|
b2 := GetTestBitcoinTypeBlock2(c.Parser)
|
||||||
if height == b2.BlockHeader.Height {
|
if height == b2.BlockHeader.Height {
|
||||||
return b2.BlockHeader.Hash, nil
|
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) {
|
func (c *fakeBlockChain) GetBlockHeader(hash string) (v *bchain.BlockHeader, err error) {
|
||||||
b1 := GetTestBitcoinTypeBlock1(c.parser)
|
b1 := GetTestBitcoinTypeBlock1(c.Parser)
|
||||||
if hash == b1.BlockHeader.Hash {
|
if hash == b1.BlockHeader.Hash {
|
||||||
return &b1.BlockHeader, nil
|
return &b1.BlockHeader, nil
|
||||||
}
|
}
|
||||||
b2 := GetTestBitcoinTypeBlock2(c.parser)
|
b2 := GetTestBitcoinTypeBlock2(c.Parser)
|
||||||
if hash == b2.BlockHeader.Hash {
|
if hash == b2.BlockHeader.Hash {
|
||||||
return &b2.BlockHeader, nil
|
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) {
|
func (c *fakeBlockChain) GetBlock(hash string, height uint32) (v *bchain.Block, err error) {
|
||||||
b1 := GetTestBitcoinTypeBlock1(c.parser)
|
b1 := GetTestBitcoinTypeBlock1(c.Parser)
|
||||||
if hash == b1.BlockHeader.Hash || height == b1.BlockHeader.Height {
|
if hash == b1.BlockHeader.Hash || height == b1.BlockHeader.Height {
|
||||||
return b1, nil
|
return b1, nil
|
||||||
}
|
}
|
||||||
b2 := GetTestBitcoinTypeBlock2(c.parser)
|
b2 := GetTestBitcoinTypeBlock2(c.Parser)
|
||||||
if hash == b2.BlockHeader.Hash || height == b2.BlockHeader.Height {
|
if hash == b2.BlockHeader.Hash || height == b2.BlockHeader.Height {
|
||||||
return b2, nil
|
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) {
|
func (c *fakeBlockChain) GetBlockInfo(hash string) (v *bchain.BlockInfo, err error) {
|
||||||
b1 := GetTestBitcoinTypeBlock1(c.parser)
|
b1 := GetTestBitcoinTypeBlock1(c.Parser)
|
||||||
if hash == b1.BlockHeader.Hash {
|
if hash == b1.BlockHeader.Hash {
|
||||||
return getBlockInfo(b1), nil
|
return getBlockInfo(b1), nil
|
||||||
}
|
}
|
||||||
b2 := GetTestBitcoinTypeBlock2(c.parser)
|
b2 := GetTestBitcoinTypeBlock2(c.Parser)
|
||||||
if hash == b2.BlockHeader.Hash {
|
if hash == b2.BlockHeader.Hash {
|
||||||
return getBlockInfo(b2), nil
|
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) {
|
func (c *fakeBlockChain) GetTransaction(txid string) (v *bchain.Tx, err error) {
|
||||||
v = getTxInBlock(GetTestBitcoinTypeBlock1(c.parser), txid)
|
v = getTxInBlock(GetTestBitcoinTypeBlock1(c.Parser), txid)
|
||||||
if v == nil {
|
if v == nil {
|
||||||
v = getTxInBlock(GetTestBitcoinTypeBlock2(c.parser), txid)
|
v = getTxInBlock(GetTestBitcoinTypeBlock2(c.Parser), txid)
|
||||||
}
|
}
|
||||||
if v != nil {
|
if v != nil {
|
||||||
return v, nil
|
return v, nil
|
||||||
@ -195,5 +195,5 @@ func (c *fakeBlockChain) GetMempoolEntry(txid string) (v *bchain.MempoolEntry, e
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *fakeBlockChain) GetChainParser() bchain.BlockChainParser {
|
func (c *fakeBlockChain) GetChainParser() bchain.BlockChainParser {
|
||||||
return c.parser
|
return c.Parser
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user