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