Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions api/syscoin_asset_metadata_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package api

import "testing"

func TestDecodeSyscoinBridgeAssetMetadata(t *testing.T) {
metadata := decodeSyscoinBridgeAssetMetadata("7", []byte(`{"description":"ERC20 Token","assetType":"ERC20","originDecimals":2}`))
if metadata.Description != "ERC20 Token" || metadata.AssetType != "ERC20" {
t.Fatalf("decoded metadata = %+v", metadata)
}
if metadata.OriginDecimals == nil || *metadata.OriginDecimals != 2 {
t.Fatalf("origin decimals = %v, want 2", metadata.OriginDecimals)
}
}

func TestDecodeSyscoinBridgeAssetMetadataPreservesLegacyText(t *testing.T) {
metadata := decodeSyscoinBridgeAssetMetadata("7", []byte("legacy metadata"))
if metadata.Description != "legacy metadata" {
t.Fatalf("description = %q, want legacy metadata", metadata.Description)
}
if metadata.AssetType != "" || metadata.OriginDecimals != nil || metadata.TokenID != "" {
t.Fatalf("legacy metadata unexpectedly gained bridge fields: %+v", metadata)
}
}

func TestDecodeSyscoinBridgeAssetMetadataRecognizesLegacySYSX(t *testing.T) {
metadata := decodeSyscoinBridgeAssetMetadata("123456", []byte("Syscoin Native Asset"))
if metadata.AssetType != "SYSX" || metadata.Description != "Syscoin Native Asset" {
t.Fatalf("legacy SYSX metadata = %+v", metadata)
}
}
37 changes: 23 additions & 14 deletions api/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,24 +147,30 @@ type AssetInfo struct {
// SYSCOIN: AssetSpecific contains Syscoin SPT metadata exposed by asset
// endpoints and explorer pages.
type AssetSpecific struct {
AssetGuid string `json:"assetGuid" ts_doc:"Syscoin SPT asset GUID."`
Contract string `json:"contract,omitempty" ts_doc:"NEVM contract address associated with this asset, if any."`
Symbol string `json:"symbol" ts_doc:"Asset symbol."`
TotalSupply *Amount `json:"totalSupply" ts_doc:"Current total supply in base units."`
MaxSupply *Amount `json:"maxSupply" ts_doc:"Maximum supply in base units."`
Decimals int `json:"decimals" ts_doc:"Number of asset decimal places."`
MetaData string `json:"metaData,omitempty" ts_doc:"Raw Syscoin asset metadata."`
AssetGuid string `json:"assetGuid" ts_doc:"Syscoin SPT asset GUID."`
AssetType string `json:"assetType,omitempty" ts_doc:"Origin bridge asset standard (SYSX, ERC20, ERC721, or ERC1155)."`
Contract string `json:"contract,omitempty" ts_doc:"NEVM contract address associated with this asset, if any."`
Symbol string `json:"symbol" ts_doc:"Asset symbol."`
TotalSupply *Amount `json:"totalSupply" ts_doc:"Current total supply in base units."`
MaxSupply *Amount `json:"maxSupply" ts_doc:"Maximum supply in base units."`
Decimals int `json:"decimals" ts_doc:"Number of asset decimal places."`
OriginDecimals *int `json:"originDecimals,omitempty" ts_doc:"Decimals registered by the bridge for the origin asset."`
TokenID string `json:"tokenId,omitempty" ts_doc:"Origin token ID for ERC721 and ERC1155 assets."`
MetaData string `json:"metaData,omitempty" ts_doc:"Raw Syscoin asset metadata."`
}

// SYSCOIN: AssetsSpecific is the compact Syscoin SPT row used by asset search.
type AssetsSpecific struct {
AssetGuid string `json:"assetGuid" ts_doc:"Syscoin SPT asset GUID."`
Contract string `json:"contract" ts_doc:"NEVM contract address associated with this asset, if any."`
Symbol string `json:"symbol" ts_doc:"Asset symbol."`
TotalSupply *Amount `json:"totalSupply" ts_doc:"Current total supply in base units."`
Decimals int `json:"precision" ts_doc:"Number of asset decimal places."`
Txs int `json:"txs" ts_doc:"Number of indexed transactions touching this asset."`
MetaData string `json:"metaData,omitempty" ts_doc:"Raw Syscoin asset metadata."`
AssetGuid string `json:"assetGuid" ts_doc:"Syscoin SPT asset GUID."`
AssetType string `json:"assetType,omitempty" ts_doc:"Origin bridge asset standard (SYSX, ERC20, ERC721, or ERC1155)."`
Contract string `json:"contract" ts_doc:"NEVM contract address associated with this asset, if any."`
Symbol string `json:"symbol" ts_doc:"Asset symbol."`
TotalSupply *Amount `json:"totalSupply" ts_doc:"Current total supply in base units."`
Decimals int `json:"precision" ts_doc:"Number of asset decimal places."`
OriginDecimals *int `json:"originDecimals,omitempty" ts_doc:"Decimals registered by the bridge for the origin asset."`
TokenID string `json:"tokenId,omitempty" ts_doc:"Origin token ID for ERC721 and ERC1155 assets."`
Txs int `json:"txs" ts_doc:"Number of indexed transactions touching this asset."`
MetaData string `json:"metaData,omitempty" ts_doc:"Raw Syscoin asset metadata."`
}

// Vin contains information about single transaction input
Expand Down Expand Up @@ -286,6 +292,9 @@ type Token struct {
UnconfirmedBalanceSat *Amount `json:"unconfirmedBalance,omitempty" ts_doc:"Unconfirmed token balance delta."`
UnconfirmedTransfers int `json:"unconfirmedTransfers,omitempty" ts_doc:"Number of unconfirmed Syscoin SPT transfers."`
AssetGuid string `json:"assetGuid,omitempty" ts_doc:"Syscoin SPT asset GUID."`
AssetType string `json:"assetType,omitempty" ts_doc:"Origin bridge asset standard for a Syscoin SPT."`
OriginDecimals *int `json:"originDecimals,omitempty" ts_doc:"Decimals registered by the bridge for the origin asset."`
TokenID string `json:"tokenId,omitempty" ts_doc:"Origin token ID for bridged ERC721 and ERC1155 assets."`
Comment on lines +295 to +297

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Regenerate the public API schemas for the new token fields

When clients consume Syscoin account balances, these fields are present in the runtime JSON but absent from both the checked-in blockbook-api.ts Token interface and the Token schema in openapi.yaml, so generated clients cannot access the metadata without bypassing their types. The deploy check does not currently catch this omission because contrib/tests/run-openapi-tests.sh compares the OpenAPI schema against the already-stale checked-in TypeScript file rather than rerunning the Go typescriptify generator; update both public API artifacts alongside these fields.

Useful? React with 👍 / 👎.

Protocols TokenProtocols `json:"protocols,omitempty" ts_type:"string[]" ts_doc:"Protocol identifiers the contract participates in (e.g., \"erc4626\"); for fresh per-vault data, use getContractInfo."`
ContractIndex string `json:"-"`
}
Expand Down
81 changes: 67 additions & 14 deletions api/worker.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,32 @@ type syscoinAssetMaskParser interface {
GetAssetsMaskFromVersion(nVersion int32) bchain.AssetsMask
}

type syscoinBridgeAssetMetadata struct {
Description string `json:"description"`
AssetType string `json:"assetType"`
OriginDecimals *int `json:"originDecimals,omitempty"`
TokenID string `json:"tokenId,omitempty"`
}

func decodeSyscoinBridgeAssetMetadata(assetGuid string, raw []byte) syscoinBridgeAssetMetadata {
metadata := syscoinBridgeAssetMetadata{Description: string(raw)}
if len(raw) == 0 {
if assetGuid == "123456" {
metadata.AssetType = "SYSX"
}
return metadata
}

var decoded syscoinBridgeAssetMetadata
if err := json.Unmarshal(raw, &decoded); err != nil || decoded.AssetType == "" {
if assetGuid == "123456" {
metadata.AssetType = "SYSX"
}
return metadata
}
return decoded
}

// SYSCOIN
func assetTypeFromVersion(parser bchain.BlockChainParser, version int32) *bchain.TokenType {
p, ok := parser.(syscoinAssetTypeParser)
Expand Down Expand Up @@ -200,6 +226,11 @@ func (w *Worker) getSyscoinAddressAssetTokens(address string, balances map[uint6
dbAsset.AssetObj.Symbol = []byte(assetGuid)
dbAsset.AssetObj.Precision = 8
}
metadata := decodeSyscoinBridgeAssetMetadata(assetGuid, dbAsset.MetaData)
contract := ""
if len(dbAsset.AssetObj.Contract) > 0 {
contract = ethcommon.BytesToAddress(dbAsset.AssetObj.Contract).Hex()
}
totalReceived := new(big.Int).Add(balance.BalanceSat, balance.SentSat)
var unconfirmed *Amount
unconfirmedTransfers := 0
Expand All @@ -220,6 +251,10 @@ func (w *Worker) getSyscoinAddressAssetTokens(address string, balances map[uint6
TotalReceivedSat: (*Amount)(totalReceived),
TotalSentSat: (*Amount)(balance.SentSat),
AssetGuid: assetGuid,
AssetType: metadata.AssetType,
Contract: contract,
OriginDecimals: metadata.OriginDecimals,
TokenID: metadata.TokenID,
Transfers: int(balance.Transfers),
})
}
Expand All @@ -238,6 +273,11 @@ func (w *Worker) getSyscoinAddressAssetTokens(address string, balances map[uint6
dbAsset.AssetObj.Precision = 8
}
zero := Amount(*new(big.Int))
metadata := decodeSyscoinBridgeAssetMetadata(assetGuid, dbAsset.MetaData)
contract := ""
if len(dbAsset.AssetObj.Contract) > 0 {
contract = ethcommon.BytesToAddress(dbAsset.AssetObj.Contract).Hex()
}
tokens = append(tokens, Token{
Type: bchain.SPTTokenType,
Standard: bchain.SPTTokenType,
Expand All @@ -250,6 +290,10 @@ func (w *Worker) getSyscoinAddressAssetTokens(address string, balances map[uint6
TotalReceivedSat: &zero,
TotalSentSat: &zero,
AssetGuid: assetGuid,
AssetType: metadata.AssetType,
Contract: contract,
OriginDecimals: metadata.OriginDecimals,
TokenID: metadata.TokenID,
})
}
sort.Sort(tokens)
Expand Down Expand Up @@ -1236,6 +1280,8 @@ func (w *Worker) FindAssetsFromFilter(filter string) []*AssetsSpecific {
filterLower := strings.ToLower(strings.ReplaceAll(filter, "0x", ""))
assetDetails := make([]*AssetsSpecific, 0)
for guid, assetCached := range *w.db.GetAssetCache() {
assetGuid := strconv.FormatUint(guid, 10)
metadata := decodeSyscoinBridgeAssetMetadata(assetGuid, assetCached.MetaData)
symbol := string(assetCached.AssetObj.Symbol)
symbolLower := strings.ToLower(symbol)
foundAsset := strings.Contains(symbolLower, filterLower)
Expand All @@ -1251,13 +1297,16 @@ func (w *Worker) FindAssetsFromFilter(filter string) []*AssetsSpecific {
}
txs := int(assetCached.Transactions)
assetDetails = append(assetDetails, &AssetsSpecific{
AssetGuid: strconv.FormatUint(guid, 10),
Symbol: symbol,
Contract: contract,
TotalSupply: (*Amount)(big.NewInt(assetCached.AssetObj.TotalSupply)),
Decimals: int(assetCached.AssetObj.Precision),
Txs: txs,
MetaData: string(assetCached.MetaData),
AssetGuid: assetGuid,
AssetType: metadata.AssetType,
Symbol: symbol,
Contract: contract,
TotalSupply: (*Amount)(big.NewInt(assetCached.AssetObj.TotalSupply)),
Decimals: int(assetCached.AssetObj.Precision),
Txs: txs,
OriginDecimals: metadata.OriginDecimals,
TokenID: metadata.TokenID,
MetaData: metadata.Description,
})
}
sort.Slice(assetDetails, func(i, j int) bool {
Expand Down Expand Up @@ -1431,15 +1480,19 @@ func (w *Worker) GetAsset(asset string, page int, txsOnPage int, option AccountD
if len(dbAsset.AssetObj.Contract) > 0 {
contract = ethcommon.BytesToAddress(dbAsset.AssetObj.Contract).Hex()
}
metadata := decodeSyscoinBridgeAssetMetadata(asset, dbAsset.MetaData)
r := &Asset{
AssetDetails: &AssetSpecific{
AssetGuid: asset,
Symbol: string(dbAsset.AssetObj.Symbol),
Contract: contract,
TotalSupply: (*Amount)(big.NewInt(dbAsset.AssetObj.TotalSupply)),
MaxSupply: (*Amount)(big.NewInt(dbAsset.AssetObj.MaxSupply)),
Decimals: int(dbAsset.AssetObj.Precision),
MetaData: string(dbAsset.MetaData),
AssetGuid: asset,
AssetType: metadata.AssetType,
Symbol: string(dbAsset.AssetObj.Symbol),
Contract: contract,
TotalSupply: (*Amount)(big.NewInt(dbAsset.AssetObj.TotalSupply)),
MaxSupply: (*Amount)(big.NewInt(dbAsset.AssetObj.MaxSupply)),
Decimals: int(dbAsset.AssetObj.Precision),
OriginDecimals: metadata.OriginDecimals,
TokenID: metadata.TokenID,
MetaData: metadata.Description,
},
Paging: pg,
UnconfirmedBalanceSat: (*Amount)(&uBalSat),
Expand Down
71 changes: 62 additions & 9 deletions bchain/coins/sys/nevm.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ package syscoin

import (
"context"
"encoding/json"
"fmt"
"math/big"
"strings"
"time"
"unicode"

ethereum "github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/accounts/abi"
Expand All @@ -17,11 +19,39 @@ import (
"github.com/trezor/blockbook/bchain/coins/btc"
)

type bridgeAssetMetadata struct {
Description string `json:"description"`
AssetType string `json:"assetType"`
OriginDecimals *uint8 `json:"originDecimals,omitempty"`
TokenID string `json:"tokenId,omitempty"`
}

func encodeBridgeAssetMetadata(metadata bridgeAssetMetadata) ([]byte, error) {
return json.Marshal(metadata)
}

const (
vaultManagerAddress = "0x7904299b3D3dC1b03d1DdEb45E9fDF3576aCBd5f"
defaultNEVMRPCTimeoutSec = 15
// Syscoin 5 Bridge V2 registry. Legacy pre-cutover SPT metadata is not indexed.
vaultManagerAddress = "0x28bD37C0926575f2568ea8f297c0745EF16174Ab"
defaultNEVMRPCTimeoutSec = 15
maxBridgeTokenSymbolRunes = 32
)

func sanitizeBridgeTokenSymbol(symbol string) string {
sanitized := strings.Map(func(r rune) rune {
if unicode.IsControl(r) || unicode.In(r, unicode.Cf) {
return -1
}
return r
}, symbol)
sanitized = strings.TrimSpace(sanitized)
runes := []rune(sanitized)
if len(runes) > maxBridgeTokenSymbolRunes {
runes = runes[:maxBridgeTokenSymbolRunes]
}
return string(runes)
}

type NEVMClient struct {
rpcClient *ethclient.Client
backupClient *ethclient.Client
Expand Down Expand Up @@ -151,11 +181,18 @@ func (c *NEVMClient) getTokenSymbol(contractAddr common.Address) (string, error)
return "", err
}

return unpacked[0].(string), nil
return sanitizeBridgeTokenSymbol(unpacked[0].(string)), nil
}

func (c *NEVMClient) FetchNEVMAssetDetails(assetGuid uint64) (*bchain.Asset, error) {
if assetGuid == 123456 {
metadata, err := encodeBridgeAssetMetadata(bridgeAssetMetadata{
Description: "Syscoin Native Asset",
AssetType: "SYSX",
})
if err != nil {
return nil, err
}
return &bchain.Asset{
Transactions: 0,
AssetObj: wire.AssetType{
Expand All @@ -165,7 +202,7 @@ func (c *NEVMClient) FetchNEVMAssetDetails(assetGuid uint64) (*bchain.Asset, err
TotalSupply: 0,
MaxSupply: 0,
},
MetaData: []byte("Syscoin Native Asset"),
MetaData: metadata,
}, nil
}

Expand Down Expand Up @@ -195,7 +232,8 @@ func (c *NEVMClient) FetchNEVMAssetDetails(assetGuid uint64) (*bchain.Asset, err
return nil, err
}

var symbol, metadata string
var symbol string
metadata := bridgeAssetMetadata{}
contractAddr := registry.AssetContract
precision := registry.Precision
switch registry.AssetType {
Expand All @@ -204,7 +242,9 @@ func (c *NEVMClient) FetchNEVMAssetDetails(assetGuid uint64) (*bchain.Asset, err
if err != nil || symbol == "" {
symbol = fmt.Sprintf("ERC20-%d", assetId)
}
metadata = "ERC20 Token"
metadata.Description = "ERC20 Token"
metadata.AssetType = "ERC20"
metadata.OriginDecimals = &registry.Precision
// SYSCOIN: UTXO-side SPT values are stored and exposed as CAmount/COIN
// units, matching Core's 8-decimal asset_value formatting.
precision = 8
Expand All @@ -218,7 +258,11 @@ func (c *NEVMClient) FetchNEVMAssetDetails(assetGuid uint64) (*bchain.Asset, err
if err != nil || symbol == "" {
symbol = fmt.Sprintf("ERC721-%d", assetId)
}
metadata = fmt.Sprintf("ERC721 NFT Token ID %s", realTokenId.String())
metadata.Description = fmt.Sprintf("ERC721 NFT Token ID %s", realTokenId.String())
metadata.AssetType = "ERC721"
metadata.TokenID = realTokenId.String()
originDecimals := uint8(0)
metadata.OriginDecimals = &originDecimals
precision = 0

case 4: // ERC1155
Expand All @@ -227,13 +271,22 @@ func (c *NEVMClient) FetchNEVMAssetDetails(assetGuid uint64) (*bchain.Asset, err
return nil, err
}
symbol = fmt.Sprintf("ERC1155-%d", assetId)
metadata = fmt.Sprintf("ERC1155 Token ID %s", realTokenId.String())
metadata.Description = fmt.Sprintf("ERC1155 Token ID %s", realTokenId.String())
metadata.AssetType = "ERC1155"
metadata.TokenID = realTokenId.String()
originDecimals := uint8(0)
metadata.OriginDecimals = &originDecimals
precision = 0

default:
return nil, fmt.Errorf("unsupported NEVM asset type %d for asset %d", registry.AssetType, assetId)
}

metadataJSON, err := encodeBridgeAssetMetadata(metadata)
if err != nil {
return nil, err
}

return &bchain.Asset{
Transactions: 0,
AssetObj: wire.AssetType{
Expand All @@ -243,6 +296,6 @@ func (c *NEVMClient) FetchNEVMAssetDetails(assetGuid uint64) (*bchain.Asset, err
TotalSupply: 0,
MaxSupply: 0,
},
MetaData: []byte(metadata),
MetaData: metadataJSON,
}, nil
}
Loading
Loading