M bin/check_electrum_wallet_completeness => bin/check_electrum_wallet_completeness +2 -1
@@ 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"
M bin/detect_duplicate_addrs => bin/detect_duplicate_addrs +2 -1
@@ 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(' ')}"
M bin/get_available_addresses => bin/get_available_addresses +1 -1
@@ 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
M bin/process_pending_btc_transactions => bin/process_pending_btc_transactions +27 -13
@@ 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}"
M bin/reassert_electrum_notification => bin/reassert_electrum_notification +5 -3
@@ 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"
M config.ru => config.ru +24 -7
@@ 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!
M lib/electrum.rb => lib/electrum.rb +5 -2
@@ 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
M lib/redis_addresses.rb => lib/redis_addresses.rb +28 -23
@@ 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