Add NFT detail page

This commit is contained in:
Martin Boehm 2022-09-14 18:44:21 +02:00 committed by Martin
parent 3932d19707
commit 070df1efcc
14 changed files with 299 additions and 86 deletions

View File

@ -163,7 +163,7 @@ type TokenTransfer struct {
Type bchain.TokenTypeName `json:"type"`
From string `json:"from"`
To string `json:"to"`
Token string `json:"token"`
Contract string `json:"contract"`
Name string `json:"name"`
Symbol string `json:"symbol"`
Decimals int `json:"decimals"`

View File

@ -532,36 +532,57 @@ func (w *Worker) GetTransactionFromMempoolTx(mempoolTx *bchain.MempoolTx) (*Tx,
return r, nil
}
func (w *Worker) getContractInfo(contract string, typeFromContext bchain.TokenTypeName) (*bchain.ContractInfo, bool, error) {
cd, err := w.chainParser.GetAddrDescFromAddress(contract)
if err != nil {
return nil, false, err
}
return w.getContractDescriptorInfo(cd, typeFromContext)
}
func (w *Worker) getContractDescriptorInfo(cd bchain.AddressDescriptor, typeFromContext bchain.TokenTypeName) (*bchain.ContractInfo, bool, error) {
var err error
validContract := true
contractInfo, err := w.db.GetContractInfo(cd, typeFromContext)
if err != nil {
return nil, false, err
}
if contractInfo == nil {
// log warning only if the contract should have been known from processing of the internal data
if eth.ProcessInternalTransactions {
glog.Warningf("Contract %v %v not found in DB", cd, typeFromContext)
}
contractInfo, err = w.chain.GetContractInfo(cd)
if err != nil {
glog.Errorf("GetContractInfo from chain error %v, contract %v", err, cd)
}
if contractInfo == nil {
contractInfo = &bchain.ContractInfo{Type: bchain.UnknownTokenType, Decimals: w.chainParser.AmountDecimals()}
addresses, _, _ := w.chainParser.GetAddressesFromAddrDesc(cd)
if len(addresses) > 0 {
contractInfo.Contract = addresses[0]
}
validContract = false
} else {
if err = w.db.StoreContractInfo(contractInfo); err != nil {
glog.Errorf("StoreContractInfo error %v, contract %v", err, cd)
}
}
}
return contractInfo, validContract, nil
}
func (w *Worker) getEthereumTokensTransfers(transfers bchain.TokenTransfers, addresses map[string]struct{}) []TokenTransfer {
sort.Sort(transfers)
tokens := make([]TokenTransfer, len(transfers))
for i := range transfers {
t := transfers[i]
cd, err := w.chainParser.GetAddrDescFromAddress(t.Contract)
if err != nil {
glog.Errorf("GetAddrDescFromAddress error %v, contract %v", err, t.Contract)
continue
}
typeName := bchain.EthereumTokenTypeMap[t.Type]
contractInfo, err := w.db.GetContractInfo(cd, typeName)
contractInfo, _, err := w.getContractInfo(t.Contract, typeName)
if err != nil {
glog.Errorf("GetContractInfo error %v, contract %v", err, t.Contract)
}
if contractInfo == nil {
// log warning only if the contract should have been known from processing of the internal data
if eth.ProcessInternalTransactions {
glog.Warningf("Contract %v %v not found in DB", t.Contract, typeName)
}
contractInfo, err = w.chain.GetContractInfo(cd)
if err != nil {
glog.Errorf("GetContractInfo from chain error %v, contract %v", err, t.Contract)
}
if contractInfo == nil {
contractInfo = &bchain.ContractInfo{Name: t.Contract, Type: bchain.UnknownTokenType, Decimals: w.chainParser.AmountDecimals()}
}
if err = w.db.StoreContractInfo(contractInfo); err != nil {
glog.Errorf("StoreContractInfo error %v, contract %v", err, t.Contract)
}
glog.Errorf("getContractInfo error %v, contract %v", err, t.Contract)
continue
}
var value *Amount
var values []MultiTokenValue
@ -578,7 +599,7 @@ func (w *Worker) getEthereumTokensTransfers(transfers bchain.TokenTransfers, add
aggregateAddress(addresses, t.To)
tokens[i] = TokenTransfer{
Type: typeName,
Token: t.Contract,
Contract: t.Contract,
From: t.From,
To: t.To,
Value: value,
@ -591,6 +612,26 @@ func (w *Worker) getEthereumTokensTransfers(transfers bchain.TokenTransfers, add
return tokens
}
func (w *Worker) GetEthereumTokenURI(contract string, id string) (string, *bchain.ContractInfo, error) {
cd, err := w.chainParser.GetAddrDescFromAddress(contract)
if err != nil {
return "", nil, err
}
tokenId, ok := new(big.Int).SetString(id, 10)
if !ok {
return "", nil, errors.New("Invalid token id")
}
uri, err := w.chain.GetTokenURI(cd, tokenId)
if err != nil {
return "", nil, err
}
ci, _, err := w.getContractDescriptorInfo(cd, bchain.UnknownTokenType)
if err != nil {
return "", nil, err
}
return uri, ci, nil
}
func (w *Worker) getAddressTxids(addrDesc bchain.AddressDescriptor, mempool bool, filter *AddressFilter, maxResults int) ([]string, error) {
var err error
txids := make([]string, 0, 4)
@ -772,29 +813,11 @@ func computePaging(count, page, itemsOnPage int) (Paging, int, int, int) {
}
func (w *Worker) getEthereumContractBalance(addrDesc bchain.AddressDescriptor, index int, c *db.AddrContract, details AccountDetails) (*Token, error) {
validContract := true
typeName := bchain.EthereumTokenTypeMap[c.Type]
ci, err := w.db.GetContractInfo(c.Contract, typeName)
ci, validContract, err := w.getContractDescriptorInfo(c.Contract, typeName)
if err != nil {
return nil, errors.Annotatef(err, "GetContractInfo %v", c.Contract)
return nil, errors.Annotatef(err, "getEthereumContractBalance %v", c.Contract)
}
if ci == nil {
glog.Warningf("Contract %v %v not found in DB", c.Contract, typeName)
ci, err = w.chain.GetContractInfo(c.Contract)
if err != nil {
glog.Errorf("GetContractInfo from chain error %v, contract %v", err, c.Contract)
}
if ci == nil {
ci = &bchain.ContractInfo{Type: bchain.UnknownTokenType}
addresses, _, _ := w.chainParser.GetAddressesFromAddrDesc(c.Contract)
if len(addresses) > 0 {
ci.Contract = addresses[0]
ci.Name = addresses[0]
}
validContract = false
}
}
t := Token{
Contract: ci.Contract,
Name: ci.Name,
@ -840,27 +863,10 @@ func (w *Worker) getEthereumContractBalance(addrDesc bchain.AddressDescriptor, i
// a fallback method in case internal transactions are not processed and there is no indexed info about contract balance for an address
func (w *Worker) getEthereumContractBalanceFromBlockchain(addrDesc, contract bchain.AddressDescriptor, details AccountDetails) (*Token, error) {
var b *big.Int
validContract := true
ci, err := w.db.GetContractInfo(contract, "")
ci, validContract, err := w.getContractDescriptorInfo(contract, bchain.UnknownTokenType)
if err != nil {
return nil, errors.Annotatef(err, "GetContractInfo %v", contract)
}
if ci == nil {
glog.Warningf("Contract %v not found in DB", contract)
ci, err = w.chain.GetContractInfo(contract)
if err != nil {
glog.Errorf("GetContractInfo from chain error %v, contract %v", err, contract)
}
if ci == nil {
ci = &bchain.ContractInfo{Type: bchain.UnknownTokenType}
addresses, _, _ := w.chainParser.GetAddressesFromAddrDesc(contract)
if len(addresses) > 0 {
ci.Contract = addresses[0]
ci.Name = addresses[0]
}
validContract = false
}
}
// do not read contract balances etc in case of Basic option
if details >= AccountDetailsTokenBalances && validContract {
b, err = w.chain.EthereumTypeGetErc20ContractBalance(addrDesc, contract)

View File

@ -63,3 +63,8 @@ func (b *BaseChain) GetContractInfo(contractDesc AddressDescriptor) (*ContractIn
func (b *BaseChain) EthereumTypeGetErc20ContractBalance(addrDesc, contractDesc AddressDescriptor) (*big.Int, error) {
return nil, errors.New("Not supported")
}
// GetContractInfo returns URI of non fungible or multi token defined by token id
func (p *BaseChain) GetTokenURI(contractDesc AddressDescriptor, tokenID *big.Int) (string, error) {
return "", errors.New("Not supported")
}

View File

@ -333,6 +333,12 @@ func (c *blockChainWithMetrics) EthereumTypeGetErc20ContractBalance(addrDesc, co
return c.b.EthereumTypeGetErc20ContractBalance(addrDesc, contractDesc)
}
// GetContractInfo returns URI of non fungible or multi token defined by token id
func (c *blockChainWithMetrics) GetTokenURI(contractDesc bchain.AddressDescriptor, tokenID *big.Int) (v string, err error) {
defer func(s time.Time) { c.observeRPCLatency("GetTokenURI", s, err) }(time.Now())
return c.b.GetTokenURI(contractDesc, tokenID)
}
type mempoolWithMetrics struct {
mempool bchain.Mempool
m *common.Metrics

View File

@ -6,6 +6,7 @@ import (
"strings"
ethcommon "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/juju/errors"
"github.com/trezor/blockbook/bchain"
)
@ -14,6 +15,8 @@ const erc20TransferMethodSignature = "0xa9059cbb" // transfer(a
const erc721TransferFromMethodSignature = "0x23b872dd" // transferFrom(address,address,uint256)
const erc721SafeTransferFromMethodSignature = "0x42842e0e" // safeTransferFrom(address,address,uint256)
const erc721SafeTransferFromWithDataMethodSignature = "0xb88d4fde" // safeTransferFrom(address,address,uint256,bytes)
const erc721TokenURIMethodSignature = "0xc87b56dd" // tokenURI(uint256)
const erc1155URIMethodSignature = "0x0e89341c" // uri(uint256)
const tokenTransferEventSignature = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"
const tokenERC1155TransferSingleEventSignature = "0xc3d58168c5ae7397731d063d5bbf3d657854427343f4c083240f7aacaa2d0f62"
@ -24,7 +27,7 @@ const nameRegisteredEventSignature = "0xca6abbe9d7f11422cb6ca7629fbf6fe9efb1c621
const contractNameSignature = "0x06fdde03"
const contractSymbolSignature = "0x95d89b41"
const contractDecimalsSignature = "0x313ce567"
const contractBalanceOf = "0x70a08231"
const contractBalanceOfSignature = "0x70a08231"
func addressFromPaddedHex(s string) (string, error) {
var t big.Int
@ -315,9 +318,9 @@ func (b *EthereumRPC) GetContractInfo(contractDesc bchain.AddressDescriptor) (*b
// EthereumTypeGetErc20ContractBalance returns balance of ERC20 contract for given address
func (b *EthereumRPC) EthereumTypeGetErc20ContractBalance(addrDesc, contractDesc bchain.AddressDescriptor) (*big.Int, error) {
addr := EIP55Address(addrDesc)
contract := EIP55Address(contractDesc)
req := contractBalanceOf + "0000000000000000000000000000000000000000000000000000000000000000"[len(addr)-2:] + addr[2:]
addr := hexutil.Encode(addrDesc)
contract := hexutil.Encode(contractDesc)
req := contractBalanceOfSignature + "0000000000000000000000000000000000000000000000000000000000000000"[len(addr)-2:] + addr[2:]
data, err := b.ethCall(req, contract)
if err != nil {
return nil, err
@ -328,3 +331,37 @@ func (b *EthereumRPC) EthereumTypeGetErc20ContractBalance(addrDesc, contractDesc
}
return r, nil
}
// GetContractInfo returns URI of non fungible or multi token defined by token id
func (b *EthereumRPC) GetTokenURI(contractDesc bchain.AddressDescriptor, tokenID *big.Int) (string, error) {
address := hexutil.Encode(contractDesc)
// CryptoKitties do not fully support ERC721 standard, do not have tokenURI method
if address == "0x06012c8cf97bead5deae237070f9587f8e7a266d" {
return "https://api.cryptokitties.co/kitties/" + tokenID.Text(10), nil
}
id := tokenID.Text(16)
if len(id) < 64 {
id = "0000000000000000000000000000000000000000000000000000000000000000"[len(id):] + id
}
// try ERC721 tokenURI method and ERC1155 uri method
for _, method := range []string{erc721TokenURIMethodSignature, erc1155URIMethodSignature} {
data, err := b.ethCall(method+id, address)
if err == nil && data != "" {
uri := parseSimpleStringProperty(data)
// try to sanitize the URI returned from the contract
i := strings.LastIndex(uri, "ipfs://")
if i >= 0 {
uri = strings.Replace(uri[i:], "ipfs://", "https://ipfs.io/ipfs/", 1)
// some contracts return uri ipfs://ifps/abcdef instead of ipfs://abcdef
uri = strings.Replace(uri, "https://ipfs.io/ipfs/ipfs/", "https://ipfs.io/ipfs/", 1)
}
i = strings.LastIndex(uri, "https://")
// allow only https:// URIs
if i >= 0 {
uri = strings.ReplaceAll(uri[i:], "{id}", id)
return uri, nil
}
}
}
return "", nil
}

View File

@ -317,6 +317,7 @@ type BlockChain interface {
EthereumTypeGetNonce(addrDesc AddressDescriptor) (uint64, error)
EthereumTypeEstimateGas(params map[string]interface{}) (uint64, error)
EthereumTypeGetErc20ContractBalance(addrDesc, contractDesc AddressDescriptor) (*big.Int, error)
GetTokenURI(contractDesc AddressDescriptor, tokenID *big.Int) (string, error)
}
// BlockChainParser defines common interface to parsing and conversions of block chain data

View File

@ -142,6 +142,9 @@ func (s *PublicServer) ConnectFullPublicInterface() {
serveMux.HandleFunc(path+"spending/", s.htmlTemplateHandler(s.explorerSpendingTx))
serveMux.HandleFunc(path+"sendtx", s.htmlTemplateHandler(s.explorerSendTx))
serveMux.HandleFunc(path+"mempool", s.htmlTemplateHandler(s.explorerMempool))
if s.chainParser.GetChainType() == bchain.ChainEthereumType {
serveMux.HandleFunc(path+"nft/", s.htmlTemplateHandler(s.explorerNftDetail))
}
} else {
// redirect to wallet requests for tx and address, possibly to external site
serveMux.HandleFunc(path+"tx/", s.txRedirect)
@ -417,6 +420,7 @@ const (
blockTpl
sendTransactionTpl
mempoolTpl
nftDetailTpl
tplCount
)
@ -445,6 +449,9 @@ type TemplateData struct {
SendTxHex string
Status string
NonZeroBalanceTokens bool
TokenId string
URI string
ContractInfo *bchain.ContractInfo
}
func (s *PublicServer) parseTemplates() []*template.Template {
@ -460,6 +467,7 @@ func (s *PublicServer) parseTemplates() []*template.Template {
"tokenTransfersCount": tokenTransfersCount,
"tokenCount": tokenCount,
"hasPrefix": strings.HasPrefix,
"jsStr": jsStr,
}
var createTemplate func(filenames ...string) *template.Template
if s.debug {
@ -509,6 +517,7 @@ func (s *PublicServer) parseTemplates() []*template.Template {
t[txTpl] = createTemplate("./static/templates/tx.html", "./static/templates/txdetail_ethereumtype.html", "./static/templates/base.html")
t[addressTpl] = createTemplate("./static/templates/address.html", "./static/templates/txdetail_ethereumtype.html", "./static/templates/paging.html", "./static/templates/base.html")
t[blockTpl] = createTemplate("./static/templates/block.html", "./static/templates/txdetail_ethereumtype.html", "./static/templates/paging.html", "./static/templates/base.html")
t[nftDetailTpl] = createTemplate("./static/templates/tokenDetail.html", "./static/templates/base.html")
} else {
t[txTpl] = createTemplate("./static/templates/tx.html", "./static/templates/txdetail.html", "./static/templates/base.html")
t[addressTpl] = createTemplate("./static/templates/address.html", "./static/templates/txdetail.html", "./static/templates/paging.html", "./static/templates/base.html")
@ -601,6 +610,10 @@ func tokenCount(tokens []api.Token, t bchain.TokenTypeName) int {
return count
}
func jsStr(s string) template.JSStr {
return template.JSStr(s)
}
func (s *PublicServer) explorerTx(w http.ResponseWriter, r *http.Request) (tpl, *TemplateData, error) {
var tx *api.Tx
var err error
@ -737,6 +750,28 @@ func (s *PublicServer) explorerAddress(w http.ResponseWriter, r *http.Request) (
return addressTpl, data, nil
}
func (s *PublicServer) explorerNftDetail(w http.ResponseWriter, r *http.Request) (tpl, *TemplateData, error) {
parts := strings.Split(r.URL.Path, "/")
if len(parts) < 3 {
return errorTpl, nil, api.NewAPIError("Missing parameters", true)
}
tokenId := parts[len(parts)-1]
contract := parts[len(parts)-2]
uri, ci, err := s.api.GetEthereumTokenURI(contract, tokenId)
s.metrics.ExplorerViews.With(common.Labels{"action": "nftDetail"}).Inc()
if err != nil {
return errorTpl, nil, api.NewAPIError(err.Error(), true)
}
if ci == nil {
return errorTpl, nil, api.NewAPIError(fmt.Sprintf("Unknown contract %s", contract), true)
}
data := s.newTemplateData()
data.TokenId = tokenId
data.ContractInfo = ci
data.URI = uri
return nftDetailTpl, data, nil
}
func (s *PublicServer) explorerXpub(w http.ResponseWriter, r *http.Request) (tpl, *TemplateData, error) {
var xpub string
i := strings.LastIndex(r.URL.Path, "xpub/")

File diff suppressed because one or more lines are too long

View File

@ -226,6 +226,7 @@ h3 {
table-layout: fixed;
border-radius: .25rem;
background: white;
overflow-wrap: break-word;
}
.data-table td, .data-table th {

View File

@ -44,7 +44,7 @@
{{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 ellipsis"><a href="/address/{{$t.Contract}}">{{if $t.Name}}{{$t.Name}}{{else}}{{$t.Contract}}{{end}}</a></td>
<td class="data">{{formatAmountWithDecimals $t.BalanceSat $t.Decimals}} {{$t.Symbol}}</td>
<td class="data">{{$t.Transfers}}</td>
</tr>
@ -69,9 +69,9 @@
{{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 ellipsis"><a href="/address/{{$t.Contract}}">{{if $t.Name}}{{$t.Name}}{{else}}{{$t.Contract}}{{end}}</a></td>
<td class="data">
{{range $i, $iv := $t.Ids}}{{if $i}}, {{end}}{{formatAmountWithDecimals $iv 0}}{{end}}
{{range $i, $iv := $t.Ids}}{{if $i}}, {{end}}<a href="/nft/{{$t.Contract}}/{{formatAmountWithDecimals $iv 0}}">{{formatAmountWithDecimals $iv 0}}</a>{{end}}
</td>
<td class="data">{{$t.Transfers}}</td>
</tr>
@ -96,9 +96,9 @@
{{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 ellipsis"><a href="/address/{{$t.Contract}}">{{if $t.Name}}{{$t.Name}}{{else}}{{$t.Contract}}{{end}}</a></td>
<td class="data">
{{range $i, $iv := $t.MultiTokenValues}}{{if $i}}, {{end}}{{formatAmountWithDecimals $iv.Id 0}}:{{formatAmountWithDecimals $iv.Value 0}} {{$t.Symbol}}{{end}}
{{range $i, $iv := $t.MultiTokenValues}}{{if $i}}, {{end}}{{$iv.Value}} of ID <a href="/nft/{{$t.Contract}}/{{$iv.Id}}">{{$iv.Id}}</a>{{end}}
</td>
<td class="data">{{$t.Transfers}}</td>
</tr>

View File

@ -0,0 +1,111 @@
{{define "specific"}}{{$data := .}}
<h1>NFT Token Detail</h1>
<div class="row">
<div class="col-md-6">
<table class="table data-table">
<tbody>
<tr>
<td style="width: 25%;">Token ID</td>
<td class="data">{{$data.TokenId}}</td>
</tr>
<tr id="name" style="display: none;">
<td>NTF Name</td>
<td class="data"></td>
</tr>
<tr id="description" style="display: none;">
<td>NTF Description</td>
<td class="data"></td>
</tr>
<tr>
<td>Contract</td>
<td class="data"><a href="/address/{{$data.ContractInfo.Contract}}">{{$data.ContractInfo.Contract}}</a> {{$data.ContractInfo.Name}}</td>
</tr>
<tr>
<td>Contract type</td>
<td class="data">{{$data.ContractInfo.Type}}</td>
</tr>
</tbody>
</table>
</div>
<div class="col-md-6" id="image"></div>
</div>
<div class="data-div" id="metadatablock">
<h5>Metadata</h5>
<div class="alert alert-data" style="word-wrap: break-word; font-size: smaller;">
<pre id="metadata" style="margin: 0;">Loading metadata from <a href="{{$data.URI}}">{{$data.URI}}</a>...</pre>
</div>
</div>
<script type="text/javascript">
function syntaxHighlight(json) {
json = JSON.stringify(json, undefined, 2);
json = json.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
return json.replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, function (match) {
var cls = 'number';
if (/^"/.test(match)) {
if (/:$/.test(match)) {
cls = 'key';
} else {
cls = 'string';
}
} else if (/true|false/.test(match)) {
cls = 'boolean';
} else if (/null/.test(match)) {
cls = 'null';
}
return '<span class="' + cls + '">' + match + '</span>';
});
}
function showImage(s) {
const img = document.createElement("img");
img.className="border w-100";
img.src = s;
const src = document.getElementById("image");
src.appendChild(img);
src.style.display="block";
}
function nftInfo(id,text) {
const src = document.getElementById(id);
src.getElementsByClassName('data')[0].innerText=text;
src.style.display='';
}
async function getMetadata(url) {
try {
const uri={{ jsStr $data.URI }};
if(uri) {
const response = await fetch(uri);
const contentType=response.headers.get('content-type');
if(contentType&&contentType.toString().startsWith("image/")) {
showImage(uri);
document.getElementById("metadatablock").style.display='none';
} else {
const data = await response.json();
document.getElementById("metadata").innerHTML = syntaxHighlight(data);
if (data.name) {
nftInfo('name',data.name)
}
if (data.description) {
nftInfo('description',data.description)
}
if (data.image||data.image_url) {
let s=data.image?.toString();
if(!s) {
s=data.image_url;
}
if(s.startsWith("ipfs://")) {
s=s.replace("ipfs://","https://ipfs.io/ipfs/");
}
if(s.startsWith("https://")) {
showImage(s);
}
}
}
} else {
document.getElementById("metadata").innerText = "Error: cannot get metadata link from blockchain";
}
} catch(e) {
document.getElementById("metadata").innerText = "Error loading metadata: "+e;
}
}
getMetadata();
</script>
{{end}}

View File

@ -111,7 +111,7 @@
<thead>
<tr>
<th style="width: 5%;">#</th>
<th>Type</th>
<th style="width: 25%;">Type</th>
<th>Data</th>
</tr>
</thead>

View File

@ -67,7 +67,7 @@
</table>
</div>
</div>
<div class="col-md-3 text-right" style="padding: .4rem 0;">
<div class="col-md-3 text-right" style="padding: .4rem 0;overflow-wrap: break-word;">
{{formatAmount $tx.ValueOutSat}} {{$cs}}
</div>
</div>
@ -132,7 +132,7 @@
</table>
</div>
</div>
<div class="col-md-3 text-right" style="padding: .4rem 0;">{{formatAmount $tt.Value}} {{$cs}}</div>
<div class="col-md-3 text-right" style="padding: .4rem 0;overflow-wrap: break-word;">{{formatAmount $tt.Value}} {{$cs}}</div>
</div>
{{- end -}}
<div class="row" style="padding: 6px 15px;"></div>
@ -176,7 +176,7 @@
</table>
</div>
</div>
<div class="col-md-3 text-right" style="padding: .4rem 0;">{{formatAmountWithDecimals $tt.Value $tt.Decimals}} {{$tt.Symbol}}</div>
<div class="col-md-3 text-right" style="padding: .4rem 0;overflow-wrap: break-word;">{{formatAmountWithDecimals $tt.Value $tt.Decimals}} {{$tt.Symbol}}</div>
</div>
{{- end -}}
{{- end -}}
@ -221,7 +221,7 @@
</table>
</div>
</div>
<div class="col-md-3 text-right" style="padding: .4rem 0;">ID {{formatAmountWithDecimals $tt.Value 0}} {{$tt.Symbol}}</div>
<div class="col-md-3 text-right" style="padding: .4rem 0;overflow-wrap: break-word;">ID <a href="/nft/{{$tt.Contract}}/{{$tt.Value}}">{{$tt.Value}}</a> {{$tt.Symbol}}</div>
</div>
{{- end -}}
{{- end -}}
@ -266,9 +266,9 @@
</table>
</div>
</div>
<div class="col-md-3 text-right" style="padding: .4rem 0;">
{{- range $iv := $tt.MultiTokenValues -}}
{{formatAmountWithDecimals $iv.Id 0}}:{{formatAmountWithDecimals $iv.Value 0}} {{$tt.Symbol}}
<div class="col-md-3 text-right" style="padding: .4rem 0;overflow-wrap: break-word;">
{{- range $i, $iv := $tt.MultiTokenValues -}}
{{if $i}}, {{end}}{{$iv.Value}} {{$tt.Symbol}} of ID <a href="/nft/{{$tt.Contract}}/{{$iv.Id}}">{{$iv.Id}}</a>
{{- end -}}
</div>
</div>

View File

@ -129,7 +129,12 @@ func (c *fakeBlockChainEthereumType) GetContractInfo(contractDesc bchain.Address
}, nil
}
// EthereumTypeGetErc20ContractBalance is not supported
// EthereumTypeGetErc20ContractBalance returns simulated balance
func (c *fakeBlockChainEthereumType) EthereumTypeGetErc20ContractBalance(addrDesc, contractDesc bchain.AddressDescriptor) (*big.Int, error) {
return big.NewInt(1000000000 + int64(addrDesc[0])*1000 + int64(contractDesc[0])), nil
}
// GetTokenURI returns URI derived from the input contractDesc
func (c *fakeBlockChainEthereumType) GetTokenURI(contractDesc bchain.AddressDescriptor, tokenID *big.Int) (string, error) {
return "https://ipfs.io/ipfs/" + contractDesc.String()[3:] + ".json", nil
}