invoices: payout addresses in the database
All checks were successful
Build and Deploy to Production / test (push) Successful in 26s
Build and Deploy to Production / build (push) Successful in 59s
Build and Deploy to Production / deploy (push) Successful in 38s

Default crypto payout addresses lived in the CRYPTO_ADDRESSES and
LTC_ADDRESS Forgejo secrets, so changing one meant editing a secret and
redeploying. Keep them in a payout_addresses table edited at
/admin/invoices/payout-addresses (read to view, write to change, every
change audited), and prefill the new-invoice form from it.

The old env values are imported once on the first boot with an empty
table, so the current addresses carry over; after that the secrets can
be removed.

Assisted-by: Claude:opus-5
This commit is contained in:
Sergei Poljanski 2026-10-10 08:36:32 +04:00
commit 1dd8f9081f
Signed by: asxpi
GPG key ID: 4F8851660FA4121B
10 changed files with 179 additions and 14 deletions

View file

@ -47,9 +47,11 @@ module CryptoAsset
end
# Default payout addresses for the new-invoice form. CRYPTO_ADDRESSES is a
# JSON object (code => address); the legacy LTC_ADDRESS env still fills LTC.
# Unknown codes are dropped so a typo in the env can't invent an asset.
def default_addresses
# Legacy source, now only used to seed the payout_addresses table once
# (PayoutAddress.seed_from_env!): CRYPTO_ADDRESSES is a JSON object
# (code => address); LTC_ADDRESS still fills LTC. Unknown codes are
# dropped so a typo in the env can't invent an asset.
def env_addresses
map = begin
JSON.parse(ENV['CRYPTO_ADDRESSES'].to_s)
rescue JSON::ParserError

36
lib/payout_address.rb Normal file
View file

@ -0,0 +1,36 @@
require 'sequel'
require_relative 'crypto_asset'
# Default payout address per coin (see migration 010).
class PayoutAddress < Sequel::Model(:payout_addresses)
unrestrict_primary_key
ADDRESS_RE = /\A[a-zA-Z0-9]{10,128}\z/ # same loose shape check as invoices
def self.map
to_hash(:coin, :address).slice(*CryptoAsset::CODES)
end
# First boot after the move: take over what the env/Forgejo secrets held.
def self.seed_from_env!
return if count.positive?
CryptoAsset.env_addresses.each do |coin, address|
next unless address.match?(ADDRESS_RE)
create(coin: coin, address: address, updated_by: 'env import', updated_at: Time.now.utc)
end
end
# Upsert or (blank address) delete. Returns an error string or nil.
def self.set!(coin, address, by:)
coin = coin.to_s.upcase
return 'unknown coin' unless CryptoAsset.valid?(coin)
address = address.to_s.strip
if address.empty?
where(coin: coin).delete
return nil
end
return "#{coin}: address looks invalid" unless address.match?(ADDRESS_RE)
row = self[coin] || new(coin: coin)
row.set(address: address, updated_by: by, updated_at: Time.now.utc).save
nil
end
end