~singpolyma/jmp-pay

7de7b2dfbeb5bd6c22ad02a404012506834ca924 — Stephen Paul Weber 8 months ago 6559375
Allow electrum of alternate currency
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}" \
		"&currency=#{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