Add NFT detail page
This commit is contained in:
parent
3932d19707
commit
070df1efcc
@ -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"`
|
||||
|
||||
130
api/worker.go
130
api/worker.go
@ -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)
|
||||
|
||||
@ -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")
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
@ -226,6 +226,7 @@ h3 {
|
||||
table-layout: fixed;
|
||||
border-radius: .25rem;
|
||||
background: white;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.data-table td, .data-table th {
|
||||
|
||||
@ -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>
|
||||
|
||||
111
static/templates/tokenDetail.html
Normal file
111
static/templates/tokenDetail.html
Normal 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, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||
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}}
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user