process_pending_btc_transactions

 1#!/usr/bin/ruby
 2# frozen_string_literal: true
 3
 4# Usage: bin/process_pending-btc_transactions '{
 5#        oxr_app_id = "",
 6#        required_confirmations = 3,
 7#        electrum = env:ELECTRUM_CONFIG,
 8#        plans = ./plans.dhall
 9#        }'
10
11require "bigdecimal"
12require "dhall"
13require "money/bank/open_exchange_rates_bank"
14require "net/http"
15require "nokogiri"
16require "pg"
17require "redis"
18
19require_relative "../lib/electrum"
20
21CONFIG =
22	Dhall::Coder
23	.new(safe: Dhall::Coder::JSON_LIKE + [Symbol])
24	.load(ARGV[0], transform_keys: :to_sym)
25
26REDIS = Redis.new
27ELECTRUM = Electrum.new(**CONFIG[:electrum])
28
29DB = PG.connect(dbname: "jmp")
30DB.type_map_for_results = PG::BasicTypeMapForResults.new(DB)
31DB.type_map_for_queries = PG::BasicTypeMapForQueries.new(DB)
32
33unless (cad_to_usd = REDIS.get("cad_to_usd")&.to_f)
34	oxr = Money::Bank::OpenExchangeRatesBank.new(Money::RatesStore::Memory.new)
35	oxr.app_id = CONFIG.fetch(:oxr_app_id)
36	oxr.update_rates
37	cad_to_usd = oxr.get_rate("CAD", "USD")
38	REDIS.set("cad_to_usd", cad_to_usd, ex: 60*60)
39end
40
41canadianbitcoins = Nokogiri::HTML.parse(
42	Net::HTTP.get(URI("https://www.canadianbitcoins.com"))
43)
44
45bitcoin_row = canadianbitcoins.at("#ticker > table > tbody > tr")
46raise "Bitcoin row has moved" unless bitcoin_row.at("td").text == "Bitcoin"
47
48btc_sell_price = {}
49btc_sell_price[:CAD] = BigDecimal.new(
50	bitcoin_row.at("td:nth-of-type(3)").text.match(/^\$(\d+\.\d+)/)[1]
51)
52btc_sell_price[:USD] = btc_sell_price[:CAD] * cad_to_usd
53
54class Plan
55	def self.for_customer(customer_id)
56		row = DB.exec_params(<<-SQL, [customer_id]).first
57			SELECT plan_name FROM customer_plans WHERE customer_id=$1 LIMIT 1
58		SQL
59		return unless row
60		plan = CONFIG[:plans].find { |p| p["plan_name"] = row["plan_name"] }
61		new(plan) if plan
62	end
63
64	def initialize(plan)
65		@plan = plan
66	end
67
68	def currency
69		@plan[:currency]
70	end
71end
72
73REDIS.hgetall("pending_btc_transactions").each do |(txid, customer_id)|
74	tx_hash, address = txid.split("/", 2)
75	transaction = ELECTRUM.gettransaction(tx_hash)
76	next unless transaction.confirmations >= CONFIG[:required_confirmations]
77	btc = transaction.amount_for(address)
78	if btc <= 0
79		warn "Transaction shows as #{btc}, skipping #{txid}"
80		next
81	end
82	DB.transaction do
83		plan = Plan.for_customer(customer_id)
84		if plan
85			amount = btc * btc_sell_price.fetch(plan.currency).round(4, :floor)
86			DB.exec_params(<<-SQL, [customer_id, txid, amount])
87				INSERT INTO transactions
88					(customer_id, transaction_id, amount, note)
89				VALUES
90						($1, $2, $3, 'Bitcoin payment')
91				ON CONFLICT (transaction_id) DO NOTHING
92			SQL
93		else
94			warn "No plan for #{customer_id} cannot save #{txid}"
95		end
96	end
97	REDIS.hdel("pending_btc_transactions", txid)
98end