diff --git a/bin/hydrate_btc_transactions.rb b/bin/hydrate_btc_transactions.rb new file mode 100755 index 0000000000000000000000000000000000000000..10b6c760fe4328fdbccd0b27451f4e89bf2c0a4c --- /dev/null +++ b/bin/hydrate_btc_transactions.rb @@ -0,0 +1,54 @@ +#!/usr/bin/ruby +# frozen_string_literal: true + +# This reads the queue of "changed addresses" from electrum webhooks in the +# form "
/" and turns those into /
=> +# customer_id keys for the bin/process_pending_btc_transactions script to run +# through and process more fully +# +# Usage: bin/process_pending-btc_transactions '{ +# pidfile : Text, +# electrum : env:ELECTRUM_CONFIG, +# }' + +require "dhall" + +CONFIG = + Dhall::Coder + .new(safe: Dhall::Coder::JSON_LIKE + [Symbol, Proc]) + .load(ARGV[0], transform_keys: :to_sym) + +require "redis" +require_relative "../lib/electrum" +require_relative "../lib/pidfile" + +REDIS = Redis.new +ELECTRUM = Electrum.new(**CONFIG[:electrum]) + +PIDFile.new(CONFIG[:pidfile]).lock do |pid| + loop do + key, value = REDIS.brpop("exciting_#{ELECTRUM.currency}_addrs", 600) + + # Rewrite the pidfile so monit can see we're still alive + pid.refresh + + # brpop allows blocking on multiple keys, and key tells us which one + # signalled but in this case we know which one, so just make sure it's + # not nil which means the timeout fired + next unless key + + address, customer_id = value.split("/", 2) + + txids = + ELECTRUM + .getaddresshistory(address) + .map { |item| "#{item['tx_hash']}/#{address}" } + + next if txids.empty? + + REDIS.hset( + "pending_#{ELECTRUM.currency}_transactions", + *txids.flat_map { |txid| [txid, customer_id] } + ) + end +end diff --git a/config.ru b/config.ru index 69f271ee9407081f81d700e175c1fb0c33096afe..a34704c747c2fdf5995068c7678ad19b84689d5e 100644 --- a/config.ru +++ b/config.ru @@ -271,18 +271,9 @@ class JmpPay < Roda route do |r| r.on "electrum_notify" do - tx_hashes = - electrum - .getaddresshistory(params["address"]) - .map { |item| item["tx_hash"] } - - txids = tx_hashes.map { |tx_hash| - "#{tx_hash}/#{params['address']}" - } - - REDIS.hset( - "pending_#{electrum.currency}_transactions", - *txids.flat_map { |txid| [txid, params["customer_id"]] } + REDIS.lpush( + "exciting_#{electrum.currency}_addrs", + "#{params['address']}/#{params["customer_id"]}" ) "OK" diff --git a/lib/pidfile.rb b/lib/pidfile.rb new file mode 100644 index 0000000000000000000000000000000000000000..b4d09f1d91cec640428799ab5905a13e94616762 --- /dev/null +++ b/lib/pidfile.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +class PIDFile + class NonExclusive < StandardError + def initalize + super("PID file exists and is locked by another process") + end + end + + class OpenPIDFile + def initialize(handle, pid) + @handle = handle + @pid = pid + end + + def refresh + # This rewrites the file to move the modified time up + # I _very likely_ don't have to rewrite the contents, because + # hopefully no one else has dumped crap into my file since that + # last time, and I could just write nothing... + # + # But since I need to do this at least once, it's not worse to do + # it later too, it's so few cycles really, and it also heals the + # file if something weird happened... + @handle.rewind + @handle.write("#{@pid}\n") + @handle.flush + @handle.truncate(@handle.pos) + end + end + + def initialize(filename, pid=Process.pid) + @pid = pid + @filename = filename + end + + def lock + File.open(@filename, File::RDWR | File::CREAT, 0o644) do |f| + raise NonExclusive unless f.flock(File::LOCK_EX | File::LOCK_NB) + + begin + open_pid = OpenPIDFile.new(f, @pid) + + # Start us off by writing to the file at least once + open_pid.refresh + + # Then run the block, which can optionally refresh if they like + yield open_pid + ensure + # Cleanup the pidfile on shutdown + # But only if we obtained the lock + File.delete(@filename) + end + end + end +end