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"`
GasPrice *Amount `json:"gasPrice"`
Data string `json:"data,omitempty"`
ParsedData *bchain.EthereumParsedInputData `json:"parsedData,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)
}
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
func (w *Worker) GetTransactionFromBchainTx(bchainTx *bchain.Tx, height int, spendingTxs bool, specificJSON bool) (*Tx, 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
if ethTxData.GasUsed != nil {
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
}
ethSpecific = &EthereumSpecific{
GasLimit: ethTxData.GasLimit,
GasPrice: (*Amount)(ethTxData.GasPrice),
GasUsed: ethTxData.GasUsed,
Nonce: ethTxData.Nonce,
Status: ethTxData.Status,
Data: ethTxData.Data,
GasLimit: ethTxData.GasLimit,
GasPrice: (*Amount)(ethTxData.GasPrice),
GasUsed: ethTxData.GasUsed,
Nonce: ethTxData.Nonce,
Status: ethTxData.Status,
Data: ethTxData.Data,
ParsedData: parsedInputData,
}
if internalData != nil {
ethSpecific.Type = internalData.Type
@ -674,7 +694,6 @@ func (w *Worker) getEthereumContractBalance(addrDesc bchain.AddressDescriptor, i
}
t := Token{
Type: ERC20TokenType,
Contract: ci.Contract,
Name: ci.Name,
Symbol: ci.Symbol,
@ -685,6 +704,7 @@ func (w *Worker) getEthereumContractBalance(addrDesc bchain.AddressDescriptor, i
// return contract balances/values only at or above AccountDetailsTokenBalances
if details >= AccountDetailsTokenBalances && validContract {
if c.Type == bchain.ERC20 {
t.Type = ERC20TokenType
// get Erc20 Contract Balance from blockchain, balance obtained from adding and subtracting transfers is not correct
b, err := w.chain.EthereumTypeGetErc20ContractBalance(addrDesc, c.Contract)
if err != nil {
@ -694,15 +714,20 @@ func (w *Worker) getEthereumContractBalance(addrDesc bchain.AddressDescriptor, i
t.BalanceSat = (*Amount)(b)
}
} else {
if len(t.Ids) > 0 {
ids := make([]Amount, len(t.Ids))
if c.Type == bchain.ERC721 {
t.Type = ERC771TokenType
} else {
t.Type = ERC1155TokenType
}
if len(c.Ids) > 0 {
ids := make([]Amount, len(c.Ids))
for j := range ids {
ids[j] = (Amount)(c.Ids[j])
}
t.Ids = ids
}
if len(t.IdValues) > 0 {
idValues := make([]TokenTransferValues, len(t.IdValues))
if len(c.IdValues) > 0 {
idValues := make([]TokenTransferValues, len(c.IdValues))
for j := range idValues {
idValues[j].Id = (*Amount)(&c.IdValues[j].Id)
idValues[j].Value = (*Amount)(&c.IdValues[j].Value)

View File

@ -4,8 +4,15 @@ import (
"bytes"
"encoding/hex"
"math/big"
"runtime/debug"
"strconv"
"strings"
"unicode"
"unicode/utf8"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/golang/glog"
"github.com/trezor/blockbook/bchain"
)
func parseSimpleNumericProperty(data string) *big.Int {
@ -58,15 +65,230 @@ func parseSimpleStringProperty(data string) string {
return ""
}
func Decamel(s string) string {
func decamel(s string) string {
var b bytes.Buffer
splittable := false
for _, v := range s {
if splittable && unicode.IsUpper(v) {
b.WriteByte(' ')
for i, v := range s {
if i == 0 {
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()
}
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
import "math/big"
import (
"math/big"
"github.com/ethereum/go-ethereum/accounts/abi"
)
// EthereumType specific
@ -12,6 +16,27 @@ type EthereumInternalTransfer struct {
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
type EthereumInternalTransactionType int

View File

@ -4,6 +4,7 @@ import (
"bytes"
"encoding/hex"
"math/big"
"sync"
"github.com/flier/gorocksdb"
"github.com/golang/glog"
@ -578,11 +579,8 @@ func (d *RocksDB) unpackEthInternalData(buf []byte) (*bchain.EthereumInternalDat
return &id, nil
}
type FourByteSignature struct {
Name string
Parameters []string
}
// FourByteSignature contains 4byte signature of transaction value with parameters
// and parsed parameters (that are not stored in DB)
func packFourByteKey(fourBytes uint32, id uint32) []byte {
key := make([]byte, 0, 8)
key = append(key, packUint(fourBytes)...)
@ -590,7 +588,7 @@ func packFourByteKey(fourBytes uint32, id uint32) []byte {
return key
}
func packFourByteSignature(signature *FourByteSignature) []byte {
func packFourByteSignature(signature *bchain.FourByteSignature) []byte {
buf := packString(signature.Name)
for i := range signature.Parameters {
buf = append(buf, packString(signature.Parameters[i])...)
@ -598,8 +596,8 @@ func packFourByteSignature(signature *FourByteSignature) []byte {
return buf
}
func unpackFourByteSignature(buf []byte) (*FourByteSignature, error) {
var signature FourByteSignature
func unpackFourByteSignature(buf []byte) (*bchain.FourByteSignature, error) {
var signature bchain.FourByteSignature
var l int
signature.Name, l = unpackString(buf)
for l < len(buf) {
@ -610,7 +608,8 @@ func unpackFourByteSignature(buf []byte) (*FourByteSignature, error) {
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)
val, err := d.db.GetCF(d.ro, d.cfh[cfFunctionSignatures], key)
if err != nil {
@ -624,12 +623,51 @@ func (d *RocksDB) GetFourByteSignature(fourBytes uint32, id uint32) (*FourByteSi
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)
wb.PutCF(d.cfh[cfFunctionSignatures], key, packFourByteSignature(signature))
cachedByteSignaturesMux.Lock()
delete(cachedByteSignatures, fourBytes)
cachedByteSignaturesMux.Unlock()
return nil
}
// GetEthereumInternalData gets transaction internal data from DB
func (d *RocksDB) GetEthereumInternalData(txid string) (*bchain.EthereumInternalData, error) {
btxID, err := d.chainParser.PackTxid(txid)
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))
}
} 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 {

View File

@ -301,7 +301,7 @@ func formatInternalData(in *bchain.EthereumInternalData) *bchain.EthereumInterna
func testFourByteSignature(t *testing.T, d *RocksDB) {
fourBytes := uint32(1234123)
id := uint32(42313)
signature := FourByteSignature{
signature := bchain.FourByteSignature{
Name: "xyz",
Parameters: []string{"address", "(bytes,uint256[],uint256)", "uint16"},
}
@ -320,6 +320,13 @@ func testFourByteSignature(t *testing.T, d *RocksDB) {
if !reflect.DeepEqual(*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
@ -1165,24 +1172,24 @@ func Test_packUnpackBlockTx(t *testing.T) {
func Test_packUnpackFourByteSignature(t *testing.T) {
tests := []struct {
name string
signature FourByteSignature
signature bchain.FourByteSignature
}{
{
name: "no params",
signature: FourByteSignature{
signature: bchain.FourByteSignature{
Name: "abcdef",
},
},
{
name: "one param",
signature: FourByteSignature{
signature: bchain.FourByteSignature{
Name: "opqr",
Parameters: []string{"uint16"},
},
},
{
name: "multiple params",
signature: FourByteSignature{
signature: bchain.FourByteSignature{
Name: "xyz",
Parameters: []string{"address", "(bytes,uint256[],uint256)", "uint16"},
},

View File

@ -11,6 +11,7 @@ import (
"github.com/flier/gorocksdb"
"github.com/golang/glog"
"github.com/trezor/blockbook/bchain"
"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")
}
func parseSignatureFromText(t string) *db.FourByteSignature {
func parseSignatureFromText(t string) *bchain.FourByteSignature {
s := strings.Index(t, "(")
e := strings.LastIndex(t, ")")
if s < 0 || e < 0 {
return nil
}
var signature db.FourByteSignature
var signature bchain.FourByteSignature
signature.Name = t[:s]
params := t[s+1 : e]
if len(params) > 0 {

View File

@ -4,26 +4,26 @@ import (
"reflect"
"testing"
"github.com/trezor/blockbook/db"
"github.com/trezor/blockbook/bchain"
)
func Test_parseSignatureFromText(t *testing.T) {
tests := []struct {
name string
signature string
want db.FourByteSignature
want bchain.FourByteSignature
}{
{
name: "_gonsPerFragment",
signature: "_gonsPerFragment()",
want: db.FourByteSignature{
want: bchain.FourByteSignature{
Name: "_gonsPerFragment",
},
},
{
name: "vestingDeposits",
signature: "vestingDeposits(address)",
want: db.FourByteSignature{
want: bchain.FourByteSignature{
Name: "vestingDeposits",
Parameters: []string{"address"},
},
@ -31,7 +31,7 @@ func Test_parseSignatureFromText(t *testing.T) {
{
name: "batchTransferTokenB",
signature: "batchTransferTokenB(address[],uint256)",
want: db.FourByteSignature{
want: bchain.FourByteSignature{
Name: "batchTransferTokenB",
Parameters: []string{"address[]", "uint256"},
},
@ -39,7 +39,7 @@ func Test_parseSignatureFromText(t *testing.T) {
{
name: "transmitAndSellTokenForEth",
signature: "transmitAndSellTokenForEth(address,uint256,uint256,uint256,address,(uint8,bytes32,bytes32),bytes)",
want: db.FourByteSignature{
want: bchain.FourByteSignature{
Name: "transmitAndSellTokenForEth",
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,
"toJSON": toJSON,
"tokenTransfersCount": tokenTransfersCount,
"tokenCount": tokenCount,
"hasPrefix": strings.HasPrefix,
}
var createTemplate func(filenames ...string) *template.Template
if s.debug {
@ -559,7 +561,7 @@ func isOwnAddress(td *TemplateData, a string) bool {
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 {
count := 0
for i := range tx.TokenTransfers {
@ -570,6 +572,17 @@ func tokenTransfersCount(tx *api.Tx, t api.TokenType) int {
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) {
var tx *api.Tx
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 {
t.Fatal(err)
}
if err := InitTestFiatRates(d); err != nil {
if err := initTestFiatRates(d); err != nil {
t.Fatal(err)
}
is.FinishedSync(block2.Height)
if parser.GetChainType() == bchain.ChainEthereumType {
if err := initEthereumTypeDB(d); err != nil {
t.Fatal(err)
}
}
return d, is, tmp
}
@ -168,8 +173,8 @@ func insertFiatRate(date string, rates map[string]float64, d *db.RocksDB) error
return d.FiatRatesStoreTicker(ticker)
}
// InitTestFiatRates initializes test data for /api/v2/tickers endpoint
func InitTestFiatRates(d *db.RocksDB) error {
// initTestFiatRates initializes test data for /api/v2/tickers endpoint
func initTestFiatRates(d *db.RocksDB) error {
if err := insertFiatRate("20180320020000", map[string]float64{
"usd": 2000.0,
"eur": 1300.0,
@ -235,7 +240,7 @@ func performHttpTests(tests []httpTests, t *testing.T, ts *httptest.Server) {
b := string(bb)
for _, c := range tt.body {
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
}
}

View File

@ -30,7 +30,7 @@
<td>Nonce</td>
<td class="data">{{$addr.Nonce}}</td>
</tr>
{{- if $addr.Tokens -}}
{{if tokenCount $addr.Tokens "ERC20"}}
<tr>
<td>ERC20 Tokens</td>
<td style="padding: 0;">
@ -41,13 +41,75 @@
<th>Tokens</th>
<th style="width: 15%;">Transfers</th>
</tr>
{{- range $t := $addr.Tokens -}}
{{range $t := $addr.Tokens}}
{{if eq $t.Type "ERC20"}}
<tr>
<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">{{$t.Transfers}}</td>
</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>
</table>
</td>

View File

@ -84,6 +84,46 @@
<div class="data-div">
{{template "txdetail" .}}
</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">
<h5>Raw Transaction</h5>
<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}}
</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.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}}
</div>
<div class="row line-mid">
@ -265,7 +268,7 @@
</div>
<div class="col-md-3 text-right" style="padding: .4rem 0;">
{{- 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 -}}
</div>
</div>