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
150 changes: 150 additions & 0 deletions api/contract.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
package api

import (
"strings"

"github.com/trezor/blockbook/bchain"
)

const contractInfoProtocolErc4626 = "erc4626"

var knownContractProtocols = []string{contractInfoProtocolErc4626}

func contractInfoSupportsRates(standard bchain.TokenStandardName) bool {
return standard == erc4626EvmFungibleStandard()
}

func contractInfoIncludesProtocol(protocols []string, protocol string) bool {
for _, value := range protocols {
if strings.EqualFold(strings.TrimSpace(value), protocol) {
return true
}
}
return false
}

// ValidateContractProtocols rejects protocol values not recognised by this API.
// Empty and whitespace-only entries are tolerated for convenience.
func ValidateContractProtocols(protocols []string) error {
for _, p := range protocols {
normalized := strings.ToLower(strings.TrimSpace(p))
if normalized == "" {
continue
}
known := false
for _, k := range knownContractProtocols {
if normalized == k {
known = true
break
}
}
if !known {
return NewAPIError("Unknown protocol: "+p, true)
}
}
return nil
}

// ValidateProtocolsForChain rejects a non-empty protocols list on coins that
// don't support any protocol enrichments, and otherwise validates the values.
func (w *Worker) ValidateProtocolsForChain(protocols []string) error {
if len(protocols) == 0 {
return nil
}
if w.chainType != bchain.ChainEthereumType {
return NewAPIError("protocols parameter is not supported on this coin", true)
}
return ValidateContractProtocols(protocols)
}

func (w *Worker) enrichTokenProtocols(tokens Tokens, protocols []string) {
if !contractInfoIncludesProtocol(protocols, contractInfoProtocolErc4626) {
return
}
w.enrichErc4626Tokens(tokens)
}

func (w *Worker) buildContractInfoRates(contract string, standard bchain.TokenStandardName, currency string) *ContractInfoRates {
if !contractInfoSupportsRates(standard) || w.fiatRates == nil {
return nil
}

currency = strings.ToLower(strings.TrimSpace(currency))
ticker := getCurrentTicker(w.fiatRates, currency, contract)
baseRate, baseRateFound := w.GetContractBaseRate(ticker, contract, 0)
if !baseRateFound && currency == "" {
return nil
}

rates := &ContractInfoRates{}
if baseRateFound {
rates.BaseRate = baseRate
}
if currency != "" {
rates.Currency = currency
if ticker != nil {
if secondaryRate := ticker.TokenRateInCurrency(contract, currency); secondaryRate > 0 {
rates.SecondaryRate = float64(secondaryRate)
}
}
}
return rates
}

func (w *Worker) GetContractInfoData(contract string, currency string, protocols []string) (*ContractInfoResult, error) {
if w.chainType != bchain.ChainEthereumType {
return nil, NewAPIError("getContractInfo is not supported on this coin", true)
}
if strings.TrimSpace(contract) == "" {
return nil, NewAPIError("Missing contract", true)
}
if err := ValidateContractProtocols(protocols); err != nil {
return nil, err
}

contractInfo, validContract, err := w.GetContractInfo(contract, bchain.UnknownTokenStandard)
if err != nil {
return nil, NewAPIError("Invalid contract, "+err.Error(), true)
}
if contractInfo == nil || !validContract {
return nil, NewAPIError("Contract not found", true)
}

bestHeight, _, err := w.db.GetBestBlock()
if err != nil {
return nil, err
}

result := &ContractInfoResult{
Type: contractInfo.Type,
Standard: contractInfo.Standard,
Contract: contractInfo.Contract,
Name: contractInfo.Name,
Symbol: contractInfo.Symbol,
Decimals: contractInfo.Decimals,
CreatedInBlock: contractInfo.CreatedInBlock,
DestructedInBlock: contractInfo.DestructedInBlock,
Rates: w.buildContractInfoRates(contractInfo.Contract, contractInfo.Standard, currency),
BlockHeight: bestHeight,
}

if !contractInfoIncludesProtocol(protocols, contractInfoProtocolErc4626) || w.chainType != bchain.ChainEthereumType || contractInfo.Standard != erc4626EvmFungibleStandard() {
return result, nil
}

probe, isVault := w.detectErc4626Vault(contractInfo.Contract)
if !isVault {
return result, nil
}

result.Protocols = &ContractInfoProtocols{
Erc4626: w.fetchErc4626TokenData(&Token{
Contract: contractInfo.Contract,
Name: contractInfo.Name,
Symbol: contractInfo.Symbol,
Decimals: contractInfo.Decimals,
Standard: contractInfo.Standard,
}, probe),
}
return result, nil
}
85 changes: 85 additions & 0 deletions api/contract_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package api

