From 4b84df0af93a34a1dbeb6bad2a3fcd999c222e67 Mon Sep 17 00:00:00 2001 From: Benjamin ter Kuile Date: Sat, 18 Oct 2025 09:13:12 -0500 Subject: [PATCH] Design updates with error handling --- README.md | 27 +++++++++++-- lib/strike_me/api/account.rb | 24 ++++++------ lib/strike_me/api/account/balances.rb | 4 ++ lib/strike_me/api/base_client.rb | 16 ++++++-- lib/strike_me/api/client.rb | 9 +++-- lib/strike_me/btc.rb | 3 +- lib/strike_me/btc_attribute_type.rb | 4 +- spec/strike_me/api/account_spec.rb | 56 +++++++++++++++++++++++++-- spec/strike_me/btc_spec.rb | 19 ++++----- 9 files changed, 124 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index 209c45b..66b6a8c 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,11 @@ -# StrikeMe::API +StrikeMe::API +============== This is a ruby gem to interact with the strike.me API. It is an inition proof of concept implementation. +As a reference, the documentation of the [Strike API](https://docs.strike.me/api) -## Installation +Installation +------------- TODO: Replace `strike_me-api` with your gem name right after releasing it to RubyGems.org. Please do not do it earlier due to security reasons. Alternatively, replace this section with instructions to install your gem from git if you don't plan to release to RubyGems.org. @@ -18,9 +21,25 @@ If bundler is not being used to manage dependencies, install the gem by executin gem install strike_me-api ``` -## Usage +Playground +----------------- +To play around with this api you can start a REPL using one of the following commands from the project's root: +```bash +pry -r strike_me/api +# or +irb -I lib -r strike_me/api +``` +Then type code like: +```ruby +account = StrikeMe::API::Account.new +account.profile.attributes +account.balances.map(&:attributes) +``` -TODO: Write usage instructions here +Usage +-------------- + +TODO: Write usage instructions here, for now, look at Playground ## Development diff --git a/lib/strike_me/api/account.rb b/lib/strike_me/api/account.rb index a794c16..d2a55b7 100644 --- a/lib/strike_me/api/account.rb +++ b/lib/strike_me/api/account.rb @@ -2,6 +2,8 @@ module StrikeMe module API + + # Factory helper object connecting the client to the models class Account include BaseClient @@ -12,14 +14,15 @@ module StrikeMe def profile return @profile if @profile.present? - response = client.get profile_url + url = profile_url + response = client.get url + #body = response.body.presence + #body = JSON.parse(body) if body.is_a? String if response.success? - body = response.body - body = JSON.parse(body) if body.is_a? String - @profile = Profile.new body.merge(account: self) + @profile = Profile.new response.body.merge(account: self) else # if you see this, something went wrong, this is test code - @profile = ResponseError.new(profile_url, response) + raise ResponseError.new(url, response.body) end end @@ -36,12 +39,11 @@ module StrikeMe return @balances if @balances.present? response = client.get 'balances' - @balances = if response.success? - Balances.new self, response.body - else - # if you see this, something went wrong, this is test code - ResponseError.new('balances', response) - end + if response.success? + @balances = Balances.new self, response.body + else + raise ResponseError.new('balances', response.body) + end end def inspect diff --git a/lib/strike_me/api/account/balances.rb b/lib/strike_me/api/account/balances.rb index 0f0e874..d494a4e 100644 --- a/lib/strike_me/api/account/balances.rb +++ b/lib/strike_me/api/account/balances.rb @@ -21,6 +21,10 @@ module StrikeMe def to_a @items.map(&:attributes) end + + def btc + @items.find { |item| item.currency == 'BTC' } + end end end end diff --git a/lib/strike_me/api/base_client.rb b/lib/strike_me/api/base_client.rb index 7a3dae3..27d3099 100644 --- a/lib/strike_me/api/base_client.rb +++ b/lib/strike_me/api/base_client.rb @@ -19,13 +19,21 @@ module StrikeMe end # cumbersome, but better for later debugging and handling - class ResponseError - attr_reader :url, :faraday_response + class ResponseError < StandardError + attr_reader :url, :response_body, :status, :reason - def initialize(url, faraday_response) + def initialize(url, response_body) + @reason = response_body.try(:[], 'data').try(:[], 'message') + @status = response_body.try(:[], 'data').try(:[], 'status') + + super "[#{status}, {}, [\"#{reason}\"]]" if reason @url = url - @faraday_response = faraday_response + @response_body = response_body end + + # def inspect + # "StrikeMe::API::BaseClient::ResponseError: #{url} status: #{faraday_response.status}" + # end end end end diff --git a/lib/strike_me/api/client.rb b/lib/strike_me/api/client.rb index f91cc5d..9d76567 100644 --- a/lib/strike_me/api/client.rb +++ b/lib/strike_me/api/client.rb @@ -3,9 +3,7 @@ module StrikeMe module API class Client - attr_reader :version, :connection - - delegate :get, to: :connection + attr_reader :version, :connection, :last_response def initialize(**args) @version = args[:version] || 'v1' @@ -23,6 +21,11 @@ module StrikeMe builder.response :json end end + + #delegate :get, to: :connection + def get(...) + @last_response = connection.get(...) + end end end end diff --git a/lib/strike_me/btc.rb b/lib/strike_me/btc.rb index dbab137..7953d50 100644 --- a/lib/strike_me/btc.rb +++ b/lib/strike_me/btc.rb @@ -24,8 +24,7 @@ module StrikeMe class BTC class Error < StandardError; end - MAX_NUMBER_OF_SATS = 21_000_000 * 100_000_000 - NUMERICAL_OPERATORS = %i[* /].freeze + MAX_NUMBER_OF_SATS = 21_000_000 * 100_000_000 attr_reader :sats diff --git a/lib/strike_me/btc_attribute_type.rb b/lib/strike_me/btc_attribute_type.rb index b6c377a..93bbd90 100644 --- a/lib/strike_me/btc_attribute_type.rb +++ b/lib/strike_me/btc_attribute_type.rb @@ -7,7 +7,7 @@ module StrikeMe end def cast(value) - StrikeMe::BTC.new((100_000_000 * value.to_f).to_i) + StrikeMe::BTC.new((100_000_000 * value.to_f).round) end def serialize(value) @@ -16,4 +16,4 @@ module StrikeMe end end -ActiveModel::Type.register(:btc, StrikeMe::BtcAttributeType) +ActiveModel::Type.register(:strike_btc, StrikeMe::BtcAttributeType) diff --git a/spec/strike_me/api/account_spec.rb b/spec/strike_me/api/account_spec.rb index aecff38..8dc6eda 100644 --- a/spec/strike_me/api/account_spec.rb +++ b/spec/strike_me/api/account_spec.rb @@ -10,7 +10,7 @@ RSpec.describe StrikeMe::API::Account do allow(ENV).to receive(:[]) expect(ENV).to receive(:[]).with('STRIKE_ACCOUNT_ID').and_return 'abc' stub_request(:get, %r{https://api.strike.me/v1/accounts/abc/profile}) - .to_return(status: 200, body: '{"id":"abc44"}') + .to_return(status: 200, body: '{"id":"abc44"}', headers: {'Content-Type' => 'application/json'}) expect(subject.id).to eq 'abc44' end @@ -19,16 +19,66 @@ RSpec.describe StrikeMe::API::Account do allow(ENV).to receive(:fetch) expect(ENV).to receive(:fetch).with('STRIKE_ACCOUNT_HANDLE').and_return 'abcd' stub_request(:get, %r{https://api.strike.me/v1/accounts/handles/abcd/profile}) - .to_return(status: 200, body: '{"id":"abc44"}') + .to_return(status: 200, body: '{"id":"abc44"}', headers: {'Content-Type' => 'application/json'}) expect(subject.id).to eq 'abc44' end + + it 'throws exception when unauthorized' do + allow(ENV).to receive(:[]) + expect(ENV).to receive(:[]).with('STRIKE_ACCOUNT_ID').and_return 'abc' + stub_request(:get, %r{https://api.strike.me/v1/accounts/abc/profile}) + .to_return(status: 401, body: '{"data":{"status":401,"code":"UNAUTHORIZED","message":"Invalid or unspecified identity."}}', headers: {'Content-Type' => 'application/json'}) + expect{ subject.id }.to raise_error '[401, {}, ["Invalid or unspecified identity."]]' + end end describe 'balances' do subject { described_class.new.balances } - before { stub_request(:get, 'https://api.strike.me/v1/balances').to_return(status: 200, body: []) } + before { stub_request(:get, 'https://api.strike.me/v1/balances').to_return(status: 200, body: '[{"currency":"BTC","current":"0.00085408","pending":"0.000023","outgoing":"0.00000532","reserved":"0.00234","available":"0.00085408","total":"0.00085408"},{"currency":"USDT","current":"50.04","pending":"40.06","outgoing":"60.15","reserved":"0.00","available":"0.00","total":"0.00"}]', headers: {'Content-Type' => 'application/json'}) } it { is_expected.to be_an_instance_of StrikeMe::API::Account::Balances } + + it '#to_a properly returns the attributes' do + expect(subject.to_a).to eq [ + { + "currency" => "BTC", + "current" => 0.00085408, + "pending" => 2.3e-05, + "outgoing" => 5.32e-06, + "reserved" => 0.00234, + "available" => 0.00085408, + "total" => 0.00085408 + }, { + "currency" => "USDT", + "current" => 50.04, + "pending" => 40.06, + "outgoing" => 60.15, + "reserved" => 0.0, + "available" => 0.0, + "total" => 0.0 + }] + end + + it '#btc returns the BTC balance' do + btc = subject.btc + expect(btc).to be_present + expect(btc).to be_a StrikeMe::API::Account::Balance + expect(btc.attributes).to eq( + "available" => 0.00085408, + "currency" => "BTC", + "current" => 0.00085408, + "outgoing" => 5.32e-06, + "pending" => 2.3e-05, + "reserved" => 0.00234, + "total" => 0.00085408 + ) + end + + it 'raises error when not authorized' do + stub_request(:get, %r{https://api.strike.me/v1/balances}) + .to_return(status: 401, body: '{"data":{"status":403,"code":"FORBIDDEN","message":"Insufficient permissions."}}', headers: {'Content-Type' => 'application/json'}) + expect{ subject }.to raise_error '[403, {}, ["Insufficient permissions."]]' + end end end diff --git a/spec/strike_me/btc_spec.rb b/spec/strike_me/btc_spec.rb index ef5b40e..dcc652c 100644 --- a/spec/strike_me/btc_spec.rb +++ b/spec/strike_me/btc_spec.rb @@ -44,7 +44,7 @@ RSpec.describe StrikeMe::BTC do context 'arithmetic' do it 'supports multiplication with numbers' do - btc1 = described_class.new(234_529) + btc1 = described_class.new 234_529 btc2 = btc1 * 3 btc3 = btc2 * 2.9 @@ -54,14 +54,14 @@ RSpec.describe StrikeMe::BTC do expect(btc2).not_to eq btc1 expect(btc2).not_to eq btc3 - expect(btc1.sats).to eq 234_529 - expect(btc1).to eq 234_529 - expect(btc2).to eq 703_587 - expect(btc3).to eq 2_040_402 + expect(btc1.sats).to eq 234_529 + expect(btc1).to eq 234_529 + expect(btc2).to eq 703_587 + expect(btc3).to eq 2_040_402 end it 'supports division with numbers' do - btc1 = described_class.new(34_529) + btc1 = described_class.new 34_529 btc2 = btc1 / 3 btc3 = btc2 / 2.9 @@ -74,7 +74,7 @@ RSpec.describe StrikeMe::BTC do expect(btc1.sats).to eq 34_529 expect(btc1).to eq 34_529 expect(btc2).to eq 11_509 - expect(btc3).to eq 3_968 + expect(btc3).to eq 3_968 end it 'supports addition with other BTC objects' do @@ -89,8 +89,8 @@ RSpec.describe StrikeMe::BTC do end it 'supports subtraction with other BTC objects' do - btc1 = described_class.new(34_529) - btc2 = described_class.new(2_556) + btc1 = described_class.new 34_529 + btc2 = described_class.new 2_556 btc3 = btc1 - btc2 expect { btc1 - 23 }.to raise_error StrikeMe::BTC::Error @@ -101,6 +101,7 @@ RSpec.describe StrikeMe::BTC do end it 'is consistent for small values' do + # if floor is used instead of round, the small number 3, multiplied by 100 million can result in 2.99999999 leading to 2 string = 3.sats.to_s expect(string).to eq '0.00000003' expect(string.to_f.btc.sats).to eq 3