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# notify_using = {
8# jid = "",
9# password = "",
10# target = \(tel: Text) -> "${tel}@cheogram.com"
11# },
12# electrum = env:ELECTRUM_CONFIG,
13# plans = ./plans.dhall
14# }'
15
16require "bigdecimal"
17require "dhall"
18require "money/bank/open_exchange_rates_bank"
19require "net/http"
20require "nokogiri"
21require "pg"
22require "redis"
23
24require_relative "../lib/blather_notify"
25require_relative "../lib/electrum"
26
27CONFIG =
28 Dhall::Coder
29 .new(safe: Dhall::Coder::JSON_LIKE + [Symbol, Proc])
30 .load(ARGV[0], transform_keys: :to_sym)
31
32REDIS = Redis.new
33ELECTRUM = Electrum.new(**CONFIG[:electrum])
34
35DB = PG.connect(dbname: "jmp")
36DB.type_map_for_results = PG::BasicTypeMapForResults.new(DB)
37DB.type_map_for_queries = PG::BasicTypeMapForQueries.new(DB)
38
39BlatherNotify.start(
40 CONFIG[:notify_using][:jid],
41 CONFIG[:notify_using][:password]
42)
43
44unless (cad_to_usd = REDIS.get("cad_to_usd")&.to_f)
45 oxr = Money::Bank::OpenExchangeRatesBank.new(Money::RatesStore::Memory.new)
46 oxr.app_id = CONFIG.fetch(:oxr_app_id)
47 oxr.update_rates
48 cad_to_usd = oxr.get_rate("CAD", "USD")
49 REDIS.set("cad_to_usd", cad_to_usd, ex: 60*60)
50end
51
52canadianbitcoins = Nokogiri::HTML.parse(
53 Net::HTTP.get(URI("https://www.canadianbitcoins.com"))
54)
55
56bitcoin_row = canadianbitcoins.at("#ticker > table > tbody > tr")
57raise "Bitcoin row has moved" unless bitcoin_row.at("td").text == "Bitcoin"
58
59btc_sell_price = {}
60btc_sell_price[:CAD] = BigDecimal.new(
61 bitcoin_row.at("td:nth-of-type(3)").text.match(/^\$(\d+\.\d+)/)[1]
62)
63btc_sell_price[:USD] = btc_sell_price[:CAD] * cad_to_usd
64
65class Plan
66 def self.for_customer(customer_id)
67 row = DB.exec_params(<<-SQL, [customer_id]).first
68 SELECT plan_name FROM customer_plans WHERE customer_id=$1 LIMIT 1
69 SQL
70 return unless row
71 plan = CONFIG[:plans].find { |p| p["plan_name"] = row["plan_name"] }
72 new(plan) if plan
73 end
74
75 def initialize(plan)
76 @plan = plan
77 end
78
79 def currency
80 @plan[:currency]
81 end
82
83 def bonus_for(fiat_amount, cad_to_usd)
84 bonus = (0.050167 * fiat_amount) - (currency == :CAD ? 1 : cad_to_usd)
85 return bonus.round(4, :floor) if bonus > 0
86 end
87end
88
89class Customer
90 def initialize(customer_id)
91 @customer_id = customer_id
92 end
93
94 def notify(body)
95 jid = REDIS.get("jmp_customer_jid-#{@customer_id}")
96 tel = REDIS.lindex("catapult_cred-#{jid}", 3)
97 BlatherNotify.say(
98 CONFIG[:notify_using][:target].call(tel.to_s),
99 body
100 )
101 end
102
103 def plan
104 Plan.for_customer(@customer_id)
105 end
106
107 def add_btc_credit(txid, fiat_amount, cad_to_usd)
108 add_transaction(txid, fiat_amount, "Bitcoin payment")
109 if (bonus = plan.bonus_for(fiat_amount, cad_to_usd))
110 add_transaction("bonus_for_#{txid}", bonus, "Bitcoin payment bonus")
111 end
112 notify_btc_credit(txid, fiat_amount, bonus)
113 end
114
115 def notify_btc_credit(txid, fiat_amount, bonus)
116 tx_hash, = txid.split("/", 2)
117 notify([
118 "Your Bitcoin transaction has been added as ",
119 "$#{'%.4f' % fiat_amount} ",
120 ("+ $#{'%.4f' % bonus} bonus " if bonus),
121 "to your account.\n(txhash: #{tx_hash})"
122 ].compact.join)
123 end
124
125protected
126
127 def add_transaction(id, amount, note)
128 DB.exec_params(<<-SQL, [@customer_id, id, amount, note])
129 INSERT INTO transactions
130 (customer_id, transaction_id, amount, note)
131 VALUES
132 ($1, $2, $3, $4)
133 ON CONFLICT (transaction_id) DO NOTHING
134 SQL
135 end
136end
137
138REDIS.hgetall("pending_btc_transactions").each do |(txid, customer_id)|
139 tx_hash, address = txid.split("/", 2)
140 transaction = ELECTRUM.gettransaction(tx_hash)
141 next unless transaction.confirmations >= CONFIG[:required_confirmations]
142 btc = transaction.amount_for(address)
143 if btc <= 0
144 warn "Transaction shows as #{btc}, skipping #{txid}"
145 next
146 end
147 DB.transaction do
148 customer = Customer.new(customer_id)
149 plan = customer.plan
150 if plan
151 amount = btc * btc_sell_price.fetch(plan.currency).round(4, :floor)
152 customer.add_btc_credit(txid, amount, cad_to_usd)
153 else
154 warn "No plan for #{customer_id} cannot save #{txid}"
155 end
156 end
157 REDIS.hdel("pending_btc_transactions", txid)
158end