import (
"math"
"testing"

"github.com/trezor/blockbook/bchain"
"github.com/trezor/blockbook/common"
"github.com/trezor/blockbook/fiat"
)

func TestContractInfoIncludesProtocol(t *testing.T) {
if !contractInfoIncludesProtocol([]string{" ERC4626 "}, contractInfoProtocolErc4626) {
t.Fatal("expected erc4626 protocol to match case-insensitively")
}
if contractInfoIncludesProtocol([]string{"staking"}, contractInfoProtocolErc4626) {
t.Fatal("unexpected erc4626 protocol match")
}
}

func TestBuildContractInfoRates(t *testing.T) {
originalGetter := getCurrentTicker
defer func() {
getCurrentTicker = originalGetter
}()

tickerCalls := 0
getCurrentTicker = func(_ *fiat.FiatRates, vsCurrency string, token string) *common.CurrencyRatesTicker {
tickerCalls++
if vsCurrency != "usd" {
t.Fatalf("unexpected currency lookup: got %q want %q", vsCurrency, "usd")
}
if token != "0xabc" {
t.Fatalf("unexpected token lookup: got %q want %q", token, "0xabc")
}
return &common.CurrencyRatesTicker{
Rates: map[string]float32{
"usd": 2.5,
},
TokenRates: map[string]float32{
"0xabc": 1.2,
},
}
}

w := &Worker{fiatRates: &fiat.FiatRates{}}
rates := w.buildContractInfoRates("0xabc", erc4626EvmFungibleStandard(), "USD")
if tickerCalls != 1 {
t.Fatalf("expected one ticker lookup, got %d", tickerCalls)
}
if rates == nil {
t.Fatal("expected rates")
}
if rates.Currency != "usd" {
t.Fatalf("unexpected currency: %q", rates.Currency)
}
if math.Abs(rates.BaseRate-1.2) > 1e-6 {
t.Fatalf("unexpected base rate: got %v want %v", rates.BaseRate, 1.2)
}
if math.Abs(rates.SecondaryRate-3.0) > 1e-6 {
t.Fatalf("unexpected secondary rate: got %v want %v", rates.SecondaryRate, 3.0)
}
}

func TestBuildContractInfoRatesSkipsUnsupportedStandards(t *testing.T) {
originalGetter := getCurrentTicker
defer func() {
getCurrentTicker = originalGetter
}()

tickerCalls := 0
getCurrentTicker = func(_ *fiat.FiatRates, _, _ string) *common.CurrencyRatesTicker {
tickerCalls++
return nil
}

w := &Worker{fiatRates: &fiat.FiatRates{}}
rates := w.buildContractInfoRates("0xabc", bchain.ERC1155TokenStandard, "usd")
if rates != nil {
t.Fatalf("expected nil rates for unsupported standard, got %+v", rates)
}
if tickerCalls != 0 {
t.Fatalf("expected no ticker lookups for unsupported standard, got %d", tickerCalls)
}
}
5 changes: 4 additions & 1 deletion api/erc4626.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,10 @@ func (w *Worker) enrichErc4626Tokens(tokens Tokens) {
if !ok {
continue
}
candidate.token.Erc4626 = w.fetchErc4626TokenData(candidate.token, probe)
if candidate.token.Protocols == nil {
candidate.token.Protocols = &ContractInfoProtocols{}
}
candidate.token.Protocols.Erc4626 = w.fetchErc4626TokenData(candidate.token, probe)
}
}

Expand Down
40 changes: 34 additions & 6 deletions api/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -192,11 +192,39 @@ type Erc4626Token struct {
Error string `json:"error,omitempty" ts_doc:"Error message for partial failures while fetching ERC4626 fields."`
}

// ContractInfoRates contains current price data for a single contract when available.
type ContractInfoRates struct {
BaseRate float64 `json:"baseRate,omitempty" ts_doc:"Current price of one whole token in the chain base currency, when available."`
Currency string `json:"currency,omitempty" ts_doc:"Requested secondary currency code for the secondaryRate field, lower-cased."`
SecondaryRate float64 `json:"secondaryRate,omitempty" ts_doc:"Current price of one whole token in the requested secondary currency, when available."`
}

