Add helper to fetch current BTC sell prices

Stephen Paul Weber created

Scrapes the sell price for Bitcoin from canadianbitcoins.com
USD price is done by converting this CAD sell price to USD via openexchangerates

Change summary

Gemfile                      |  2 +
lib/btc_sell_prices.rb       | 57 ++++++++++++++++++++++++++++++++++++++
test/test_btc_sell_prices.rb | 31 ++++++++++++++++++++
3 files changed, 90 insertions(+)

Detailed changes

Gemfile 🔗

@@ -6,9 +6,11 @@ gem "blather", git: "https://github.com/singpolyma/blather.git", branch: "ergono
 gem "braintree"
 gem "dhall"
 gem "em-hiredis"
+gem "em-http-request"
 gem "em-pg-client", git: "https://github.com/royaltm/ruby-em-pg-client"
 gem "em_promise.rb"
 gem "eventmachine"
+gem "money-open-exchange-rates"
 
 group(:development) do
 	gem "pry-reload"

lib/btc_sell_prices.rb 🔗

@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+require "em-http"
+require "money/bank/open_exchange_rates_bank"
+require "nokogiri"
+
+require_relative "em"
+
+class BTCSellPrices
+	def initialize(redis, oxr_app_id)
+		@redis = redis
+		@oxr = Money::Bank::OpenExchangeRatesBank.new(
+			Money::RatesStore::Memory.new
+		)
+		@oxr.app_id = oxr_app_id
+	end
+
+	def cad
+		fetch_canadianbitcoins.then do |http|
+			canadianbitcoins = Nokogiri::HTML.parse(http.response)
+
+			bitcoin_row = canadianbitcoins.at("#ticker > table > tbody > tr")
+			raise "Bitcoin row has moved" unless bitcoin_row.at("td").text == "Bitcoin"
+
+			BigDecimal.new(
+				bitcoin_row.at("td:nth-of-type(3)").text.match(/^\$(\d+\.\d+)/)[1]
+			)
+		end
+	end
+
+	def usd
+		EMPromise.all([cad, cad_to_usd]).then { |(a, b)| a * b }
+	end
+
+protected
+
+	def fetch_canadianbitcoins
+		EM::HttpRequest.new(
+			"https://www.canadianbitcoins.com",
+			tls: { verify_peer: true }
+		).get
+	end
+
+	def cad_to_usd
+		@redis.get("cad_to_usd").then do |rate|
+			next rate.to_f if rate
+
+			EM.promise_defer {
+				# OXR gem is not async, so defer to threadpool
+				oxr.update_rates
+				oxr.get_rate("CAD", "USD")
+			}.then do |orate|
+				@redis.set("cad_to_usd", orate, ex: 60 * 60).then { orate }
+			end
+		end
+	end
+end

test/test_btc_sell_prices.rb 🔗

@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+require "em-hiredis"
+require "test_helper"
+require "btc_sell_prices"
+
+class BTCSellPricesTest < Minitest::Test
+	def setup
+		@redis = Minitest::Mock.new
+		@subject = BTCSellPrices.new(@redis, "")
+	end
+
+	def test_cad
+		stub_request(:get, "https://www.canadianbitcoins.com").to_return(
+			body: "<div id='ticker'><table><tbody><tr>" \
+			      "<td>Bitcoin</td><td></td><td>$123.00</td>"
+		)
+		assert_equal BigDecimal.new(123), @subject.cad.sync
+	end
+	em :test_cad
+
+	def test_usd
+		stub_request(:get, "https://www.canadianbitcoins.com").to_return(
+			body: "<div id='ticker'><table><tbody><tr>" \
+			      "<td>Bitcoin<td></td><td>$123.00</td>"
+		)
+		@redis.expect(:get, EMPromise.resolve("0.5"), ["cad_to_usd"])
+		assert_equal BigDecimal.new(123) / 2, @subject.usd.sync
+	end
+	em :test_usd
+end