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