// ContractInfoProtocols contains optional protocol-specific contract enrichments.
type ContractInfoProtocols struct {
Erc4626 *Erc4626Token `json:"erc4626,omitempty" ts_doc:"ERC4626 vault details when explicitly requested and detected."`
}

// ContractInfoResult contains contract metadata and optional enrichments for a single contract.
type ContractInfoResult struct {
// Deprecated: Use Standard instead.
Type bchain.TokenStandardName `json:"type" ts_type:"'' | 'XPUBAddress' | 'ERC20' | 'ERC721' | 'ERC1155' | 'BEP20' | 'BEP721' | 'BEP1155' | 'TRC20' | 'TRC721' | 'TRC1155'" ts_doc:"@deprecated: Use standard instead."`
Standard bchain.TokenStandardName `json:"standard" ts_type:"'' | 'XPUBAddress' | 'ERC20' | 'ERC721' | 'ERC1155' | 'BEP20' | 'BEP721' | 'BEP1155' | 'TRC20' | 'TRC721' | 'TRC1155'"`
Contract string `json:"contract" ts_doc:"Smart contract address."`
Name string `json:"name" ts_doc:"Readable name of the contract."`
Symbol string `json:"symbol" ts_doc:"Symbol for tokens under this contract, if applicable."`
Decimals int `json:"decimals" ts_doc:"Number of decimal places, if applicable."`
CreatedInBlock uint32 `json:"createdInBlock,omitempty" ts_doc:"Block height where contract was first created."`
DestructedInBlock uint32 `json:"destructedInBlock,omitempty" ts_doc:"Block height where contract was destroyed (if any)."`
Rates *ContractInfoRates `json:"rates,omitempty" ts_doc:"Current rate data for the contract when available."`
Protocols *ContractInfoProtocols `json:"protocols,omitempty" ts_doc:"Optional protocol-specific enrichments requested by the caller."`
BlockHeight uint32 `json:"blockHeight" ts_doc:"Indexed best block height used as freshness metadata for this response."`
}

