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