Return internal data and ERC721 and ERC1155 tokens from API and explorer

This commit is contained in:
Martin Boehm 2022-01-24 01:11:18 +01:00 committed by Martin
parent ae2d0e3958
commit 72e0ac23bc
7 changed files with 255 additions and 40 deletions

View File

@ -187,16 +187,25 @@ type TokenTransfer struct {
Values []TokenTransferValues `json:"values,omitempty"`
}
type EthereumInternalTransfer struct {
Type bchain.EthereumInternalTransactionType `json:"type"`
From string `json:"from"`
To string `json:"to"`
Value *Amount `json:"value"`
}
// EthereumSpecific contains ethereum specific transaction data
type EthereumSpecific struct {
TxType string `json:"txType,omitempty"`
Type bchain.EthereumInternalTransactionType `json:"type,omitempty"`
CreatedContract string `json:"createdContract,omitempty"`
Status eth.TxStatus `json:"status"` // 1 OK, 0 Fail, -1 pending
Error string `json:"error,omitempty"`
Nonce uint64 `json:"nonce"`
GasLimit *big.Int `json:"gasLimit"`
GasUsed *big.Int `json:"gasUsed"`
GasPrice *Amount `json:"gasPrice"`
Data string `json:"data,omitempty"`
InternalTransfers []bchain.EthereumInternalTransfer `json:"internalTransfers,omitempty"`
InternalTransfers []EthereumInternalTransfer `json:"internalTransfers,omitempty"`
}
// Tx holds information about a transaction
@ -279,6 +288,7 @@ type Address struct {
UnconfirmedTxs int `json:"unconfirmedTxs"`
Txs int `json:"txs"`
NonTokenTxs int `json:"nonTokenTxs,omitempty"`
InternalTxs int `json:"internalTxs,omitempty"`
Transactions []*Tx `json:"transactions,omitempty"`
Txids []string `json:"txids,omitempty"`
Nonce string `json:"nonce,omitempty"`

View File

@ -261,6 +261,15 @@ func (w *Worker) GetTransactionFromBchainTx(bchainTx *bchain.Tx, height int, spe
}
tokens = w.getEthereumTokensTransfers(tokenTransfers)
ethTxData := eth.GetEthereumTxData(bchainTx)
var internalData *bchain.EthereumInternalData
if eth.ProcessInternalTransactions {
internalData, err = w.db.GetEthereumInternalData(bchainTx.Txid)
if err != nil {
return nil, err
}
}
// mempool txs do not have fees yet
if ethTxData.GasUsed != nil {
feesSat.Mul(ethTxData.GasPrice, ethTxData.GasUsed)
@ -276,6 +285,21 @@ func (w *Worker) GetTransactionFromBchainTx(bchainTx *bchain.Tx, height int, spe
Status: ethTxData.Status,
Data: ethTxData.Data,
}
if internalData != nil {
ethSpecific.Type = internalData.Type
ethSpecific.CreatedContract = internalData.Contract
ethSpecific.Error = internalData.Error
ethSpecific.InternalTransfers = make([]EthereumInternalTransfer, len(internalData.Transfers))
for i := range internalData.Transfers {
f := &internalData.Transfers[i]
t := &ethSpecific.InternalTransfers[i]
t.From = f.From
t.To = f.To
t.Type = f.Type
t.Value = (*Amount)(&f.Value)
}
}
}
// for now do not return size, we would have to compute vsize of segwit transactions
// size:=len(bchainTx.Hex) / 2
@ -427,13 +451,25 @@ func (w *Worker) getEthereumTokensTransfers(transfers bchain.TokenTransfers) []T
if erc20c == nil {
erc20c = &bchain.Erc20Contract{Name: t.Contract}
}
var value *Amount
var values []TokenTransferValues
if t.Type == bchain.ERC1155 {
values = make([]TokenTransferValues, len(t.IdValues))
for j := range values {
values[j].Id = (*Amount)(&t.IdValues[j].Id)
values[j].Value = (*Amount)(&t.IdValues[j].Value)
}
} else {
value = (*Amount)(&t.Value)
}
tokens[i] = TokenTransfer{
Type: TokenTypeMap[t.Type],
Token: t.Contract,
From: t.From,
To: t.To,
Value: value,
Values: values,
Decimals: erc20c.Decimals,
Value: (*Amount)(&t.Value),
Name: erc20c.Name,
Symbol: erc20c.Symbol,
}
@ -657,29 +693,30 @@ func (w *Worker) getEthereumToken(index int, addrDesc, contract bchain.AddressDe
}, nil
}
func (w *Worker) getEthereumTypeAddressBalances(addrDesc bchain.AddressDescriptor, details AccountDetails, filter *AddressFilter) (*db.AddrBalance, []Token, *bchain.Erc20Contract, uint64, int, int, error) {
func (w *Worker) getEthereumTypeAddressBalances(addrDesc bchain.AddressDescriptor, details AccountDetails, filter *AddressFilter) (*db.AddrBalance, []Token, *bchain.Erc20Contract, uint64, int, int, int, error) {
var (
ba *db.AddrBalance
tokens []Token
ci *bchain.Erc20Contract
n uint64
nonContractTxs int
internalTxs int
)
// unknown number of results for paging
totalResults := -1
ca, err := w.db.GetAddrDescContracts(addrDesc)
if err != nil {
return nil, nil, nil, 0, 0, 0, NewAPIError(fmt.Sprintf("Address not found, %v", err), true)
return nil, nil, nil, 0, 0, 0, 0, NewAPIError(fmt.Sprintf("Address not found, %v", err), true)
}
b, err := w.chain.EthereumTypeGetBalance(addrDesc)
if err != nil {
return nil, nil, nil, 0, 0, 0, errors.Annotatef(err, "EthereumTypeGetBalance %v", addrDesc)
return nil, nil, nil, 0, 0, 0, 0, errors.Annotatef(err, "EthereumTypeGetBalance %v", addrDesc)
}
var filterDesc bchain.AddressDescriptor
if filter.Contract != "" {
filterDesc, err = w.chainParser.GetAddrDescFromAddress(filter.Contract)
if err != nil {
return nil, nil, nil, 0, 0, 0, NewAPIError(fmt.Sprintf("Invalid contract filter, %v", err), true)
return nil, nil, nil, 0, 0, 0, 0, NewAPIError(fmt.Sprintf("Invalid contract filter, %v", err), true)
}
}
if ca != nil {
@ -691,7 +728,7 @@ func (w *Worker) getEthereumTypeAddressBalances(addrDesc bchain.AddressDescripto
}
n, err = w.chain.EthereumTypeGetNonce(addrDesc)
if err != nil {
return nil, nil, nil, 0, 0, 0, errors.Annotatef(err, "EthereumTypeGetNonce %v", addrDesc)
return nil, nil, nil, 0, 0, 0, 0, errors.Annotatef(err, "EthereumTypeGetNonce %v", addrDesc)
}
if details > AccountDetailsBasic {
tokens = make([]Token, len(ca.Contracts))
@ -706,7 +743,7 @@ func (w *Worker) getEthereumTypeAddressBalances(addrDesc bchain.AddressDescripto
}
t, err := w.getEthereumToken(i+db.ContractIndexOffset, addrDesc, c.Contract, details, int(c.Txs))
if err != nil {
return nil, nil, nil, 0, 0, 0, err
return nil, nil, nil, 0, 0, 0, 0, err
}
tokens[j] = *t
j++
@ -716,7 +753,7 @@ func (w *Worker) getEthereumTypeAddressBalances(addrDesc bchain.AddressDescripto
if len(filterDesc) > 0 && j == 0 && details >= AccountDetailsTokens {
t, err := w.getEthereumToken(0, addrDesc, filterDesc, details, 0)
if err != nil {
return nil, nil, nil, 0, 0, 0, err
return nil, nil, nil, 0, 0, 0, 0, err
}
tokens = []Token{*t}
// switch off query for transactions, there are no transactions
@ -727,7 +764,7 @@ func (w *Worker) getEthereumTypeAddressBalances(addrDesc bchain.AddressDescripto
}
ci, err = w.chain.EthereumTypeGetErc20ContractInfo(addrDesc)
if err != nil {
return nil, nil, nil, 0, 0, 0, err
return nil, nil, nil, 0, 0, 0, 0, err
}
if filter.FromHeight == 0 && filter.ToHeight == 0 {
// compute total results for paging
@ -742,6 +779,7 @@ func (w *Worker) getEthereumTypeAddressBalances(addrDesc bchain.AddressDescripto
}
}
nonContractTxs = int(ca.NonContractTxs)
internalTxs = int(ca.InternalTxs)
} else {
// addresses without any normal transactions can have internal transactions and therefore balance
if b != nil {
@ -753,14 +791,14 @@ func (w *Worker) getEthereumTypeAddressBalances(addrDesc bchain.AddressDescripto
if len(filterDesc) > 0 && details >= AccountDetailsTokens {
t, err := w.getEthereumToken(0, addrDesc, filterDesc, details, 0)
if err != nil {
return nil, nil, nil, 0, 0, 0, err
return nil, nil, nil, 0, 0, 0, 0, err
}
tokens = []Token{*t}
// switch off query for transactions, there are no transactions
filter.Vout = AddressFilterVoutQueryNotNecessary
}
}
return ba, tokens, ci, n, nonContractTxs, totalResults, nil
return ba, tokens, ci, n, nonContractTxs, internalTxs, totalResults, nil
}
func (w *Worker) txFromTxid(txid string, bestheight uint32, option AccountDetails, blockInfo *db.BlockInfo) (*Tx, error) {
@ -865,6 +903,7 @@ func (w *Worker) GetAddress(address string, page int, txsOnPage int, option Acco
nonce string
unconfirmedTxs int
nonTokenTxs int
internalTxs int
totalResults int
)
addrDesc, address, err := w.getAddrDescAndNormalizeAddress(address)
@ -873,7 +912,7 @@ func (w *Worker) GetAddress(address string, page int, txsOnPage int, option Acco
}
if w.chainType == bchain.ChainEthereumType {
var n uint64
ba, tokens, erc20c, n, nonTokenTxs, totalResults, err = w.getEthereumTypeAddressBalances(addrDesc, option, filter)
ba, tokens, erc20c, n, nonTokenTxs, internalTxs, totalResults, err = w.getEthereumTypeAddressBalances(addrDesc, option, filter)
if err != nil {
return nil, err
}
@ -977,6 +1016,7 @@ func (w *Worker) GetAddress(address string, page int, txsOnPage int, option Acco
TotalSentSat: (*Amount)(totalSent),
Txs: int(ba.Txs),
NonTokenTxs: nonTokenTxs,
InternalTxs: internalTxs,
UnconfirmedBalanceSat: (*Amount)(&uBalSat),
UnconfirmedTxs: unconfirmedTxs,
Transactions: txs,

View File

@ -44,6 +44,7 @@ type Configuration struct {
MempoolTxTimeoutHours int `json:"mempoolTxTimeoutHours"`
QueryBackendOnMempoolResync bool `json:"queryBackendOnMempoolResync"`
ProcessInternalTransactions bool `json:"processInternalTransactions"`
ProcessZeroInternalTransactions bool `json:"processZeroInternalTransactions"`
}
// EthereumRPC is an interface to JSON-RPC eth service.
@ -65,6 +66,9 @@ type EthereumRPC struct {
ChainConfig *Configuration
}
// ProcessInternalTransactions specifies if internal transactions are processed
var ProcessInternalTransactions bool
// NewEthereumRPC returns new EthRPC instance.
func NewEthereumRPC(config json.RawMessage, pushHandler func(bchain.NotificationType)) (bchain.BlockChain, error) {
var err error
@ -90,6 +94,8 @@ func NewEthereumRPC(config json.RawMessage, pushHandler func(bchain.Notification
ChainConfig: &c,
}
ProcessInternalTransactions = c.ProcessInternalTransactions
// always create parser
s.Parser = NewEthereumParser(c.BlockAddressesToKeep)
s.timeout = time.Duration(c.RPCTimeout) * time.Second
@ -498,7 +504,7 @@ func (b *EthereumRPC) getTokenTransferEventsForBlock(blockNumber string) (map[st
err := b.rpc.CallContext(ctx, &logs, "eth_getLogs", map[string]interface{}{
"fromBlock": blockNumber,
"toBlock": blockNumber,
"topics": []string{tokenTransferEventSignature, tokenERC1155TransferSingleEventSignature, tokenERC1155TransferBatchEventSignature},
// "topics": []string{tokenTransferEventSignature, tokenERC1155TransferSingleEventSignature, tokenERC1155TransferBatchEventSignature},
})
if err != nil {
return nil, errors.Annotatef(err, "blockNumber %v", blockNumber)
@ -543,7 +549,7 @@ func (b *EthereumRPC) processCallTrace(call *rpcCallTrace, d *bchain.EthereumInt
From: call.From,
To: call.To,
})
} else if err == nil && value.BitLen() > 0 {
} else if err == nil && (value.BitLen() > 0 || b.ChainConfig.ProcessZeroInternalTransactions) {
d.Transfers = append(d.Transfers, bchain.EthereumInternalTransfer{
Value: *value,
From: call.From,
@ -561,7 +567,7 @@ func (b *EthereumRPC) processCallTrace(call *rpcCallTrace, d *bchain.EthereumInt
// getInternalDataForBlock fetches debug trace using callTracer, extracts internal transfers and creations and destructions of contracts
func (b *EthereumRPC) getInternalDataForBlock(blockHash string, transactions []bchain.RpcTransaction) ([]bchain.EthereumInternalData, error) {
data := make([]bchain.EthereumInternalData, len(transactions))
if b.ChainConfig.ProcessInternalTransactions {
if ProcessInternalTransactions {
ctx, cancel := context.WithTimeout(context.Background(), b.timeout)
defer cancel()
var trace []rpcTraceResult
@ -640,6 +646,7 @@ func (b *EthereumRPC) GetBlock(hash string, height uint32) (*bchain.Block, error
internalData, err := b.getInternalDataForBlock(head.Hash, body.Transactions)
if err != nil {
blockSpecificData = &bchain.EthereumBlockSpecificData{InternalDataError: err.Error()}
glog.Info("InternalDataError ", bbh.Height, ": ", err.Error())
}
btxs := make([]bchain.Tx, len(body.Transactions))

View File

@ -456,6 +456,7 @@ func (s *PublicServer) parseTemplates() []*template.Template {
"setTxToTemplateData": setTxToTemplateData,
"isOwnAddress": isOwnAddress,
"toJSON": toJSON,
"tokenTransfersCount": tokenTransfersCount,
}
var createTemplate func(filenames ...string) *template.Template
if s.debug {
@ -558,6 +559,17 @@ func isOwnAddress(td *TemplateData, a string) bool {
return a == td.AddrStr
}
// called from template, returns count of token transfers of given type
func tokenTransfersCount(tx *api.Tx, t api.TokenType) int {
count := 0
for i := range tx.TokenTransfers {
if tx.TokenTransfers[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

View File

@ -34,7 +34,7 @@ func httpTestsEthereumType(t *testing.T, ts *httptest.Server) {
status: http.StatusOK,
contentType: "application/json; charset=utf-8",
body: []string{
`{"page":1,"totalPages":1,"itemsOnPage":1000,"address":"0x4Bda106325C335dF99eab7fE363cAC8A0ba2a24D","balance":"123450075","unconfirmedBalance":"0","unconfirmedTxs":0,"txs":1,"nonTokenTxs":1,"txids":["0xc92919ad24ffd58f760b18df7949f06e1190cf54a50a0e3745a385608ed3cbf2"],"nonce":"75","tokens":[{"type":"ERC20","name":"Contract 13","contract":"0x0d0F936Ee4c93e25944694D6C121de94D9760F11","transfers":2,"symbol":"S13","decimals":18},{"type":"ERC20","name":"Contract 74","contract":"0x4af4114F73d1c1C903aC9E0361b379D1291808A2","transfers":2,"symbol":"S74","decimals":18}],"erc20Contract":{"contract":"0x4Bda106325C335dF99eab7fE363cAC8A0ba2a24D","name":"Contract 75","symbol":"S75","decimals":18}}`,
`{"page":1,"totalPages":1,"itemsOnPage":1000,"address":"0x4Bda106325C335dF99eab7fE363cAC8A0ba2a24D","balance":"123450075","unconfirmedBalance":"0","unconfirmedTxs":0,"txs":1,"nonTokenTxs":1,"internalTxs":1,"txids":["0xc92919ad24ffd58f760b18df7949f06e1190cf54a50a0e3745a385608ed3cbf2"],"nonce":"75","tokens":[{"type":"ERC20","name":"Contract 13","contract":"0x0d0F936Ee4c93e25944694D6C121de94D9760F11","transfers":2,"symbol":"S13","decimals":18},{"type":"ERC20","name":"Contract 74","contract":"0x4af4114F73d1c1C903aC9E0361b379D1291808A2","transfers":2,"symbol":"S74","decimals":18}],"erc20Contract":{"contract":"0x4Bda106325C335dF99eab7fE363cAC8A0ba2a24D","name":"Contract 75","symbol":"S75","decimals":18}}`,
},
},
}

View File

@ -22,6 +22,10 @@
<td>Non-contract Transactions</td>
<td class="data">{{$addr.NonTokenTxs}}</td>
</tr>
<tr>
<td>Internal Transactions</td>
<td class="data">{{$addr.InternalTxs}}</td>
</tr>
<tr>
<td>Nonce</td>
<td class="data">{{$addr.Nonce}}</td>

View File

@ -6,6 +6,7 @@
{{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.Error}}<div class="col-12">Error: {{$tx.EthereumSpecific.Error}}</div>{{end}}
</div>
<div class="row line-mid">
<div class="col-md-4">
@ -67,19 +68,20 @@
{{formatAmount $tx.ValueOutSat}} {{$cs}}
</div>
</div>
{{- if $tx.TokenTransfers -}}
{{if $tx.EthereumSpecific.InternalTransfers}}
<div class="row line-top" style="padding: 15px 0 6px 15px;font-weight: bold;">
ERC20 Token Transfers
Internal Transactions
</div>
{{- range $erc20 := $tx.TokenTransfers -}}
{{- range $tt := $tx.EthereumSpecific.InternalTransfers -}}
<div class="row" style="padding: 2px 15px;">
<div class="col-md-4">
<div class="row tx-in">
<table class="table data-table">
<tbody>
<tr{{if isOwnAddress $data $erc20.From}} class="tx-own"{{end}}>
<tr{{if isOwnAddress $data $tt.From}} class="tx-own"{{end}}>
<td>
<span class="ellipsis tx-addr">{{if ne $erc20.From $addr}}<a href="/address/{{$erc20.From}}">{{$erc20.From}}</a>{{else}}{{$erc20.From}}{{end}}</span>
<span class="ellipsis tx-addr">{{if ne $tt.From $addr}}<a href="/address/{{$tt.From}}">{{$tt.From}}</a>{{else}}{{$tt.From}}{{end}}</span>
</td>
</tr>
</tbody>
@ -95,20 +97,160 @@
<div class="row tx-out">
<table class="table data-table">
<tbody>
<tr{{if isOwnAddress $data $erc20.To}} class="tx-own"{{end}}>
<tr{{if isOwnAddress $data $tt.To}} class="tx-own"{{end}}>
<td>
<span class="ellipsis tx-addr">{{if ne $erc20.To $addr}}<a href="/address/{{$erc20.To}}">{{$erc20.To}}</a>{{else}}{{$erc20.To}}{{end}}</span>
<span class="ellipsis tx-addr">{{if ne $tt.To $addr}}<a href="/address/{{$tt.To}}">{{$tt.To}}</a>{{else}}{{$tt.To}}{{end}}</span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="col-md-3 text-right" style="padding: .4rem 0;">{{formatAmountWithDecimals $erc20.Value $erc20.Decimals}} {{$erc20.Symbol}}</div>
<div class="col-md-3 text-right" style="padding: .4rem 0;">{{formatAmount $tt.Value}} {{$cs}}</div>
</div>
{{- end -}}
<div class="row" style="padding: 6px 15px;"></div>
{{- end -}}
{{- if tokenTransfersCount $tx "ERC20" -}}
<div class="row line-top" style="padding: 15px 0 6px 15px;font-weight: bold;">
ERC20 Token Transfers
</div>
{{- range $tt := $tx.TokenTransfers -}}
{{if eq $tt.Type "ERC20"}}
<div class="row" style="padding: 2px 15px;">
<div class="col-md-4">
<div class="row tx-in">
<table class="table data-table">
<tbody>
<tr{{if isOwnAddress $data $tt.From}} class="tx-own"{{end}}>
<td>
<span class="ellipsis tx-addr">{{if ne $tt.From $addr}}<a href="/address/{{$tt.From}}">{{$tt.From}}</a>{{else}}{{$tt.From}}{{end}}</span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="col-md-1 col-xs-12 text-center">
<svg class="octicon" viewBox="0 0 8 16">
<path fill-rule="evenodd" d="M7.5 8l-5 5L1 11.5 4.75 8 1 4.5 2.5 3l5 5z"></path>
</svg>
</div>
<div class="col-md-4">
<div class="row tx-out">
<table class="table data-table">
<tbody>
<tr{{if isOwnAddress $data $tt.To}} class="tx-own"{{end}}>
<td>
<span class="ellipsis tx-addr">{{if ne $tt.To $addr}}<a href="/address/{{$tt.To}}">{{$tt.To}}</a>{{else}}{{$tt.To}}{{end}}</span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="col-md-3 text-right" style="padding: .4rem 0;">{{formatAmountWithDecimals $tt.Value $tt.Decimals}} {{$tt.Symbol}}</div>
</div>
{{- end -}}
{{- end -}}
<div class="row" style="padding: 6px 15px;"></div>
{{- end -}}
{{- if tokenTransfersCount $tx "ERC721" -}}
<div class="row line-top" style="padding: 15px 0 6px 15px;font-weight: bold;">
ERC721 Token Transfers
</div>
{{- range $tt := $tx.TokenTransfers -}}
{{if eq $tt.Type "ERC721"}}
<div class="row" style="padding: 2px 15px;">
<div class="col-md-4">
<div class="row tx-in">
<table class="table data-table">
<tbody>
<tr{{if isOwnAddress $data $tt.From}} class="tx-own"{{end}}>
<td>
<span class="ellipsis tx-addr">{{if ne $tt.From $addr}}<a href="/address/{{$tt.From}}">{{$tt.From}}</a>{{else}}{{$tt.From}}{{end}}</span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="col-md-1 col-xs-12 text-center">
<svg class="octicon" viewBox="0 0 8 16">
<path fill-rule="evenodd" d="M7.5 8l-5 5L1 11.5 4.75 8 1 4.5 2.5 3l5 5z"></path>
</svg>
</div>
<div class="col-md-4">
<div class="row tx-out">
<table class="table data-table">
<tbody>
<tr{{if isOwnAddress $data $tt.To}} class="tx-own"{{end}}>
<td>
<span class="ellipsis tx-addr">{{if ne $tt.To $addr}}<a href="/address/{{$tt.To}}">{{$tt.To}}</a>{{else}}{{$tt.To}}{{end}}</span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="col-md-3 text-right" style="padding: .4rem 0;">ID {{formatAmountWithDecimals $tt.Value 0}} {{$tt.Symbol}}</div>
</div>
{{- end -}}
{{- end -}}
<div class="row" style="padding: 6px 15px;"></div>
{{- end -}}
{{- if tokenTransfersCount $tx "ERC1155" -}}
<div class="row line-top" style="padding: 15px 0 6px 15px;font-weight: bold;">
ERC1155 Token Transfers
</div>
{{- range $tt := $tx.TokenTransfers -}}
{{if eq $tt.Type "ERC1155"}}
<div class="row" style="padding: 2px 15px;">
<div class="col-md-4">
<div class="row tx-in">
<table class="table data-table">
<tbody>
<tr{{if isOwnAddress $data $tt.From}} class="tx-own"{{end}}>
<td>
<span class="ellipsis tx-addr">{{if ne $tt.From $addr}}<a href="/address/{{$tt.From}}">{{$tt.From}}</a>{{else}}{{$tt.From}}{{end}}</span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="col-md-1 col-xs-12 text-center">
<svg class="octicon" viewBox="0 0 8 16">
<path fill-rule="evenodd" d="M7.5 8l-5 5L1 11.5 4.75 8 1 4.5 2.5 3l5 5z"></path>
</svg>
</div>
<div class="col-md-4">
<div class="row tx-out">
<table class="table data-table">
<tbody>
<tr{{if isOwnAddress $data $tt.To}} class="tx-own"{{end}}>
<td>
<span class="ellipsis tx-addr">{{if ne $tt.To $addr}}<a href="/address/{{$tt.To}}">{{$tt.To}}</a>{{else}}{{$tt.To}}{{end}}</span>
</td>
</tr>
</tbody>
</table>
</div>
</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}}
{{- end -}}
</div>
</div>
{{- end -}}
{{- end -}}
<div class="row" style="padding: 6px 15px;"></div>
{{- end -}}
<div class="row line-top">
<div class="col-xs-6 col-sm-4 col-md-4">
{{- if $tx.FeesSat -}}