// Token contains info about tokens held by an address
type Token struct {
// Deprecated: Use Standard instead.
Type bchain.TokenStandardName `json:"type" ts_type:"'' | 'XPUBAddress' | 'ERC20' | 'ERC721' | 'ERC1155' | 'BEP20' | 'BEP721' | 'BEP1155'" ts_doc:"@deprecated: Use standard instead."`
Standard bchain.TokenStandardName `json:"standard" ts_type:"'' | 'XPUBAddress' | 'ERC20' | 'ERC721' | 'ERC1155' | 'BEP20' | 'BEP721' | 'BEP1155'"`
Type bchain.TokenStandardName `json:"type" ts_type:"'' | 'XPUBAddress' | 'ERC20' | 'ERC721' | 'ERC1155' | 'BEP20' | 'BEP721' | 'BEP1155' | 'TRC20' | 'TRC721' | 'TRC1155'" ts_doc:"@deprecated: Use standard instead."`
Standard bchain.TokenStandardName `json:"standard" ts_type:"'' | 'XPUBAddress' | 'ERC20' | 'ERC721' | 'ERC1155' | 'BEP20' | 'BEP721' | 'BEP1155' | 'TRC20' | 'TRC721' | 'TRC1155'"`
Name string `json:"name" ts_doc:"Readable name of the token."`
Path string `json:"path,omitempty" ts_doc:"Derivation path if this token is derived from an XPUB-based address."`
Contract string `json:"contract,omitempty" ts_doc:"Contract address on-chain."`
Expand All @@ -210,7 +238,7 @@ type Token struct {
MultiTokenValues []MultiTokenValue `json:"multiTokenValues,omitempty" ts_doc:"Multiple ERC1155 token balances (id + value)."`
TotalReceivedSat *Amount `json:"totalReceived,omitempty" ts_doc:"Total amount of tokens received."`
TotalSentSat *Amount `json:"totalSent,omitempty" ts_doc:"Total amount of tokens sent."`
Erc4626 *Erc4626Token `json:"erc4626,omitempty" ts_doc:"ERC4626 vault details when requested and detected."`
Protocols *ContractInfoProtocols `json:"protocols,omitempty" ts_doc:"Optional protocol-specific enrichments requested by the caller."`
ContractIndex string `json:"-"`
}

Expand Down Expand Up @@ -244,8 +272,8 @@ func (a Tokens) Less(i, j int) bool {
// TokenTransfer contains info about a token transfer done in a transaction
type TokenTransfer struct {
// Deprecated: Use Standard instead.
Type bchain.TokenStandardName `json:"type" ts_type:"'' | 'XPUBAddress' | 'ERC20' | 'ERC721' | 'ERC1155' | 'BEP20' | 'BEP721' | 'BEP1155'" ts_doc:"@deprecated: Use standard instead."`
Standard bchain.TokenStandardName `json:"standard" ts_type:"'' | 'XPUBAddress' | 'ERC20' | 'ERC721' | 'ERC1155' | 'BEP20' | 'BEP721' | 'BEP1155'"`
Type bchain.TokenStandardName `json:"type" ts_type:"'' | 'XPUBAddress' | 'ERC20' | 'ERC721' | 'ERC1155' | 'BEP20' | 'BEP721' | 'BEP1155' | 'TRC20' | 'TRC721' | 'TRC1155'" ts_doc:"@deprecated: Use standard instead."`
Standard bchain.TokenStandardName `json:"standard" ts_type:"'' | 'XPUBAddress' | 'ERC20' | 'ERC721' | 'ERC1155' | 'BEP20' | 'BEP721' | 'BEP1155' | 'TRC20' | 'TRC721' | 'TRC1155'"`
From string `json:"from" ts_doc:"Source address of the token transfer."`
To string `json:"to" ts_doc:"Destination address of the token transfer."`
Contract string `json:"contract" ts_doc:"Contract address of the token."`
Expand Down Expand Up @@ -365,7 +393,7 @@ type AddressFilter struct {
FromHeight uint32 `ts_doc:"Starting block height for filtering transactions."`
ToHeight uint32 `ts_doc:"Ending block height for filtering transactions."`
TokensToReturn TokensToReturn `ts_doc:"Which tokens to include in the result set."`
IncludeErc4626 bool `ts_doc:"If true, enriches fungible EVM tokens with ERC4626 vault data when available."`
Protocols []string `ts_doc:"Optional protocol enrichments to include. Supported values currently include 'erc4626'."`
// OnlyConfirmed set to true will ignore mempool transactions; mempool is also ignored if FromHeight/ToHeight filter is specified
OnlyConfirmed bool `ts_doc:"If true, ignores mempool (unconfirmed) transactions."`
}
Expand Down
4 changes: 1 addition & 3 deletions api/worker.go
Original file line number Diff line number Diff line change
Expand Up @@ -1305,9 +1305,7 @@ func (w *Worker) getEthereumTypeAddressBalances(addrDesc bchain.AddressDescripto
}
d.tokens = d.tokens[:j]
sort.Sort(d.tokens)
if filter.IncludeErc4626 {
w.enrichErc4626Tokens(d.tokens)
}
w.enrichTokenProtocols(d.tokens, filter.Protocols)
}
d.contractInfo, err = w.db.GetContractInfo(addrDesc, bchain.UnknownTokenStandard)
if err != nil {
Expand Down
4 changes: 2 additions & 2 deletions bchain/types_ethereum_type.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,8 @@ type EthereumInternalData struct {
// ContractInfo contains info about a contract
type ContractInfo struct {
// Deprecated: Use Standard instead.
Type TokenStandardName `json:"type" ts_type:"'' | 'XPUBAddress' | 'ERC20' | 'ERC721' | 'ERC1155' | 'BEP20' | 'BEP721' | 'BEP1155'" ts_doc:"@deprecated: Use standard instead."`
Standard TokenStandardName `json:"standard" ts_type:"'' | 'XPUBAddress' | 'ERC20' | 'ERC721' | 'ERC1155' | 'BEP20' | 'BEP721' | 'BEP1155'"`
Type TokenStandardName `json:"type" ts_type:"'' | 'XPUBAddress' | 'ERC20' | 'ERC721' | 'ERC1155' | 'BEP20' | 'BEP721' | 'BEP1155' | 'TRC20' | 'TRC721' | 'TRC1155'" ts_doc:"@deprecated: Use standard instead."`
Standard TokenStandardName `json:"standard" ts_type:"'' | 'XPUBAddress' | 'ERC20' | 'ERC721' | 'ERC1155' | 'BEP20' | 'BEP721' | 'BEP1155' | 'TRC20' | 'TRC721' | 'TRC1155'"`
Contract string `json:"contract" ts_doc:"Smart contract address."`
Name string `json:"name" ts_doc:"Readable name of the contract."`
Symbol string `json:"symbol" ts_doc:"Symbol for tokens under this contract, if applicable."`
Expand Down
Loading
Loading