Move back to brandfetch (#1427)

* Move back to brandfetch

* Update security.rb

* Update security.rb
This commit is contained in:
soky srm
2026-04-10 17:42:16 +02:00
committed by GitHub
parent 0aca297e9c
commit dcebda05de
6 changed files with 129 additions and 189 deletions

View File

@@ -31,16 +31,6 @@ class Provider::BinancePublic < Provider
"TRY" => "TRY"
}.freeze
# Per-asset logo PNGs served via jsDelivr from a GitHub repo that tracks the
# full Binance-listed asset set. We originally used bin.bnbstatic.com directly
# — Binance's own CDN — but that host enforces Referer-based hotlink
# protection at CloudFront: any request with a non-Binance Referer returns
# 403. A server-side HEAD from Faraday (no Referer) succeeds, which masked
# the breakage until the URL hit an actual <img> tag in the browser. jsDelivr
# is CORS-open and hotlink-friendly, so the URL we persist is the URL the
# browser can actually load. File names are uppercase PNGs (BTC.png, ETH.png).
LOGO_CDN_BASE = "https://cdn.jsdelivr.net/gh/lindomar-oliveira/binance-data-plus/assets/img".freeze
KLINE_MAX_LIMIT = 1000
MS_PER_DAY = 24 * 60 * 60 * 1000
SEARCH_LIMIT = 25
@@ -116,7 +106,12 @@ class Provider::BinancePublic < Provider
Security.new(
symbol: "#{base}#{display_currency}",
name: base,
logo_url: "#{LOGO_CDN_BASE}/#{base}.png",
# Brandfetch /crypto/{base} URL — unknown coins (rare) will 400 and
# render as a broken img in the dropdown; same tradeoff as stocks
# with obscure tickers. `::Security` reaches the AR model —
# unqualified `Security` here resolves to the Data value-object
# from SecurityConcept.
logo_url: ::Security.brandfetch_crypto_url(base),
exchange_operating_mic: BINANCE_MIC,
country_code: nil,
currency: display_currency
@@ -130,11 +125,14 @@ class Provider::BinancePublic < Provider
parsed = parse_ticker(symbol)
raise Error, "Unsupported Binance ticker: #{symbol}" if parsed.nil?
# logo_url is intentionally nil — crypto logos are set at save time by
# Security#generate_logo_url_from_brandfetch via the /crypto/{base}
# route, not returned from this provider.
SecurityInfo.new(
symbol: symbol,
name: parsed[:base],
links: "https://www.binance.com/en/trade/#{parsed[:binance_pair]}",
logo_url: verified_logo_url(parsed[:base]),
logo_url: nil,
description: nil,
kind: "crypto",
exchange_operating_mic: exchange_operating_mic
@@ -281,34 +279,6 @@ class Provider::BinancePublic < Provider
Time.utc(date.year, date.month, date.day).to_i * 1000
end
# Returns the asset-specific jsDelivr logo URL if the HEAD succeeds, else
# nil. Returning nil (rather than a hard-coded fallback URL) lets
# Security#display_logo_url swap in a Brandfetch binance.com URL at render
# time — a config-dependent path that can't be baked into a constant here.
# Cached per base asset for 30 days so we HEAD at most once per coin and
# only when Security#import_provider_details runs (never during search,
# which must stay fast).
def verified_logo_url(base_asset)
Rails.cache.fetch("binance_public:logo:#{base_asset}", expires_in: 30.days) do
candidate = "#{LOGO_CDN_BASE}/#{base_asset}.png"
logo_client.head(candidate)
candidate
rescue Faraday::Error
nil
end
end
# Dedicated Faraday client for the logo CDN host (jsdelivr.net is a
# different origin from data-api.binance.vision). HEAD-only with a tight
# timeout so CDN hiccups can't stall Security info imports.
def logo_client
@logo_client ||= Faraday.new(url: LOGO_CDN_BASE, ssl: self.class.faraday_ssl_options) do |faraday|
faraday.options.timeout = 3
faraday.options.open_timeout = 2
faraday.response :raise_error
end
end
# Preserve BinancePublic::Error subclasses (e.g. InvalidSecurityPriceError)
# through with_provider_response. The inherited RateLimitable transformer
# only preserves RateLimitError and would otherwise downcast our custom

View File

@@ -18,6 +18,15 @@ class Security < ApplicationRecord
Provider::Registry.for_concept(:securities).provider_keys.map(&:to_s)
end
# Builds the Brandfetch crypto URL for a base asset (e.g. "BTC"). Returns
# nil when Brandfetch isn't configured.
def self.brandfetch_crypto_url(base_asset)
return nil if base_asset.blank?
return nil unless Setting.brand_fetch_client_id.present?
size = Setting.brand_fetch_logo_size
"https://cdn.brandfetch.io/crypto/#{base_asset}/icon/fallback/lettermark/w/#{size}/h/#{size}?c=#{Setting.brand_fetch_client_id}"
end
before_validation :upcase_symbols
before_save :generate_logo_url_from_brandfetch, if: :should_generate_logo?
before_save :reset_first_provider_price_on_if_provider_changed
@@ -60,19 +69,26 @@ class Security < ApplicationRecord
exchange_operating_mic == Provider::BinancePublic::BINANCE_MIC
end
# Single source of truth for which logo URL the UI should render. The order
# differs by asset class:
#
# - Crypto: prefer the verified per-asset jsDelivr logo (set during
# import by Provider::BinancePublic#verified_logo_url). On a miss, fall
# back to Brandfetch with a forced `binance.com` identifier so the
# generic Binance brand mark shows instead of a ticker lettermark.
#
# - Everything else: Brandfetch first (domain-derived or ticker lettermark),
# then any provider-set logo_url.
# Strips the display-currency suffix from a crypto ticker (BTCUSD -> BTC,
# ETHEUR -> ETH). Returns nil for non-crypto securities or when the ticker
# doesn't end in a supported quote.
def crypto_base_asset
return nil unless crypto?
Provider::BinancePublic::QUOTE_TO_CURRENCY.each_value do |suffix|
next unless ticker.end_with?(suffix)
base = ticker.delete_suffix(suffix)
return base unless base.empty?
end
nil
end
# Single source of truth for which logo URL the UI should render. Crypto
# and stocks share the same shape: prefer a freshly computed Brandfetch
# URL (honors current client_id + size) and fall back to any stored
# logo_url for the provider-returns-its-own-URL case (e.g. Tiingo S3).
def display_logo_url
if crypto?
logo_url.presence || brandfetch_icon_url(identifier: "binance.com")
self.class.brandfetch_crypto_url(crypto_base_asset).presence || logo_url.presence
else
brandfetch_icon_url.presence || logo_url.presence
end
@@ -106,13 +122,13 @@ class Security < ApplicationRecord
)
end
def brandfetch_icon_url(width: nil, height: nil, identifier: nil)
def brandfetch_icon_url(width: nil, height: nil)
return nil unless Setting.brand_fetch_client_id.present?
w = width || Setting.brand_fetch_logo_size
h = height || Setting.brand_fetch_logo_size
identifier ||= extract_domain(website_url) if website_url.present?
identifier = extract_domain(website_url) if website_url.present?
identifier ||= ticker
return nil unless identifier.present?
@@ -137,8 +153,7 @@ class Security < ApplicationRecord
def should_generate_logo?
return false if cash?
url = brandfetch_icon_url
return false unless url.present?
return false unless Setting.brand_fetch_client_id.present?
return true if logo_url.blank?
return false unless logo_url.include?("cdn.brandfetch.io")
@@ -147,7 +162,11 @@ class Security < ApplicationRecord
end
def generate_logo_url_from_brandfetch
self.logo_url = brandfetch_icon_url
self.logo_url = if crypto?
self.class.brandfetch_crypto_url(crypto_base_asset)
else
brandfetch_icon_url
end
end
# When a user remaps a security to a different provider (via the holdings

View File

@@ -18,7 +18,11 @@ class Setting < RailsSettings::Base
BRAND_FETCH_LOGO_SIZE_STANDARD = 40
BRAND_FETCH_LOGO_SIZE_HIGH_RES = 120
BRAND_FETCH_URL_PATTERN = %r{(https://cdn\.brandfetch\.io/[^/]+/icon/fallback/lettermark/)w/\d+/h/\d+(\?c=.+)}
# Matches both legacy single-segment URLs (`/apple.com/icon/...`) and
# explicit type-routed URLs introduced 2026 (`/crypto/BTC/icon/...`,
# `/domain/apple.com/icon/...`). `[^?]+` reaches across the extra slash
# so transform_brand_fetch_url can rewrite the size params on both shapes.
BRAND_FETCH_URL_PATTERN = %r{(https://cdn\.brandfetch\.io/[^?]+/icon/fallback/lettermark/)w/\d+/h/\d+(\?c=.+)}
def self.brand_fetch_logo_size
brand_fetch_high_res_logos ? BRAND_FETCH_LOGO_SIZE_HIGH_RES : BRAND_FETCH_LOGO_SIZE_STANDARD