invoices: multi-crypto payment support
All checks were successful
Build and Deploy to Production / test (push) Successful in 18s
Build and Deploy to Production / build (push) Successful in 49s
Build and Deploy to Production / deploy (push) Successful in 31s

One asset per invoice from a registry (lib/crypto_asset.rb): BTC, LTC,
ETH, XMR, SOL, ALGO, USDT/USDC on ERC-20/TRC-20/BEP-20/Solana/Algorand.
Migration 003 generalizes the ltc_* columns to crypto_* + crypto_coin
(existing LTC invoices backfilled). QR payload adapts per asset: BIP21
amount URIs where supported, bare address for tokens with a network
hint on the PDF. Default addresses come from the CRYPTO_ADDRESSES
secret (JSON code=>address); LTC_ADDRESS still works as legacy.
This commit is contained in:
Sergei Poljanski 2026-07-03 02:01:57 +04:00
commit 0fbb6ee809
Signed by: asxpi
GPG key ID: 4F8851660FA4121B
20 changed files with 410 additions and 143 deletions

View file

@ -1,57 +0,0 @@
require 'net/http'
require 'json'
require 'uri'
require 'bigdecimal'
# Fetches the current Litecoin price from CoinGecko (free, no API key).
#
# CoinGecko's simple/price supports usd and eur directly. GEL is not a
# CoinGecko vs_currency we rely on, so GEL is derived from the invoice's own
# captured gel_rate (USD->GEL): ltc_in_gel = ltc_in_usd * gel_rate.
module LtcRate
ENDPOINT = 'https://api.coingecko.com/api/v3/simple/price'.freeze
SUPPORTED = %w[USD EUR].freeze
TIMEOUT = 6 # seconds
class Error < StandardError; end
module_function
# Returns a BigDecimal: the price of 1 LTC in `currency`.
# `gel_rate` (USD->GEL) is required only when currency == 'GEL'.
def fetch(currency, gel_rate: nil)
currency = currency.to_s.upcase
if currency == 'GEL'
raise Error, 'gel_rate required to derive LTC/GEL' if gel_rate.nil?
usd = fetch_simple('usd')
(usd * BigDecimal(gel_rate.to_s)).round(8)
elsif SUPPORTED.include?(currency)
fetch_simple(currency.downcase).round(8)
else
raise Error, "Unsupported currency: #{currency}"
end
end
def fetch_simple(vs)
uri = URI(ENDPOINT)
uri.query = URI.encode_www_form(ids: 'litecoin', vs_currencies: vs)
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true,
open_timeout: TIMEOUT, read_timeout: TIMEOUT) do |http|
http.get(uri.request_uri, 'Accept' => 'application/json')
end
raise Error, "CoinGecko HTTP #{res.code}" unless res.is_a?(Net::HTTPSuccess)
data = JSON.parse(res.body)
price = data.dig('litecoin', vs)
raise Error, "No price for #{vs} in response" if price.nil?
BigDecimal(price.to_s)
rescue JSON::ParserError => e
raise Error, "Bad JSON from CoinGecko: #{e.message}"
rescue Net::OpenTimeout, Net::ReadTimeout
raise Error, 'CoinGecko timed out'
end
end