#!/usr/bin/ruby # frozen_string_literal: true # Usage: bin/process_pending-btc_transactions '{ # oxr_app_id = "", # required_confirmations = 3, # notify_using = { # jid = "", # password = "", # target = \(tel: Text) -> "${tel}@cheogram.com" # }, # electrum = env:ELECTRUM_CONFIG, # plans = ./plans.dhall # }' require "bigdecimal" require "dhall" require "money/bank/open_exchange_rates_bank" require "net/http" require "nokogiri" require "pg" require "redis" require_relative "../lib/blather_notify" require_relative "../lib/electrum" CONFIG = Dhall::Coder .new(safe: Dhall::Coder::JSON_LIKE + [Symbol, Proc]) .load(ARGV[0], transform_keys: :to_sym) REDIS = Redis.new ELECTRUM = Electrum.new(**CONFIG[:electrum]) DB = PG.connect(dbname: "jmp") DB.type_map_for_results = PG::BasicTypeMapForResults.new(DB) DB.type_map_for_queries = PG::BasicTypeMapForQueries.new(DB) BlatherNotify.start( CONFIG[:notify_using][:jid], CONFIG[:notify_using][:password] ) unless (cad_to_usd = REDIS.get("cad_to_usd")&.to_f) oxr = Money::Bank::OpenExchangeRatesBank.new(Money::RatesStore::Memory.new) oxr.app_id = CONFIG.fetch(:oxr_app_id) oxr.update_rates cad_to_usd = oxr.get_rate("CAD", "USD") REDIS.set("cad_to_usd", cad_to_usd, ex: 60*60) end canadianbitcoins = Nokogiri::HTML.parse( Net::HTTP.get(URI("https://www.canadianbitcoins.com")) ) bitcoin_row = canadianbitcoins.at("#ticker > table > tbody > tr") raise "Bitcoin row has moved" unless bitcoin_row.at("td").text == "Bitcoin" btc_sell_price = {} btc_sell_price[:CAD] = BigDecimal.new( bitcoin_row.at("td:nth-of-type(3)").text.match(/^\$(\d+\.\d+)/)[1] ) btc_sell_price[:USD] = btc_sell_price[:CAD] * cad_to_usd class Plan def self.for_customer(customer_id) row = DB.exec_params(<<-SQL, [customer_id]).first SELECT plan_name FROM customer_plans WHERE customer_id=$1 LIMIT 1 SQL return unless row plan = CONFIG[:plans].find { |p| p["plan_name"] = row["plan_name"] } new(plan) if plan end def initialize(plan) @plan = plan end def currency @plan[:currency] end end class Customer def initialize(customer_id) @customer_id = customer_id end def notify(body) jid = REDIS.get("jmp_customer_jid-#{@customer_id}") tel = REDIS.lindex("catapult_cred-#{jid}", 3) BlatherNotify.say( CONFIG[:notify_using][:target].call(tel.to_s), body ) end def plan Plan.for_customer(@customer_id) end def add_btc_credit(txid, fiat_amount) DB.exec_params(<<-SQL, [@customer_id, txid, fiat_amount]) INSERT INTO transactions (customer_id, transaction_id, amount, note) VALUES ($1, $2, $3, 'Bitcoin payment') ON CONFLICT (transaction_id) DO NOTHING SQL notify_btc_credit(txid, fiat_amount) end def notify_btc_credit(txid, fiat_amount) tx_hash, = txid.split("/", 2) notify( "Your Bitcoin transaction has been added as $#{'%.4f' % fiat_amount} " \ "to your account.\n(txhash: #{tx_hash})" ) end end REDIS.hgetall("pending_btc_transactions").each do |(txid, customer_id)| tx_hash, address = txid.split("/", 2) transaction = ELECTRUM.gettransaction(tx_hash) next unless transaction.confirmations >= CONFIG[:required_confirmations] btc = transaction.amount_for(address) if btc <= 0 warn "Transaction shows as #{btc}, skipping #{txid}" next end DB.transaction do customer = Customer.new(customer_id) plan = customer.plan if plan amount = btc * btc_sell_price.fetch(plan.currency).round(4, :floor) customer.add_btc_credit(txid, amount) else warn "No plan for #{customer_id} cannot save #{txid}" end end REDIS.hdel("pending_btc_transactions", txid) end