Explorer redesing tuning

This commit is contained in:
Martin Boehm 2022-12-03 00:30:20 +01:00 committed by Martin
parent 9919f1a685
commit dca00bf770
15 changed files with 221 additions and 67 deletions

View File

@ -5,7 +5,6 @@ import (
"errors" "errors"
"math/big" "math/big"
"sort" "sort"
"strings"
"time" "time"
"github.com/trezor/blockbook/bchain" "github.com/trezor/blockbook/bchain"
@ -190,14 +189,17 @@ func (a Tokens) Less(i, j int) bool {
} else if ti.BaseValue > tj.BaseValue { } else if ti.BaseValue > tj.BaseValue {
return true return true
} }
c := strings.Compare(ti.Name, tj.Name) if ti.Name == "" {
if c == 1 { if tj.Name != "" {
return false return false
} else if c == -1 { }
return true } else {
if tj.Name == "" {
return true
}
return ti.Name < tj.Name
} }
c = strings.Compare(ti.Contract, tj.Contract) return ti.Contract < tj.Contract
return c == -1
} }
// TokenTransfer contains info about a token transfer done in a transaction // TokenTransfer contains info about a token transfer done in a transaction
@ -329,6 +331,7 @@ type Address struct {
Nonce string `json:"nonce,omitempty"` Nonce string `json:"nonce,omitempty"`
UsedTokens int `json:"usedTokens,omitempty"` UsedTokens int `json:"usedTokens,omitempty"`
Tokens Tokens `json:"tokens,omitempty"` Tokens Tokens `json:"tokens,omitempty"`
FiatValue float64 `json:"fiatValue,omitempty"`
TokensBaseValue float64 `json:"tokensBaseValue,omitempty"` TokensBaseValue float64 `json:"tokensBaseValue,omitempty"`
TokensFiatValue float64 `json:"tokensFiatValue,omitempty"` TokensFiatValue float64 `json:"tokensFiatValue,omitempty"`
TotalBaseValue float64 `json:"totalBaseValue,omitempty"` TotalBaseValue float64 `json:"totalBaseValue,omitempty"`

View File

@ -6,6 +6,7 @@ import (
"encoding/json" "encoding/json"
"math/big" "math/big"
"reflect" "reflect"
"sort"
"testing" "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)
}
})
}
}

View File

