Module: OpenReceive::Rates
- Defined in:
- lib/openreceive/rates.rb
Overview
Ruby port of packages/js/core/src/rates/index.ts: the built-in BTC price feed (static provider plus cached live feed with primary/fallback failover). Constants are hand-written and drift-checked against spec/data/rates/price-sources.json by test/rates_test.rb, exactly like the JS constants are checked by tests/rates.test.mjs.
Defined Under Namespace
Classes: CachedPriceFeed, HttpSimplePriceProvider, StaticPriceProvider
Constant Summary collapse
- PRICE_FEED_CACHE_SECONDS =
How long a cached price-feed read stays usable before a live refresh.
60- INVOICE_QUOTE_TTL_SECONDS =
600- PRICE_FEED_CLOCK_SKEW_SECONDS =
A cache stamp this far in the future means the clock stepped backwards: treat the stamp as stale rather than "fresh until wall-clock catches up".
5- PRICE_FEED_PRIMARY_TIMEOUT_MS =
The primary feed must answer within this window before the fallback is tried.
5000- PRICE_SOURCE_IDS =
%w[static_mock primary fallback].freeze
- STATIC_PRICE_SOURCE_ID =
"static_mock"- STATIC_BTC_FIAT_RATES =
{ "bitcoin" => { "usd" => "50000.00" }.freeze }.freeze
- PRICE_FEED_VS_CURRENCIES =
The fixed fiat list both live feeds price Bitcoin against. Hard-coded so the primary and fallback URLs always request the same currencies.
"usd,aed,ars,aud,bdt,bhd,bmd,brl,cad,chf,clp,cny,czk,dkk,eur,gbp,gel,hkd,huf,idr,ils,inr,jpy,krw,kwd,lkr,mmk,mxn,myr,ngn,nok,nzd,php,pkr,pln,rub,sar,sek,sgd,thb,try,twd,uah,vef,vnd,zar"- PRICE_FEED_CURRENCIES =
PRICE_FEED_VS_CURRENCIES.split(",").freeze
- SIMPLE_PRICE_BASE_URL =
"https://api.coingecko.com/api/v3/simple/price"- PRIMARY_PRICE_FEED_URL =
Primary live feed: the canonical public Simple Price endpoint.
"#{SIMPLE_PRICE_BASE_URL}?ids=bitcoin&vs_currencies=#{PRICE_FEED_VS_CURRENCIES}"- FALLBACK_PRICE_FEED_URL =
Fallback live feed: the OpenReceive mirror, in the same response shape.
"https://openreceive.org/api/v3/simple/price?ids=bitcoin&vs_currencies=#{PRICE_FEED_VS_CURRENCIES}"- PRICE_FEED_PRIMARY_URL_ENV =
Dev override env var names. Hosts read these (see read_price_feed_url_overrides) and pass any override into the factory; the feed itself never reads the environment.
"OPENRECEIVE_PRICE_FEED_PRIMARY_URL"- PRICE_FEED_FALLBACK_URL_ENV =
"OPENRECEIVE_PRICE_FEED_FALLBACK_URL"- CURRENCY_PATTERN =
/\A[A-Z]{3}\z/- RATE_KEY_PATTERN =
/\A[a-z]{3}\z/
Class Method Summary collapse
- .as_record(value) ⇒ Object
-
.create_cached_live_price_feed(currencies:, http: nil, clock: nil, cache_seconds: nil, primary_url: nil, fallback_url: nil, primary_timeout_ms: nil) ⇒ Object
Wires the hard-coded (or overridden) feeds to a disposable local cache.
-
.create_live_price_feed_providers(http: nil, primary_url: nil, fallback_url: nil, primary_timeout_ms: nil) ⇒ Object
Builds the primary and fallback live feed providers from the hard-coded URLs (or caller overrides).
-
.default_http_get(url, headers, timeout_ms) ⇒ Object
Default HTTP transport on stdlib Net::HTTP.
- .normalize_btc_fiat_rate(value, field) ⇒ Object
- .normalize_fiat_currency(currency) ⇒ Object
-
.number_to_plain_decimal_string(value) ⇒ Object
Expand any JSON number an upstream price source returns to plain decimal notation (never exponent form), matching JS numberToPlainDecimalString.
-
.parse_available_simple_price_response(response) ⇒ Object
Tolerant parse for caching the whole feed: keeps every well-formed currency the response carries and skips ones an upstream returned unusably (so a single dropped currency never fails the refresh).
-
.parse_simple_price_response(response, currencies) ⇒ Object
Strict select: every requested currency must be present and well formed.
- .presence(value) ⇒ Object
-
.read_price_feed_url_overrides(env = ENV) ⇒ Object
Host-side helper (the Ruby analogue of the node service's env reader): returns non-empty URL overrides from the well-known env var names.
- .static_btc_fiat_price(currency) ⇒ Object
Class Method Details
.as_record(value) ⇒ Object
140 141 142 143 |
# File 'lib/openreceive/rates.rb', line 140 def as_record(value) raise ArgumentError, "expected object" unless value.is_a?(Hash) value end |
.create_cached_live_price_feed(currencies:, http: nil, clock: nil, cache_seconds: nil, primary_url: nil, fallback_url: nil, primary_timeout_ms: nil) ⇒ Object
Wires the hard-coded (or overridden) feeds to a disposable local cache.
164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 |
# File 'lib/openreceive/rates.rb', line 164 def create_cached_live_price_feed(currencies:, http: nil, clock: nil, cache_seconds: nil, primary_url: nil, fallback_url: nil, primary_timeout_ms: nil) providers = create_live_price_feed_providers( http: http, primary_url: primary_url, fallback_url: fallback_url, primary_timeout_ms: primary_timeout_ms ) CachedPriceFeed.new( currencies: currencies, primary: providers.fetch(:primary), fallback: providers.fetch(:fallback), cache_seconds: cache_seconds, clock: clock ) end |
.create_live_price_feed_providers(http: nil, primary_url: nil, fallback_url: nil, primary_timeout_ms: nil) ⇒ Object
Builds the primary and fallback live feed providers from the hard-coded URLs (or caller overrides). The primary provider carries the 5s timeout.
147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 |
# File 'lib/openreceive/rates.rb', line 147 def create_live_price_feed_providers(http: nil, primary_url: nil, fallback_url: nil, primary_timeout_ms: nil) { primary: HttpSimplePriceProvider.new( url: primary_url || PRIMARY_PRICE_FEED_URL, source: "primary", http: http, timeout_ms: primary_timeout_ms || PRICE_FEED_PRIMARY_TIMEOUT_MS ), fallback: HttpSimplePriceProvider.new( url: fallback_url || FALLBACK_PRICE_FEED_URL, source: "fallback", http: http ) } end |
.default_http_get(url, headers, timeout_ms) ⇒ Object
Default HTTP transport on stdlib Net::HTTP. Injectable replacements must be callable as call(url, headers, timeout_ms) and return a Hash with :status (Integer) and :body (String).
198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 |
# File 'lib/openreceive/rates.rb', line 198 def default_http_get(url, headers, timeout_ms) uri = URI.parse(url) http = Net::HTTP.new(uri.host, uri.port) http.use_ssl = uri.scheme == "https" unless timeout_ms.nil? seconds = timeout_ms / 1000.0 http.open_timeout = seconds http.read_timeout = seconds http.write_timeout = seconds if http.respond_to?(:write_timeout=) end request = Net::HTTP::Get.new(uri.request_uri) headers.each { |key, value| request[key] = value } response = http.start { |connection| connection.request(request) } { status: Integer(response.code), body: response.body.to_s } end |
.normalize_btc_fiat_rate(value, field) ⇒ Object
113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 |
# File 'lib/openreceive/rates.rb', line 113 def normalize_btc_fiat_rate(value, field) if value.is_a?(Numeric) unless value.finite? && value.positive? raise ArgumentError, "#{field} must be a positive number" end normalized = number_to_plain_decimal_string(value) Money.decimal(normalized, field) return normalized end if value.is_a?(String) Money.decimal(value, field) return value end raise ArgumentError, "#{field} must be a number or decimal string" end |
.normalize_fiat_currency(currency) ⇒ Object
68 69 70 71 72 73 |
# File 'lib/openreceive/rates.rb', line 68 def normalize_fiat_currency(currency) unless currency.is_a?(String) && CURRENCY_PATTERN.match?(currency) raise ArgumentError, "fiat.currency must be an ISO 4217 uppercase code" end currency.downcase end |
.number_to_plain_decimal_string(value) ⇒ Object
Expand any JSON number an upstream price source returns to plain decimal notation (never exponent form), matching JS numberToPlainDecimalString.
133 134 135 136 137 138 |
# File 'lib/openreceive/rates.rb', line 133 def number_to_plain_decimal_string(value) return value.to_s if value.is_a?(Integer) decimal = BigDecimal(value.to_s) text = decimal.to_s("F") decimal.frac.zero? ? text.sub(/\.0+\z/, "") : text end |
.parse_available_simple_price_response(response) ⇒ Object
Tolerant parse for caching the whole feed: keeps every well-formed currency the response carries and skips ones an upstream returned unusably (so a single dropped currency never fails the refresh). Raises only when the response is not Simple Price shaped or carries no usable rate at all.
97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 |
# File 'lib/openreceive/rates.rb', line 97 def parse_available_simple_price_response(response) bitcoin = as_record(as_record(response)["bitcoin"]) rates = {} bitcoin.each do |key, value| rate_key = key.to_s.downcase next unless RATE_KEY_PATTERN.match?(rate_key) begin rates[rate_key] = normalize_btc_fiat_rate(value, "bitcoin.#{key}") rescue ArgumentError # Skip a currency the upstream returned in an unusable form. end end raise ArgumentError, "price response contained no usable BTC fiat rates" if rates.empty? { "bitcoin" => rates } end |
.parse_simple_price_response(response, currencies) ⇒ Object
Strict select: every requested currency must be present and well formed.
82 83 84 85 86 87 88 89 90 |
# File 'lib/openreceive/rates.rb', line 82 def parse_simple_price_response(response, currencies) bitcoin = as_record(as_record(response)["bitcoin"]) rates = {} currencies.each do |currency| key = normalize_fiat_currency(currency) rates[key] = normalize_btc_fiat_rate(bitcoin[key], "bitcoin.#{key}") end { "bitcoin" => rates } end |
.presence(value) ⇒ Object
190 191 192 193 |
# File 'lib/openreceive/rates.rb', line 190 def presence(value) text = value.to_s.strip text.empty? ? nil : text end |
.read_price_feed_url_overrides(env = ENV) ⇒ Object
Host-side helper (the Ruby analogue of the node service's env reader): returns non-empty URL overrides from the well-known env var names.
183 184 185 186 187 188 |
# File 'lib/openreceive/rates.rb', line 183 def read_price_feed_url_overrides(env = ENV) { primary_url: presence(env[PRICE_FEED_PRIMARY_URL_ENV]), fallback_url: presence(env[PRICE_FEED_FALLBACK_URL_ENV]) } end |
.static_btc_fiat_price(currency) ⇒ Object
75 76 77 78 79 |
# File 'lib/openreceive/rates.rb', line 75 def static_btc_fiat_price(currency) rate = STATIC_BTC_FIAT_RATES.fetch("bitcoin")[normalize_fiat_currency(currency)] raise ArgumentError, "unsupported static fiat currency: #{currency}" if rate.nil? rate end |