Explorer redesing tuning
This commit is contained in:
parent
9919f1a685
commit
dca00bf770
19
api/types.go
19
api/types.go
@ -5,7 +5,6 @@ import (
|
||||
"errors"
|
||||
"math/big"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/trezor/blockbook/bchain"
|
||||
@ -190,14 +189,17 @@ func (a Tokens) Less(i, j int) bool {
|
||||
} else if ti.BaseValue > tj.BaseValue {
|
||||
return true
|
||||
}
|
||||
c := strings.Compare(ti.Name, tj.Name)
|
||||
if c == 1 {
|
||||
return false
|
||||
} else if c == -1 {
|
||||
return true
|
||||
if ti.Name == "" {
|
||||
if tj.Name != "" {
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
if tj.Name == "" {
|
||||
return true
|
||||
}
|
||||
return ti.Name < tj.Name
|
||||
}
|
||||
c = strings.Compare(ti.Contract, tj.Contract)
|
||||
return c == -1
|
||||
return ti.Contract < tj.Contract
|
||||
}
|
||||
|
||||
// TokenTransfer contains info about a token transfer done in a transaction
|
||||
@ -329,6 +331,7 @@ type Address struct {
|
||||
Nonce string `json:"nonce,omitempty"`
|
||||
UsedTokens int `json:"usedTokens,omitempty"`
|
||||
Tokens Tokens `json:"tokens,omitempty"`
|
||||
FiatValue float64 `json:"fiatValue,omitempty"`
|
||||
TokensBaseValue float64 `json:"tokensBaseValue,omitempty"`
|
||||
TokensFiatValue float64 `json:"tokensFiatValue,omitempty"`
|
||||
TotalBaseValue float64 `json:"totalBaseValue,omitempty"`
|
||||
|
||||
@ -6,6 +6,7 @@ import (
|
||||
"encoding/json"
|
||||
"math/big"
|
||||
"reflect"
|
||||
"sort"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@ -219,3 +220,104 @@ func TestAmount_Compare(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTokens_Sort(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
unsorted Tokens
|
||||
sorted Tokens
|
||||
}{
|
||||
{
|
||||
name: "one",
|
||||
unsorted: Tokens{
|
||||
{
|
||||
Name: "a",
|
||||
Contract: "0x1",
|
||||
BaseValue: 12.34,
|
||||
},
|
||||
},
|
||||
sorted: Tokens{
|
||||
{
|
||||
Name: "a",
|
||||
Contract: "0x1",
|
||||
BaseValue: 12.34,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "mix",
|
||||
unsorted: Tokens{
|
||||
{
|
||||
Name: "",
|
||||
Contract: "0x6",
|
||||
BaseValue: 0,
|
||||
},
|
||||
{
|
||||
Name: "",
|
||||
Contract: "0x5",
|
||||
BaseValue: 0,
|
||||
},
|
||||
{
|
||||
Name: "b",
|
||||
Contract: "0x2",
|
||||
BaseValue: 1,
|
||||
},
|
||||
{
|
||||
Name: "d",
|
||||
Contract: "0x4",
|
||||
BaseValue: 0,
|
||||
},
|
||||
{
|
||||
Name: "a",
|
||||
Contract: "0x1",
|
||||
BaseValue: 12.34,
|
||||
},
|
||||
{
|
||||
Name: "c",
|
||||
Contract: "0x3",
|
||||
BaseValue: 0,
|
||||
},
|
||||
},
|
||||
sorted: Tokens{
|
||||
{
|
||||
Name: "a",
|
||||
Contract: "0x1",
|
||||
BaseValue: 12.34,
|
||||
},
|
||||
{
|
||||
Name: "b",
|
||||
Contract: "0x2",
|
||||
BaseValue: 1,
|
||||
},
|
||||
{
|
||||
Name: "c",
|
||||
Contract: "0x3",
|
||||
BaseValue: 0,
|
||||
},
|
||||
{
|
||||
Name: "d",
|
||||
Contract: "0x4",
|
||||
BaseValue: 0,
|
||||
},
|
||||
{
|
||||
Name: "",
|
||||
Contract: "0x5",
|
||||
BaseValue: 0,
|
||||
},
|
||||
{
|
||||
Name: "",
|
||||
Contract: "0x6",
|
||||
BaseValue: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
sort.Sort(tt.unsorted)
|
||||
if !reflect.DeepEqual(tt.unsorted, tt.sorted) {
|
||||
t.Errorf("Tokens Sort got %v, want %v", tt.unsorted, tt.sorted)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -1256,22 +1256,26 @@ func (w *Worker) GetAddress(address string, page int, txsOnPage int, option Acco
|
||||
}
|
||||
}
|
||||
}
|
||||
var secondaryRate, totalFiatValue float64
|
||||
ticker := w.is.GetCurrentTicker("", "")
|
||||
totalBaseValue, err := strconv.ParseFloat((*Amount)(&ba.BalanceSat).DecimalString(w.chainParser.AmountDecimals()), 64)
|
||||
if ticker != nil && err == nil {
|
||||
r, found := ticker.Rates[secondaryCoin]
|
||||
if found {
|
||||
secondaryRate = float64(r)
|
||||
}
|
||||
}
|
||||
if w.chainType == bchain.ChainBitcoinType {
|
||||
totalReceived = ba.ReceivedSat()
|
||||
totalSent = &ba.SentSat
|
||||
} else {
|
||||
totalBaseValue += ed.tokensBaseValue
|
||||
}
|
||||
totalFiatValue = secondaryRate * totalBaseValue
|
||||
var secondaryRate, totalFiatValue, totalBaseValue, fiatValue float64
|
||||
if secondaryCoin != "" {
|
||||
ticker := w.is.GetCurrentTicker("", "")
|
||||
balance, err := strconv.ParseFloat((*Amount)(&ba.BalanceSat).DecimalString(w.chainParser.AmountDecimals()), 64)
|
||||
if ticker != nil && err == nil {
|
||||
r, found := ticker.Rates[secondaryCoin]
|
||||
if found {
|
||||
secondaryRate = float64(r)
|
||||
}
|
||||
}
|
||||
fiatValue = secondaryRate * balance
|
||||
if w.chainType == bchain.ChainEthereumType {
|
||||
totalBaseValue += balance + ed.tokensBaseValue
|
||||
totalFiatValue = secondaryRate * totalBaseValue
|
||||
}
|
||||
}
|
||||
r := &Address{
|
||||
Paging: pg,
|
||||
AddrStr: address,
|
||||
@ -1286,6 +1290,7 @@ func (w *Worker) GetAddress(address string, page int, txsOnPage int, option Acco
|
||||
Transactions: txs,
|
||||
Txids: txids,
|
||||
Tokens: ed.tokens,
|
||||
FiatValue: fiatValue,
|
||||
TokensBaseValue: ed.tokensBaseValue,
|
||||
TokensFiatValue: ed.tokensFiatValue,
|
||||
TotalBaseValue: totalBaseValue,
|
||||
|
||||
18
api/xpub.go
18
api/xpub.go
@ -4,6 +4,7 @@ import (
|
||||
"fmt"
|
||||
"math/big"
|
||||
"sort"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@ -387,7 +388,7 @@ func (w *Worker) getXpubData(xd *bchain.XpubDescriptor, page int, txsOnPage int,
|
||||
}
|
||||
|
||||
// GetXpubAddress computes address value and gets transactions for given address
|
||||
func (w *Worker) GetXpubAddress(xpub string, page int, txsOnPage int, option AccountDetails, filter *AddressFilter, gap int) (*Address, error) {
|
||||
func (w *Worker) GetXpubAddress(xpub string, page int, txsOnPage int, option AccountDetails, filter *AddressFilter, gap int, secondaryCoin string) (*Address, error) {
|
||||
start := time.Now()
|
||||
page--
|
||||
if page < 0 {
|
||||
@ -567,6 +568,20 @@ func (w *Worker) GetXpubAddress(xpub string, page int, txsOnPage int, option Acc
|
||||
setIsOwnAddresses(txs, xpubAddresses)
|
||||
var totalReceived big.Int
|
||||
totalReceived.Add(&data.balanceSat, &data.sentSat)
|
||||
|
||||
var fiatValue float64
|
||||
if secondaryCoin != "" {
|
||||
ticker := w.is.GetCurrentTicker("", "")
|
||||
balance, err := strconv.ParseFloat((*Amount)(&data.balanceSat).DecimalString(w.chainParser.AmountDecimals()), 64)
|
||||
if ticker != nil && err == nil {
|
||||
r, found := ticker.Rates[secondaryCoin]
|
||||
if found {
|
||||
secondaryRate := float64(r)
|
||||
fiatValue = secondaryRate * balance
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addr := Address{
|
||||
Paging: pg,
|
||||
AddrStr: xpub,
|
||||
@ -580,6 +595,7 @@ func (w *Worker) GetXpubAddress(xpub string, page int, txsOnPage int, option Acc
|
||||
Txids: txids,
|
||||
UsedTokens: usedTokens,
|
||||
Tokens: tokens,
|
||||
FiatValue: fiatValue,
|
||||
XPubAddresses: xpubAddresses,
|
||||
AddressAliases: w.getAddressAliases(addresses),
|
||||
}
|
||||
|
||||
@ -592,7 +592,7 @@ func (b *EthereumRPC) getCreationContractInfo(contract string, height uint32) *b
|
||||
|
||||
func (b *EthereumRPC) processCallTrace(call *rpcCallTrace, d *bchain.EthereumInternalData, contracts []bchain.ContractInfo, blockHeight uint32) []bchain.ContractInfo {
|
||||
value, err := hexutil.DecodeBig(call.Value)
|
||||
if call.Type == "CREATE" {
|
||||
if call.Type == "CREATE" || call.Type == "CREATE2" {
|
||||
d.Transfers = append(d.Transfers, bchain.EthereumInternalTransfer{
|
||||
Type: bchain.CREATE,
|
||||
Value: *value,
|
||||
@ -600,7 +600,6 @@ func (b *EthereumRPC) processCallTrace(call *rpcCallTrace, d *bchain.EthereumInt
|
||||
To: call.To, // new contract address
|
||||
})
|
||||
contracts = append(contracts, *b.getCreationContractInfo(call.To, blockHeight))
|
||||
|
||||
} else if call.Type == "SELFDESTRUCT" {
|
||||
d.Transfers = append(d.Transfers, bchain.EthereumInternalTransfer{
|
||||
Type: bchain.SELFDESTRUCT,
|
||||
|
||||
@ -850,8 +850,12 @@ func (s *PublicServer) summaryValuesSpan(baseValue float64, secondaryValue float
|
||||
rv.WriteString(")</span>")
|
||||
}
|
||||
} else {
|
||||
if td.SecondaryCoin != "" {
|
||||
rv.WriteString("-")
|
||||
if baseValue > 0 {
|
||||
appendAmountSpan(&rv, "", strconv.FormatFloat(baseValue, 'f', 6, 64), td.CoinShortcut, "")
|
||||
} else {
|
||||
if td.SecondaryCoin != "" {
|
||||
rv.WriteString("-")
|
||||
}
|
||||
}
|
||||
}
|
||||
return template.HTML(rv.String())
|
||||
@ -1184,16 +1188,16 @@ func (s *PublicServer) explorerXpub(w http.ResponseWriter, r *http.Request) (tpl
|
||||
return errorTpl, nil, api.NewAPIError("Missing xpub", true)
|
||||
}
|
||||
s.metrics.ExplorerViews.With(common.Labels{"action": "xpub"}).Inc()
|
||||
page, _, _, filter, filterParam, gap := s.getAddressQueryParams(r, api.AccountDetailsTxHistoryLight, txsOnPage)
|
||||
// do not allow txsOnPage and details to be changed by query params
|
||||
address, err := s.api.GetXpubAddress(xpub, page, txsOnPage, api.AccountDetailsTxHistoryLight, filter, gap)
|
||||
page, _, _, filter, filterParam, gap := s.getAddressQueryParams(r, api.AccountDetailsTxHistoryLight, txsOnPage)
|
||||
data := s.newTemplateData(r)
|
||||
address, err := s.api.GetXpubAddress(xpub, page, txsOnPage, api.AccountDetailsTxHistoryLight, filter, gap, strings.ToLower(data.SecondaryCoin))
|
||||
if err != nil {
|
||||
if err == api.ErrUnsupportedXpub {
|
||||
err = api.NewAPIError("XPUB functionality is not supported", true)
|
||||
}
|
||||
return errorTpl, nil, err
|
||||
}
|
||||
data := s.newTemplateData(r)
|
||||
data.AddrStr = address.AddrStr
|
||||
data.Address = address
|
||||
data.Page = address.Page
|
||||
@ -1267,7 +1271,7 @@ func (s *PublicServer) explorerSearch(w http.ResponseWriter, r *http.Request) (t
|
||||
var err error
|
||||
s.metrics.ExplorerViews.With(common.Labels{"action": "search"}).Inc()
|
||||
if len(q) > 0 {
|
||||
address, err = s.api.GetXpubAddress(q, 0, 1, api.AccountDetailsBasic, &api.AddressFilter{Vout: api.AddressFilterVoutOff}, 0)
|
||||
address, err = s.api.GetXpubAddress(q, 0, 1, api.AccountDetailsBasic, &api.AddressFilter{Vout: api.AddressFilterVoutOff}, 0, "")
|
||||
if err == nil {
|
||||
http.Redirect(w, r, joinURL("/xpub/", url.QueryEscape(address.AddrStr)), http.StatusFound)
|
||||
return noTpl, nil, nil
|
||||
@ -1501,7 +1505,8 @@ func (s *PublicServer) apiXpub(r *http.Request, apiVersion int) (interface{}, er
|
||||
var err error
|
||||
s.metrics.ExplorerViews.With(common.Labels{"action": "api-xpub"}).Inc()
|
||||
page, pageSize, details, filter, _, gap := s.getAddressQueryParams(r, api.AccountDetailsTxidHistory, txsInAPI)
|
||||
address, err = s.api.GetXpubAddress(xpub, page, pageSize, details, filter, gap)
|
||||
secondaryCoin := strings.ToLower(r.URL.Query().Get("secondary"))
|
||||
address, err = s.api.GetXpubAddress(xpub, page, pageSize, details, filter, gap, secondaryCoin)
|
||||
if err == nil && apiVersion == apiV1 {
|
||||
return s.api.AddressToV1(address), nil
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -538,7 +538,7 @@ func (s *WebsocketServer) getAccountInfo(req *accountInfoReq) (res *api.Address,
|
||||
if req.PageSize == 0 {
|
||||
req.PageSize = txsOnPage
|
||||
}
|
||||
a, err := s.api.GetXpubAddress(req.Descriptor, req.Page, req.PageSize, opt, &filter, req.Gap)
|
||||
a, err := s.api.GetXpubAddress(req.Descriptor, req.Page, req.PageSize, opt, &filter, req.Gap, strings.ToLower(req.SecondaryCurrency))
|
||||
if err != nil {
|
||||
return s.api.GetAddress(req.Descriptor, req.Page, req.PageSize, opt, &filter, strings.ToLower(req.SecondaryCurrency))
|
||||
}
|
||||
|
||||
@ -15,7 +15,7 @@ body {
|
||||
body {
|
||||
min-height: 100%;
|
||||
margin: 0;
|
||||
background: linear-gradient(to bottom, #f6f6f6 300px, #e5e5e5 0), #e5e5e5;
|
||||
background: linear-gradient(to bottom, #f6f6f6 360px, #e5e5e5 0), #e5e5e5;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
@ -588,6 +588,16 @@ span.btn-paging:hover {
|
||||
@media (max-width: 768px) {
|
||||
body {
|
||||
font-size: 0.8rem;
|
||||
background: linear-gradient(to bottom, #f6f6f6 500px, #e5e5e5 0), #e5e5e5;
|
||||
}
|
||||
|
||||
.container {
|
||||
padding-left: 2px;
|
||||
padding-right: 2px;
|
||||
}
|
||||
|
||||
.accordion-body {
|
||||
padding: var(--bs-accordion-body-padding-y) 0;
|
||||
}
|
||||
|
||||
.octicon {
|
||||
@ -595,6 +605,10 @@ span.btn-paging:hover {
|
||||
margin-top: -2px;
|
||||
}
|
||||
|
||||
.unconfirmed {
|
||||
padding: 0.1rem 0.8rem;
|
||||
}
|
||||
|
||||
.btn {
|
||||
--bs-btn-font-size: 0.8rem;
|
||||
}
|
||||
|
||||
@ -1,12 +1,19 @@
|
||||
{{define "specific"}}{{$addr := .Address}}{{$data := .}}
|
||||
<div class="row">
|
||||
<div class="row g-0 ms-2 ms-lg-0">
|
||||
<div class="col-md-10 order-2 order-md-1">
|
||||
<h1>{{if $addr.ContractInfo}}Contract {{$addr.ContractInfo.Name}}{{if $addr.ContractInfo.Symbol}} ({{$addr.ContractInfo.Symbol}}){{end}}{{else}}Address {{addressAlias $addr.AddrStr $data}}{{end}}</h1>
|
||||
<h5 class="col-12 d-flex h-data pb-2"><span class="ellipsis copyable">{{$addr.AddrStr}}</span></h5>
|
||||
<h4>
|
||||
{{formattedAmountSpan $addr.BalanceSat 0 $data.CoinShortcut $data "copyable"}}
|
||||
{{if $addr.TotalFiatValue}}<span class="ps-5"{{if eq .ChainType 1}} tt="Address value including tokens"{{end}}>{{summaryValuesSpan $addr.TotalBaseValue $addr.TotalFiatValue $data}}</span>{{end}}
|
||||
<h4 class="row">
|
||||
<div class="col-lg-6">{{formattedAmountSpan $addr.BalanceSat 0 $data.CoinShortcut $data "copyable"}}</div>
|
||||
{{if $addr.FiatValue}}<div class="col-lg-6">{{summaryValuesSpan 0 $addr.FiatValue $data}}</div>{{end}}
|
||||
</h4>
|
||||
{{if gt $addr.TotalFiatValue $addr.FiatValue}}
|
||||
<div class="row g-0 small text-muted">Including Tokens</div>
|
||||
<h4 class="row">
|
||||
<div class="col-lg-6">{{summaryValuesSpan $addr.TotalBaseValue 0 $data}}</div>
|
||||
<div class="col-lg-6">{{summaryValuesSpan 0 $addr.TotalFiatValue $data}}</div>
|
||||
</h4>
|
||||
{{end}}
|
||||
</div>
|
||||
<div class="col-md-2 order-1 order-md-2 d-flex justify-content-center justify-content-md-end mb-3 mb-md-0">
|
||||
<div id="qrcode"></div>
|
||||
@ -121,7 +128,7 @@
|
||||
<th style="width: 25%;">Contract</th>
|
||||
<th style="width: 30%;">Quantity</th>
|
||||
<th style="width: 35%;">Value</th>
|
||||
<th class="text-end" style="width: 10%;">Transfers</th>
|
||||
<th class="text-end" style="width: 10%;"><span class="d-none d-md-block">Transfers</span><span class="d-block d-md-none">#</span></th>
|
||||
</tr>
|
||||
{{range $t := $addr.Tokens}}
|
||||
{{if eq $t.Type "ERC20"}}
|
||||
@ -157,7 +164,7 @@
|
||||
<tr>
|
||||
<th style="width: 25%;">Contract</th>
|
||||
<th style="width: 65%;">Tokens</th>
|
||||
<th class="text-end" style="width: 10%;">Transfers</th>
|
||||
<th class="text-end" style="width: 10%;"><span class="d-none d-md-block">Transfers</span><span class="d-block d-md-none">#</span></th>
|
||||
</tr>
|
||||
{{range $t := $addr.Tokens}}
|
||||
{{if eq $t.Type "ERC721"}}
|
||||
@ -194,7 +201,7 @@
|
||||
<tr>
|
||||
<th style="width: 25%;">Contract</th>
|
||||
<th style="width: 65%;">Tokens</th>
|
||||
<th class="text-end" style="width: 10%;">Transfers</th>
|
||||
<th class="text-end" style="width: 10%;"><span class="d-none d-md-block">Transfers</span><span class="d-block d-md-none">#</span></th>
|
||||
</tr>
|
||||
{{range $t := $addr.Tokens}}
|
||||
{{if eq $t.Type "ERC1155"}}
|
||||
|
||||
@ -82,7 +82,7 @@
|
||||
<a class="nav-link" href="/sendtx">Send Transaction</a>
|
||||
</span>
|
||||
<span class="navbar-nav ml-md-auto d-lg-flex d-none">
|
||||
<a class="nav-link" href="http://trezor.io/compare" target="_blank" rel="noopener noreferrer">Don't have a Trezor? Get one!</a>
|
||||
<a class="nav-link" href="https://trezor.io/compare" target="_blank" rel="noopener noreferrer">Don't have a Trezor? Get one!</a>
|
||||
</span>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
@ -48,7 +48,7 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Transactions in Mempool</td>
|
||||
<td>{{if .InternalExplorer}}<a href="/mempool">{{$bb.MempoolSize}}</a>{{else}}{{formatInt $bb.MempoolSize}}{{end}}</td>
|
||||
<td>{{if .InternalExplorer}}<a href="/mempool">{{formatInt $bb.MempoolSize}}</a>{{else}}{{formatInt $bb.MempoolSize}}{{end}}</td>
|
||||
</tr>
|
||||
{{if $bb.HasFiatRates}}
|
||||
<tr>
|
||||
|
||||
@ -111,8 +111,8 @@
|
||||
<div id="inputDataBody" class="accordion-collapse collapse" aria-labelledby="inputDataHeading" data-bs-parent="#inputData">
|
||||
<div class="accordion-body">
|
||||
<div class="row">
|
||||
<div class="col-12"><span class="copyable" style="overflow-wrap: break-word;">{{$tx.EthereumSpecific.Data}}</span></div>
|
||||
<div class="col-12 pt-2"><span class="copyable">{{$tx.EthereumSpecific.ParsedData.Function}}</span></div>
|
||||
<div class="col-12 mx-1 mx-md-0"><span class="copyable" style="overflow-wrap: break-word;">{{$tx.EthereumSpecific.Data}}</span></div>
|
||||
<div class="col-12 mx-1 mx-md-0 pt-2"><span class="copyable">{{$tx.EthereumSpecific.ParsedData.Function}}</span></div>
|
||||
{{if $tx.EthereumSpecific.ParsedData.Params}}
|
||||
<div class="col-12">
|
||||
<table class="table data-table mt-2 mb-0">
|
||||
|
||||
@ -3,7 +3,10 @@
|
||||
<div class="col-md-10 order-2 order-md-1">
|
||||
<h1>XPUB</h1>
|
||||
<h5 class="col-12 d-flex h-data pb-2"><span class="ellipsis copyable">{{$addr.AddrStr}}</span></h5>
|
||||
<h4>{{amountSpan $addr.BalanceSat $data "copyable"}}</h4>
|
||||
<h4 class="row">
|
||||
<div class="col-lg-6">{{formattedAmountSpan $addr.BalanceSat 0 $data.CoinShortcut $data "copyable"}}</div>
|
||||
{{if $addr.FiatValue}}<div class="col-lg-6">{{summaryValuesSpan 0 $addr.FiatValue $data}}</div>{{end}}
|
||||
</h4>
|
||||
</div>
|
||||
<div class="col-md-2 order-1 order-md-2 d-flex justify-content-center justify-content-md-end mb-3 mb-md-0">
|
||||
<div id="qrcode"></div>
|
||||
@ -60,7 +63,7 @@
|
||||
<td>{{amountSpan $t.BalanceSat $data "copyable"}}</td>
|
||||
<td>{{formatInt $t.Transfers}}</td>
|
||||
<td>{{$t.Path}}</td>
|
||||
</tr>
|
||||
</tr>
|
||||
{{end}}
|
||||
{{else}}
|
||||
<tr>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user