bull_bitcoin.rb

 1# frozen_string_literal: true
 2
 3require "em-http"
 4require "em_promise"
 5require "em-synchrony/em-http" # For aget vs get
 6require "json"
 7
 8require_relative "em"
 9
10class BullBitcoin
11	Error = Class.new(StandardError)
12
13	def fetch_btc_cad
14		EM::HttpRequest.new(
15			"https://www.bullbitcoin.com/api/price",
16			tls: { verify_peer: true }
17		).apost(
18			head: {
19				"Accept" => "application/json",
20				"Content-Type" => "application/json"
21			},
22			body: {
23				id: request_id,
24				jsonrpc: "2.0",
25				method: "getUserRate",
26				params: {
27					element: {
28						fromCurrency: "BTC",
29						toCurrency: "CAD"
30					}
31				}
32			}.to_json
33		).then(&method(:parse_response))
34	end
35
36private
37
38	def request_id
39		"#{(Time.now.to_f * 1000).to_i}-#{rand(100...999)}"
40	end
41
42	def parse_response(http)
43		status = http.response_header.status
44		if status != 200
45			raise Error, "Bull Bitcoin price request failed with HTTP #{status}"
46		end
47
48		el = extract_element(http.response)
49		BigDecimal(el.fetch("userPrice").to_s) / (10**el.fetch("precision"))
50	rescue JSON::ParserError, KeyError => e
51		raise Error, "Bull Bitcoin price response invalid: #{e.message}"
52	end
53
54	def extract_element(response)
55		json = JSON.parse(response)
56		if json["error"]
57			raise Error,
58			      "Bull Bitcoin price response error: #{format_error(json['error'])}"
59		end
60
61		el = json.dig("result", "element")
62		return el if el
63
64		raise Error, "Bull Bitcoin price response missing result.element"
65	end
66
67	def format_error(error)
68		if error["message"] == "Validation error" && error["data"].is_a?(Hash)
69			nested = error.dig("data", "apiError", "message")
70			return "#{error['message']}: #{nested}" if nested
71		end
72
73		error["message"]
74	end
75end