Parse ethereum input data

This commit is contained in:
Martin Boehm 2022-04-14 16:24:26 +02:00 committed by Martin
parent 0ccb9b37b4
commit 6fdf6e297c
15 changed files with 873 additions and 52 deletions

View File

@ -207,6 +207,7 @@ type EthereumSpecific struct {
GasUsed *big.Int `json:"gasUsed"` GasUsed *big.Int `json:"gasUsed"`
GasPrice *Amount `json:"gasPrice"` GasPrice *Amount `json:"gasPrice"`
Data string `json:"data,omitempty"` Data string `json:"data,omitempty"`
ParsedData *bchain.EthereumParsedInputData `json:"parsedData,omitempty"`
InternalTransfers []EthereumInternalTransfer `json:"internalTransfers,omitempty"` InternalTransfers []EthereumInternalTransfer `json:"internalTransfers,omitempty"`
} }

View File

@ -127,6 +127,23 @@ func (w *Worker) GetTransaction(txid string, spendingTxs bool, specificJSON bool
return w.GetTransactionFromBchainTx(bchainTx, height, spendingTxs, specificJSON) return w.GetTransactionFromBchainTx(bchainTx, height, spendingTxs, specificJSON)
} }
func (w *Worker) getParsedEthereumInputData(data string) *bchain.EthereumParsedInputData {
var err error
var signatures *[]bchain.FourByteSignature
fourBytes := eth.GetSignatureFromData(data)
if fourBytes != 0 {
signatures, err = w.db.GetFourByteSignatures(fourBytes)
if err != nil {
glog.Errorf("GetFourByteSignatures(%v) error %v", fourBytes, err)
return nil
}
if signatures == nil {
return nil
}
}
return eth.ParseInputData(signatures, data)
}
// GetTransactionFromBchainTx reads transaction data from txid // GetTransactionFromBchainTx reads transaction data from txid
func (w *Worker) GetTransactionFromBchainTx(bchainTx *bchain.Tx, height int, spendingTxs bool, specificJSON bool) (*Tx, error) { func (w *Worker) GetTransactionFromBchainTx(bchainTx *bchain.Tx, height int, spendingTxs bool, specificJSON bool) (*Tx, error) {
var err error var err error
@ -270,6 +287,8 @@ func (w *Worker) GetTransactionFromBchainTx(bchainTx *bchain.Tx, height int, spe
} }
} }
parsedInputData := w.getParsedEthereumInputData(ethTxData.Data)
// mempool txs do not have fees yet // mempool txs do not have fees yet
if ethTxData.GasUsed != nil { if ethTxData.GasUsed != nil {
feesSat.Mul(ethTxData.GasPrice, ethTxData.GasUsed) feesSat.Mul(ethTxData.GasPrice, ethTxData.GasUsed)
@ -278,12 +297,13 @@ func (w *Worker) GetTransactionFromBchainTx(bchainTx *bchain.Tx, height int, spe
valOutSat = bchainTx.Vout[0].ValueSat valOutSat = bchainTx.Vout[0].ValueSat
} }
ethSpecific = &EthereumSpecific{ ethSpecific = &EthereumSpecific{
GasLimit: ethTxData.GasLimit, GasLimit: ethTxData.GasLimit,
GasPrice: (*Amount)(ethTxData.GasPrice), GasPrice: (*Amount)(ethTxData.GasPrice),
GasUsed: ethTxData.GasUsed, GasUsed: ethTxData.GasUsed,
Nonce: ethTxData.Nonce, Nonce: ethTxData.Nonce,
Status: ethTxData.Status, Status: ethTxData.Status,
Data: ethTxData.Data, Data: ethTxData.Data,
ParsedData: parsedInputData,
} }
if internalData != nil { if internalData != nil {
ethSpecific.Type = internalData.Type ethSpecific.Type = internalData.Type
@ -674,7 +694,6 @@ func (w *Worker) getEthereumContractBalance(addrDesc bchain.AddressDescriptor, i
} }
t := Token{ t := Token{
Type: ERC20TokenType,
Contract: ci.Contract, Contract: ci.Contract,
Name: ci.Name, Name: ci.Name,
Symbol: ci.Symbol, Symbol: ci.Symbol,
@ -685,6 +704,7 @@ func (w *Worker) getEthereumContractBalance(addrDesc bchain.AddressDescriptor, i
// return contract balances/values only at or above AccountDetailsTokenBalances // return contract balances/values only at or above AccountDetailsTokenBalances
if details >= AccountDetailsTokenBalances && validContract { if details >= AccountDetailsTokenBalances && validContract {
if c.Type == bchain.ERC20 { if c.Type == bchain.ERC20 {
t.Type = ERC20TokenType
// get Erc20 Contract Balance from blockchain, balance obtained from adding and subtracting transfers is not correct // get Erc20 Contract Balance from blockchain, balance obtained from adding and subtracting transfers is not correct
b, err := w.chain.EthereumTypeGetErc20ContractBalance(addrDesc, c.Contract) b, err := w.chain.EthereumTypeGetErc20ContractBalance(addrDesc, c.Contract)
if err != nil { if err != nil {
@ -694,15 +714,20 @@ func (w *Worker) getEthereumContractBalance(addrDesc bchain.AddressDescriptor, i
t.BalanceSat = (*Amount)(b) t.BalanceSat = (*Amount)(b)
} }
} else { } else {
if len(t.Ids) > 0 { if c.Type == bchain.ERC721 {
ids := make([]Amount, len(t.Ids)) t.Type = ERC771TokenType
} else {
t.Type = ERC1155TokenType
}
if len(c.Ids) > 0 {
ids := make([]Amount, len(c.Ids))
for j := range ids { for j := range ids {
ids[j] = (Amount)(c.Ids[j]) ids[j] = (Amount)(c.Ids[j])
} }
t.Ids = ids t.Ids = ids
} }
if len(t.IdValues) > 0 { if len(c.IdValues) > 0 {
idValues := make([]TokenTransferValues, len(t.IdValues)) idValues := make([]TokenTransferValues, len(c.IdValues))
for j := range idValues { for j := range idValues {
idValues[j].Id = (*Amount)(&c.IdValues[j].Id) idValues[j].Id = (*Amount)(&c.IdValues[j].Id)
idValues[j].Value = (*Amount)(&c.IdValues[j].Value) idValues[j].Value = (*Amount)(&c.IdValues[j].Value)

View File

@ -4,8 +4,15 @@ import (
"bytes" "bytes"
"encoding/hex" "encoding/hex"
"math/big" "math/big"
"runtime/debug"
"strconv"
"strings"
"unicode" "unicode"
"unicode/utf8" "unicode/utf8"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/golang/glog"
"github.com/trezor/blockbook/bchain"
) )
func parseSimpleNumericProperty(data string) *big.Int { func parseSimpleNumericProperty(data string) *big.Int {
@ -58,15 +65,230 @@ func parseSimpleStringProperty(data string) string {
return "" return ""
} }
func Decamel(s string) string { func decamel(s string) string {
var b bytes.Buffer var b bytes.Buffer
splittable := false splittable := false
for _, v := range s { for i, v := range s {
if splittable && unicode.IsUpper(v) { if i == 0 {
b.WriteByte(' ') b.WriteRune(unicode.ToUpper(v))
} else {
if splittable && unicode.IsUpper(v) {
b.WriteByte(' ')
}
b.WriteRune(v)
splittable = unicode.IsLower(v) || unicode.IsNumber(v)
} }
b.WriteRune(v)
splittable = unicode.IsLower(v) || unicode.IsNumber(v)
} }
return b.String() return b.String()
} }
func GetSignatureFromData(data string) uint32 {
if has0xPrefix(data) {
data = data[2:]
}
if len(data) < 8 {
return 0
}
sig, err := strconv.ParseUint(data[:8], 16, 32)
if err != nil {
return 0
}
return uint32(sig)
}
const ErrorTy byte = 255
func processParam(data string, index int, t *abi.Type, processed []bool) ([]string, int, bool) {
var retval []string
d := index << 6
if d+64 > len(data) {
return nil, 0, false
}
block := data[d : d+64]
switch t.T {
// static types
case abi.IntTy, abi.UintTy, abi.BoolTy:
var n big.Int
_, ok := n.SetString(block, 16)
if !ok {
return nil, 0, false
}
if t.T == abi.BoolTy {
if n.Int64() != 0 {
retval = []string{"true"}
} else {
retval = []string{"false"}
}
} else {
retval = []string{n.String()}
}
processed[index] = true
index++
case abi.AddressTy:
b, err := hex.DecodeString(block[24:])
if err != nil {
return nil, 0, false
}
retval = []string{EIP55Address(b)}
processed[index] = true
index++
case abi.FixedBytesTy:
retval = []string{"0x" + block[:t.Size<<1]}
processed[index] = true
index++
case abi.ArrayTy:
for i := 0; i < t.Size; i++ {
var r []string
var ok bool
r, index, ok = processParam(data, index, t.Elem, processed)
if !ok {
return nil, 0, false
}
retval = append(retval, r...)
}
// dynamic types
case abi.StringTy, abi.BytesTy, abi.SliceTy:
// get offset of dynamic type
offset, err := strconv.ParseInt(block, 16, 64)
if err != nil {
return nil, 0, false
}
processed[index] = true
index++
offset <<= 1
d = int(offset)
dynIndex := d >> 6
if d+64 > len(data) || d < 0 {
return nil, 0, false
}
// get element count of dynamic type
c, err := strconv.ParseInt(data[d:d+64], 16, 64)
count := int(c)
if err != nil {
return nil, 0, false
}
processed[dynIndex] = true
dynIndex++
if t.T == abi.StringTy || t.T == abi.BytesTy {
d += 64
de := d + (count << 1)
if de > len(data) {
return nil, 0, false
}
if count == 0 {
retval = []string{""}
} else {
block = data[d:de]
if t.T == abi.StringTy {
b, err := hex.DecodeString(block)
if err != nil {
return nil, 0, false
}
retval = []string{string(b)}
} else {
retval = []string{"0x" + block}
}
count = ((count - 1) >> 5) + 1
for i := 0; i < count; i++ {
processed[dynIndex] = true
dynIndex++
}
}
} else {
for i := 0; i < count; i++ {
var r []string
var ok bool
r, dynIndex, ok = processParam(data, dynIndex, t.Elem, processed)
if !ok {
return nil, 0, false
}
retval = append(retval, r...)
}
}
// types not processed
case abi.HashTy, abi.FixedPointTy, abi.FunctionTy, abi.TupleTy:
fallthrough
default:
return nil, 0, false
}
return retval, index, true
}
func tryParseParams(data string, params []string, parsedParams []abi.Type) []bchain.EthereumParsedInputParam {
processed := make([]bool, len(data)/64)
parsed := make([]bchain.EthereumParsedInputParam, len(params))
index := 0
var values []string
var ok bool
for i := range params {
t := &parsedParams[i]
values, index, ok = processParam(data, index, t, processed)
if !ok {
return nil
}
parsed[i] = bchain.EthereumParsedInputParam{Type: params[i], Values: values}
}
// all data must be processed, otherwise wrong signature
for _, p := range processed {
if !p {
return nil
}
}
return parsed
}
// ParseInputData tries to parse transaction input data from known FourByteSignatures
// as there may be multiple signatures for the same four bytes, it tries to match the input to the known parameters
// it does not parse tuples for now
func ParseInputData(signatures *[]bchain.FourByteSignature, data string) *bchain.EthereumParsedInputData {
if len(data) <= 2 { // data is empty or 0x
return &bchain.EthereumParsedInputData{Name: "Transfer"}
}
if len(data) < 10 || (len(data)-10)%64 != 0 {
return nil
}
parsed := bchain.EthereumParsedInputData{
MethodId: data[:10],
}
defer func() {
if r := recover(); r != nil {
glog.Error("ParseInputData recovered from panic: ", r, ", ", data, ",signatures ", signatures)
debug.PrintStack()
}
}()
if signatures != nil {
data = data[10:]
for i := range *signatures {
s := &(*signatures)[i]
// if not yet done, set DecamelName and Function and parse parameter types from string to abi.Type
// the signatures are stored in cache
if s.DecamelName == "" {
s.DecamelName = decamel(s.Name)
s.Function = s.Name + "(" + strings.Join(s.Parameters, ", ") + ")"
s.ParsedParameters = make([]abi.Type, len(s.Parameters))
for j := range s.Parameters {
var t abi.Type
if len(s.Parameters[j]) > 0 && s.Parameters[j][0] == '(' {
// Tuple type is not supported for now
t = abi.Type{T: abi.TupleTy}
} else {
var err error
t, err = abi.NewType(s.Parameters[j], "", nil)
if err != nil {
t = abi.Type{T: ErrorTy}
}
}
s.ParsedParameters[j] = t
}
}
parsedParams := tryParseParams(data, s.Parameters, s.ParsedParameters)
if parsedParams != nil {
parsed.Name = s.DecamelName
parsed.Function = s.Function
parsed.Params = parsedParams
break
}
}
}
return &parsed
}

File diff suppressed because one or more lines are too long

View File

@ -1,6 +1,10 @@
package bchain package bchain
import "math/big" import (
"math/big"
"github.com/ethereum/go-ethereum/accounts/abi"
)
// EthereumType specific // EthereumType specific
@ -12,6 +16,27 @@ type EthereumInternalTransfer struct {
Value big.Int `json:"value"` Value big.Int `json:"value"`
} }
type FourByteSignature struct {
// stored in DB
Name string
Parameters []string
// processed from DB data and stored only in cache
DecamelName string
Function string
ParsedParameters []abi.Type
}
type EthereumParsedInputParam struct {
Type string `json:"type"`
Values []string `json:"values,omitempty"`
}
type EthereumParsedInputData struct {
MethodId string `json:"methodId"`
Name string `json:"name"`
Function string `json:"function,omitempty"`
Params []EthereumParsedInputParam `json:"params,omitempty"`
}
// EthereumInternalTransactionType - type of ethereum transaction from internal data // EthereumInternalTransactionType - type of ethereum transaction from internal data
type EthereumInternalTransactionType int type EthereumInternalTransactionType int

View File

@ -4,6 +4,7 @@ import (
"bytes" "bytes"
"encoding/hex" "encoding/hex"
"math/big" "math/big"
"sync"
"github.com/flier/gorocksdb" "github.com/flier/gorocksdb"
"github.com/golang/glog" "github.com/golang/glog"
@ -578,11 +579,8 @@ func (d *RocksDB) unpackEthInternalData(buf []byte) (*bchain.EthereumInternalDat
return &id, nil return &id, nil
} }
type FourByteSignature struct { // FourByteSignature contains 4byte signature of transaction value with parameters
Name string // and parsed parameters (that are not stored in DB)
Parameters []string
}
func packFourByteKey(fourBytes uint32, id uint32) []byte { func packFourByteKey(fourBytes uint32, id uint32) []byte {
key := make([]byte, 0, 8) key := make([]byte, 0, 8)
key = append(key, packUint(fourBytes)...) key = append(key, packUint(fourBytes)...)
@ -590,7 +588,7 @@ func packFourByteKey(fourBytes uint32, id uint32) []byte {
return key return key
} }
func packFourByteSignature(signature *FourByteSignature) []byte { func packFourByteSignature(signature *bchain.FourByteSignature) []byte {
buf := packString(signature.Name) buf := packString(signature.Name)
for i := range signature.Parameters { for i := range signature.Parameters {
buf = append(buf, packString(signature.Parameters[i])...) buf = append(buf, packString(signature.Parameters[i])...)
@ -598,8 +596,8 @@ func packFourByteSignature(signature *FourByteSignature) []byte {
return buf return buf
} }
func unpackFourByteSignature(buf []byte) (*FourByteSignature, error) { func unpackFourByteSignature(buf []byte) (*bchain.FourByteSignature, error) {
var signature FourByteSignature var signature bchain.FourByteSignature
var l int var l int
signature.Name, l = unpackString(buf) signature.Name, l = unpackString(buf)
for l < len(buf) { for l < len(buf) {
@ -610,7 +608,8 @@ func unpackFourByteSignature(buf []byte) (*FourByteSignature, error) {
return &signature, nil return &signature, nil
} }
func (d *RocksDB) GetFourByteSignature(fourBytes uint32, id uint32) (*FourByteSignature, error) { // GetFourByteSignature gets all 4byte signature of given fourBytes and id
func (d *RocksDB) GetFourByteSignature(fourBytes uint32, id uint32) (*bchain.FourByteSignature, error) {
key := packFourByteKey(fourBytes, id) key := packFourByteKey(fourBytes, id)
val, err := d.db.GetCF(d.ro, d.cfh[cfFunctionSignatures], key) val, err := d.db.GetCF(d.ro, d.cfh[cfFunctionSignatures], key)
if err != nil { if err != nil {
@ -624,12 +623,51 @@ func (d *RocksDB) GetFourByteSignature(fourBytes uint32, id uint32) (*FourByteSi
return unpackFourByteSignature(buf) return unpackFourByteSignature(buf)
} }
func (d *RocksDB) StoreFourByteSignature(wb *gorocksdb.WriteBatch, fourBytes uint32, id uint32, signature *FourByteSignature) error { var cachedByteSignatures = make(map[uint32]*[]bchain.FourByteSignature)
var cachedByteSignaturesMux sync.Mutex
// GetFourByteSignatures gets all 4byte signatures of given fourBytes
// (there may be more than one signature starting with the same four bytes)
func (d *RocksDB) GetFourByteSignatures(fourBytes uint32) (*[]bchain.FourByteSignature, error) {
cachedByteSignaturesMux.Lock()
signatures, found := cachedByteSignatures[fourBytes]
cachedByteSignaturesMux.Unlock()
if !found {
retval := []bchain.FourByteSignature{}
key := packUint(fourBytes)
it := d.db.NewIteratorCF(d.ro, d.cfh[cfFunctionSignatures])
defer it.Close()
for it.Seek(key); it.Valid(); it.Next() {
current := it.Key().Data()
if bytes.Compare(current[:4], key) > 0 {
break
}
val := it.Value().Data()
signature, err := unpackFourByteSignature(val)
if err != nil {
return nil, err
}
retval = append(retval, *signature)
}
cachedByteSignaturesMux.Lock()
cachedByteSignatures[fourBytes] = &retval
cachedByteSignaturesMux.Unlock()
return &retval, nil
}
return signatures, nil
}
// StoreFourByteSignature stores 4byte signature in DB
func (d *RocksDB) StoreFourByteSignature(wb *gorocksdb.WriteBatch, fourBytes uint32, id uint32, signature *bchain.FourByteSignature) error {
key := packFourByteKey(fourBytes, id) key := packFourByteKey(fourBytes, id)
wb.PutCF(d.cfh[cfFunctionSignatures], key, packFourByteSignature(signature)) wb.PutCF(d.cfh[cfFunctionSignatures], key, packFourByteSignature(signature))
cachedByteSignaturesMux.Lock()
delete(cachedByteSignatures, fourBytes)
cachedByteSignaturesMux.Unlock()
return nil return nil
} }
// GetEthereumInternalData gets transaction internal data from DB
func (d *RocksDB) GetEthereumInternalData(txid string) (*bchain.EthereumInternalData, error) { func (d *RocksDB) GetEthereumInternalData(txid string) (*bchain.EthereumInternalData, error) {
btxID, err := d.chainParser.PackTxid(txid) btxID, err := d.chainParser.PackTxid(txid)
if err != nil { if err != nil {
@ -902,7 +940,9 @@ func (d *RocksDB) disconnectAddress(btxID []byte, internal bool, addrDesc bchain
glog.Warning("AddressContracts ", addrDesc, ", contract ", contractIndex, " Txs would be negative, tx ", hex.EncodeToString(btxID)) glog.Warning("AddressContracts ", addrDesc, ", contract ", contractIndex, " Txs would be negative, tx ", hex.EncodeToString(btxID))
} }
} else { } else {
glog.Warning("AddressContracts ", addrDesc, ", contract ", btxContract.contract, " not found, tx ", hex.EncodeToString(btxID)) if !isZeroAddress(addrDesc) {
glog.Warning("AddressContracts ", addrDesc, ", contract ", btxContract.contract, " not found, tx ", hex.EncodeToString(btxID))
}
} }
} }
} else { } else {

View File

@ -301,7 +301,7 @@ func formatInternalData(in *bchain.EthereumInternalData) *bchain.EthereumInterna
func testFourByteSignature(t *testing.T, d *RocksDB) { func testFourByteSignature(t *testing.T, d *RocksDB) {
fourBytes := uint32(1234123) fourBytes := uint32(1234123)
id := uint32(42313) id := uint32(42313)
signature := FourByteSignature{ signature := bchain.FourByteSignature{
Name: "xyz", Name: "xyz",
Parameters: []string{"address", "(bytes,uint256[],uint256)", "uint16"}, Parameters: []string{"address", "(bytes,uint256[],uint256)", "uint16"},
} }
@ -320,6 +320,13 @@ func testFourByteSignature(t *testing.T, d *RocksDB) {
if !reflect.DeepEqual(*got, signature) { if !reflect.DeepEqual(*got, signature) {
t.Errorf("testFourByteSignature: got %+v, want %+v", got, signature) t.Errorf("testFourByteSignature: got %+v, want %+v", got, signature)
} }
gotSlice, err := d.GetFourByteSignatures(fourBytes)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(*gotSlice, []bchain.FourByteSignature{signature}) {
t.Errorf("testFourByteSignature: got %+v, want %+v", *gotSlice, []bchain.FourByteSignature{signature})
}
} }
// TestRocksDB_Index_EthereumType is an integration test probing the whole indexing functionality for EthereumType chains // TestRocksDB_Index_EthereumType is an integration test probing the whole indexing functionality for EthereumType chains
@ -1165,24 +1172,24 @@ func Test_packUnpackBlockTx(t *testing.T) {
func Test_packUnpackFourByteSignature(t *testing.T) { func Test_packUnpackFourByteSignature(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
signature FourByteSignature signature bchain.FourByteSignature
}{ }{
{ {
name: "no params", name: "no params",
signature: FourByteSignature{ signature: bchain.FourByteSignature{
Name: "abcdef", Name: "abcdef",
}, },
}, },
{ {
name: "one param", name: "one param",
signature: FourByteSignature{ signature: bchain.FourByteSignature{
Name: "opqr", Name: "opqr",
Parameters: []string{"uint16"}, Parameters: []string{"uint16"},
}, },
}, },
{ {
name: "multiple params", name: "multiple params",
signature: FourByteSignature{ signature: bchain.FourByteSignature{
Name: "xyz", Name: "xyz",
Parameters: []string{"address", "(bytes,uint256[],uint256)", "uint16"}, Parameters: []string{"address", "(bytes,uint256[],uint256)", "uint16"},
}, },

View File

@ -11,6 +11,7 @@ import (
"github.com/flier/gorocksdb" "github.com/flier/gorocksdb"
"github.com/golang/glog" "github.com/golang/glog"
"github.com/trezor/blockbook/bchain"
"github.com/trezor/blockbook/db" "github.com/trezor/blockbook/db"
) )
@ -98,13 +99,13 @@ func (fd *FourByteSignaturesDownloader) getPageWithRetry(url string) (*signature
return nil, errors.New("Too many retries to 4byte signatures") return nil, errors.New("Too many retries to 4byte signatures")
} }
func parseSignatureFromText(t string) *db.FourByteSignature { func parseSignatureFromText(t string) *bchain.FourByteSignature {
s := strings.Index(t, "(") s := strings.Index(t, "(")
e := strings.LastIndex(t, ")") e := strings.LastIndex(t, ")")
if s < 0 || e < 0 { if s < 0 || e < 0 {
return nil return nil
} }
var signature db.FourByteSignature var signature bchain.FourByteSignature
signature.Name = t[:s] signature.Name = t[:s]
params := t[s+1 : e] params := t[s+1 : e]
if len(params) > 0 { if len(params) > 0 {

View File

@ -4,26 +4,26 @@ import (
"reflect" "reflect"
"testing" "testing"
"github.com/trezor/blockbook/db" "github.com/trezor/blockbook/bchain"
) )
func Test_parseSignatureFromText(t *testing.T) { func Test_parseSignatureFromText(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
signature string signature string
want db.FourByteSignature want bchain.FourByteSignature
}{ }{
{ {
name: "_gonsPerFragment", name: "_gonsPerFragment",
signature: "_gonsPerFragment()", signature: "_gonsPerFragment()",
want: db.FourByteSignature{ want: bchain.FourByteSignature{
Name: "_gonsPerFragment", Name: "_gonsPerFragment",
}, },
}, },
{ {
name: "vestingDeposits", name: "vestingDeposits",
signature: "vestingDeposits(address)", signature: "vestingDeposits(address)",
want: db.FourByteSignature{ want: bchain.FourByteSignature{
Name: "vestingDeposits", Name: "vestingDeposits",
Parameters: []string{"address"}, Parameters: []string{"address"},
}, },
@ -31,7 +31,7 @@ func Test_parseSignatureFromText(t *testing.T) {
{ {
name: "batchTransferTokenB", name: "batchTransferTokenB",
signature: "batchTransferTokenB(address[],uint256)", signature: "batchTransferTokenB(address[],uint256)",
want: db.FourByteSignature{ want: bchain.FourByteSignature{
Name: "batchTransferTokenB", Name: "batchTransferTokenB",
Parameters: []string{"address[]", "uint256"}, Parameters: []string{"address[]", "uint256"},
}, },
@ -39,7 +39,7 @@ func Test_parseSignatureFromText(t *testing.T) {
{ {
name: "transmitAndSellTokenForEth", name: "transmitAndSellTokenForEth",
signature: "transmitAndSellTokenForEth(address,uint256,uint256,uint256,address,(uint8,bytes32,bytes32),bytes)", signature: "transmitAndSellTokenForEth(address,uint256,uint256,uint256,address,(uint8,bytes32,bytes32),bytes)",
want: db.FourByteSignature{ want: bchain.FourByteSignature{
Name: "transmitAndSellTokenForEth", Name: "transmitAndSellTokenForEth",
Parameters: []string{"address", "uint256", "uint256", "uint256", "address", "(uint8,bytes32,bytes32)", "bytes"}, Parameters: []string{"address", "uint256", "uint256", "uint256", "address", "(uint8,bytes32,bytes32)", "bytes"},
}, },

View File

@ -457,6 +457,8 @@ func (s *PublicServer) parseTemplates() []*template.Template {
"isOwnAddress": isOwnAddress, "isOwnAddress": isOwnAddress,
"toJSON": toJSON, "toJSON": toJSON,
"tokenTransfersCount": tokenTransfersCount, "tokenTransfersCount": tokenTransfersCount,
"tokenCount": tokenCount,
"hasPrefix": strings.HasPrefix,
} }
var createTemplate func(filenames ...string) *template.Template var createTemplate func(filenames ...string) *template.Template
if s.debug { if s.debug {
@ -559,7 +561,7 @@ func isOwnAddress(td *TemplateData, a string) bool {
return a == td.AddrStr return a == td.AddrStr
} }
// called from template, returns count of token transfers of given type // called from template, returns count of token transfers of given type in a tx
func tokenTransfersCount(tx *api.Tx, t api.TokenType) int { func tokenTransfersCount(tx *api.Tx, t api.TokenType) int {
count := 0 count := 0
for i := range tx.TokenTransfers { for i := range tx.TokenTransfers {
@ -570,6 +572,17 @@ func tokenTransfersCount(tx *api.Tx, t api.TokenType) int {
return count return count
} }
// called from template, returns count of tokens in array of given type
func tokenCount(tokens []api.Token, t api.TokenType) int {
count := 0
for i := range tokens {
if tokens[i].Type == t {
count++
}
}
return count
}
func (s *PublicServer) explorerTx(w http.ResponseWriter, r *http.Request) (tpl, *TemplateData, error) { func (s *PublicServer) explorerTx(w http.ResponseWriter, r *http.Request) (tpl, *TemplateData, error) {
var tx *api.Tx var tx *api.Tx
var err error var err error

File diff suppressed because one or more lines are too long

View File

@ -74,10 +74,15 @@ func setupRocksDB(parser bchain.BlockChainParser, chain bchain.BlockChain, t *te
if err := d.ConnectBlock(block2); err != nil { if err := d.ConnectBlock(block2); err != nil {
t.Fatal(err) t.Fatal(err)
} }
if err := InitTestFiatRates(d); err != nil { if err := initTestFiatRates(d); err != nil {
t.Fatal(err) t.Fatal(err)
} }
is.FinishedSync(block2.Height) is.FinishedSync(block2.Height)
if parser.GetChainType() == bchain.ChainEthereumType {
if err := initEthereumTypeDB(d); err != nil {
t.Fatal(err)
}
}
return d, is, tmp return d, is, tmp
} }
@ -168,8 +173,8 @@ func insertFiatRate(date string, rates map[string]float64, d *db.RocksDB) error
return d.FiatRatesStoreTicker(ticker) return d.FiatRatesStoreTicker(ticker)
} }
// InitTestFiatRates initializes test data for /api/v2/tickers endpoint // initTestFiatRates initializes test data for /api/v2/tickers endpoint
func InitTestFiatRates(d *db.RocksDB) error { func initTestFiatRates(d *db.RocksDB) error {
if err := insertFiatRate("20180320020000", map[string]float64{ if err := insertFiatRate("20180320020000", map[string]float64{
"usd": 2000.0, "usd": 2000.0,
"eur": 1300.0, "eur": 1300.0,
@ -235,7 +240,7 @@ func performHttpTests(tests []httpTests, t *testing.T, ts *httptest.Server) {
b := string(bb) b := string(bb)
for _, c := range tt.body { for _, c := range tt.body {
if !strings.Contains(b, c) { if !strings.Contains(b, c) {
t.Errorf("got %v, want to contain %v", b, c) t.Errorf("got\n%v\nwant to contain %v", b, c)
break break
} }
} }

View File

@ -30,7 +30,7 @@
<td>Nonce</td> <td>Nonce</td>
<td class="data">{{$addr.Nonce}}</td> <td class="data">{{$addr.Nonce}}</td>
</tr> </tr>
{{- if $addr.Tokens -}} {{if tokenCount $addr.Tokens "ERC20"}}
<tr> <tr>
<td>ERC20 Tokens</td> <td>ERC20 Tokens</td>
<td style="padding: 0;"> <td style="padding: 0;">
@ -41,13 +41,75 @@
<th>Tokens</th> <th>Tokens</th>
<th style="width: 15%;">Transfers</th> <th style="width: 15%;">Transfers</th>
</tr> </tr>
{{- range $t := $addr.Tokens -}} {{range $t := $addr.Tokens}}
{{if eq $t.Type "ERC20"}}
<tr> <tr>
<td class="data ellipsis">{{if $t.Contract}}<a href="/address/{{$t.Contract}}">{{$t.Name}}</a>{{else}}{{$t.Name}}{{end}}</td> <td class="data ellipsis">{{if $t.Contract}}<a href="/address/{{$t.Contract}}">{{$t.Name}}</a>{{else}}{{$t.Name}}{{end}}</td>
<td class="data">{{formatAmountWithDecimals $t.BalanceSat $t.Decimals}} {{$t.Symbol}}</td> <td class="data">{{formatAmountWithDecimals $t.BalanceSat $t.Decimals}} {{$t.Symbol}}</td>
<td class="data">{{$t.Transfers}}</td> <td class="data">{{$t.Transfers}}</td>
</tr> </tr>
{{- end -}} {{end}}
{{end}}
</tbody>
</table>
</td>
</tr>
{{- end -}}
{{if tokenCount $addr.Tokens "ERC721"}}
<tr>
<td>ERC721 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 $t := $addr.Tokens}}
{{if eq $t.Type "ERC721"}}
<tr>
<td class="data ellipsis">{{if $t.Contract}}<a href="/address/{{$t.Contract}}">{{$t.Name}}</a>{{else}}{{$t.Name}}{{end}}</td>
<td class="data">
{{range $i, $iv := $t.Ids}}
{{if $i}}, {{end}}
{{formatAmountWithDecimals $iv 0}}
{{end}}
</td>
<td class="data">{{$t.Transfers}}</td>
</tr>
{{end}}
{{end}}
</tbody>
</table>
</td>
</tr>
{{- end -}}
{{if tokenCount $addr.Tokens "ERC1155"}}
<tr>
<td>ERC1155 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 $t := $addr.Tokens}}
{{if eq $t.Type "ERC1155"}}
<tr>
<td class="data ellipsis">{{if $t.Contract}}<a href="/address/{{$t.Contract}}">{{$t.Name}}</a>{{else}}{{$t.Name}}{{end}}</td>
<td class="data">
{{range $i, $iv := $t.IdValues}}
{{if $i}}, {{end}}
{{formatAmountWithDecimals $iv.Id 0}}:{{formatAmountWithDecimals $iv.Value 0}} {{$t.Symbol}}
{{end}}
</td>
<td class="data">{{$t.Transfers}}</td>
</tr>
{{end}}
{{end}}
</tbody> </tbody>
</table> </table>
</td> </td>

View File

@ -84,6 +84,46 @@
<div class="data-div"> <div class="data-div">
{{template "txdetail" .}} {{template "txdetail" .}}
</div> </div>
{{if eq .ChainType 1}}
{{if $tx.EthereumSpecific.ParsedData}}
{{if $tx.EthereumSpecific.ParsedData.Function }}
<div class="data-div">
<h5>Input Data</h5>
<div class="row">
{{if $tx.EthereumSpecific.ParsedData.Name}}<div class="col-6">{{$tx.EthereumSpecific.ParsedData.Name}}</div>{{end}}{{if $tx.EthereumSpecific.ParsedData.MethodId}}<div class="col-6">Method ID: {{$tx.EthereumSpecific.ParsedData.MethodId}}</div>{{end}}
{{if $tx.EthereumSpecific.ParsedData.Function}}<div class="col-12">Function: {{$tx.EthereumSpecific.ParsedData.Function}}</div>{{end}}
{{if $tx.EthereumSpecific.ParsedData.Params}}
<div class="col-12">
<table class="table data-table">
<thead>
<tr>
<th style="width: 5%;">#</th>
<th>Type</th>
<th>Data</th>
</tr>
</thead>
<tbody>
{{range $i,$p := $tx.EthereumSpecific.ParsedData.Params}}
<tr>
<td>{{$i}}</td>
<td>{{$p.Type}}</td>
<td>
{{range $j,$v := $p.Values}}
{{if $j}}<br>{{end}}
{{if hasPrefix $p.Type "address"}}<a href="/address/{{$v}}">{{$v}}</a>{{else}}{{$v}}{{end}}
{{end}}
</td>
</tr>
{{end}}
</tbody>
</table>
</div>
{{end}}
</div>
</div>
{{end}}
{{end}}
{{end}}
<div class="data-div"> <div class="data-div">
<h5>Raw Transaction</h5> <h5>Raw Transaction</h5>
<div class="alert alert-data" style="word-wrap: break-word; font-size: smaller;"> <div class="alert alert-data" style="word-wrap: break-word; font-size: smaller;">

View File

@ -6,6 +6,9 @@
{{if eq $tx.EthereumSpecific.Status 1}}<span class="text-success"></span>{{end}}{{if eq $tx.EthereumSpecific.Status 0}}<span class="text-danger"></span>{{end}} {{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> </div>
{{- if $tx.Blocktime}}<div class="col-xs-5 col-md-4 text-muted text-right">{{if $tx.Confirmations}}mined{{else}}first seen{{end}} {{formatUnixTime $tx.Blocktime}}</div>{{end -}} {{- if $tx.Blocktime}}<div class="col-xs-5 col-md-4 text-muted text-right">{{if $tx.Confirmations}}mined{{else}}first seen{{end}} {{formatUnixTime $tx.Blocktime}}</div>{{end -}}
{{if $tx.EthereumSpecific.ParsedData}}
{{if $tx.EthereumSpecific.ParsedData.Name}}<div class="col-6">{{$tx.EthereumSpecific.ParsedData.Name}}</div>{{end}}{{if $tx.EthereumSpecific.ParsedData.MethodId}}<div class="col-6">Method ID: {{$tx.EthereumSpecific.ParsedData.MethodId}}</div>{{end}}
{{end}}
{{if $tx.EthereumSpecific.Error}}<div class="col-12">Error: {{$tx.EthereumSpecific.Error}}</div>{{end}} {{if $tx.EthereumSpecific.Error}}<div class="col-12">Error: {{$tx.EthereumSpecific.Error}}</div>{{end}}
</div> </div>
<div class="row line-mid"> <div class="row line-mid">
@ -265,7 +268,7 @@
</div> </div>
<div class="col-md-3 text-right" style="padding: .4rem 0;"> <div class="col-md-3 text-right" style="padding: .4rem 0;">
{{- range $iv := $tt.Values -}} {{- range $iv := $tt.Values -}}
{{formatAmountWithDecimals $iv.Id 0}}:{{formatAmountWithDecimals $iv.Value $tt.Decimals}} {{$tt.Symbol}} {{formatAmountWithDecimals $iv.Id 0}}:{{formatAmountWithDecimals $iv.Value 0}} {{$tt.Symbol}}
{{- end -}} {{- end -}}
</div> </div>
</div> </div>