diff --git a/bin/check_electrum_wallet_completeness b/bin/check_electrum_wallet_completeness index 4500ad70bc17f7ea01592d8eb766ecab8c441fad..880fc562d8f90c07ea7a5943b14f6bf8cc38320d 100755 --- a/bin/check_electrum_wallet_completeness +++ b/bin/check_electrum_wallet_completeness @@ -16,7 +16,8 @@ electrum = Electrum.new(**config) electrum_addrs = electrum.listaddresses -get_addresses_with_users(redis).each do |addr, keys| +addrs = RedisAddresses.new(redis, config[:currency]) +addrs.get_addresses_with_users.each do |addr, keys| unless electrum_addrs.include?(addr) puts "The address #{addr} (included in #{keys.join(', ')}) "\ "isn't included in electrum's list" diff --git a/bin/detect_duplicate_addrs b/bin/detect_duplicate_addrs index bcfdb6a8730ee207ee76d67f7bb5d4e11e066f4f..cf1737da1819243d599c7dba794861bff70d0331 100755 --- a/bin/detect_duplicate_addrs +++ b/bin/detect_duplicate_addrs @@ -6,7 +6,8 @@ require_relative "../lib/redis_addresses" redis = Redis.new -get_addresses_with_users(redis).each do |addr, keys| +addrs = RedisAddresses.new(redis, "btc") +addrs.get_addresses_with_users(redis).each do |addr, keys| if keys.length > 1 puts "#{addr} is used by the following " \ "#{keys.length} keys: #{keys.join(' ')}" diff --git a/bin/get_available_addresses b/bin/get_available_addresses index 38b4aee02f0ac9534fa43a9c613297294f8b2b2c..3a478513a730f227299286b1dc41d3b48af25583 100755 --- a/bin/get_available_addresses +++ b/bin/get_available_addresses @@ -17,7 +17,7 @@ electrum = Electrum.new(**config) addresses = Set.new(electrum.listaddresses) -RedisBtcAddresses.each_user(redis) do |_, addrs| +RedisAddresses.new(redis, config[:currency]).each_user do |_, addrs| addresses.subtract(addrs) end diff --git a/bin/process_pending_btc_transactions b/bin/process_pending_btc_transactions index 460329bf6efae864ad313c83b7d1fd4cebd1d0fb..f8483e7d7924a79a92130a1f89ccaf179b97cd99 100755 --- a/bin/process_pending_btc_transactions +++ b/bin/process_pending_btc_transactions @@ -56,14 +56,22 @@ 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" +case CONFIG[:electrum][:currency] + when "btc" + exchange_row = canadianbitcoins.at("#ticker > table > tbody > tr") + raise "Bitcoin row has moved" unless exchange_row.at("td").text == "Bitcoin" + when "bch" + exchange_row = canadianbitcoins.at("#ticker > table > tbody > tr:nth-child(2)") + raise "Bitcoin Cash row has moved" unless exchange_row.at("td").text == "Bitcoin Cash" + else + raise "Unknown currency #{CONFIG[:electrum][:currency]}" +end -btc_sell_price = {} -btc_sell_price[:CAD] = BigDecimal( - bitcoin_row.at("td:nth-of-type(3)").text.match(/^\$(\d+\.\d+)/)[1] +sell_price = {} +sell_price[:CAD] = BigDecimal( + exchange_row.at("td:nth-of-type(4)").text.match(/^\$(\d+\.\d+)/)[1] ) -btc_sell_price[:USD] = btc_sell_price[:CAD] * cad_to_usd +sell_price[:USD] = sell_price[:CAD] * cad_to_usd class Plan def self.for_customer(customer) @@ -180,7 +188,12 @@ class Customer end def add_btc_credit(txid, btc_amount, fiat_amount) - tx = Transaction.new(@customer_id, txid, fiat_amount, "Bitcoin payment") + tx = Transaction.new( + @customer_id, + txid, + fiat_amount, + "Cryptocurrency payment" + ) return unless tx.save tx.bonus&.save @@ -190,15 +203,16 @@ class Customer def notify_btc_credit(txid, btc_amount, fiat_amount, bonus) tx_hash, = txid.split("/", 2) notify([ - "Your Bitcoin transaction of #{btc_amount.to_s('F')} BTC ", - "has been added as $#{'%.4f' % fiat_amount} (#{plan.currency}) ", + "Your transaction of #{btc_amount.to_s('F')} ", + CONFIG[:electrum][:currency], + " has been added as $#{'%.4f' % fiat_amount} (#{plan.currency}) ", ("+ $#{'%.4f' % bonus} bonus " if bonus.positive?), "to your account.\n(txhash: #{tx_hash})" ].compact.join) end end -done = REDIS.hgetall("pending_btc_transactions").map { |(txid, customer_id)| +done = REDIS.hgetall("pending_#{CONFIG[:electrum][:currency]}_transactions").map { |(txid, customer_id)| tx_hash, address = txid.split("/", 2) transaction = begin @@ -213,16 +227,16 @@ done = REDIS.hgetall("pending_btc_transactions").map { |(txid, customer_id)| btc = transaction.amount_for(address) if btc <= 0 # This is a send, not a receive, do not record it - REDIS.hdel("pending_btc_transactions", txid) + REDIS.hdel("pending_#{CONFIG[:electrum][:currency]}_transactions", txid) next end DB.transaction do customer = Customer.new(customer_id) if (plan = customer.plan) - amount = btc * btc_sell_price.fetch(plan.currency).round(4, :floor) + amount = btc * sell_price.fetch(plan.currency).round(4, :floor) customer.add_btc_credit(txid, btc, amount) plan.notify_any_pending_plan! - REDIS.hdel("pending_btc_transactions", txid) + REDIS.hdel("pending_#{CONFIG[:electrum][:currency]}_transactions", txid) txid else warn "No plan for #{customer_id} cannot save #{txid}" diff --git a/bin/reassert_electrum_notification b/bin/reassert_electrum_notification index 4b3d4f82ba3733320ab45c45a842e20f90bbcc1d..1dc68923b7ad4b5dfa6234605e5bcd6317d8f666 100755 --- a/bin/reassert_electrum_notification +++ b/bin/reassert_electrum_notification @@ -13,8 +13,9 @@ config = redis = Redis.new electrum = Electrum.new(**config) +addrs = RedisAddresses.new(redis, config[:currency]) -get_addresses_with_users(redis).each do |addr, keys| +addrs.get_addresses_with_users(redis).each do |addr, keys| match = keys.first.match(/.*-(\d+)$/) unless match puts "Can't understand key #{keys.first}, skipping" @@ -22,8 +23,9 @@ get_addresses_with_users(redis).each do |addr, keys| end customer_id = match[1] - url = "https://pay.jmp.chat/electrum_notify?"\ - "address=#{addr}&customer_id=#{customer_id}" + url = "https://pay.jmp.chat/electrum_notify?" \ + "address=#{addr}&customer_id=#{customer_id}" \ + "¤cy=#{config[:currency]}" unless electrum.notify(addr, url) puts "Failed to setup #{addr} to notify #{url}. Skipping" diff --git a/config.ru b/config.ru index 103d0c9826cec0aa307f84b0147aed29a69d2f67..2067d50e01bae0d15481089f34a033bc362b3458 100644 --- a/config.ru +++ b/config.ru @@ -34,6 +34,9 @@ BRAINTREE_CONFIG = Dhall.load("env:BRAINTREE_CONFIG").sync ELECTRUM = Electrum.new( **Dhall::Coder.load("env:ELECTRUM_CONFIG", transform_keys: :to_sym) ) +ELECTRUM_BCH = Electrum.new( + **Dhall::Coder.load("env:ELECTRON_CASH_CONFIG", transform_keys: :to_sym) +) DB = PG.connect(dbname: "jmp") DB.type_map_for_results = PG::BasicTypeMapForResults.new(DB) @@ -161,8 +164,9 @@ class CreditCardGateway end class UnknownTransactions - def self.from(customer_id, address, tx_hashes) + def self.from(currency, customer_id, address, tx_hashes) self.for( + currency, customer_id, fetch_rows_for(address, tx_hashes).map { |row| row["transaction_id"] @@ -184,18 +188,21 @@ class UnknownTransactions SQL end - def self.for(customer_id, transaction_ids) - transaction_ids.empty? ? None.new : new(customer_id, transaction_ids) + def self.for(currency, customer_id, transaction_ids) + return None.new if transaction_ids.empty? + + new(currency, customer_id, transaction_ids) end - def initialize(customer_id, transaction_ids) + def initialize(currency, customer_id, transaction_ids) + @currency = currency @customer_id = customer_id @transaction_ids = transaction_ids end def enqueue! REDIS.hset( - "pending_btc_transactions", + "pending_#{@currency}_transactions", *@transaction_ids.flat_map { |txid| [txid, @customer_id] } ) end @@ -269,8 +276,17 @@ class JmpPay < Roda extend Forwardable def_delegators :request, :params + def electrum + case params["currency"] + when "bch" + ELECTRUM_BCH + else + ELECTRUM + end + end + def redis_key_btc_addresses - "jmp_customer_btc_addresses-#{params['customer_id']}" + "jmp_customer_#{electrum.currency}_addresses-#{params['customer_id']}" end def verify_address_customer_id(r) @@ -289,9 +305,10 @@ class JmpPay < Roda verify_address_customer_id(r) UnknownTransactions.from( + electrum.currency, params["customer_id"], params["address"], - ELECTRUM + electrum .getaddresshistory(params["address"]) .map { |item| item["tx_hash"] } ).enqueue! diff --git a/lib/electrum.rb b/lib/electrum.rb index f0de91d9debff98abbbf40a996e6750ef2507868..918adc2db33fbae5e440c339634bb159a1ffda08 100644 --- a/lib/electrum.rb +++ b/lib/electrum.rb @@ -8,10 +8,13 @@ require "securerandom" class Electrum class NoTransaction < StandardError; end - def initialize(rpc_uri:, rpc_username:, rpc_password:) + attr_reader :currency + + def initialize(rpc_uri:, rpc_username:, rpc_password:, currency:) @rpc_uri = URI(rpc_uri) @rpc_username = rpc_username @rpc_password = rpc_password + @currency = currency end def getaddresshistory(address) @@ -39,7 +42,7 @@ class Electrum class Transaction def initialize(electrum, tx_hash, tx) - raise NoTransaction, "No tx found for #{tx_hash}" unless tx + raise NoTransaction, "No tx for #{@currency} #{tx_hash}" unless tx @electrum = electrum @tx_hash = tx_hash diff --git a/lib/redis_addresses.rb b/lib/redis_addresses.rb index 17ba12ee0da7bfca8c382661497e171e72627826..0ecc676ee1e3881742f16b348d55fde88a2e11e5 100644 --- a/lib/redis_addresses.rb +++ b/lib/redis_addresses.rb @@ -2,39 +2,44 @@ require "redis" -# This returns a hash -# The keys are the bitcoin addresses, the values are all of the keys which -# contain that address -# If there are no duplicates, then each value will be a singleton list -def get_addresses_with_users(redis) - addrs = Hash.new { |h, k| h[k] = [] } - - # I picked 1000 because it made a relatively trivial case take 15 seconds - # instead of forever. - # Basically it's "how long does each command take" - # The lower it is (default is 10), it will go back and forth to the client a - # ton - redis.scan_each(match: "jmp_customer_btc_addresses-*", count: 1000) do |key| - redis.smembers(key).each do |addr| - addrs[addr] << key - end +class RedisAddresses + def initialize(redis, currency) + @redis = redis + @currency = currency.downcase end - addrs -end - -module RedisBtcAddresses - def self.each_user(redis) + def each_user(redis) # I picked 1000 because it made a relatively trivial case take # 15 seconds instead of forever. # Basically it's "how long does each command take" # The lower it is (default is 10), it will go back and forth # to the client a ton - redis.scan_each( - match: "jmp_customer_btc_addresses-*", + @redis.scan_each( + match: "jmp_customer_#{@currency}_addresses-*", count: 1000 ) do |key| yield key, redis.smembers(key) end end + + # This returns a hash + # The keys are the bitcoin addresses, the values are all of the keys which + # contain that address + # If there are no duplicates, then each value will be a singleton list + def get_addresses_with_users + addrs = Hash.new { |h, k| h[k] = [] } + + # 1000 because it made a relatively trivial case take 15 seconds + # instead of forever. + # Basically it's "how long does each command take" + # The lower it is (default is 10), it will go back and forth + # to the client a ton + @redis.scan_each(match: "jmp_customer_#{@currency}_addresses-*", count: 1000) do |key| + @redis.smembers(key).each do |addr| + addrs[addr] << key + end + end + + addrs + end end