@ -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 { if w.chainType == bchain.ChainBitcoinType {
totalReceived = ba.ReceivedSat() totalReceived = ba.ReceivedSat()
totalSent = &ba.SentSat 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{ r := &Address{
Paging: pg, Paging: pg,
AddrStr: address, AddrStr: address,
@ -1286,6 +1290,7 @@ func (w *Worker) GetAddress(address string, page int, txsOnPage int, option Acco
Transactions: txs, Transactions: txs,
Txids: txids, Txids: txids,
Tokens: ed.tokens, Tokens: ed.tokens,
FiatValue: fiatValue,
TokensBaseValue: ed.tokensBaseValue, TokensBaseValue: ed.tokensBaseValue,
TokensFiatValue: ed.tokensFiatValue, TokensFiatValue: ed.tokensFiatValue,
TotalBaseValue: totalBaseValue, TotalBaseValue: totalBaseValue,

View File

@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"math/big" "math/big"
"sort" "sort"
"strconv"
"sync" "sync"
"time" "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 // 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() start := time.Now()
page-- page--
if page < 0 { if page < 0 {
@ -567,6 +568,20 @@ func (w *Worker) GetXpubAddress(xpub string, page int, txsOnPage int, option Acc
setIsOwnAddresses(txs, xpubAddresses) setIsOwnAddresses(txs, xpubAddresses)
var totalReceived big.Int var totalReceived big.Int
totalReceived.Add(&data.balanceSat, &data.sentSat) 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{ addr := Address{
Paging: pg, Paging: pg,
AddrStr: xpub, AddrStr: xpub,
@ -580,6 +595,7 @@ func (w *Worker) GetXpubAddress(xpub string, page int, txsOnPage int, option Acc
Txids: txids, Txids: txids,
UsedTokens: usedTokens, UsedTokens: usedTokens,
Tokens: tokens, Tokens: tokens,
FiatValue: fiatValue,
XPubAddresses: xpubAddresses, XPubAddresses: xpubAddresses,
AddressAliases: w.getAddressAliases(addresses), AddressAliases: w.getAddressAliases(addresses),
} }

View File

@ -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 { func (b *EthereumRPC) processCallTrace(call *rpcCallTrace, d *bchain.EthereumInternalData, contracts []bchain.ContractInfo, blockHeight uint32) []bchain.ContractInfo {
value, err := hexutil.DecodeBig(call.Value) value, err := hexutil.DecodeBig(call.Value)
if call.Type == "CREATE" { if call.Type == "CREATE" || call.Type == "CREATE2" {
d.Transfers = append(d.Transfers, bchain.EthereumInternalTransfer{ d.Transfers = append(d.Transfers, bchain.EthereumInternalTransfer{
Type: bchain.CREATE, Type: bchain.CREATE,
Value: *value, Value: *value,
@ -600,7 +600,6 @@ func (b *EthereumRPC) processCallTrace(call *rpcCallTrace, d *bchain.EthereumInt
To: call.To, // new contract address To: call.To, // new contract address
}) })
contracts = append(contracts, *b.getCreationContractInfo(call.To, blockHeight)) contracts = append(contracts, *b.getCreationContractInfo(call.To, blockHeight))
} else if call.Type == "SELFDESTRUCT" { } else if call.Type == "SELFDESTRUCT" {
d.Transfers = append(d.Transfers, bchain.EthereumInternalTransfer{ d.Transfers = append(d.Transfers, bchain.EthereumInternalTransfer{
Type: bchain.SELFDESTRUCT, Type: bchain.SELFDESTRUCT,

View File

@ -850,8 +850,12 @@ func (s *PublicServer) summaryValuesSpan(baseValue float64, secondaryValue float
rv.WriteString(")</span>") rv.WriteString(")</span>")
} }
} else { } else {
if td.SecondaryCoin != "" { if baseValue > 0 {
rv.WriteString("-") appendAmountSpan(&rv, "", strconv.FormatFloat(baseValue, 'f', 6, 64), td.CoinShortcut, "")
} else {
if td.SecondaryCoin != "" {
rv.WriteString("-")
}
} }
} }
return template.HTML(rv.String()) 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) return errorTpl, nil, api.NewAPIError("Missing xpub", true)
} }
s.metrics.ExplorerViews.With(common.Labels{"action": "xpub"}).Inc() 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 // 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 != nil {
if err == api.ErrUnsupportedXpub { if err == api.ErrUnsupportedXpub {
err = api.NewAPIError("XPUB functionality is not supported", true) err = api.NewAPIError("XPUB functionality is not supported", true)
} }
return errorTpl, nil, err return errorTpl, nil, err
} }
data := s.newTemplateData(r)
data.AddrStr = address.AddrStr data.AddrStr = address.AddrStr
data.Address = address data.Address = address
data.Page = address.Page data.Page = address.Page
@ -1267,7 +1271,7 @@ func (s *PublicServer) explorerSearch(w http.ResponseWriter, r *http.Request) (t
var err error var err error
s.metrics.ExplorerViews.With(common.Labels{"action": "search"}).Inc() s.metrics.ExplorerViews.With(common.Labels{"action": "search"}).Inc()
if len(q) > 0 { 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 { if err == nil {
http.Redirect(w, r, joinURL("/xpub/", url.QueryEscape(address.AddrStr)), http.StatusFound) http.Redirect(w, r, joinURL("/xpub/", url.QueryEscape(address.AddrStr)), http.StatusFound)
return noTpl, nil, nil return noTpl, nil, nil
@ -1501,7 +1505,8 @@ func (s *PublicServer) apiXpub(r *http.Request, apiVersion int) (interface{}, er
var err error var err error
s.metrics.ExplorerViews.With(common.Labels{"action": "api-xpub"}).Inc() s.metrics.ExplorerViews.With(common.Labels{"action": "api-xpub"}).Inc()
page, pageSize, details, filter, _, gap := s.getAddressQueryParams(r, api.AccountDetailsTxidHistory, txsInAPI) 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 { if err == nil && apiVersion == apiV1 {
return s.api.AddressToV1(address), nil 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

View File

@ -538,7 +538,7 @@ func (s *WebsocketServer) getAccountInfo(req *accountInfoReq) (res *api.Address,
if req.PageSize == 0 { if req.PageSize == 0 {
req.PageSize = txsOnPage 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 { if err != nil {
return s.api.GetAddress(req.Descriptor, req.Page, req.PageSize, opt, &filter, strings.ToLower(req.SecondaryCurrency)) return s.api.GetAddress(req.Descriptor, req.Page, req.PageSize, opt, &filter, strings.ToLower(req.SecondaryCurrency))
} }

View File

@ -15,7 +15,7 @@ body {
body { body {
min-height: 100%; min-height: 100%;
margin: 0; 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; background-repeat: no-repeat;
} }
@ -588,6 +588,16 @@ span.btn-paging:hover {
@media (max-width: 768px) { @media (max-width: 768px) {
body { body {
font-size: 0.8rem; 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 { .octicon {
@ -595,6 +605,10 @@ span.btn-paging:hover {
margin-top: -2px; margin-top: -2px;
} }
.unconfirmed {
padding: 0.1rem 0.8rem;
}
.btn { .btn {
--bs-btn-font-size: 0.8rem; --bs-btn-font-size: 0.8rem;
} }

View File

@ -1,12 +1,19 @@
{{define "specific"}}{{$addr := .Address}}{{$data := .}} {{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"> <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> <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> <h5 class="col-12 d-flex h-data pb-2"><span class="ellipsis copyable">{{$addr.AddrStr}}</span></h5>
<h4> <h4 class="row">
{{formattedAmountSpan $addr.BalanceSat 0 $data.CoinShortcut $data "copyable"}} <div class="col-lg-6">{{formattedAmountSpan $addr.BalanceSat 0 $data.CoinShortcut $data "copyable"}}</div>
{{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}} {{if $addr.FiatValue}}<div class="col-lg-6">{{summaryValuesSpan 0 $addr.FiatValue $data}}</div>{{end}}
</h4> </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>
<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 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> <div id="qrcode"></div>
@ -121,7 +128,7 @@
<th style="width: 25%;">Contract</th> <th style="width: 25%;">Contract</th>
<th style="width: 30%;">Quantity</th> <th style="width: 30%;">Quantity</th>
<th style="width: 35%;">Value</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> </tr>
{{range $t := $addr.Tokens}} {{range $t := $addr.Tokens}}
{{if eq $t.Type "ERC20"}} {{if eq $t.Type "ERC20"}}
@ -157,7 +164,7 @@
<tr> <tr>
<th style="width: 25%;">Contract</th> <th style="width: 25%;">Contract</th>
<th style="width: 65%;">Tokens</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> </tr>
{{range $t := $addr.Tokens}} {{range $t := $addr.Tokens}}
{{if eq $t.Type "ERC721"}} {{if eq $t.Type "ERC721"}}
@ -194,7 +201,7 @@
<tr> <tr>
<th style="width: 25%;">Contract</th> <th style="width: 25%;">Contract</th>
<th style="width: 65%;">Tokens</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> </tr>
{{range $t := $addr.Tokens}} {{range $t := $addr.Tokens}}
{{if eq $t.Type "ERC1155"}} {{if eq $t.Type "ERC1155"}}

View File

@ -82,7 +82,7 @@
<a class="nav-link" href="/sendtx">Send Transaction</a> <a class="nav-link" href="/sendtx">Send Transaction</a>
</span> </span>
<span class="navbar-nav ml-md-auto d-lg-flex d-none"> <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> </span>
</nav> </nav>
</div> </div>

View File

@ -48,7 +48,7 @@
</tr> </tr>
<tr> <tr>
<td>Transactions in Mempool</td> <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> </tr>
{{if $bb.HasFiatRates}} {{if $bb.HasFiatRates}}
<tr> <tr>

View File

@ -111,8 +111,8 @@
<div id="inputDataBody" class="accordion-collapse collapse" aria-labelledby="inputDataHeading" data-bs-parent="#inputData"> <div id="inputDataBody" class="accordion-collapse collapse" aria-labelledby="inputDataHeading" data-bs-parent="#inputData">
<div class="accordion-body"> <div class="accordion-body">
<div class="row"> <div class="row">
<div class="col-12"><span class="copyable" style="overflow-wrap: break-word;">{{$tx.EthereumSpecific.Data}}</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 pt-2"><span class="copyable">{{$tx.EthereumSpecific.ParsedData.Function}}</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}} {{if $tx.EthereumSpecific.ParsedData.Params}}
<div class="col-12"> <div class="col-12">
<table class="table data-table mt-2 mb-0"> <table class="table data-table mt-2 mb-0">

View File

@ -3,7 +3,10 @@
<div class="col-md-10 order-2 order-md-1"> <div class="col-md-10 order-2 order-md-1">
<h1>XPUB</h1> <h1>XPUB</h1>
<h5 class="col-12 d-flex h-data pb-2"><span class="ellipsis copyable">{{$addr.AddrStr}}</span></h5> <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>
<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 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> <div id="qrcode"></div>
@ -60,7 +63,7 @@
<td>{{amountSpan $t.BalanceSat $data "copyable"}}</td> <td>{{amountSpan $t.BalanceSat $data "copyable"}}</td>
<td>{{formatInt $t.Transfers}}</td> <td>{{formatInt $t.Transfers}}</td>
<td>{{$t.Path}}</td> <td>{{$t.Path}}</td>
</tr> </tr>
{{end}} {{end}}
{{else}} {{else}}
<tr> <tr>