├── VERSION ├── .rspec ├── README.md ├── .document ├── .gitignore ├── .travis.yml ├── spec ├── spec_helper.rb ├── lib │ ├── blockchain_adapter_spec.rb │ ├── exchange_rate_adapters │ │ ├── coinbase_adapter_spec.rb │ │ ├── okcoin_adapter_spec.rb │ │ ├── bitpay_adapter_spec.rb │ │ ├── bitstamp_adapter_spec.rb │ │ ├── localbitcoins_adapter_spec.rb │ │ ├── btce_adapter_spec.rb │ │ ├── kraken_adapter_spec.rb │ │ └── average_rate_adapter_spec.rb │ ├── exchange_rate_adapter_spec.rb │ ├── blockchain_adapters │ │ ├── biteasy_adapter_spec.rb │ │ ├── insight_adapter_spec.rb │ │ ├── blockchain_info_adapter_spec.rb │ │ └── mycelium_adapter_spec.rb │ ├── address_providers │ │ └── bip32_spec.rb │ ├── gateway_spec.rb │ └── order_spec.rb └── fixtures │ └── vcr │ ├── exchange_rate_btce_adapter.yml │ ├── exchange_rate_kraken_adapter.yml │ ├── exchange_rate_okcoin_adapter.yml │ ├── exchange_rate_localbitcoins_adapter.yml │ ├── exchange_rate_bitpay_adapter.yml │ ├── exchange_rate_coinbase_adapter.yml │ ├── exchange_rate_average_rate_adapter.yml │ └── blockchain_insight_adapter.yml ├── Gemfile ├── lib ├── straight │ ├── exchange_rate_adapters │ │ ├── coinbase_adapter.rb │ │ ├── localbitcoins_adapter.rb │ │ ├── bitstamp_adapter.rb │ │ ├── okcoin_adapter.rb │ │ ├── btce_adapter.rb │ │ ├── kraken_adapter.rb │ │ ├── bitpay_adapter.rb │ │ └── average_rate_adapter.rb │ ├── address_providers │ │ ├── bip32.rb │ │ └── base.rb │ ├── blockchain_adapter.rb │ ├── exchange_rate_adapter.rb │ ├── blockchain_adapters │ │ ├── insight_adapter.rb │ │ ├── biteasy_adapter.rb │ │ ├── blockchain_info_adapter.rb │ │ └── mycelium_adapter.rb │ ├── gateway.rb │ └── order.rb └── straight.rb ├── LICENSE.txt ├── Rakefile ├── Gemfile.lock └── straight.gemspec /VERSION: -------------------------------------------------------------------------------- 1 | 1.0.0 -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This repository has moved to https://github.com/MyceliumGear/straight 2 | -------------------------------------------------------------------------------- /.document: -------------------------------------------------------------------------------- 1 | lib/**/*.rb 2 | bin/* 3 | - 4 | features/**/*.feature 5 | LICENSE.txt 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .bundle 2 | 3 | # jeweler generated 4 | pkg 5 | 6 | .DS_Store 7 | .ruby-version 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 2.2.1 4 | notifications: 5 | slack: mycelium-gear:aqCsWhyRsg9iRfre4VaBUvVP 6 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require_relative "../lib/straight" 2 | require 'webmock/rspec' 3 | require 'vcr' 4 | 5 | VCR.configure do |config| 6 | config.cassette_library_dir = 'spec/fixtures/vcr' 7 | config.hook_into :webmock 8 | end 9 | 10 | if ENV['VCR_OFF'] 11 | WebMock.allow_net_connect! 12 | VCR.turn_off! ignore_cassettes: true 13 | end 14 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem 'btcruby', '~> 1.0' 4 | 5 | # Used in exchange rate adapters 6 | gem 'satoshi-unit', '~> 0.1' 7 | gem 'httparty', '~> 0.13.5' 8 | gem 'faraday' 9 | 10 | group :development do 11 | gem "bundler", "~> 1.0" 12 | gem "jeweler", "~> 2.0.1" 13 | gem "github_api", "0.11.3" 14 | end 15 | 16 | group :test do 17 | gem 'rspec' 18 | gem 'webmock' 19 | gem 'vcr' 20 | end 21 | -------------------------------------------------------------------------------- /lib/straight/exchange_rate_adapters/coinbase_adapter.rb: -------------------------------------------------------------------------------- 1 | module Straight 2 | module ExchangeRate 3 | 4 | class CoinbaseAdapter < Adapter 5 | 6 | FETCH_URL = 'https://coinbase.com/api/v1/currencies/exchange_rates' 7 | 8 | def rate_for(currency_code) 9 | super 10 | rate = get_rate_value_from_hash(@rates, "btc_to_#{currency_code.downcase}") 11 | rate_to_f(rate) 12 | end 13 | 14 | end 15 | 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/straight/exchange_rate_adapters/localbitcoins_adapter.rb: -------------------------------------------------------------------------------- 1 | module Straight 2 | module ExchangeRate 3 | 4 | class LocalbitcoinsAdapter < Adapter 5 | 6 | FETCH_URL = 'https://localbitcoins.com/bitcoinaverage/ticker-all-currencies/' 7 | 8 | def rate_for(currency_code) 9 | super 10 | rate = get_rate_value_from_hash(@rates, currency_code.upcase, 'rates', 'last') 11 | rate_to_f(rate) 12 | end 13 | 14 | end 15 | 16 | end 17 | end -------------------------------------------------------------------------------- /lib/straight/exchange_rate_adapters/bitstamp_adapter.rb: -------------------------------------------------------------------------------- 1 | module Straight 2 | module ExchangeRate 3 | 4 | class BitstampAdapter < Adapter 5 | 6 | FETCH_URL = 'https://www.bitstamp.net/api/ticker/' 7 | 8 | def rate_for(currency_code) 9 | super 10 | raise CurrencyNotSupported if currency_code != 'USD' 11 | rate = get_rate_value_from_hash(@rates, "last") 12 | rate_to_f(rate) 13 | end 14 | 15 | end 16 | 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/straight/exchange_rate_adapters/okcoin_adapter.rb: -------------------------------------------------------------------------------- 1 | module Straight 2 | module ExchangeRate 3 | 4 | class OkcoinAdapter < Adapter 5 | 6 | FETCH_URL = 'https://www.okcoin.com/api/ticker.do?ok=1' 7 | 8 | def rate_for(currency_code) 9 | super 10 | raise CurrencyNotSupported if currency_code != 'USD' 11 | rate = get_rate_value_from_hash(@rates, 'ticker', 'last') 12 | rate_to_f(rate) 13 | end 14 | 15 | end 16 | 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/straight/exchange_rate_adapters/btce_adapter.rb: -------------------------------------------------------------------------------- 1 | module Straight 2 | module ExchangeRate 3 | 4 | class BtceAdapter < Adapter 5 | 6 | FETCH_URL = 'https://btc-e.com/api/2/btc_usd/ticker' 7 | 8 | def rate_for(currency_code) 9 | super 10 | raise CurrencyNotSupported if !FETCH_URL.include?("btc_#{currency_code.downcase}") 11 | rate = get_rate_value_from_hash(@rates, 'ticker', 'last') 12 | rate_to_f(rate) 13 | end 14 | 15 | end 16 | 17 | end 18 | end -------------------------------------------------------------------------------- /lib/straight/exchange_rate_adapters/kraken_adapter.rb: -------------------------------------------------------------------------------- 1 | module Straight 2 | module ExchangeRate 3 | 4 | class KrakenAdapter < Adapter 5 | 6 | FETCH_URL = 'https://api.kraken.com/0/public/Ticker?pair=xbtusd' 7 | 8 | def rate_for(currency_code) 9 | super 10 | rate = get_rate_value_from_hash(@rates, 'result', 'XXBTZ' + currency_code.upcase, 'c') 11 | rate = rate.kind_of?(Array) ? rate.first : raise(CurrencyNotSupported) 12 | rate_to_f(rate) 13 | end 14 | 15 | end 16 | 17 | end 18 | end -------------------------------------------------------------------------------- /lib/straight/exchange_rate_adapters/bitpay_adapter.rb: -------------------------------------------------------------------------------- 1 | module Straight 2 | module ExchangeRate 3 | 4 | class BitpayAdapter < Adapter 5 | 6 | FETCH_URL = 'https://bitpay.com/api/rates' 7 | 8 | def rate_for(currency_code) 9 | super 10 | @rates.each do |rt| 11 | if rt['code'] == currency_code 12 | rate = get_rate_value_from_hash(rt, 'rate') 13 | return rate_to_f(rate) 14 | end 15 | end 16 | raise CurrencyNotSupported 17 | end 18 | 19 | end 20 | 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /spec/lib/blockchain_adapter_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe Straight::Blockchain do 4 | 5 | it "should return nil when adapter not exist or not loaded" do 6 | expect(Straight::Blockchain.const_get("Notexist")).to be nil 7 | end 8 | 9 | it "should return not namespaced adapter" do 10 | class MyAdapter; end 11 | expect(Straight::Blockchain.const_get("MyAdapter")).to eq(MyAdapter) 12 | end 13 | 14 | it "should return real constant" do 15 | expect(Straight::Blockchain.const_get("MyceliumAdapter")).to eq(Straight::Blockchain::MyceliumAdapter) 16 | end 17 | 18 | end 19 | -------------------------------------------------------------------------------- /lib/straight/address_providers/bip32.rb: -------------------------------------------------------------------------------- 1 | require_relative 'base' 2 | 3 | module Straight 4 | module AddressProvider 5 | class Bip32 < Base 6 | def new_address(keychain_id:, **args) 7 | path = 8 | if gateway.address_derivation_scheme.to_s.empty? 9 | # First check the depth. If the depth is 4 use '/i' notation (Mycelium iOS wallet) 10 | if gateway.keychain.depth > 3 11 | keychain_id.to_s 12 | else # Otherwise, use 'm/0/n' - both Electrum and Mycelium on Android 13 | "m/0/#{keychain_id.to_s}" 14 | end 15 | else 16 | gateway.address_derivation_scheme.to_s.downcase.sub('n', keychain_id.to_s) 17 | end 18 | gateway.keychain.derived_key(path).address.to_s 19 | end 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/straight/address_providers/base.rb: -------------------------------------------------------------------------------- 1 | module Straight 2 | module AddressProvider 3 | class Base 4 | 5 | attr_reader :gateway 6 | 7 | def initialize(gateway) 8 | @gateway = gateway 9 | end 10 | 11 | # @param [Hash] args see GatewayModule::Includable#new_order 12 | # @return [String] bitcoin address 13 | # Returns a Base58-encoded Bitcoin address to which the payment transaction 14 | # is expected to arrive. keychain_id is an integer > 0 (hopefully not too large and hopefully 15 | # the one a user of this class is going to properly increment) that is used to generate a 16 | # an BIP32 bitcoin address deterministically. 17 | def new_address(keychain_id:, **args) 18 | raise NotImplementedError 19 | end 20 | 21 | # If this method returns true, then address provider is expected to define 22 | # #new_address_and_amount which returns ['address', Integer(amount in satoshi)] 23 | def takes_fees? 24 | false 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Roman Snitko 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'rubygems' 4 | require 'bundler' 5 | begin 6 | Bundler.setup(:default, :development) 7 | rescue Bundler::BundlerError => e 8 | $stderr.puts e.message 9 | $stderr.puts "Run `bundle install` to install missing gems" 10 | exit e.status_code 11 | end 12 | require 'rake' 13 | 14 | require 'jeweler' 15 | Jeweler::Tasks.new do |gem| 16 | # gem is a Gem::Specification... see http://guides.rubygems.org/specification-reference/ for more options 17 | gem.name = "straight" 18 | gem.homepage = "http://github.com/snitko/straight" 19 | gem.license = "MIT" 20 | gem.summary = %Q{An engine for the Straight payment gateway software} 21 | gem.description = %Q{An engine for the Straight payment gateway software. Requires no state to be saved (that is, no storage or DB). Its responsibilities only include processing data coming from an actual gateway.} 22 | gem.email = "roman.snitko@gmail.com" 23 | gem.authors = ["Roman Snitko"] 24 | gem.files.exclude 'spec/**/*' 25 | end 26 | Jeweler::RubygemsDotOrgTasks.new 27 | 28 | begin 29 | require 'rspec/core/rake_task' 30 | RSpec::Core::RakeTask.new(:spec) 31 | task default: :spec 32 | rescue LoadError 33 | # no rspec available 34 | end 35 | -------------------------------------------------------------------------------- /lib/straight.rb: -------------------------------------------------------------------------------- 1 | require 'btcruby' 2 | require 'satoshi-unit' 3 | require 'json' 4 | require 'uri' 5 | require 'open-uri' 6 | require 'yaml' 7 | require 'singleton' 8 | require 'httparty' 9 | require 'faraday' 10 | 11 | module Straight 12 | StraightError = Class.new(StandardError) 13 | end 14 | 15 | require_relative 'straight/blockchain_adapter' 16 | require_relative 'straight/blockchain_adapters/blockchain_info_adapter' 17 | require_relative 'straight/blockchain_adapters/biteasy_adapter' 18 | require_relative 'straight/blockchain_adapters/mycelium_adapter' 19 | require_relative 'straight/blockchain_adapters/insight_adapter' 20 | 21 | require_relative 'straight/exchange_rate_adapter' 22 | require_relative 'straight/exchange_rate_adapters/bitpay_adapter' 23 | require_relative 'straight/exchange_rate_adapters/coinbase_adapter' 24 | require_relative 'straight/exchange_rate_adapters/bitstamp_adapter' 25 | require_relative 'straight/exchange_rate_adapters/localbitcoins_adapter' 26 | require_relative 'straight/exchange_rate_adapters/okcoin_adapter' 27 | require_relative 'straight/exchange_rate_adapters/btce_adapter' 28 | require_relative 'straight/exchange_rate_adapters/kraken_adapter' 29 | require_relative 'straight/exchange_rate_adapters/average_rate_adapter' 30 | 31 | require_relative 'straight/address_providers/bip32' 32 | 33 | require_relative 'straight/order' 34 | require_relative 'straight/gateway' 35 | -------------------------------------------------------------------------------- /spec/lib/exchange_rate_adapters/coinbase_adapter_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe Straight::ExchangeRate::CoinbaseAdapter do 4 | 5 | before :all do 6 | VCR.insert_cassette 'exchange_rate_coinbase_adapter' 7 | end 8 | 9 | after :all do 10 | VCR.eject_cassette 11 | end 12 | 13 | before(:each) do 14 | @exchange_adapter = Straight::ExchangeRate::CoinbaseAdapter.instance 15 | end 16 | 17 | it "finds the rate for currency code" do 18 | expect(@exchange_adapter.rate_for('USD')).to be_kind_of(Float) 19 | expect( -> { @exchange_adapter.rate_for('FEDcoin') }).to raise_error(Straight::ExchangeRate::Adapter::CurrencyNotSupported) 20 | end 21 | 22 | it "raises exception if rate is nil" do 23 | json_response_1 = '{}' 24 | json_response_2 = '{"btc_to_urd":"224.41","usd_to_xpf":"105.461721","bsd_to_btc":"0.004456"}' 25 | json_response_3 = '{"btc_to_usd":null,"usd_to_xpf":"105.461721","bsd_to_btc":"0.004456"}' 26 | uri_mock = double('uri mock') 27 | allow(uri_mock).to receive(:read).with(read_timeout: 4).and_return(json_response_1, json_response_2, json_response_3) 28 | allow(URI).to receive(:parse).and_return(uri_mock) 29 | 3.times do 30 | @exchange_adapter.fetch_rates! 31 | expect( -> { @exchange_adapter.rate_for('USD') }).to raise_error(Straight::ExchangeRate::Adapter::CurrencyNotSupported) 32 | end 33 | end 34 | 35 | end 36 | -------------------------------------------------------------------------------- /spec/lib/exchange_rate_adapters/okcoin_adapter_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe Straight::ExchangeRate::OkcoinAdapter do 4 | 5 | before :all do 6 | VCR.insert_cassette 'exchange_rate_okcoin_adapter' 7 | end 8 | 9 | after :all do 10 | VCR.eject_cassette 11 | end 12 | 13 | before(:each) do 14 | @exchange_adapter = Straight::ExchangeRate::OkcoinAdapter.instance 15 | end 16 | 17 | it "finds the rate for currency code" do 18 | expect(@exchange_adapter.rate_for('USD')).to be_kind_of(Float) 19 | expect( -> { @exchange_adapter.rate_for('FEDcoin') }).to raise_error(Straight::ExchangeRate::Adapter::CurrencyNotSupported) 20 | end 21 | 22 | it "raises exception if rate is nil" do 23 | response = [ 24 | '{"date":"1422679981","ticker":{}}', 25 | '{"date":"1422679981","ticker":{"buy":"227.27","high":"243.55","bambo":"226.89","low":"226.0","sell":"227.74","vol":"16065.2085"}}', 26 | '{"date":"1422679981","ticker":{"buy":"227.27","high":"243.55","last":null,"low":"226.0","sell":"227.74","vol":"16065.2085"}}', 27 | ] 28 | 3.times do |i| 29 | @exchange_adapter.instance_variable_set :@rates_updated_at, Time.now 30 | @exchange_adapter.instance_variable_set :@rates, JSON.parse(response[i]) 31 | expect( -> { @exchange_adapter.rate_for('USD') }).to raise_error(Straight::ExchangeRate::Adapter::CurrencyNotSupported) 32 | end 33 | end 34 | 35 | end 36 | -------------------------------------------------------------------------------- /spec/fixtures/vcr/exchange_rate_btce_adapter.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: get 5 | uri: https://btc-e.com/api/2/btc_usd/ticker 6 | body: 7 | encoding: US-ASCII 8 | string: '' 9 | headers: 10 | Accept-Encoding: 11 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 12 | Accept: 13 | - "*/*" 14 | User-Agent: 15 | - Ruby 16 | response: 17 | status: 18 | code: 200 19 | message: OK 20 | headers: 21 | Server: 22 | - cloudflare-nginx 23 | Date: 24 | - Mon, 22 Jun 2015 14:10:30 GMT 25 | Content-Type: 26 | - text/html; charset=utf-8 27 | Transfer-Encoding: 28 | - chunked 29 | Connection: 30 | - keep-alive 31 | Set-Cookie: 32 | - __cfduid=d84de25580d0b24d0880154fb74ee81f71434982230; expires=Tue, 21-Jun-16 33 | 14:10:30 GMT; path=/; domain=.btc-e.com; HttpOnly 34 | X-Frame-Options: 35 | - DENY 36 | Cache-Control: 37 | - no-cache, must-revalidate 38 | Cf-Ray: 39 | - 1fa8897b71930c71-AMS 40 | body: 41 | encoding: ASCII-8BIT 42 | string: '{"ticker":{"high":247.99001,"low":240.567,"avg":244.278505,"vol":1559486.0021,"vol_cur":6379.59513,"last":245.985,"buy":245.986,"sell":245.985,"updated":1434982230,"server_time":1434982230}}' 43 | http_version: 44 | recorded_at: Mon, 22 Jun 2015 14:10:30 GMT 45 | recorded_with: VCR 2.9.3 46 | -------------------------------------------------------------------------------- /spec/fixtures/vcr/exchange_rate_kraken_adapter.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: get 5 | uri: https://api.kraken.com/0/public/Ticker?pair=xbtusd 6 | body: 7 | encoding: US-ASCII 8 | string: '' 9 | headers: 10 | Accept-Encoding: 11 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 12 | Accept: 13 | - "*/*" 14 | User-Agent: 15 | - Ruby 16 | response: 17 | status: 18 | code: 200 19 | message: OK 20 | headers: 21 | Server: 22 | - cloudflare-nginx 23 | Date: 24 | - Mon, 22 Jun 2015 14:10:31 GMT 25 | Content-Type: 26 | - application/json; charset=utf-8 27 | Transfer-Encoding: 28 | - chunked 29 | Connection: 30 | - keep-alive 31 | Set-Cookie: 32 | - __cfduid=dc7c83161a7d914febf2a2d9e61609e3e1434982231; expires=Tue, 21-Jun-16 33 | 14:10:31 GMT; path=/; domain=.kraken.com; HttpOnly 34 | Vary: 35 | - Accept-Encoding 36 | Cf-Ray: 37 | - 1fa88983ea07147f-AMS 38 | body: 39 | encoding: ASCII-8BIT 40 | string: '{"error":[],"result":{"XXBTZUSD":{"a":["245.80522","1"],"b":["243.78633","1"],"c":["244.50000","1.00000000"],"v":["14.71088818","19.71088818"],"p":["244.99461","244.12416"],"t":[18,23],"l":["241.00000","241.00000"],"h":["246.65969","246.65969"],"o":"241.00000"}}}' 41 | http_version: 42 | recorded_at: Mon, 22 Jun 2015 14:10:31 GMT 43 | recorded_with: VCR 2.9.3 44 | -------------------------------------------------------------------------------- /spec/lib/exchange_rate_adapters/bitpay_adapter_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe Straight::ExchangeRate::BitpayAdapter do 4 | 5 | before :all do 6 | VCR.insert_cassette 'exchange_rate_bitpay_adapter' 7 | end 8 | 9 | after :all do 10 | VCR.eject_cassette 11 | end 12 | 13 | before(:each) do 14 | @exchange_adapter = Straight::ExchangeRate::BitpayAdapter.instance 15 | end 16 | 17 | it "finds the rate for currency code" do 18 | expect(@exchange_adapter.rate_for('USD')).to be_kind_of(Float) 19 | expect( -> { @exchange_adapter.rate_for('FEDcoin') }).to raise_error(Straight::ExchangeRate::Adapter::CurrencyNotSupported) 20 | end 21 | 22 | it "raises exception if rate is nil" do 23 | json_response_1 = '[{},{}]' 24 | json_response_2 = '[{"code":"USD","name":"US Dollar","rat":223.59},{"code":"EUR","name":"Eurozone Euro","rate":197.179544}]' 25 | json_response_3 = '[{"code":"USD","name":"US Dollar","rate":null},{"code":"EUR","name":"Eurozone Euro","rate":197.179544}]' 26 | uri_mock = double('uri mock') 27 | allow(uri_mock).to receive(:read).with(read_timeout: 4).and_return(json_response_1, json_response_2, json_response_3) 28 | allow(URI).to receive(:parse).and_return(uri_mock) 29 | 3.times do 30 | @exchange_adapter.fetch_rates! 31 | expect( -> { @exchange_adapter.rate_for('USD') }).to raise_error(Straight::ExchangeRate::Adapter::CurrencyNotSupported) 32 | end 33 | end 34 | 35 | end 36 | -------------------------------------------------------------------------------- /spec/lib/exchange_rate_adapters/bitstamp_adapter_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe Straight::ExchangeRate::BitstampAdapter do 4 | 5 | before :all do 6 | VCR.insert_cassette 'exchange_rate_bitstamp_adapter' 7 | end 8 | 9 | after :all do 10 | VCR.eject_cassette 11 | end 12 | 13 | before(:each) do 14 | @exchange_adapter = Straight::ExchangeRate::BitstampAdapter.instance 15 | end 16 | 17 | it "finds the rate for currency code" do 18 | expect(@exchange_adapter.rate_for('USD')).to be_kind_of(Float) 19 | expect( -> { @exchange_adapter.rate_for('FEDcoin') }).to raise_error(Straight::ExchangeRate::Adapter::CurrencyNotSupported) 20 | end 21 | 22 | it "raises exception if rate is nil" do 23 | json_response_1 = '{}' 24 | json_response_2 = '{"high": "232.89", "list": "224.13", "timestamp": "1423457015", "bid": "224.00", "vwap": "224.57", "volume": "14810.41127494", "low": "217.28", "ask": "224.13"}' 25 | json_response_3 = '{"high": "232.89", "last": null, "timestamp": "1423457015", "bid": "224.00", "vwap": "224.57", "volume": "14810.41127494", "low": "217.28", "ask": "224.13"}' 26 | uri_mock = double('uri mock') 27 | allow(uri_mock).to receive(:read).with(read_timeout: 4).and_return(json_response_1, json_response_2, json_response_3) 28 | allow(URI).to receive(:parse).and_return(uri_mock) 29 | 3.times do 30 | @exchange_adapter.fetch_rates! 31 | expect( -> { @exchange_adapter.rate_for('USD') }).to raise_error(Straight::ExchangeRate::Adapter::CurrencyNotSupported) 32 | end 33 | end 34 | 35 | end 36 | -------------------------------------------------------------------------------- /spec/lib/exchange_rate_adapters/localbitcoins_adapter_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe Straight::ExchangeRate::LocalbitcoinsAdapter do 4 | 5 | before :all do 6 | VCR.insert_cassette 'exchange_rate_localbitcoins_adapter' 7 | end 8 | 9 | after :all do 10 | VCR.eject_cassette 11 | end 12 | 13 | before(:each) do 14 | @exchange_adapter = Straight::ExchangeRate::LocalbitcoinsAdapter.instance 15 | end 16 | 17 | it "finds the rate for currency code" do 18 | expect(@exchange_adapter.rate_for('USD')).to be_kind_of(Float) 19 | expect( -> { @exchange_adapter.rate_for('FEDcoin') }).to raise_error(Straight::ExchangeRate::Adapter::CurrencyNotSupported) 20 | end 21 | 22 | it "rases exception if rate is nil" do 23 | json_response_1 = '{"USD": {}}' 24 | json_response_2 = '{"USD": {"volume_btc": "2277.85", "rates": {"bambo": "263.78"}, "avg_1h": 287.6003904801631, "avg_24h": 253.58144543993674, "avg_12h": 252.29202866050034}}' 25 | json_response_3 = '{"USD": {"volume_btc": "2277.85", "rates": {"last": null}, "avg_1h": 287.6003904801631, "avg_24h": 253.58144543993674, "avg_12h": 252.29202866050034}}' 26 | uri_mock = double('uri mock') 27 | allow(uri_mock).to receive(:read).with(read_timeout: 4).and_return(json_response_1, json_response_2, json_response_3) 28 | allow(URI).to receive(:parse).and_return(uri_mock) 29 | 3.times do 30 | @exchange_adapter.fetch_rates! 31 | expect( -> { @exchange_adapter.rate_for('USD') }).to raise_error(Straight::ExchangeRate::Adapter::CurrencyNotSupported) 32 | end 33 | end 34 | 35 | end 36 | -------------------------------------------------------------------------------- /spec/lib/exchange_rate_adapters/btce_adapter_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe Straight::ExchangeRate::BtceAdapter do 4 | 5 | before :all do 6 | VCR.insert_cassette 'exchange_rate_btce_adapter' 7 | end 8 | 9 | after :all do 10 | VCR.eject_cassette 11 | end 12 | 13 | before(:each) do 14 | @exchange_adapter = Straight::ExchangeRate::BtceAdapter.instance 15 | end 16 | 17 | it "finds the rate for currency code" do 18 | expect(@exchange_adapter.rate_for('USD')).to be_kind_of(Float) 19 | expect( -> { @exchange_adapter.rate_for('FEDcoin') }).to raise_error(Straight::ExchangeRate::Adapter::CurrencyNotSupported) 20 | end 21 | 22 | it "rases exception if rate is nil" do 23 | json_response_1 = '{"ticker":{}}' 24 | json_response_2 = '{"ticker":{"high":235,"low":215.89999,"avg":225.449995,"vol":2848293.72397,"vol_cur":12657.55799,"bambo":221.444,"buy":221.629,"sell":220.98,"updated":1422678812,"server_time":1422678813}}' 25 | json_response_3 = '{"ticker":{"high":235,"low":215.89999,"avg":225.449995,"vol":2848293.72397,"vol_cur":12657.55799,"last":null,"buy":221.629,"sell":220.98,"updated":1422678812,"server_time":1422678813}}' 26 | uri_mock = double('uri mock') 27 | allow(uri_mock).to receive(:read).with(read_timeout: 4).and_return(json_response_1, json_response_2, json_response_3) 28 | allow(URI).to receive(:parse).and_return(uri_mock) 29 | 3.times do 30 | @exchange_adapter.fetch_rates! 31 | expect( -> { @exchange_adapter.rate_for('USD') }).to raise_error(Straight::ExchangeRate::Adapter::CurrencyNotSupported) 32 | end 33 | end 34 | 35 | end 36 | -------------------------------------------------------------------------------- /spec/fixtures/vcr/exchange_rate_okcoin_adapter.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: get 5 | uri: https://www.okcoin.com/api/ticker.do?ok=1 6 | body: 7 | encoding: US-ASCII 8 | string: '' 9 | headers: 10 | Accept-Encoding: 11 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 12 | Accept: 13 | - "*/*" 14 | User-Agent: 15 | - Ruby 16 | response: 17 | status: 18 | code: 200 19 | message: OK 20 | headers: 21 | Server: 22 | - cloudflare-nginx 23 | Date: 24 | - Mon, 22 Jun 2015 14:22:53 GMT 25 | Content-Type: 26 | - text/html;charset=UTF-8 27 | Transfer-Encoding: 28 | - chunked 29 | Connection: 30 | - keep-alive 31 | Set-Cookie: 32 | - __cfduid=defec7de9bd99274685a88ba518fe2c3b1434982972; expires=Tue, 21-Jun-16 33 | 14:22:52 GMT; path=/; domain=.okcoin.com; HttpOnly 34 | - coin_session_id_o=166d1d53-8519-4816-94bb-491db382e0f9IDnr; Domain=okcoin.com; 35 | Path=/; Secure; HttpOnly 36 | - language=1; Expires=Mon, 29-Jun-2015 14:22:53 GMT; Path=/ 37 | Cache-Control: 38 | - no-store 39 | Expires: 40 | - Thu, 01 Jan 1970 00:00:00 GMT 41 | Pragma: 42 | - no-cache 43 | X-Frame-Options: 44 | - SAMEORIGIN 45 | Cf-Ray: 46 | - 1fa89b9c2ced16ac-ARN 47 | body: 48 | encoding: ASCII-8BIT 49 | string: '{"date":"1434982973","ticker":{"buy":"245.7","high":"247.0","last":"245.72","low":"240.24","sell":"245.72","vol":"9381.637"}}' 50 | http_version: 51 | recorded_at: Mon, 22 Jun 2015 14:22:53 GMT 52 | recorded_with: VCR 2.9.3 53 | -------------------------------------------------------------------------------- /lib/straight/exchange_rate_adapters/average_rate_adapter.rb: -------------------------------------------------------------------------------- 1 | module Straight 2 | module ExchangeRate 3 | 4 | class AverageRateAdapter < Adapter 5 | 6 | # Takes exchange rate adapters instances or classes as arguments 7 | def self.instance(*adapters) 8 | instance = super() 9 | instance.instance_variable_set(:@adapters, adapters.map { |adapter| adapter.respond_to?(:instance) ? adapter.instance : adapter }) 10 | instance 11 | end 12 | 13 | def fetch_rates! 14 | failed_fetches = 0 15 | @adapters.each do |adapter| 16 | begin 17 | adapter.fetch_rates! 18 | rescue => e 19 | failed_fetches += 1 20 | raise e if failed_fetches == @adapters.size 21 | end 22 | end 23 | end 24 | 25 | def rate_for(currency_code) 26 | rates = [] 27 | @adapters.each do |adapter| 28 | begin 29 | rates << adapter.rate_for(currency_code) 30 | rescue CurrencyNotSupported 31 | rates << nil 32 | end 33 | end 34 | 35 | unless rates.select(&:nil?).size == @adapters.size 36 | rates.compact! 37 | rates.inject {|sum, rate| sum + rate} / rates.size 38 | else 39 | raise CurrencyNotSupported 40 | end 41 | end 42 | 43 | def get_rate_value_from_hash(rates_hash, *keys) 44 | raise "This method is not supposed to be used in #{self.class}." 45 | end 46 | 47 | def rate_to_f(rate) 48 | raise "This method is not supposed to be used in #{self.class}." 49 | end 50 | 51 | end 52 | 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /spec/lib/exchange_rate_adapters/kraken_adapter_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe Straight::ExchangeRate::KrakenAdapter do 4 | 5 | before :all do 6 | VCR.insert_cassette 'exchange_rate_kraken_adapter' 7 | end 8 | 9 | after :all do 10 | VCR.eject_cassette 11 | end 12 | 13 | before(:each) do 14 | @exchange_adapter = Straight::ExchangeRate::KrakenAdapter.instance 15 | end 16 | 17 | it "finds the rate for currency code" do 18 | expect(@exchange_adapter.rate_for('USD')).to be_kind_of(Float) 19 | expect( -> { @exchange_adapter.rate_for('FEDcoin') }).to raise_error(Straight::ExchangeRate::Adapter::CurrencyNotSupported) 20 | end 21 | 22 | it "raises exception if rate is nil" do 23 | json_response_1 = '{"error":[],"result":{}}' 24 | json_response_2 = '{"error":[],"result":{"XXBTZUSD":{"a":["244.95495","1"],"b":["227.88761","1"],"abc":["228.20494","0.10000000"],"v":["5.18645337","24.04033530"],"p":["234.92296","234.41356"],"t":[45,74],"l":["228.20494","228.20494"],"h":["239.36069","239.36069"],"o":"231.82976"}}}' 25 | json_response_3 = '{"error":[],"result":{"XXBTZUSD":{"a":["244.95495","1"],"b":["227.88761","1"],"c":[null,"0.10000000"],"v":["5.18645337","24.04033530"],"p":["234.92296","234.41356"],"t":[45,74],"l":["228.20494","228.20494"],"h":["239.36069","239.36069"],"o":"231.82976"}}}' 26 | uri_mock = double('uri mock') 27 | allow(uri_mock).to receive(:read).with(read_timeout: 4).and_return(json_response_1, json_response_2, json_response_3) 28 | allow(URI).to receive(:parse).and_return(uri_mock) 29 | 3.times do 30 | @exchange_adapter.fetch_rates! 31 | expect( -> { @exchange_adapter.rate_for('USD') }).to raise_error(Straight::ExchangeRate::Adapter::CurrencyNotSupported) 32 | end 33 | end 34 | 35 | end 36 | -------------------------------------------------------------------------------- /lib/straight/blockchain_adapter.rb: -------------------------------------------------------------------------------- 1 | module Straight 2 | 3 | module Blockchain 4 | # A base class, providing guidance for the interfaces of 5 | # all blockchain adapters as well as supplying some useful methods. 6 | class Adapter 7 | 8 | include Singleton 9 | 10 | # Raised when blockchain data cannot be retrived for any reason. 11 | # We're not really intereste in the precise reason, although it is 12 | # stored in the message. 13 | class RequestError < StraightError; end 14 | 15 | # Raised when an invalid address is used, for example a mainnet address 16 | # is used on testnet and vice versa. 17 | class BitcoinAddressInvalid < StraightError; end 18 | 19 | # How much times try to connect to servers if ReadTimeout error appears 20 | MAX_TRIES = 5 21 | 22 | def fetch_transaction(tid) 23 | raise "Please implement #fetch_transaction in #{self.to_s}" 24 | end 25 | 26 | def fetch_transactions_for(address) 27 | raise "Please implement #fetch_transactions_for in #{self.to_s}" 28 | end 29 | 30 | def fetch_balance_for(address) 31 | raise "Please implement #fetch_balance_for in #{self.to_s}" 32 | end 33 | 34 | private 35 | 36 | # Converts transaction info received from the source into the 37 | # unified format expected by users of BlockchainAdapter instances. 38 | def straighten_transaction(transaction) 39 | raise "Please implement #straighten_transaction in #{self.to_s}" 40 | end 41 | 42 | end 43 | 44 | # Look for the adapter without namespace if not found it in a specific module 45 | # @return nil 46 | def self.const_missing(name) 47 | Kernel.const_get(name) 48 | rescue NameError 49 | puts "WARNING: No blockchain adapter with the name #{name.to_s} was found!" 50 | nil 51 | end 52 | 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /spec/lib/exchange_rate_adapter_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe Straight::ExchangeRate::Adapter do 4 | 5 | class Straight::ExchangeRate::Adapter 6 | FETCH_URL = '' 7 | end 8 | 9 | before(:each) do 10 | @exchange_adapter = Straight::ExchangeRate::Adapter.instance 11 | end 12 | 13 | describe "converting currencies" do 14 | 15 | before(:each) do 16 | allow(@exchange_adapter).to receive(:fetch_rates!) 17 | allow(@exchange_adapter).to receive(:rate_for).with('USD').and_return(450.5412) 18 | end 19 | 20 | it "converts amount from currency into BTC" do 21 | expect(@exchange_adapter.convert_from_currency(2252.706, currency: 'USD')).to eq(500000000) 22 | end 23 | 24 | it "converts from btc into currency" do 25 | expect(@exchange_adapter.convert_to_currency(500000000, currency: 'USD')).to eq(2252.706) 26 | end 27 | 28 | it "shows btc amounts in various denominations" do 29 | expect(@exchange_adapter.convert_from_currency(2252.706, currency: 'USD', btc_denomination: :btc)).to eq(5) 30 | expect(@exchange_adapter.convert_to_currency(5, currency: 'USD', btc_denomination: :btc)).to eq(2252.706) 31 | end 32 | 33 | it "accepts string as amount and converts it properly" do 34 | expect(@exchange_adapter.convert_from_currency('2252.706', currency: 'USD', btc_denomination: :btc)).to eq(5) 35 | expect(@exchange_adapter.convert_to_currency('5', currency: 'USD', btc_denomination: :btc)).to eq(2252.706) 36 | end 37 | 38 | end 39 | 40 | it "when checking for rates, only calls fetch_rates! if they were checked long time ago or never" do 41 | uri_mock = double('uri mock') 42 | expect(URI).to receive(:parse).and_return(uri_mock).twice 43 | expect(uri_mock).to receive(:read).and_return('{ "USD": 534.4343 }').twice 44 | @exchange_adapter.rate_for('USD') 45 | @exchange_adapter.rate_for('USD') # not calling fetch_rates! because we've just checked 46 | @exchange_adapter.instance_variable_set(:@rates_updated_at, Time.now-1900) 47 | @exchange_adapter.rate_for('USD') 48 | end 49 | 50 | it "raises exception if rate is nil" do 51 | rate = nil 52 | expect( -> { @exchange_adapter.rate_to_f(rate) }).to raise_error(Straight::ExchangeRate::Adapter::CurrencyNotSupported) 53 | end 54 | 55 | end 56 | -------------------------------------------------------------------------------- /lib/straight/exchange_rate_adapter.rb: -------------------------------------------------------------------------------- 1 | module Straight 2 | module ExchangeRate 3 | 4 | class Adapter 5 | 6 | include Singleton 7 | 8 | class FetchingFailed < StraightError; end 9 | class CurrencyNotSupported < StraightError; end 10 | 11 | def initialize(rates_expire_in: 1800) 12 | @rates_expire_in = rates_expire_in # in seconds 13 | end 14 | 15 | def convert_from_currency(amount_in_currency, btc_denomination: :satoshi, currency: 'USD') 16 | btc_amount = amount_in_currency.to_f/rate_for(currency) 17 | Satoshi.new(btc_amount, from_unit: :btc, to_unit: btc_denomination).to_unit 18 | end 19 | 20 | def convert_to_currency(amount, btc_denomination: :satoshi, currency: 'USD') 21 | amount_in_btc = Satoshi.new(amount.to_f, from_unit: btc_denomination).to_btc 22 | amount_in_btc*rate_for(currency) 23 | end 24 | 25 | def fetch_rates! 26 | raise "FETCH_URL is not defined!" unless self.class::FETCH_URL 27 | uri = URI.parse(self.class::FETCH_URL) 28 | begin 29 | @rates = JSON.parse(uri.read(read_timeout: 4)) 30 | @rates_updated_at = Time.now 31 | rescue OpenURI::HTTPError => e 32 | raise FetchingFailed 33 | end 34 | end 35 | 36 | def rate_for(currency_code) 37 | if !@rates_updated_at || (Time.now - @rates_updated_at) > @rates_expire_in 38 | fetch_rates! 39 | end 40 | nil # this should be changed in descendant classes 41 | end 42 | 43 | # This method will get value we are interested in from hash and 44 | # prevent failing with 'undefined method [] for Nil' if at some point hash doesn't have such key value pair 45 | def get_rate_value_from_hash(rates_hash, *keys) 46 | keys.inject(rates_hash) do |intermediate, key| 47 | if intermediate.respond_to?(:[]) 48 | intermediate[key] 49 | else 50 | raise CurrencyNotSupported 51 | end 52 | end 53 | end 54 | 55 | # We dont want to have false positive rate, because nil.to_f is 0.0 56 | # This method checks that rate value is not nil 57 | def rate_to_f(rate) 58 | rate ? rate.to_f : raise(CurrencyNotSupported) 59 | end 60 | 61 | end 62 | 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /spec/lib/blockchain_adapters/biteasy_adapter_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe Straight::Blockchain::BiteasyAdapter do 4 | 5 | subject(:adapter) { Straight::Blockchain::BiteasyAdapter.mainnet_adapter } 6 | 7 | before :all do 8 | VCR.insert_cassette 'blockchain_biteasy_adapter' 9 | end 10 | 11 | after :all do 12 | VCR.eject_cassette 13 | end 14 | 15 | it "fetches the balance for a given address" do 16 | address = "3B1QZ8FpAaHBgkSB5gFt76ag5AW9VeP8xp" 17 | expect(adapter.fetch_balance_for(address)).to be_kind_of(Integer) 18 | end 19 | 20 | it "fetches a single transaction" do 21 | tid = 'ae0d040f48d75fdc46d9035236a1782164857d6f0cca1f864640281115898560' 22 | expect(adapter.fetch_transaction(tid)[:total_amount]).to eq(832947) 23 | end 24 | 25 | it "calculates total_amount of a transaction for the given address only" do 26 | t = { 'data' => {'outputs' => [{ 'value' => 1, 'to_address' => 'address1'}, { 'value' => 2, 'to_address' => 'address2'}] } } 27 | expect(adapter.send(:straighten_transaction, t, address: 'address1')[:total_amount]).to eq(1) 28 | end 29 | 30 | it "fetches all transactions for the current address" do 31 | address = "3B1QZ8FpAaHBgkSB5gFt76ag5AW9VeP8xp" 32 | expect(adapter).to receive(:straighten_transaction).with(anything, address: address).at_least(:once) 33 | expect(adapter.fetch_transactions_for(address)).not_to be_empty 34 | end 35 | 36 | it "calculates the number of confirmations for each transaction" do 37 | tid = 'ae0d040f48d75fdc46d9035236a1782164857d6f0cca1f864640281115898560' 38 | expect(adapter.fetch_transaction(tid)[:confirmations]).to be > 0 39 | end 40 | 41 | it "gets a transaction id among other data" do 42 | tid = 'ae0d040f48d75fdc46d9035236a1782164857d6f0cca1f864640281115898560' 43 | expect(adapter.fetch_transaction(tid)[:tid]).to eq(tid) 44 | end 45 | 46 | it "raises an exception when something goes wrong with fetching data" do 47 | expect( -> { adapter.send(:api_request, "/a-404-request") }).to raise_error(Straight::Blockchain::Adapter::RequestError) 48 | end 49 | 50 | it "uses the same Singleton instance" do 51 | a = Straight::Blockchain::BiteasyAdapter.mainnet_adapter 52 | b = Straight::Blockchain::BiteasyAdapter.mainnet_adapter 53 | expect(a).to eq(b) 54 | end 55 | 56 | end 57 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | addressable (2.3.6) 5 | btcruby (1.0.3) 6 | ffi (~> 1.9, >= 1.9.3) 7 | builder (3.2.2) 8 | crack (0.4.2) 9 | safe_yaml (~> 1.0.0) 10 | descendants_tracker (0.0.4) 11 | thread_safe (~> 0.3, >= 0.3.1) 12 | diff-lcs (1.2.5) 13 | faraday (0.9.0) 14 | multipart-post (>= 1.2, < 3) 15 | ffi (1.9.8) 16 | git (1.2.8) 17 | github_api (0.11.3) 18 | addressable (~> 2.3) 19 | descendants_tracker (~> 0.0.1) 20 | faraday (~> 0.8, < 0.10) 21 | hashie (>= 1.2) 22 | multi_json (>= 1.7.5, < 2.0) 23 | nokogiri (~> 1.6.0) 24 | oauth2 25 | hashie (3.3.1) 26 | highline (1.6.21) 27 | httparty (0.13.5) 28 | json (~> 1.8) 29 | multi_xml (>= 0.5.2) 30 | jeweler (2.0.1) 31 | builder 32 | bundler (>= 1.0) 33 | git (>= 1.2.5) 34 | github_api 35 | highline (>= 1.6.15) 36 | nokogiri (>= 1.5.10) 37 | rake 38 | rdoc 39 | json (1.8.1) 40 | jwt (1.0.0) 41 | mini_portile (0.6.0) 42 | multi_json (1.10.1) 43 | multi_xml (0.5.5) 44 | multipart-post (2.0.0) 45 | nokogiri (1.6.3.1) 46 | mini_portile (= 0.6.0) 47 | oauth2 (1.0.0) 48 | faraday (>= 0.8, < 0.10) 49 | jwt (~> 1.0) 50 | multi_json (~> 1.3) 51 | multi_xml (~> 0.5) 52 | rack (~> 1.2) 53 | rack (1.5.2) 54 | rake (10.3.2) 55 | rdoc (4.1.2) 56 | json (~> 1.4) 57 | rspec (3.1.0) 58 | rspec-core (~> 3.1.0) 59 | rspec-expectations (~> 3.1.0) 60 | rspec-mocks (~> 3.1.0) 61 | rspec-core (3.1.2) 62 | rspec-support (~> 3.1.0) 63 | rspec-expectations (3.1.0) 64 | diff-lcs (>= 1.2.0, < 2.0) 65 | rspec-support (~> 3.1.0) 66 | rspec-mocks (3.1.0) 67 | rspec-support (~> 3.1.0) 68 | rspec-support (3.1.0) 69 | safe_yaml (1.0.4) 70 | satoshi-unit (0.1.7) 71 | thread_safe (0.3.4) 72 | vcr (2.9.3) 73 | webmock (1.21.0) 74 | addressable (>= 2.3.6) 75 | crack (>= 0.3.2) 76 | 77 | PLATFORMS 78 | ruby 79 | 80 | DEPENDENCIES 81 | btcruby (~> 1.0) 82 | bundler (~> 1.0) 83 | faraday 84 | github_api (= 0.11.3) 85 | httparty (~> 0.13.5) 86 | jeweler (~> 2.0.1) 87 | rspec 88 | satoshi-unit (~> 0.1) 89 | vcr 90 | webmock 91 | 92 | BUNDLED WITH 93 | 1.10.5 94 | -------------------------------------------------------------------------------- /spec/lib/blockchain_adapters/insight_adapter_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe Straight::Blockchain::InsightAdapter do 4 | 5 | subject(:mainnet_adapter) { Straight::Blockchain::InsightAdapter.mainnet_adapter(main_url: "https://insight.mycelium.com/api") } 6 | 7 | before :all do 8 | VCR.configure do |c| 9 | c.default_cassette_options = {:record => :new_episodes} 10 | end 11 | VCR.insert_cassette 'blockchain_insight_adapter' 12 | end 13 | 14 | after :all do 15 | VCR.eject_cassette 16 | end 17 | 18 | let(:tid) { 'b168b57a9ae38c0671c5eef3be6c8305782bd1351e75028dac491185388d5424' } 19 | 20 | it "fetches a single transaction" do 21 | expect(mainnet_adapter.fetch_transaction(tid)[:total_amount]).to eq(499900000) 22 | end 23 | 24 | it "returns correct prepared data" do 25 | expect(mainnet_adapter.fetch_transaction(tid)[:total_amount]).to eq(499900000) 26 | expect(mainnet_adapter.fetch_transaction(tid)[:tid]).to eq("b168b57a9ae38c0671c5eef3be6c8305782bd1351e75028dac491185388d5424") 27 | expect(mainnet_adapter.fetch_transaction(tid)[:outs].first[:amount]).to eq(187000000) 28 | end 29 | 30 | it "fetches first transaction for the given address" do 31 | address = "1CBWzY7PEnUtT4b36bth4UZuNmby9pTT7A" 32 | expect(mainnet_adapter.fetch_transactions_for(address)).to be_kind_of(Array) 33 | expect(mainnet_adapter.fetch_transactions_for(address)).not_to be_empty 34 | end 35 | 36 | it "fetches balance for given address" do 37 | address = "16iKJsRM3LrA4k7NeTQbCB9ZDpV64Fkm6" 38 | expect(mainnet_adapter.fetch_balance_for(address)).to eq(0) 39 | end 40 | 41 | it "raises exception if something wrong with network" do 42 | expect( -> { mainnet_adapter.send(:api_request, "/a-404-request", "tid") }).to raise_error(Straight::Blockchain::Adapter::RequestError) 43 | end 44 | 45 | it "raises exception if worng main_url" do 46 | adapter = Straight::Blockchain::InsightAdapter.mainnet_adapter(main_url: "https://insight.mycelium.com/wrong_api") 47 | expect{ adapter.fetch_transaction(tid)[:total_amount] }.to raise_error(Straight::Blockchain::Adapter::RequestError) 48 | end 49 | 50 | it "should return message if given wrong address" do 51 | expect{ mainnet_adapter.fetch_transactions_for("wrong_address") }.to raise_error(Straight::Blockchain::Adapter::BitcoinAddressInvalid) 52 | end 53 | 54 | end 55 | -------------------------------------------------------------------------------- /lib/straight/blockchain_adapters/insight_adapter.rb: -------------------------------------------------------------------------------- 1 | module Straight 2 | module Blockchain 3 | 4 | class InsightAdapter < Adapter 5 | 6 | @@test_url = nil 7 | 8 | def self.mainnet_adapter(main_url:, test_url: nil) 9 | @@test_url = test_url 10 | new(main_url) 11 | end 12 | 13 | def self.testnet_adapter 14 | raise "Testnet not implemented" unless @@test_url 15 | new(@@test_url) 16 | end 17 | 18 | def initialize(host_url) 19 | @base_url = host_url 20 | end 21 | 22 | def fetch_transaction(tid, address: nil) 23 | res = api_request("/tx/", tid) 24 | straighten_transaction(res, address: address) 25 | end 26 | 27 | def fetch_transactions_for(address) 28 | res = api_request("/addr/", address) 29 | return [] if res["transactions"].empty? 30 | [fetch_transaction(res["transactions"].first, address: address)] 31 | end 32 | 33 | def fetch_balance_for(address) 34 | res = api_request("/addr/", address) 35 | res["balanceSat"].to_i 36 | end 37 | 38 | private 39 | 40 | def api_request(place, val) 41 | req_url = @base_url + place + val 42 | res = HTTParty.get( 43 | req_url, 44 | body: { 'Content-Type' => 'application/json' }, 45 | timeout: 15, 46 | verify: false 47 | ).body 48 | JSON.parse(res) 49 | rescue HTTParty::Error => e 50 | raise RequestError, YAML::dump(e) 51 | rescue JSON::ParserError => e 52 | raise BitcoinAddressInvalid, message: "address in question: #{val}" if e.message.include?("Invalid address") 53 | raise RequestError, YAML::dump(e) 54 | end 55 | 56 | def straighten_transaction(transaction, address: nil) 57 | total_amount = 0 58 | tid = transaction["txid"] 59 | transaction["vout"].each do |o| 60 | total_amount += Satoshi.new(o["value"]) if address.nil? || address == o["scriptPubKey"]["addresses"].first 61 | end 62 | confirmations = transaction["confirmations"] 63 | outs = transaction["vout"].map { |o| {amount: Satoshi.new(o["value"]).to_i, receiving_address: o["scriptPubKey"]["addresses"].first} } 64 | 65 | { 66 | tid: tid, 67 | total_amount: total_amount, 68 | confirmations: confirmations || 0, 69 | outs: outs || [] 70 | } 71 | end 72 | 73 | end 74 | 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /spec/lib/exchange_rate_adapters/average_rate_adapter_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe Straight::ExchangeRate::AverageRateAdapter do 4 | 5 | before :all do 6 | VCR.insert_cassette 'exchange_rate_average_rate_adapter' 7 | end 8 | 9 | after :all do 10 | VCR.eject_cassette 11 | end 12 | 13 | before(:each) do 14 | @average_rates_adapter = Straight::ExchangeRate::AverageRateAdapter.instance( 15 | Straight::ExchangeRate::BitstampAdapter, 16 | Straight::ExchangeRate::BitpayAdapter.instance, 17 | ) 18 | end 19 | 20 | it "calculates average rate" do 21 | json_response_bistamp = '{"high": "232.89", "last": "100", "timestamp": "1423457015", "bid": "224.00", "vwap": "224.57", "volume": "14810.41127494", "low": "217.28", "ask": "224.13"}' 22 | json_response_bitpay = '[{"code":"USD","name":"US Dollar","rate":200},{"code":"EUR","name":"Eurozone Euro","rate":197.179544}]' 23 | uri_mock = double('uri mock') 24 | allow(uri_mock).to receive(:read).with(read_timeout: 4).and_return(json_response_bistamp, json_response_bitpay) 25 | allow(URI).to receive(:parse).and_return(uri_mock) 26 | expect(@average_rates_adapter.rate_for('USD')).to eq 150 27 | end 28 | 29 | it "fetches rates for all adapters" do 30 | expect(@average_rates_adapter.fetch_rates!).not_to be_empty 31 | end 32 | 33 | it 'raises error if all adapters failed to fetch rates' do 34 | adapter_mocks = [double('adapter_1'), double('adapter_2')] 35 | adapter_mocks.each do |adapter| 36 | expect(adapter).to receive(:fetch_rates!).and_raise(Straight::ExchangeRate::Adapter::FetchingFailed) 37 | end 38 | average_rates_adapter = Straight::ExchangeRate::AverageRateAdapter.instance(*adapter_mocks) 39 | expect( -> { average_rates_adapter.fetch_rates! }).to raise_error(Straight::ExchangeRate::Adapter::FetchingFailed) 40 | end 41 | 42 | it "raises exception if all adapters fail to get rates" do 43 | expect( -> { @average_rates_adapter.rate_for('FEDcoin') }).to raise_error(Straight::ExchangeRate::Adapter::CurrencyNotSupported) 44 | end 45 | 46 | it "raises exception if unallowed method is called" do # fetch_rates! is not to be used in AverageRateAdapter itself 47 | expect( -> { @average_rates_adapter.get_rate_value_from_hash(nil, 'nothing') }).to raise_error("This method is not supposed to be used in #{@average_rates_adapter.class}.") 48 | expect( -> { @average_rates_adapter.rate_to_f(nil) }).to raise_error("This method is not supposed to be used in #{@average_rates_adapter.class}.") 49 | end 50 | 51 | end 52 | -------------------------------------------------------------------------------- /lib/straight/blockchain_adapters/biteasy_adapter.rb: -------------------------------------------------------------------------------- 1 | module Straight 2 | module Blockchain 3 | 4 | class BiteasyAdapter < Adapter 5 | 6 | def self.mainnet_adapter 7 | instance = self.instance 8 | instance._initialize("https://api.biteasy.com/blockchain/v1") 9 | instance 10 | end 11 | 12 | def self.testnet_adapter 13 | raise "Not Supported Yet" 14 | end 15 | 16 | def _initialize(base_url) 17 | @base_url = base_url 18 | end 19 | 20 | # Returns the current balance of the address 21 | def fetch_balance_for(address) 22 | JSON.parse(api_request("/addresses/#{address}"))['data']['balance'] 23 | end 24 | 25 | # Returns transaction info for the tid 26 | def fetch_transaction(tid, address: nil) 27 | straighten_transaction JSON.parse(api_request("/transactions/#{tid}"), address: address) 28 | end 29 | 30 | # Returns all transactions for the address 31 | def fetch_transactions_for(address) 32 | transactions = JSON.parse(api_request("/transactions?address=#{address}"))['data']['transactions'] 33 | transactions.map { |t| straighten_transaction(t, address: address) } 34 | end 35 | 36 | private 37 | 38 | def api_request(url) 39 | begin 40 | response = HTTParty.get("#{@base_url}/#{url}", timeout: 4, verify: false) 41 | unless response.code == 200 42 | raise RequestError, "Cannot access remote API, response code was #{response.code}" 43 | end 44 | response.body 45 | rescue HTTParty::Error => e 46 | raise RequestError, YAML::dump(e) 47 | rescue JSON::ParserError => e 48 | raise RequestError, YAML::dump(e) 49 | end 50 | end 51 | 52 | # Converts transaction info received from the source into the 53 | # unified format expected by users of BlockchainAdapter instances. 54 | def straighten_transaction(transaction, address: nil) 55 | outs = [] 56 | total_amount = 0 57 | transaction['data']['outputs'].each do |out| 58 | total_amount += out['value'] if address.nil? || address == out['to_address'] 59 | outs << { amount: out['value'], receiving_address: out['to_address'] } 60 | end 61 | 62 | { 63 | tid: transaction['data']['hash'], 64 | total_amount: total_amount, 65 | confirmations: transaction['data']['confirmations'], 66 | outs: outs 67 | } 68 | end 69 | 70 | end 71 | 72 | end 73 | 74 | end 75 | -------------------------------------------------------------------------------- /spec/lib/blockchain_adapters/blockchain_info_adapter_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe Straight::Blockchain::BlockchainInfoAdapter do 4 | 5 | subject(:adapter) { Straight::Blockchain::BlockchainInfoAdapter.mainnet_adapter } 6 | 7 | before :all do 8 | VCR.insert_cassette 'blockchain_blockchain_info_adapter' 9 | end 10 | 11 | after :all do 12 | VCR.eject_cassette 13 | end 14 | 15 | it "fetches all transactions for the current address" do 16 | address = "3B1QZ8FpAaHBgkSB5gFt76ag5AW9VeP8xp" 17 | expect(adapter).to receive(:straighten_transaction).with(anything, address: address).at_least(:once) 18 | expect(adapter.fetch_transactions_for(address)).not_to be_empty 19 | end 20 | 21 | it "fetches the balance for a given address" do 22 | address = "3B1QZ8FpAaHBgkSB5gFt76ag5AW9VeP8xp" 23 | expect(adapter.fetch_balance_for(address)).to be_kind_of(Integer) 24 | end 25 | 26 | it "fetches a single transaction" do 27 | tid = 'ae0d040f48d75fdc46d9035236a1782164857d6f0cca1f864640281115898560' 28 | expect(adapter.fetch_transaction(tid)[:total_amount]).to eq(832947) 29 | end 30 | 31 | it "calculates the number of confirmations for each transaction" do 32 | tid = 'ae0d040f48d75fdc46d9035236a1782164857d6f0cca1f864640281115898560' 33 | expect(adapter.fetch_transaction(tid)[:confirmations]).to be > 0 34 | end 35 | 36 | it "gets a transaction id among other data" do 37 | tid = 'ae0d040f48d75fdc46d9035236a1782164857d6f0cca1f864640281115898560' 38 | expect(adapter.fetch_transaction(tid)[:tid]).to eq(tid) 39 | end 40 | 41 | it "caches blockchain.info latestblock requests" do 42 | expect(adapter).to receive(:api_request).once.and_return('{ "height": 1 }') 43 | adapter.send(:calculate_confirmations, { "block_height" => 1 }, force_latest_block_reload: true) 44 | adapter.send(:calculate_confirmations, { "block_height" => 1 }) 45 | adapter.send(:calculate_confirmations, { "block_height" => 1 }) 46 | adapter.send(:calculate_confirmations, { "block_height" => 1 }) 47 | adapter.send(:calculate_confirmations, { "block_height" => 1 }) 48 | end 49 | 50 | it "raises an exception when something goes wrong with fetching datd" do 51 | expect( -> { adapter.send(:api_request, "/a-404-request") }).to raise_error(Straight::Blockchain::Adapter::RequestError) 52 | end 53 | 54 | it "calculates total_amount of a transaction for the given address only" do 55 | t = { 'out' => [{ 'value' => 1, 'addr' => 'address1'}, { 'value' => 1, 'addr' => 'address2'}] } 56 | expect(adapter.send(:straighten_transaction, t, address: 'address1')[:total_amount]).to eq(1) 57 | end 58 | 59 | it "uses the same Singleton instance" do 60 | a = Straight::Blockchain::BlockchainInfoAdapter.mainnet_adapter 61 | b = Straight::Blockchain::BlockchainInfoAdapter.mainnet_adapter 62 | expect(a).to eq(b) 63 | end 64 | 65 | end 66 | -------------------------------------------------------------------------------- /straight.gemspec: -------------------------------------------------------------------------------- 1 | # Generated by jeweler 2 | # DO NOT EDIT THIS FILE DIRECTLY 3 | # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec' 4 | # -*- encoding: utf-8 -*- 5 | # stub: straight 0.2.3 ruby lib 6 | 7 | Gem::Specification.new do |s| 8 | s.name = "straight" 9 | s.version = "1.0.0" 10 | 11 | s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= 12 | s.require_paths = ["lib"] 13 | s.authors = ["Roman Snitko"] 14 | s.date = "2015-06-25" 15 | s.description = "An engine for the Straight payment gateway software. Requires no state to be saved (that is, no storage or DB). Its responsibilities only include processing data coming from an actual gateway." 16 | s.email = "roman.snitko@gmail.com" 17 | s.extra_rdoc_files = [ 18 | "LICENSE.txt", 19 | "README.md" 20 | ] 21 | s.files = [ 22 | ".document", 23 | ".rspec", 24 | "Gemfile", 25 | "Gemfile.lock", 26 | "LICENSE.txt", 27 | "README.md", 28 | "Rakefile", 29 | "VERSION", 30 | "lib/straight.rb", 31 | "lib/straight/address_providers/base.rb", 32 | "lib/straight/address_providers/bip32.rb", 33 | "lib/straight/blockchain_adapter.rb", 34 | "lib/straight/blockchain_adapters/biteasy_adapter.rb", 35 | "lib/straight/blockchain_adapters/blockchain_info_adapter.rb", 36 | "lib/straight/blockchain_adapters/mycelium_adapter.rb", 37 | "lib/straight/exchange_rate_adapter.rb", 38 | "lib/straight/exchange_rate_adapters/average_rate_adapter.rb", 39 | "lib/straight/exchange_rate_adapters/bitpay_adapter.rb", 40 | "lib/straight/exchange_rate_adapters/bitstamp_adapter.rb", 41 | "lib/straight/exchange_rate_adapters/btce_adapter.rb", 42 | "lib/straight/exchange_rate_adapters/coinbase_adapter.rb", 43 | "lib/straight/exchange_rate_adapters/kraken_adapter.rb", 44 | "lib/straight/exchange_rate_adapters/localbitcoins_adapter.rb", 45 | "lib/straight/exchange_rate_adapters/okcoin_adapter.rb", 46 | "lib/straight/gateway.rb", 47 | "lib/straight/order.rb", 48 | "straight.gemspec" 49 | ] 50 | s.homepage = "http://github.com/snitko/straight" 51 | s.licenses = ["MIT"] 52 | s.rubygems_version = "2.4.5" 53 | s.summary = "An engine for the Straight payment gateway software" 54 | 55 | if s.respond_to? :specification_version then 56 | s.specification_version = 4 57 | 58 | if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then 59 | s.add_runtime_dependency(%q, ["~> 1.0"]) 60 | s.add_runtime_dependency(%q, ["~> 0.1"]) 61 | s.add_runtime_dependency(%q, ["~> 0.13.5"]) 62 | s.add_development_dependency(%q, ["~> 1.0"]) 63 | s.add_development_dependency(%q, ["~> 2.0.1"]) 64 | s.add_development_dependency(%q, ["= 0.11.3"]) 65 | else 66 | s.add_dependency(%q, ["~> 1.0"]) 67 | s.add_dependency(%q, ["~> 0.1"]) 68 | s.add_dependency(%q, ["~> 0.13.5"]) 69 | s.add_dependency(%q, ["~> 1.0"]) 70 | s.add_dependency(%q, ["~> 2.0.1"]) 71 | s.add_dependency(%q, ["= 0.11.3"]) 72 | end 73 | else 74 | s.add_dependency(%q, ["~> 1.0"]) 75 | s.add_dependency(%q, ["~> 0.1"]) 76 | s.add_dependency(%q, ["~> 0.13.5"]) 77 | s.add_dependency(%q, ["~> 1.0"]) 78 | s.add_dependency(%q, ["~> 2.0.1"]) 79 | s.add_dependency(%q, ["= 0.11.3"]) 80 | end 81 | end 82 | 83 | -------------------------------------------------------------------------------- /spec/lib/address_providers/bip32_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe Straight::AddressProvider::Bip32 do 4 | 5 | before :each do 6 | @gateway = Straight::Gateway.new 7 | @bip32 = described_class.new(@gateway) 8 | end 9 | 10 | it "checks the depth of the xpub key and uses derivation notation according to the depth" do 11 | xpubs_by_depth = [ 12 | 'xpub661MyMwAqRbcFpmhdatYB5jtFnDCzAwM4trP5CvpH76sbDwEDr65ZLeoabjvZ74QJvLr85WQtpzsNE438ci8niFjLYfFL9ARMZvRyp6zBnZ', # BTC::Keychain.new(seed: 'test'), depth 0 13 | 'xpub696oZHFwkKZvnVw7D5N7ZuBVPZEhAZiJ4k39LszPtTF6fqHUgwhUhsKm87UVJ9ESugNxE16kBWqghHz8X7FYgMWjWLZDqGb2TprJu3DCdBB', # derived_keychain(0), depth 1 14 | 'xpub69yG7yz3Hw3uYvdEuW7vA6ovtQ873iLQvEuaLKUr7JEkK3NJ2G5ScS3CsiaXHpSauno5f7QVYfe8QKBSeGd8u3HSTxHjTxVwwJHXd9tBZh1', # derived_keychain(0), depth 2 15 | 'xpub6Bz6sYpLBA6PvDnmUsehRbUNmJWD8gWTg1Fuopyn7ChGRDZU7VNwBJMVFYZBwcSeHNJVPme1XQzBscwNzVk3eyByVfwY2sUM3BP1PQnHtK5', # derived_keychain(0), depth 3 16 | 'xpub6F1nL3gg1CJCbaCvqrDekgueaZaBjxwgMucarAJjuLcPVhMfgv4EzGU8Nj4MsmVAGdUASqEU9qFLG3PPRukLwystu1pdSLWkujmDQ4X43Dk', # derived_keychain(0), depth 4 17 | 'xpub6FwAPiwmPXe2a1zVkASULH1o3PYg7YRU32p2onywjqntCKuPZhotjemCrxjGymrcobVZu7Esfrs6eemuU7LhquLbuzHChYRz8R7q563AQFn', # derived_keychain(0), depth 5 18 | ] 19 | expected_addresses = [ 20 | '158nkHoQi9cNUEj2aefHWX9UYR1aqNG88Q', # derived_key('m/0/0') 21 | '1KN9CJvJGeM7z87jdze8kvdab1si5oMeDn', # derived_key('m/0/0') 22 | '17ScZf5WF2sPpjAo9MR5PLPNqBkrm4XrwC', # derived_key('m/0/0') 23 | '16uPb94ook3D5hXGpobyjaEnVznvYN3hMn', # derived_key('m/0/0') 24 | '16uPb94ook3D5hXGpobyjaEnVznvYN3hMn', # derived_key('0') 25 | '1nFN9omP2qkYeHPLgKXG5Eq2bFmGNift2', # derived_key('0') 26 | ] 27 | xpubs_by_depth.each_with_index do |xpub, i| 28 | @gateway.instance_variable_set :@keychain, nil 29 | @gateway.pubkey = xpub 30 | expect(@bip32.new_address(keychain_id: 0)).to eq expected_addresses[i] 31 | end 32 | end 33 | 34 | it "uses address_derivation_scheme if it's not blank" do 35 | @gateway.pubkey = 'xpub661MyMwAqRbcFpmhdatYB5jtFnDCzAwM4trP5CvpH76sbDwEDr65ZLeoabjvZ74QJvLr85WQtpzsNE438ci8niFjLYfFL9ARMZvRyp6zBnZ' 36 | @gateway.address_derivation_scheme = 'n' 37 | expect(@bip32.new_address(keychain_id: 0)).to eq "13VDxG5Dh7mb8c3ji7RZDgjKeYGHfxkyyz" 38 | @gateway.address_derivation_scheme = 'm/0/n' 39 | expect(@bip32.new_address(keychain_id: 0)).to eq "158nkHoQi9cNUEj2aefHWX9UYR1aqNG88Q" 40 | @gateway.address_derivation_scheme = 'M/0/N' 41 | %w{ 42 | 158nkHoQi9cNUEj2aefHWX9UYR1aqNG88Q 43 | 1Fan3Zb9Co6tjYdHDtBSHoi1xVF9eCfU2e 44 | 19FbGA3W16xXd8eJA2rwHUZSSkUwTQC4aZ 45 | 15NzdZeqrbsNqvKoy9rPSBXKUtVWfv8NfQ 46 | 113AtyN4MrF1347eDpwwFyxSsoz7S85Evc 47 | 13z5RZXCm3sEShf4idLBELxcxMWXkU6Eve 48 | 1FZoUZ2oHSMjboJoFbV7sPopyHN3A4yYCo 49 | 14YWFJjU7nGdsq8MrqX1aZKjmF8hdUW38U 50 | 1DBVG6RiWh72T3W3C3WjzoFAzSK7VvA3v5 51 | 1NiNoaeUEzhCP8xgAALXkx5sRPLmsTdNVn 52 | 1Lg9w6vAoUW4aQ28AThssa7TTKaT79gH9t 53 | 1LtePLSBD5ZxPFqgKBWkETXu5P9rMJJnrP 54 | 1BDRKTnExpdV1aeprrVN8TZsEHndHMhzhA 55 | 18hyL5bww1ZqFgPeWze1xuF86DdKqkQzUS 56 | 1BBuDxo22CMFiWcoc4h7MRHCQ4LbMaiGLe 57 | 19H8nGPcgM7QLFqn3Xr1WzjmVadkJhxjUw 58 | 1HzLnz5D7qADJpePFSeArDaGwdWyfnBbCo 59 | 15TVMTWbKKrqi1Zn2CHR36BLogNXDR37Vm 60 | 1L6mGCZXmN9qDUbupSa2DEcu9haYTeSwUX 61 | 1BcSPUisV1B4pskvJb2AGjis65QF1UfqZv 62 | }.each_with_index do |address, i| 63 | expect(@bip32.new_address(keychain_id: i)).to eq address 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /lib/straight/blockchain_adapters/blockchain_info_adapter.rb: -------------------------------------------------------------------------------- 1 | module Straight 2 | module Blockchain 3 | 4 | class BlockchainInfoAdapter < Adapter 5 | 6 | def self.mainnet_adapter 7 | instance = self.instance 8 | instance._initialize("https://blockchain.info") 9 | instance 10 | end 11 | 12 | def self.testnet_adapter 13 | raise "Not Supported Yet" 14 | end 15 | 16 | def _initialize(base_url) 17 | @latest_block = { cache_timestamp: nil, block: nil } 18 | @base_url = base_url 19 | end 20 | 21 | # Returns transaction info for the tid 22 | def fetch_transaction(tid, address: nil) 23 | straighten_transaction JSON.parse(api_request("/rawtx/#{tid}"), address: address) 24 | end 25 | 26 | # Returns all transactions for the address 27 | def fetch_transactions_for(address) 28 | transactions = JSON.parse(api_request("/rawaddr/#{address}"))['txs'] 29 | transactions.map { |t| straighten_transaction(t, address: address) } 30 | end 31 | 32 | # Returns the current balance of the address 33 | def fetch_balance_for(address) 34 | JSON.parse(api_request("/rawaddr/#{address}"))['final_balance'] 35 | end 36 | 37 | def latest_block(force_reload: false) 38 | # If we checked Blockchain.info latest block data 39 | # more than a minute ago, check again. Otherwise, use cached version. 40 | if @latest_block[:cache_timestamp].nil? || 41 | @latest_block[:cache_timestamp] < (Time.now - 60) || 42 | force_reload 43 | @latest_block = { 44 | cache_timestamp: Time.now, 45 | block: JSON.parse(api_request("/latestblock")) 46 | } 47 | else 48 | @latest_block 49 | end 50 | end 51 | 52 | private 53 | 54 | def api_request(url) 55 | attempts = 0 56 | begin 57 | attempts += 1 58 | response = HTTParty.get("#{@base_url}/#{url}", timeout: 4, verify: false) 59 | unless response.code == 200 60 | raise RequestError, "Cannot access remote API, response code was #{response.code}" 61 | end 62 | response.body 63 | rescue HTTParty::Error => e 64 | raise RequestError, YAML::dump(e) 65 | rescue JSON::ParserError => e 66 | raise RequestError, YAML::dump(e) 67 | rescue Net::ReadTimeout 68 | raise HTTParty::Error if atempts >= MAX_TRIES 69 | sleep 0.5 70 | retry 71 | end 72 | end 73 | 74 | # Converts transaction info received from the source into the 75 | # unified format expected by users of BlockchainAdapter instances. 76 | def straighten_transaction(transaction, address: nil) 77 | outs = [] 78 | total_amount = 0 79 | transaction['out'].each do |out| 80 | total_amount += out['value'] if address.nil? || address == out['addr'] 81 | outs << { amount: out['value'], receiving_address: out['addr'] } 82 | end 83 | 84 | { 85 | tid: transaction['hash'], 86 | total_amount: total_amount, 87 | confirmations: calculate_confirmations(transaction), 88 | outs: outs 89 | } 90 | end 91 | 92 | 93 | # When we call #calculate_confirmations, it doesn't always make a new 94 | # request to the blockchain API. Instead, it checks if cached_id matches the one in 95 | # the hash. It's useful when we want to calculate confirmations for all transactions for 96 | # a certain address without making any new requests to the Blockchain API. 97 | def calculate_confirmations(transaction, force_latest_block_reload: false) 98 | 99 | if transaction["block_height"] 100 | latest_block(force_reload: force_latest_block_reload)[:block]["height"] - transaction["block_height"] + 1 101 | else 102 | 0 103 | end 104 | 105 | end 106 | 107 | end 108 | 109 | end 110 | end 111 | -------------------------------------------------------------------------------- /spec/lib/blockchain_adapters/mycelium_adapter_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe Straight::Blockchain::MyceliumAdapter do 4 | 5 | subject(:adapter) { Straight::Blockchain::MyceliumAdapter.mainnet_adapter } 6 | 7 | before :all do 8 | VCR.insert_cassette 'blockchain_mycelium_adapter' 9 | end 10 | 11 | after :all do 12 | VCR.eject_cassette 13 | end 14 | 15 | it "fetches all transactions for the current address" do 16 | address = "3B1QZ8FpAaHBgkSB5gFt76ag5AW9VeP8xp" 17 | expect(adapter).to receive(:straighten_transaction).with(anything, address: address).at_least(:once) 18 | expect(adapter.fetch_transactions_for(address)).not_to be_empty 19 | end 20 | 21 | it "fetches the balance for a given address" do 22 | address = "1NX8bgWdPq2NahtTbTUAAdsTwpMpvt7nLy" 23 | expect(adapter.fetch_balance_for(address)).to be_kind_of(Integer) 24 | end 25 | 26 | it "fetches a single transaction" do 27 | tid = 'ae0d040f48d75fdc46d9035236a1782164857d6f0cca1f864640281115898560' 28 | expect(adapter.fetch_transaction(tid)[:total_amount]).to eq(832947) 29 | end 30 | 31 | it "calculates the number of confirmations for each transaction" do 32 | tid = 'ae0d040f48d75fdc46d9035236a1782164857d6f0cca1f864640281115898560' 33 | expect(adapter.fetch_transaction(tid)[:confirmations]).to be > 0 34 | end 35 | 36 | it "gets a transaction id among other data" do 37 | tid = 'ae0d040f48d75fdc46d9035236a1782164857d6f0cca1f864640281115898560' 38 | expect(adapter.fetch_transaction(tid)[:tid]).to eq(tid) 39 | end 40 | 41 | it "gets the latest block number" do 42 | expect(adapter.latest_block[:block]["height"]).to be_kind_of(Integer) 43 | end 44 | 45 | it "caches latestblock requests" do 46 | latest_block_response = double('Mycelium WAPI latest block response') 47 | expect(latest_block_response).to receive(:body).and_return('{ "r": { "height": 1 }}') 48 | faraday_mock = double("Faraday Request Mock") 49 | expect(faraday_mock).to receive(:post).and_return(latest_block_response) 50 | expect(Faraday).to receive(:new).once.and_return(faraday_mock) 51 | adapter.send(:calculate_confirmations, 1, force_latest_block_reload: true) 52 | adapter.send(:calculate_confirmations, 1) 53 | adapter.send(:calculate_confirmations, 1) 54 | adapter.send(:calculate_confirmations, 1) 55 | adapter.send(:calculate_confirmations, 1) 56 | end 57 | 58 | it "raises an exception when something goes wrong with fetching datd" do 59 | expect( -> { adapter.send(:api_request, "/a-404-request") }).to raise_error(Straight::Blockchain::Adapter::RequestError) 60 | end 61 | 62 | # For now disable singleton instances for adapters 63 | # it "uses the same Singleton instance" do 64 | # a = Straight::Blockchain::MyceliumAdapter.mainnet_adapter 65 | # b = Straight::Blockchain::MyceliumAdapter.mainnet_adapter 66 | # expect(a).to eq(b) 67 | # end 68 | 69 | it "using next server if previous failed" do 70 | block_response = double('Mycelium WAPI latest block response') 71 | expect(Faraday).to receive(:new).once.and_raise(Exception) 72 | begin 73 | adapter.send(:calculate_confirmations, 1) 74 | rescue Exception 75 | expect(adapter.instance_variable_get(:@base_url)).to eq(Straight::Blockchain::MyceliumAdapter::MAINNET_SERVERS[2]) 76 | end 77 | end 78 | 79 | it "raise errors if all servers failed" do 80 | response_mock = double("Faraday Response Mock") 81 | latest_block_response = double('Mycelium WAPI latest block response') 82 | expect(latest_block_response).to receive(:body).and_return('') 83 | faraday_mock = double("Faraday Request Mock") 84 | expect(faraday_mock).to receive(:post).and_return(latest_block_response) 85 | expect(Faraday).to receive(:new).once.and_return(faraday_mock) 86 | expect { 87 | adapter.send(:calculate_confirmations, 1) 88 | }.to raise_error(Straight::Blockchain::Adapter::RequestError) 89 | end 90 | 91 | it "fetches data from testnet for specific address" do 92 | VCR.use_cassette "wapitestnet" do 93 | adapter = Straight::Blockchain::MyceliumAdapter.testnet_adapter 94 | address = "mjRmkmYzvZN3cA3aBKJgYJ65epn3WCG84H" 95 | expect(adapter).to receive(:straighten_transaction).with(anything, address: address).at_least(:once).and_return(1) 96 | expect(adapter.fetch_transactions_for(address)).to eq([1]) 97 | end 98 | end 99 | 100 | end 101 | -------------------------------------------------------------------------------- /spec/lib/gateway_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe Straight::Gateway do 4 | 5 | before(:each) do 6 | @mock_adapter = double("mock blockchain adapter") 7 | allow(@mock_adapter).to receive(:testnet_adapter) 8 | @gateway = Straight::Gateway.new 9 | @gateway.pubkey = "pubkey" 10 | @gateway.order_class = "Straight::Order" 11 | @gateway.blockchain_adapters = [@mock_adapter] 12 | @gateway.status_check_schedule = Straight::Gateway::DEFAULT_STATUS_CHECK_SCHEDULE 13 | @gateway.order_callbacks = [] 14 | end 15 | 16 | it "shares exchange rate adapter(s) instances between all/multiple gateway instances" do 17 | gateway_2 = Straight::Gateway.new.tap do |g| 18 | g.pubkey = "pubkey" 19 | g.order_class = "Straight::Order" 20 | g.blockchain_adapters = [@mock_adapter] 21 | g.status_check_schedule = Straight::Gateway::DEFAULT_STATUS_CHECK_SCHEDULE 22 | g.order_callbacks = [] 23 | end 24 | # Checking if exchange rate adapters are the same objects for both gateways 25 | @gateway.instance_variable_get(:@exchange_rate_adapters).each_with_index do |adapter, i| 26 | expect(gateway_2.instance_variable_get(:@exchange_rate_adapters)[i]).to be adapter 27 | end 28 | end 29 | 30 | it "passes methods on to the available adapter" do 31 | @gateway.instance_variable_set('@blockchain_adapters', [@mock_adapter]) 32 | expect(@mock_adapter).to receive(:fetch_transaction).once 33 | @gateway.fetch_transaction("xxx") 34 | end 35 | 36 | it "uses the next availabale adapter when something goes wrong with the current one" do 37 | another_mock_adapter = double("another_mock blockchain adapter") 38 | @gateway.instance_variable_set('@blockchain_adapters', [@mock_adapter, another_mock_adapter]) 39 | allow(@mock_adapter).to receive(:fetch_transaction).once.and_raise(Straight::StraightError) 40 | expect(another_mock_adapter).to receive(:fetch_transaction).once 41 | @gateway.fetch_transaction("xxx") 42 | end 43 | 44 | it "creates new orders and addresses for them" do 45 | @gateway.pubkey = 'xpub661MyMwAqRbcFhUeRviyfia1NdfX4BAv5zCsZ6HqsprRjdBDK8vwh3kfcnTvqNbmi5S1yZ5qL9ugZTyVqtyTZxccKZzMVMCQMhARycvBZvx' 46 | expected_address = '1NEvrcxS3REbJgup8rMA4QvMFFSdWTLvM' 47 | expect(@gateway.new_order(amount: 1, keychain_id: 1).address).to eq(expected_address) 48 | end 49 | 50 | it "calls all the order callbacks" do 51 | callback1 = double('callback1') 52 | callback2 = double('callback1') 53 | @gateway.pubkey = BTC::Keychain.new(seed: 'test').xpub 54 | @gateway.order_callbacks = [callback1, callback2] 55 | 56 | order = @gateway.new_order(amount: 1, keychain_id: 1) 57 | expect(callback1).to receive(:call).with(order) 58 | expect(callback2).to receive(:call).with(order) 59 | @gateway.order_status_changed(order) 60 | end 61 | 62 | describe "exchange rate calculation" do 63 | 64 | it "sets order amount in satoshis calculated from another currency" do 65 | adapter = Straight::ExchangeRate::BitpayAdapter.instance 66 | allow(adapter).to receive(:rate_for).and_return(450.5412) 67 | @gateway.exchange_rate_adapters = [adapter] 68 | expect(@gateway.amount_from_exchange_rate(2252.706, currency: 'USD')).to eq(500000000) 69 | end 70 | 71 | it "tries various exchange adapters until one of them actually returns an exchange rate" do 72 | adapter1 = Straight::ExchangeRate::BitpayAdapter.instance 73 | adapter2 = Straight::ExchangeRate::BitpayAdapter.instance 74 | allow(adapter1).to receive(:rate_for).and_return( -> { raise "connection problem" }) 75 | allow(adapter2).to receive(:rate_for).and_return(450.5412) 76 | @gateway.exchange_rate_adapters = [adapter1, adapter2] 77 | expect(@gateway.amount_from_exchange_rate(2252.706, currency: 'USD')).to eq(500000000) 78 | end 79 | 80 | it "converts btc denomination into satoshi if provided with :btc_denomination" do 81 | expect(@gateway.amount_from_exchange_rate(5, currency: 'BTC', btc_denomination: :btc)).to eq(500000000) 82 | end 83 | 84 | it "accepts string as amount and converts it properly" do 85 | expect(@gateway.amount_from_exchange_rate('0.5', currency: 'BTC', btc_denomination: :btc)).to eq(50000000) 86 | end 87 | 88 | it "simply fetches current exchange rate for 1 BTC" do 89 | @adapter = @gateway.exchange_rate_adapters[-1] 90 | allow(@adapter).to receive(:get_rate_value_from_hash).and_return('21.5') 91 | expect(@gateway.current_exchange_rate('USD')).not_to be_nil 92 | end 93 | 94 | end 95 | 96 | describe "test mode" do 97 | 98 | let(:testnet_adapters) { [Straight::Blockchain::MyceliumAdapter.testnet_adapter] } 99 | 100 | it "is not activated on initialize" do 101 | expect(@gateway.test_mode).to be false 102 | end 103 | 104 | it "is using testnet" do 105 | @gateway.test_mode = true 106 | allow(@mock_adapter).to receive(:testnet_adapters).and_return(true) 107 | expect(@gateway.blockchain_adapters).to eq(@gateway.test_blockchain_adapters) 108 | end 109 | 110 | it "is disabled and return previous saved adapters" do 111 | expect(@gateway.blockchain_adapters).to eq([@mock_adapter]) 112 | end 113 | 114 | it "generate get keychain in testnet" do 115 | 116 | end 117 | it "creates new orders and addresses for them" do 118 | @gateway.pubkey = 'tpubDCzMzH5R7dvZAN7jNyZRUXxuo8XdRmMd7gmzvHs9LYG4w2EBvEjQ1Drm8ZXv4uwxrtUh3MqCZQJaq56oPMghsbtFnoLi9JBfG7vRLXLH21r' 119 | expected_address = '1LUCZQ5habZZMRz6XeSqpAQUZEULggzzgE' 120 | expect(@gateway.new_order(amount: 1, keychain_id: 1).address).to eq(expected_address) 121 | end 122 | 123 | 124 | end 125 | 126 | end 127 | -------------------------------------------------------------------------------- /lib/straight/blockchain_adapters/mycelium_adapter.rb: -------------------------------------------------------------------------------- 1 | module Straight 2 | module Blockchain 3 | 4 | class MyceliumAdapter < Adapter 5 | 6 | MAINNET_SERVERS = ["https://mws2.mycelium.com/wapi/wapi", 7 | "https://mws6.mycelium.com/wapi/wapi", 8 | "https://mws7.mycelium.com/wapi/wapi"] 9 | TESTNET_SERVERS = ["https://node3.mycelium.com/wapitestnet/wapi"] 10 | 11 | def self.mainnet_adapter 12 | instance = new 13 | instance._initialize(MAINNET_SERVERS) 14 | instance 15 | end 16 | 17 | def self.testnet_adapter 18 | instance = new 19 | instance._initialize(TESTNET_SERVERS) 20 | instance 21 | end 22 | 23 | def _initialize(servers) 24 | @latest_block = { cache_timestamp: nil, block: nil } 25 | @api_servers = servers 26 | set_base_url 27 | end 28 | 29 | # Set url for API request. 30 | # @param num [Integer] a number of server in array 31 | def set_base_url(num = 0) 32 | return nil if num >= @api_servers.size 33 | @base_url = @api_servers[num] 34 | end 35 | 36 | def next_server 37 | set_base_url(@api_servers.index(@base_url) + 1) 38 | end 39 | 40 | # Returns transaction info for the tid 41 | def fetch_transaction(tid, address: nil) 42 | transaction = api_request('getTransactions', { txIds: [tid] })['transactions'].first 43 | straighten_transaction transaction, address: address 44 | end 45 | 46 | # Supposed to returns all transactions for the address, but 47 | # currently actually returns the first one, since we only need one. 48 | def fetch_transactions_for(address) 49 | # API may return nil instead of an empty array if address turns out to be invalid 50 | # (for example when trying to supply a testnet address instead of mainnet while using 51 | # mainnet adapter. 52 | if api_response = api_request('queryTransactionInventory', { addresses: [address], limit: 1 }) 53 | tid = api_response["txIds"].first 54 | tid ? [fetch_transaction(tid, address: address)] : [] 55 | else 56 | raise BitcoinAddressInvalid, message: "address in question: #{address}" 57 | end 58 | end 59 | 60 | # Returns the current balance of the address 61 | def fetch_balance_for(address) 62 | unspent = 0 63 | api_request('queryUnspentOutputs', { addresses: [address]})['unspent'].each do |out| 64 | unspent += out['value'] 65 | end 66 | unspent 67 | end 68 | 69 | def latest_block(force_reload: false) 70 | # If we checked Blockchain.info latest block data 71 | # more than a minute ago, check again. Otherwise, use cached version. 72 | if @latest_block[:cache_timestamp].nil? || 73 | @latest_block[:cache_timestamp] < (Time.now - 60) || 74 | force_reload 75 | @latest_block = { 76 | cache_timestamp: Time.now, 77 | block: api_request('queryUnspentOutputs', { addresses: []} ) 78 | } 79 | else 80 | @latest_block 81 | end 82 | end 83 | 84 | private 85 | 86 | def api_request(method, params={}) 87 | begin 88 | conn = Faraday.new(url: "#{@base_url}/#{method}", ssl: { verify: false }) do |faraday| 89 | faraday.request :url_encoded # form-encode POST params 90 | faraday.adapter Faraday.default_adapter # make requests with Net::HTTP 91 | end 92 | result = conn.post do |req| 93 | req.body = params.merge({version: 1}).to_json 94 | req.headers['Content-Type'] = 'application/json' 95 | end 96 | JSON.parse(result.body || '')['r'] 97 | rescue JSON::ParserError => e 98 | raise RequestError, YAML::dump(e) 99 | rescue Exception => e 100 | next_server ? retry : (raise e) 101 | end 102 | end 103 | 104 | # Converts transaction info received from the source into the 105 | # unified format expected by users of BlockchainAdapter instances. 106 | def straighten_transaction(transaction, address: nil) 107 | # Get the block number this transaction was included into 108 | block_height = transaction['height'] 109 | tid = transaction['txid'] 110 | 111 | # Converting from Base64 to binary 112 | transaction = transaction['binary'].unpack('m0')[0] 113 | 114 | # Decoding 115 | transaction = BTC::Transaction.new(data: transaction) 116 | 117 | outs = [] 118 | total_amount = 0 119 | 120 | transaction.outputs.each do |out| 121 | amount = out.value 122 | receiving_address = out.script.standard_address 123 | total_amount += amount if address.nil? || address == receiving_address.to_s 124 | outs << {amount: amount, receiving_address: receiving_address} 125 | end 126 | 127 | { 128 | tid: tid, 129 | total_amount: total_amount.to_i, 130 | confirmations: calculate_confirmations(block_height), 131 | outs: outs 132 | } 133 | end 134 | 135 | # When we call #calculate_confirmations, it doesn't always make a new 136 | # request to the blockchain API. Instead, it checks if cached_id matches the one in 137 | # the hash. It's useful when we want to calculate confirmations for all transactions for 138 | # a certain address without making any new requests to the Blockchain API. 139 | def calculate_confirmations(block_height, force_latest_block_reload: false) 140 | 141 | if block_height && block_height != -1 142 | latest_block(force_reload: force_latest_block_reload)[:block]["height"] - block_height + 1 143 | else 144 | 0 145 | end 146 | 147 | end 148 | 149 | end 150 | 151 | end 152 | end 153 | -------------------------------------------------------------------------------- /spec/lib/order_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe Straight::Order do 4 | 5 | class Straight::Order 6 | 7 | def status=(new_value) 8 | # we later make sure this method also gets called 9 | @original_status_setter_called = true 10 | end 11 | 12 | end 13 | 14 | before(:each) do 15 | @gateway = double("Straight Gateway mock") 16 | @order = Straight::Order.new 17 | @order.amount = 10 18 | @order.gateway = @gateway 19 | @order.address = 'address' 20 | @order.keychain_id = 1 21 | allow(@gateway).to receive(:order_status_changed).with(@order) 22 | allow(@gateway).to receive(:fetch_transactions_for).with(@order.address).and_return([{ tid: 'xxx', total_amount: 10}]) 23 | end 24 | 25 | it "follows status check schedule" do 26 | allow(@gateway).to receive(:fetch_transactions_for).with('address').and_return([]) 27 | allow(@gateway).to receive(:status_check_schedule).and_return(Straight::Gateway::DEFAULT_STATUS_CHECK_SCHEDULE) 28 | [10, 20, 40, 80, 160, 320, 640].each do |i| 29 | expect(@order).to receive(:sleep).with(i).exactly(20).times 30 | end 31 | @order.start_periodic_status_check(duration: 25400) 32 | end 33 | 34 | it "gets the last transaction for the current address, caches the request" do 35 | expect(@gateway).to receive(:fetch_transactions_for).with(@order.address).once.and_return(true) 36 | @order.transactions 37 | @order.transactions 38 | end 39 | 40 | it "gets all transactions for the current address, caches the request" do 41 | expect(@gateway).to receive(:fetch_transactions_for).with(@order.address).once.and_return(['t1', 't2']) 42 | expect(@order.transaction).to eq('t1') 43 | expect(@order.transaction).to eq('t1') 44 | end 45 | 46 | it "sets transaction id when the status is changed from 0" do 47 | @order.status = 2 48 | expect(@order.tid).to eq('xxx') # xxx because that's what we set in the allow() in before(:each) block 49 | end 50 | 51 | it "displays order attributes as json" do 52 | allow(@order).to receive(:status).and_return(1) 53 | expect(@order.to_json).to eq('{"status":1,"amount":10,"address":"address","tid":null}') 54 | end 55 | 56 | it "returns amount in btc as a string" do 57 | @order.amount = 1 58 | expect(@order.amount_in_btc).to eq(0.00000001) 59 | expect(@order.amount_in_btc(as: :string)).to eq('0.00000001') 60 | end 61 | 62 | it "returns amount_paid in btc as a string" do 63 | @order.amount_paid = 1 64 | expect(@order.amount_in_btc(field: @order.amount_paid)).to eq(0.00000001) 65 | expect(@order.amount_in_btc(field: @order.amount_paid, as: :string)).to eq("0.00000001") 66 | end 67 | 68 | describe "assigning statuses" do 69 | 70 | before(:each) do 71 | allow(@gateway).to receive(:confirmations_required).and_return(1) 72 | end 73 | 74 | it "doesn't reload the transaction unless forced" do 75 | @order.instance_variable_set(:@status, 2) 76 | expect(@order).to_not receive(:transaction) 77 | @order.status 78 | end 79 | 80 | it "sets status to :new upon order creation" do 81 | expect(@order.instance_variable_get(:@status)).to eq(0) 82 | expect(@order.instance_variable_get(:@old_status)).to eq(nil) 83 | end 84 | 85 | it "sets status to :new if no transaction issued" do 86 | expect(@order).to receive(:transaction).at_most(3).times.and_return(nil) 87 | expect(@order.status(reload: true)).to eq(0) 88 | expect(@order.status(as_sym: true)).to eq :new 89 | end 90 | 91 | it "sets status to :unconfirmed if transaction doesn't have enough confirmations" do 92 | transaction = { confirmations: 0 } 93 | expect(@order).to receive(:transaction).at_most(3).times.and_return(transaction) 94 | expect(@order.status(reload: true)).to eq(1) 95 | end 96 | 97 | it "sets status to :paid if transaction has enough confirmations and the amount is correct" do 98 | transaction = { confirmations: 1, total_amount: @order.amount } 99 | expect(@order).to receive(:transaction).at_most(3).times.and_return(transaction) 100 | expect(@order.status(reload: true)).to eq(2) 101 | expect(@order.status(as_sym: true)).to eq :paid 102 | end 103 | 104 | it "sets status to :underpaid if the total amount in a transaction is less than the amount of order" do 105 | transaction = { confirmations: 1, total_amount: @order.amount-1 } 106 | expect(@order).to receive(:transaction).at_most(3).times.and_return(transaction) 107 | expect(@order.status(reload: true)).to eq(3) 108 | end 109 | 110 | it "sets status to :overderpaid if the total amount in a transaction is more than the amount of order" do 111 | transaction = { confirmations: 1, total_amount: @order.amount+1 } 112 | expect(@order).to receive(:transaction).at_most(3).times.and_return(transaction) 113 | expect(@order.status(reload: true)).to eq(4) 114 | end 115 | 116 | it "invokes a callback on the gateway when status changes" do 117 | transaction = { confirmations: 1, total_amount: @order.amount } 118 | allow(@order).to receive(:transaction).and_return(transaction) 119 | expect(@gateway).to receive(:order_status_changed).with(@order) 120 | @order.status(reload: true) 121 | end 122 | 123 | it "calls the original status setter of the class that the module is included into" do 124 | expect(@order.instance_variable_get(:@original_status_setter_called)).to be_falsy 125 | @order.status = 1 126 | expect(@order.instance_variable_get(:@original_status_setter_called)).to be_truthy 127 | end 128 | 129 | it "saves the old status in the old_status property" do 130 | @order.status = 2 131 | expect(@order.old_status).to eq(0) 132 | end 133 | 134 | it 'is have be nil in amount_paid if order not paid' do 135 | @order.status 136 | expect(@order.amount_paid).to eq(nil) 137 | end 138 | 139 | it 'is have amount_paid set to total_amount if order paid' do 140 | transaction = { confirmations: 100, total_amount: 10 } 141 | allow(@order).to receive(:transaction).and_return(transaction) 142 | 143 | @order.status(reload: true) 144 | expect(@order.amount_paid).to eq(10) 145 | end 146 | 147 | end 148 | 149 | end 150 | -------------------------------------------------------------------------------- /spec/fixtures/vcr/exchange_rate_localbitcoins_adapter.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: get 5 | uri: https://localbitcoins.com/bitcoinaverage/ticker-all-currencies/ 6 | body: 7 | encoding: US-ASCII 8 | string: '' 9 | headers: 10 | Accept-Encoding: 11 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 12 | Accept: 13 | - "*/*" 14 | User-Agent: 15 | - Ruby 16 | response: 17 | status: 18 | code: 200 19 | message: OK 20 | headers: 21 | Server: 22 | - cloudflare-nginx 23 | Date: 24 | - Mon, 22 Jun 2015 14:10:32 GMT 25 | Content-Type: 26 | - application/json 27 | Transfer-Encoding: 28 | - chunked 29 | Connection: 30 | - keep-alive 31 | Set-Cookie: 32 | - __cfduid=d3dbc0404e4d267b836e4997004926bf71434982232; expires=Tue, 21-Jun-16 33 | 14:10:32 GMT; path=/; domain=.localbitcoins.com; HttpOnly 34 | - django_language=en 35 | - localbitcoinssession=None; Domain=localbitcoins.com; expires=Mon, 22-Jun-2015 36 | 20:10:32 GMT; httponly; Max-Age=21600; Path=/; secure 37 | X-Content-Type-Options: 38 | - nosniff 39 | Content-Language: 40 | - en 41 | Strict-Transport-Security: 42 | - max-age=31536000; includeSubDomains 43 | Vary: 44 | - Accept-Language,Cookie 45 | X-Frame-Options: 46 | - DENY 47 | Cf-Ray: 48 | - 1fa88986a2770a5a-ARN 49 | body: 50 | encoding: ASCII-8BIT 51 | string: '{"ZAR": {"volume_btc": "88.58", "rates": {"last": "3739.61"}, "avg_1h": 52 | 3885.3916333565144, "avg_24h": 3779.2450448063473, "avg_12h": 3795.3951086149787}, 53 | "GBP": {"volume_btc": "673.63", "rates": {"last": "165.20"}, "avg_1h": 165.22515540287347, 54 | "avg_24h": 160.919271063879, "avg_12h": 162.71870120643266}, "USD": {"volume_btc": 55 | "820.60", "rates": {"last": "253.50"}, "avg_1h": 276.2394528995401, "avg_24h": 56 | 295.2421813585169, "avg_12h": 285.7764867342024}, "EUR": {"volume_btc": "554.07", 57 | "rates": {"last": "216.67"}, "avg_1h": 227.55145712690702, "avg_24h": 209.7161676625416, 58 | "avg_12h": 207.75169549642152}, "RUB": {"volume_btc": "226.75", "rates": {"last": 59 | "14107.03"}, "avg_1h": 14041.683916726943, "avg_24h": 13825.710205304587, 60 | "avg_12h": 13860.924280212983}, "CZK": {"volume_btc": "2.11", "rates": {"last": 61 | "6086.43"}, "avg_1h": 6086.43, "avg_24h": 5971.985245125876, "avg_12h": 5936.248157091048}, 62 | "MYR": {"volume_btc": "17.14", "rates": {"last": "2200.22"}, "avg_1h": 1349.5260627530365, 63 | "avg_24h": 964.9735515370705, "avg_12h": 963.8449197162815}, "VEF": {"volume_btc": 64 | "4.68", "rates": {"last": "116279.07"}, "avg_1h": 0, "avg_24h": 116375.28697868338, 65 | "avg_12h": 116279.07}, "AUD": {"volume_btc": "346.83", "rates": {"last": "390.00"}, 66 | "avg_1h": 349.6846390192452, "avg_24h": 332.2108522505623, "avg_12h": 331.9539576973287}, 67 | "RON": {"volume_btc": "7.26", "rates": {"last": "922.39"}, "avg_1h": 935.580697384807, 68 | "avg_24h": 925.326271702585, "avg_12h": 930.23755968004}, "SEK": {"volume_btc": 69 | "85.05", "rates": {"last": "1926.18"}, "avg_1h": 0, "avg_24h": 2070.678314310035, 70 | "avg_12h": 2084.101771119344}, "HRK": {"volume_btc": "0.28", "rates": {"last": 71 | "1733.95"}, "avg_1h": 0, "avg_24h": 1735.011853832442, "avg_12h": 1733.95}, 72 | "THB": {"volume_btc": "68.38", "rates": {"last": "8846.58"}, "avg_1h": 8826.159936511553, 73 | "avg_24h": 8596.323150042701, "avg_12h": 8847.262272389526}, "CNY": {"volume_btc": 74 | "2.40", "rates": {"last": "1469.78"}, "avg_1h": 0, "avg_24h": 1543.540706994959, 75 | "avg_12h": 1536.0356773548526}, "INR": {"volume_btc": "33.02", "rates": {"last": 76 | "15657.89"}, "avg_1h": 15552.123731338002, "avg_24h": 15613.72395310243, "avg_12h": 77 | 15673.440392111386}, "CAD": {"volume_btc": "41.52", "rates": {"last": "330.36"}, 78 | "avg_1h": 330.36, "avg_24h": 296.3621451815311, "avg_12h": 304.2733766198631}, 79 | "PLN": {"volume_btc": "1.36", "rates": {"last": "929.02"}, "avg_1h": 0, "avg_24h": 80 | 935.8568377641947, "avg_12h": 929.02}, "MXN": {"volume_btc": "2.08", "rates": 81 | {"last": "4184.10"}, "avg_1h": 4184.1, "avg_24h": 3881.690549798503, "avg_12h": 82 | 4184.1}, "NOK": {"volume_btc": "8.13", "rates": {"last": "2032.33"}, "avg_1h": 83 | 0, "avg_24h": 2036.5666374010132, "avg_12h": 2026.8080469053687}, "PKR": {"volume_btc": 84 | "0.22", "rates": {"last": "22244.89"}, "avg_1h": 0, "avg_24h": 22244.89, "avg_12h": 85 | 0}, "BRL": {"volume_btc": "0.06", "rates": {"last": "793.65"}, "avg_1h": 0, 86 | "avg_24h": 793.65, "avg_12h": 0}, "NZD": {"volume_btc": "19.66", "rates": 87 | {"last": "379.12"}, "avg_1h": 0, "avg_24h": 374.51214368187345, "avg_12h": 88 | 376.03122979984664}, "ARS": {"volume_btc": "2.74", "rates": {"last": "2901.25"}, 89 | "avg_1h": 2901.25, "avg_24h": 3060.145169417263, "avg_12h": 3097.594482905528}, 90 | "COP": {"volume_btc": "0.46", "rates": {"last": "635129.22"}, "avg_1h": 0, 91 | "avg_24h": 635129.22, "avg_12h": 0}, "HKD": {"volume_btc": "9.60", "rates": 92 | {"last": "1974.77"}, "avg_1h": 0, "avg_24h": 1937.0098630907141, "avg_12h": 93 | 1937.0098630907141}, "PHP": {"volume_btc": "33.08", "rates": {"last": "10752.08"}, 94 | "avg_1h": 0, "avg_24h": 11020.184466302666, "avg_12h": 11020.184466302666}, 95 | "JPY": {"volume_btc": "0.11", "rates": {"last": "45045.05"}, "avg_1h": 0, 96 | "avg_24h": 45085.66357078449, "avg_12h": 45085.66357078449}, "CHF": {"volume_btc": 97 | "28.25", "rates": {"last": "273.00"}, "avg_1h": 0, "avg_24h": 234.06784167466165, 98 | "avg_12h": 234.06784167466165}, "SGD": {"volume_btc": "6.12", "rates": {"last": 99 | "303.95"}, "avg_1h": 303.95, "avg_24h": 329.64863681657056, "avg_12h": 329.64863681657056}, 100 | "KES": {"volume_btc": "0.30", "rates": {"last": "26281.21"}, "avg_1h": 0, 101 | "avg_24h": 26281.21, "avg_12h": 26281.21}, "HUF": {"volume_btc": "4.04", "rates": 102 | {"last": "72115.38"}, "avg_1h": 0, "avg_24h": 76576.12958886796, "avg_12h": 103 | 76576.12958886796}, "TZS": {"volume_btc": "0.39", "rates": {"last": "636942.68"}, 104 | "avg_1h": 0, "avg_24h": 636942.68, "avg_12h": 636942.68}, "UAH": {"volume_btc": 105 | "0.81", "rates": {"last": "6410.26"}, "avg_1h": 0, "avg_24h": 5173.366497973222, 106 | "avg_12h": 5173.366497973222}, "DKK": {"volume_btc": "0.35", "rates": {"last": 107 | "1988.07"}, "avg_1h": 0, "avg_24h": 1988.07, "avg_12h": 1988.07}}' 108 | http_version: 109 | recorded_at: Mon, 22 Jun 2015 14:10:32 GMT 110 | recorded_with: VCR 2.9.3 111 | -------------------------------------------------------------------------------- /lib/straight/gateway.rb: -------------------------------------------------------------------------------- 1 | module Straight 2 | 3 | # This module should be included into your own class to extend it with Gateway functionality. 4 | # For example, if you have a ActiveRecord model called Gateway, you can include GatewayModule into it 5 | # and you'll now be able to do everything Straight::Gateway can do, but you'll also get AR Database storage 6 | # funcionality, its validations etc. 7 | # 8 | # The right way to implement this would be to do it the other way: inherit from Straight::Gateway, then 9 | # include ActiveRecord, but at this point ActiveRecord doesn't work this way. Furthermore, some other libraries, like Sequel, 10 | # also require you to inherit from them. Thus, the module. 11 | # 12 | # When this module is included, it doesn't actually *include* all the methods, some are prepended (see Ruby docs on #prepend). 13 | # It is important specifically for getters and setters and as a general rule only getters and setters are prepended. 14 | # 15 | # If you don't want to bother yourself with modules, please use Straight::Gateway class and simply create new instances of it. 16 | # However, if you are contributing to the library, all new funcionality should go to either Straight::GatewayModule::Includable or 17 | # Straight::GatewayModule::Prependable (most likely the former). 18 | module GatewayModule 19 | 20 | # Raised when adapter's list (either Exchange or Blockchain adapters) is empty 21 | class NoAdaptersAvailable < StraightError;end 22 | class OrderAmountInvalid < StraightError;end 23 | 24 | # Only add getters and setters for those properties in the extended class 25 | # that don't already have them. This is very useful with ActiveRecord for example 26 | # where we don't want to override AR getters and setters that set attributes. 27 | def self.included(base) 28 | base.class_eval do 29 | [ 30 | :pubkey, 31 | :test_pubkey, 32 | :confirmations_required, 33 | :status_check_schedule, 34 | :blockchain_adapters, 35 | :exchange_rate_adapters, 36 | :order_callbacks, 37 | :order_class, 38 | :default_currency, 39 | :name, 40 | :address_provider, 41 | :address_provider_type, 42 | :address_derivation_scheme, 43 | :test_mode 44 | ].each do |field| 45 | attr_reader field unless base.method_defined?(field) 46 | attr_writer field unless base.method_defined?("#{field}=") 47 | prepend Prependable 48 | include Includable 49 | end 50 | end 51 | end 52 | 53 | # Determines the algorithm for consequitive checks of the order status. 54 | DEFAULT_STATUS_CHECK_SCHEDULE = -> (period, iteration_index) do 55 | iteration_index += 1 56 | if iteration_index >= 20 57 | period *= 2 58 | iteration_index = 0 59 | end 60 | return { period: period, iteration_index: iteration_index } 61 | end 62 | 63 | # If you are defining methods in this module, it means you most likely want to 64 | # call super() somehwere inside those methods. 65 | # 66 | # In short, the idea is to let the class we're being prepended to do its magic 67 | # after our methods are finished. 68 | module Prependable 69 | end 70 | 71 | module Includable 72 | 73 | def blockchain_adapters 74 | return test_blockchain_adapters if test_mode 75 | @blockchain_adapters 76 | end 77 | 78 | def test_blockchain_adapters 79 | @blockchain_adapters.map{ |el| el.class.testnet_adapter rescue next }.compact 80 | end 81 | 82 | # Creates a new order for the address derived from the pubkey and the keychain_id argument provided. 83 | # See explanation of this keychain_id argument is in the description for the AddressProvider::Base#new_address method. 84 | def new_order(args) 85 | 86 | # Args: amount:, keychain_id: nil, currency: nil, btc_denomination: :satoshi 87 | # 88 | # The reason these arguments are supplied as a hash and not as named arguments 89 | # is because we don't know in advance which arguments are required for a particular 90 | # AddressAdapter. So we accpet all, check manually for required ones like :amount, 91 | # set default values where needed and then hand them all to address_adapter. 92 | if args[:amount].nil? || !args[:amount].kind_of?(Numeric) || args[:amount] <= 0 93 | raise OrderAmountInvalid, "amount cannot be nil and should be more than 0" 94 | end 95 | # Setting default values 96 | args[:currency] ||= default_currency 97 | args[:btc_denomination] ||= :satoshi 98 | 99 | amount = args[:amount_from_exchange_rate] = amount_from_exchange_rate( 100 | args[:amount], 101 | currency: args[:currency], 102 | btc_denomination: args[:btc_denomination] 103 | ) 104 | 105 | if address_provider.takes_fees? 106 | address, amount = address_provider.new_address_and_amount(**args) 107 | else 108 | address = address_provider.new_address(**args) 109 | end 110 | 111 | order = Kernel.const_get(order_class).new 112 | order.gateway = self 113 | order.keychain_id = args[:keychain_id] 114 | order.address = address 115 | order.amount = amount 116 | order 117 | end 118 | 119 | def fetch_transaction(tid, address: nil) 120 | try_adapters(blockchain_adapters, type: "blockchain") { |b| b.fetch_transaction(tid, address: address) } 121 | end 122 | 123 | def fetch_transactions_for(address) 124 | try_adapters(blockchain_adapters, type: "blockchain", raise_exceptions: [Blockchain::Adapter::BitcoinAddressInvalid]) { |b| b.fetch_transactions_for(address) } 125 | end 126 | 127 | def fetch_balance_for(address) 128 | try_adapters(blockchain_adapters, type: "blockchain") { |b| b.fetch_balance_for(address) } 129 | end 130 | 131 | def keychain 132 | key = self.test_mode ? self.test_pubkey : self.pubkey 133 | @keychain ||= BTC::Keychain.new(xpub: key) 134 | end 135 | 136 | # This is a callback method called from each order 137 | # whenever an order status changes. 138 | def order_status_changed(order) 139 | @order_callbacks.each do |c| 140 | c.call(order) 141 | end 142 | end 143 | 144 | # Gets exchange rates from one of the exchange rate adapters, 145 | # then calculates how much BTC does the amount in the given currency represents. 146 | # 147 | # You can also feed this method various bitcoin denominations. 148 | # It will always return amount in Satoshis. 149 | def amount_from_exchange_rate(amount, currency:, btc_denomination: :satoshi) 150 | currency = self.default_currency if currency.nil? 151 | btc_denomination = :satoshi if btc_denomination.nil? 152 | currency = currency.to_s.upcase 153 | if currency == 'BTC' 154 | return Satoshi.new(amount, from_unit: btc_denomination).to_i 155 | end 156 | 157 | try_adapters(@exchange_rate_adapters, type: "exchange rate") do |a| 158 | a.convert_from_currency(amount, currency: currency) 159 | end 160 | end 161 | 162 | def current_exchange_rate(currency=self.default_currency) 163 | currency = currency.to_s.upcase 164 | try_adapters(@exchange_rate_adapters, type: "exchange rate") do |a| 165 | a.rate_for(currency) 166 | end 167 | end 168 | 169 | def test_pubkey_missing? 170 | address_provider_type == :Bip32 && test_mode && test_pubkey.to_s.empty? 171 | end 172 | 173 | def pubkey_missing? 174 | address_provider_type == :Bip32 && !test_mode && pubkey.to_s.empty? 175 | end 176 | 177 | def address_provider_type 178 | @address_provider ? @address_provider.class.name.split('::')[-1].to_sym : :Bip32 179 | end 180 | 181 | private 182 | 183 | # Calls the block with each adapter until one of them does not fail. 184 | # Fails with the last exception. 185 | def try_adapters(adapters, type: nil, raise_exceptions: [], &block) 186 | 187 | # TODO: specify which adapters are unavailable (blockchain or exchange rate) 188 | raise NoAdaptersAvailable, "the list of #{type} adapters is empty or nil" if adapters.nil? || adapters.empty? 189 | 190 | last_exception = nil 191 | adapters.each do |adapter| 192 | begin 193 | result = yield(adapter) 194 | last_exception = nil 195 | return result 196 | rescue => e 197 | raise e if raise_exceptions.include?(e) 198 | last_exception = e 199 | # If an Exception is raised, it passes on 200 | # to the next adapter and attempts to call a method on it. 201 | end 202 | end 203 | raise last_exception if last_exception 204 | end 205 | 206 | end 207 | 208 | end 209 | 210 | 211 | class Gateway 212 | 213 | include GatewayModule 214 | 215 | def initialize 216 | @default_currency = 'BTC' 217 | @blockchain_adapters = [ 218 | Blockchain::BlockchainInfoAdapter.mainnet_adapter, 219 | Blockchain::MyceliumAdapter.mainnet_adapter, 220 | Blockchain::InsightAdapter.mainnet_adapter(main_url: "https://insight.mycelium.com/api") 221 | ] 222 | @exchange_rate_adapters = [ 223 | ExchangeRate::BitpayAdapter.instance, 224 | ExchangeRate::CoinbaseAdapter.instance, 225 | ExchangeRate::BitstampAdapter.instance, 226 | ExchangeRate::BtceAdapter.instance, 227 | ExchangeRate::KrakenAdapter.instance, 228 | ExchangeRate::LocalbitcoinsAdapter.instance, 229 | ExchangeRate::OkcoinAdapter.instance 230 | ] 231 | @status_check_schedule = DEFAULT_STATUS_CHECK_SCHEDULE 232 | @address_provider = AddressProvider::Bip32.new(self) 233 | @test_mode = false 234 | end 235 | 236 | def order_class 237 | "Straight::Order" 238 | end 239 | 240 | end 241 | 242 | end 243 | -------------------------------------------------------------------------------- /lib/straight/order.rb: -------------------------------------------------------------------------------- 1 | module Straight 2 | 3 | # This module should be included into your own class to extend it with Order functionality. 4 | # For example, if you have a ActiveRecord model called Order, you can include OrderModule into it 5 | # and you'll now be able to do everything to check order's status, but you'll also get AR Database storage 6 | # funcionality, its validations etc. 7 | # 8 | # The right way to implement this would be to do it the other way: inherit from Straight::Order, then 9 | # include ActiveRecord, but at this point ActiveRecord doesn't work this way. Furthermore, some other libraries, like Sequel, 10 | # also require you to inherit from them. Thus, the module. 11 | # 12 | # When this module is included, it doesn't actually *include* all the methods, some are prepended (see Ruby docs on #prepend). 13 | # It is important specifically for getters and setters and as a general rule only getters and setters are prepended. 14 | # 15 | # If you don't want to bother yourself with modules, please use Straight::Order class and simply create new instances of it. 16 | # However, if you are contributing to the library, all new funcionality should go to either Straight::OrderModule::Includable or 17 | # Straight::OrderModule::Prependable (most likely the former). 18 | module OrderModule 19 | 20 | # Only add getters and setters for those properties in the extended class 21 | # that don't already have them. This is very useful with ActiveRecord for example 22 | # where we don't want to override AR getters and setters that set attribtues. 23 | def self.included(base) 24 | base.class_eval do 25 | [:amount, :amount_paid, :address, :gateway, :keychain_id, :status, :tid, :title, :callback_url, :test_mode].each do |field| 26 | attr_reader field unless base.method_defined?(field) 27 | attr_writer field unless base.method_defined?("#{field}=") 28 | end 29 | prepend Prependable 30 | include Includable 31 | end 32 | end 33 | 34 | # Worth noting that statuses above 1 are immutable. That is, an order status cannot be changed 35 | # if it is more than 1. It makes sense because if an order is paid (5) or expired (2), nothing 36 | # else should be able to change the status back. Similarly, if an order is overpaid (4) or 37 | # underpaid (5), it requires admin supervision and possibly a new order to be created. 38 | STATUSES = { 39 | new: 0, # no transactions received 40 | unconfirmed: 1, # transaction has been received doesn't have enough confirmations yet 41 | paid: 2, # transaction received with enough confirmations and the correct amount 42 | underpaid: 3, # amount that was received in a transaction was not enough 43 | overpaid: 4, # amount that was received in a transaction was too large 44 | expired: 5, # too much time passed since creating an order 45 | canceled: 6, # user decides to economize 46 | } 47 | 48 | attr_reader :old_status 49 | 50 | class IncorrectAmount < StraightError; end 51 | 52 | # If you are defining methods in this module, it means you most likely want to 53 | # call super() somehwere inside those methods. An example would be the #status= 54 | # setter. We do our thing, then call super() so that the class this module is prepended to 55 | # could do its thing. For instance, if we included it into ActiveRecord, then after 56 | # #status= is executed, it would call ActiveRecord model setter #status= 57 | # 58 | # In short, the idea is to let the class we're being prepended to do its magic 59 | # after out methods are finished. 60 | module Prependable 61 | 62 | # Checks #transaction and returns one of the STATUSES based 63 | # on the meaning of each status and the contents of transaction 64 | # If as_sym is set to true, then each status is returned as Symbol, otherwise 65 | # an equivalent Integer from STATUSES is returned. 66 | def status(as_sym: false, reload: false) 67 | 68 | if defined?(super) 69 | begin 70 | @status = super 71 | # if no method with arguments found in the class 72 | # we're prepending to, then let's use a standard getter 73 | # with no argument. 74 | rescue ArgumentError 75 | @status = super() 76 | end 77 | end 78 | 79 | # Prohibit status update if the order was paid in some way. 80 | # This is just a caching workaround so we don't query 81 | # the blockchain needlessly. The actual safety switch is in the setter. 82 | if (reload || @status.nil?) && !status_locked? 83 | self.status = get_transaction_status(reload: reload) 84 | end 85 | 86 | as_sym ? STATUSES.invert[@status] : @status 87 | end 88 | 89 | def status=(new_status) 90 | # Prohibit status update if the order was paid in some way, 91 | # so statuses above 1 are in fact immutable. 92 | return false if status_locked? 93 | 94 | self.tid = transaction[:tid] if transaction 95 | 96 | # Pay special attention to the order of these statements. If you place 97 | # the assignment @status = new_status below the callback call, 98 | # you may get a "Stack level too deep" error if the callback checks 99 | # for the status and it's nil (therefore, force reload and the cycle continues). 100 | # 101 | # The order in which these statements currently are prevents that error, because 102 | # by the time a callback checks the status it's already set. 103 | @status_changed = (@status != new_status) 104 | @old_status = @status 105 | @status = new_status 106 | gateway.order_status_changed(self) if status_changed? 107 | super if defined?(super) 108 | end 109 | 110 | def set_amount_paid(transaction) 111 | self.amount_paid = transaction[:total_amount] 112 | end 113 | 114 | def get_transaction_status(reload: false) 115 | t = transaction(reload: reload) 116 | 117 | return STATUSES[:new] if t.nil? 118 | return STATUSES[:unconfirmed] if status_unconfirmed?(t[:confirmations]) 119 | 120 | set_amount_paid(t) 121 | if t[:total_amount] == amount 122 | STATUSES[:paid] 123 | elsif t[:total_amount] < amount 124 | STATUSES[:underpaid] 125 | else 126 | STATUSES[:overpaid] 127 | end 128 | end 129 | 130 | def status_unconfirmed?(confirmations) 131 | confirmations < gateway.confirmations_required 132 | end 133 | 134 | def status_locked? 135 | @status && @status > 1 136 | end 137 | 138 | def status_changed? 139 | @status_changed 140 | end 141 | 142 | end 143 | 144 | module Includable 145 | 146 | # Returns an array of transactions for the order's address, each as a hash: 147 | # [ {tid: "feba9e7bfea...", amount: 1202000, ...} ] 148 | # 149 | # An order is supposed to have only one transaction to its address, but we cannot 150 | # always guarantee that (especially when a merchant decides to reuse the address 151 | # for some reason -- he shouldn't but you know people). 152 | # 153 | # Therefore, this method returns all of the transactions. 154 | # For compliance, there's also a #transaction method which always returns 155 | # the last transaction made to the address. 156 | def transactions(reload: false) 157 | @transactions = gateway.fetch_transactions_for(address) if reload || !@transactions 158 | @transactions 159 | end 160 | 161 | # Last transaction made to the address. Always use this method to check whether a transaction 162 | # for this order has arrived. We pick last and not first because an address may be reused and we 163 | # always assume it's the last transaction that we want to check. 164 | def transaction(reload: false) 165 | transactions(reload: reload).first 166 | end 167 | 168 | # Starts a loop which calls #status(reload: true) according to the schedule 169 | # determined in @status_check_schedule. This method is supposed to be 170 | # called in a separate thread, for example: 171 | # 172 | # Thread.new do 173 | # order.start_periodic_status_check 174 | # end 175 | # 176 | # `duration` argument (value is in seconds) allows you to 177 | # control in what time an order expires. In other words, we 178 | # keep checking for new transactions until the time passes. 179 | # Then we stop and set Order's status to STATUS[:expired]. See 180 | # #check_status_on_schedule for the implementation details. 181 | def start_periodic_status_check(duration: 600) 182 | check_status_on_schedule(duration: duration) 183 | end 184 | 185 | # Recursion here! Keeps calling itself according to the schedule until 186 | # either the status changes or the schedule tells it to stop. 187 | def check_status_on_schedule(period: 10, iteration_index: 0, duration: 600, time_passed: 0) 188 | self.status(reload: true) 189 | time_passed += period 190 | if duration >= time_passed # Stop checking if status is >= 2 191 | if self.status < 2 192 | schedule = gateway.status_check_schedule.call(period, iteration_index) 193 | sleep period 194 | check_status_on_schedule( 195 | period: schedule[:period], 196 | iteration_index: schedule[:iteration_index], 197 | duration: duration, 198 | time_passed: time_passed 199 | ) 200 | end 201 | elsif self.status < 2 202 | self.status = STATUSES[:expired] 203 | end 204 | end 205 | 206 | def to_json 207 | to_h.to_json 208 | end 209 | 210 | def to_h 211 | { status: status, amount: amount, address: address, tid: tid } 212 | end 213 | 214 | def amount_in_btc(field: amount, as: :number) 215 | a = Satoshi.new(field, from_unit: :satoshi, to_unit: :btc) 216 | as == :string ? a.to_unit(as: :string) : a.to_unit 217 | end 218 | 219 | end 220 | 221 | end 222 | 223 | # Instances of this class are generated when we'd like to start watching 224 | # some addresses to check whether a transaction containing a certain amount 225 | # has arrived to it. 226 | # 227 | # It is worth noting that instances do not know how store themselves anywhere, 228 | # so as the class is written here, those instances are only supposed to exist 229 | # in memory. Storing orders is entirely up to you. 230 | class Order 231 | include OrderModule 232 | 233 | def initialize 234 | @status = 0 235 | end 236 | 237 | end 238 | 239 | end 240 | -------------------------------------------------------------------------------- /spec/fixtures/vcr/exchange_rate_bitpay_adapter.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: get 5 | uri: https://bitpay.com/api/rates 6 | body: 7 | encoding: US-ASCII 8 | string: '' 9 | headers: 10 | Accept-Encoding: 11 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 12 | Accept: 13 | - "*/*" 14 | User-Agent: 15 | - Ruby 16 | response: 17 | status: 18 | code: 200 19 | message: OK 20 | headers: 21 | Server: 22 | - cloudflare-nginx 23 | Date: 24 | - Mon, 22 Jun 2015 14:27:31 GMT 25 | Content-Type: 26 | - application/json 27 | Transfer-Encoding: 28 | - chunked 29 | Connection: 30 | - keep-alive 31 | Set-Cookie: 32 | - __cfduid=d47a21cd5ce6ecfe3f65ad40c66f2cf8a1434983251; expires=Tue, 21-Jun-16 33 | 14:27:31 GMT; path=/; domain=.bitpay.com; HttpOnly 34 | Strict-Transport-Security: 35 | - max-age=31536000 36 | Access-Control-Allow-Origin: 37 | - "*" 38 | Cache-Control: 39 | - public, max-age=1800 40 | Expires: 41 | - Mon, 22 Jun 2015 14:57:31 GMT 42 | Cf-Cache-Status: 43 | - HIT 44 | Vary: 45 | - Accept-Encoding 46 | Cf-Ray: 47 | - 1fa8a267a5d40c9b-AMS 48 | body: 49 | encoding: ASCII-8BIT 50 | string: !binary |- 51 | W3siY29kZSI6IkJUQyIsIm5hbWUiOiJCaXRjb2luIiwicmF0ZSI6MX0seyJj 52 | b2RlIjoiVVNEIiwibmFtZSI6IlVTIERvbGxhciIsInJhdGUiOjI0NS40OX0s 53 | eyJjb2RlIjoiRVVSIiwibmFtZSI6IkV1cm96b25lIEV1cm8iLCJyYXRlIjoy 54 | MTUuMDA5MTMyfSx7ImNvZGUiOiJHQlAiLCJuYW1lIjoiUG91bmQgU3Rlcmxp 55 | bmciLCJyYXRlIjoxNTQuMzUwMzM3fSx7ImNvZGUiOiJKUFkiLCJuYW1lIjoi 56 | SmFwYW5lc2UgWWVuIiwicmF0ZSI6MzAwMzkuMzM5MDZ9LHsiY29kZSI6IkNB 57 | RCIsIm5hbWUiOiJDYW5hZGlhbiBEb2xsYXIiLCJyYXRlIjoyOTkuMDYyNDE4 58 | fSx7ImNvZGUiOiJBVUQiLCJuYW1lIjoiQXVzdHJhbGlhbiBEb2xsYXIiLCJy 59 | YXRlIjozMTQuMzkxMTk4fSx7ImNvZGUiOiJDTlkiLCJuYW1lIjoiQ2hpbmVz 60 | ZSBZdWFuIiwicmF0ZSI6MTUxMS4yNTcxMzh9LHsiY29kZSI6IkNIRiIsIm5h 61 | bWUiOiJTd2lzcyBGcmFuYyIsInJhdGUiOjIyMi44Mzk2MTl9LHsiY29kZSI6 62 | IlNFSyIsIm5hbWUiOiJTd2VkaXNoIEtyb25hIiwicmF0ZSI6MTk4NC4yMDU1 63 | NjN9LHsiY29kZSI6Ik5aRCIsIm5hbWUiOiJOZXcgWmVhbGFuZCBEb2xsYXIi 64 | LCJyYXRlIjozNTQuNjIwODJ9LHsiY29kZSI6IktSVyIsIm5hbWUiOiJTb3V0 65 | aCBLb3JlYW4gV29uIiwicmF0ZSI6MjY1OTU2LjgwMTY4MX0seyJjb2RlIjoi 66 | QUVEIiwibmFtZSI6IlVBRSBEaXJoYW0iLCJyYXRlIjo4OTcuMTE4MTI4fSx7 67 | ImNvZGUiOiJBRk4iLCJuYW1lIjoiQWZnaGFuIEFmZ2hhbmkiLCJyYXRlIjox 68 | Mzk4MC44MzE3NX0seyJjb2RlIjoiQUxMIiwibmFtZSI6IkFsYmFuaWFuIExl 69 | ayIsInJhdGUiOjMwMjc1LjExMTAyNX0seyJjb2RlIjoiQU1EIiwibmFtZSI6 70 | IkFybWVuaWFuIERyYW0iLCJyYXRlIjoxMTU2MDguMjU4fSx7ImNvZGUiOiJB 71 | TkciLCJuYW1lIjoiTmV0aGVybGFuZHMgQW50aWxsZWFuIEd1aWxkZXIiLCJy 72 | YXRlIjo0MzYuODEwMTU4fSx7ImNvZGUiOiJBT0EiLCJuYW1lIjoiQW5nb2xh 73 | biBLd2FuemEiLCJyYXRlIjoyOTI3MC4xNjQzMn0seyJjb2RlIjoiQVJTIiwi 74 | bmFtZSI6IkFyZ2VudGluZSBQZXNvIiwicmF0ZSI6MzEyMS42NDI4fSx7ImNv 75 | ZGUiOiJBV0ciLCJuYW1lIjoiQXJ1YmFuIEZsb3JpbiIsInJhdGUiOjQzNy42 76 | MzI1ODF9LHsiY29kZSI6IkFaTiIsIm5hbWUiOiJBemVyYmFpamFuaSBNYW5h 77 | dCIsInJhdGUiOjI1Ni40NTY2MzV9LHsiY29kZSI6IkJBTSIsIm5hbWUiOiJC 78 | b3NuaWEtSGVyemVnb3ZpbmEgQ29udmVydGlibGUgTWFyayIsInJhdGUiOjQy 79 | MC43NTE3NzN9LHsiY29kZSI6IkJCRCIsIm5hbWUiOiJCYXJiYWRpYW4gRG9s 80 | bGFyIiwicmF0ZSI6NDg4LjUyfSx7ImNvZGUiOiJCRFQiLCJuYW1lIjoiQmFu 81 | Z2xhZGVzaGkgVGFrYSIsInJhdGUiOjE4OTIyLjkzNDU2fSx7ImNvZGUiOiJC 82 | R04iLCJuYW1lIjoiQnVsZ2FyaWFuIExldiIsInJhdGUiOjQyMC42NTg3MX0s 83 | eyJjb2RlIjoiQkhEIiwibmFtZSI6IkJhaHJhaW5pIERpbmFyIiwicmF0ZSI6 84 | OTEuNjAyMTQxfSx7ImNvZGUiOiJCSUYiLCJuYW1lIjoiQnVydW5kaWFuIEZy 85 | YW5jIiwicmF0ZSI6MzgxMTA2LjY2NX0seyJjb2RlIjoiQk1EIiwibmFtZSI6 86 | IkJlcm11ZGFuIERvbGxhciIsInJhdGUiOjI0NC4yNn0seyJjb2RlIjoiQk5E 87 | IiwibmFtZSI6IkJydW5laSBEb2xsYXIiLCJyYXRlIjozMjYuMTIzNzM5fSx7 88 | ImNvZGUiOiJCT0IiLCJuYW1lIjoiQm9saXZpYW4gQm9saXZpYW5vIiwicmF0 89 | ZSI6MTY3Ni40NDY3NTZ9LHsiY29kZSI6IkJSTCIsIm5hbWUiOiJCcmF6aWxp 90 | YW4gUmVhbCIsInJhdGUiOjc1NC43Mzg5NzR9LHsiY29kZSI6IkJTRCIsIm5h 91 | bWUiOiJCYWhhbWlhbiBEb2xsYXIiLCJyYXRlIjoyNDQuMjZ9LHsiY29kZSI6 92 | IkJUTiIsIm5hbWUiOiJCaHV0YW5lc2UgTmd1bHRydW0iLCJyYXRlIjoxNTQ2 93 | OS41OTY0NX0seyJjb2RlIjoiQldQIiwibmFtZSI6IkJvdHN3YW5hbiBQdWxh 94 | IiwicmF0ZSI6MjQwMy4wOTkwMDZ9LHsiY29kZSI6IkJZUiIsIm5hbWUiOiJC 95 | ZWxhcnVzaWFuIFJ1YmxlIiwicmF0ZSI6MzczNjk0MS4wMDY3MzV9LHsiY29k 96 | ZSI6IkJaRCIsIm5hbWUiOiJCZWxpemUgRG9sbGFyIiwicmF0ZSI6NDg5LjI2 97 | NDk5M30seyJjb2RlIjoiQ0RGIiwibmFtZSI6IkNvbmdvbGVzZSBGcmFuYyIs 98 | InJhdGUiOjIyMzg2NC4yOX0seyJjb2RlIjoiQ0xGIiwibmFtZSI6IkNoaWxl 99 | YW4gVW5pdCBvZiBBY2NvdW50IChVRikiLCJyYXRlIjo2LjAwODMwN30seyJj 100 | b2RlIjoiQ0xQIiwibmFtZSI6IkNoaWxlYW4gUGVzbyIsInJhdGUiOjE1NDE5 101 | OC41OTAwNzV9LHsiY29kZSI6IkNPUCIsIm5hbWUiOiJDb2xvbWJpYW4gUGVz 102 | byIsInJhdGUiOjYyMzgyNy44Mjd9LHsiY29kZSI6IkNSQyIsIm5hbWUiOiJD 103 | b3N0YSBSaWNhbiBDb2zDs24iLCJyYXRlIjoxMzAxNTUuMTU3NDE1fSx7ImNv 104 | ZGUiOiJDVkUiLCJuYW1lIjoiQ2FwZSBWZXJkZWFuIEVzY3VkbyIsInJhdGUi 105 | OjIzNzA3Ljk4MTk2NH0seyJjb2RlIjoiQ1pLIiwibmFtZSI6IkN6ZWNoIEtv 106 | cnVuYSIsInJhdGUiOjU4NTIuNTMzODR9LHsiY29kZSI6IkRKRiIsIm5hbWUi 107 | OiJEamlib3V0aWFuIEZyYW5jIiwicmF0ZSI6NDMzMDYuMDc2N30seyJjb2Rl 108 | IjoiREtLIiwibmFtZSI6IkRhbmlzaCBLcm9uZSIsInJhdGUiOjE2MDQuMTUx 109 | NjU4fSx7ImNvZGUiOiJET1AiLCJuYW1lIjoiRG9taW5pY2FuIFBlc28iLCJy 110 | YXRlIjoxMDk3Ni41NDk3NzR9LHsiY29kZSI6IkRaRCIsIm5hbWUiOiJBbGdl 111 | cmlhbiBEaW5hciIsInJhdGUiOjI0MDI3LjM4Nn0seyJjb2RlIjoiRUVLIiwi 112 | bmFtZSI6IkVzdG9uaWFuIEtyb29uIiwicmF0ZSI6MzM2Ni4xNTMxNjd9LHsi 113 | Y29kZSI6IkVHUCIsIm5hbWUiOiJFZ3lwdGlhbiBQb3VuZCIsInJhdGUiOjE4 114 | NjEuNjY4ODd9LHsiY29kZSI6IkVUQiIsIm5hbWUiOiJFdGhpb3BpYW4gQmly 115 | ciIsInJhdGUiOjUwNDEuOTM1NTM2fSx7ImNvZGUiOiJGSkQiLCJuYW1lIjoi 116 | RmlqaWFuIERvbGxhciIsInJhdGUiOjUwNC45NTI1OTJ9LHsiY29kZSI6IkZL 117 | UCIsIm5hbWUiOiJGYWxrbGFuZCBJc2xhbmRzIFBvdW5kIiwicmF0ZSI6MTU0 118 | LjM1MDMzN30seyJjb2RlIjoiR0VMIiwibmFtZSI6Ikdlb3JnaWFuIExhcmki 119 | LCJyYXRlIjo1NDcuMzk0NzIxfSx7ImNvZGUiOiJHSFMiLCJuYW1lIjoiR2hh 120 | bmFpYW4gQ2VkaSIsInJhdGUiOjEwNjkuMjMzMDA2fSx7ImNvZGUiOiJHSVAi 121 | LCJuYW1lIjoiR2licmFsdGFyIFBvdW5kIiwicmF0ZSI6MTU0LjM1MDMzN30s 122 | eyJjb2RlIjoiR01EIiwibmFtZSI6IkdhbWJpYW4gRGFsYXNpIiwicmF0ZSI6 123 | MTA0NjQuOTIyNzc4fSx7ImNvZGUiOiJHTkYiLCJuYW1lIjoiR3VpbmVhbiBG 124 | cmFuYyIsInJhdGUiOjE3Nzk4NjYuNzQ1NTI1fSx7ImNvZGUiOiJHVFEiLCJu 125 | YW1lIjoiR3VhdGVtYWxhbiBRdWV0emFsIiwicmF0ZSI6MTg2Mi4zMDQxOX0s 126 | eyJjb2RlIjoiR1lEIiwibmFtZSI6Ikd1eWFuYWVzZSBEb2xsYXIiLCJyYXRl 127 | Ijo1MDAyNi4yNzk5NX0seyJjb2RlIjoiSEtEIiwibmFtZSI6IkhvbmcgS29u 128 | ZyBEb2xsYXIiLCJyYXRlIjoxODkzLjYzNzg2M30seyJjb2RlIjoiSE5MIiwi 129 | bmFtZSI6IkhvbmR1cmFuIExlbXBpcmEiLCJyYXRlIjo1MzIyLjQ2MjAzOX0s 130 | eyJjb2RlIjoiSFJLIiwibmFtZSI6IkNyb2F0aWFuIEt1bmEiLCJyYXRlIjox 131 | NjI3Ljk3Nzc1Nn0seyJjb2RlIjoiSFRHIiwibmFtZSI6IkhhaXRpYW4gR291 132 | cmRlIiwicmF0ZSI6MTE3NzkuNTM2MjA0fSx7ImNvZGUiOiJIVUYiLCJuYW1l 133 | IjoiSHVuZ2FyaWFuIEZvcmludCIsInJhdGUiOjY2OTgxLjc3MTA0NX0seyJj 134 | b2RlIjoiSURSIiwibmFtZSI6IkluZG9uZXNpYW4gUnVwaWFoIiwicmF0ZSI6 135 | MzI1MzY2NS4zM30seyJjb2RlIjoiSUxTIiwibmFtZSI6IklzcmFlbGkgU2hl 136 | a2VsIiwicmF0ZSI6OTM2LjMxMDg2Nn0seyJjb2RlIjoiSU5SIiwibmFtZSI6 137 | IkluZGlhbiBSdXBlZSIsInJhdGUiOjE1NTExLjU2NjQyNH0seyJjb2RlIjoi 138 | SVFEIiwibmFtZSI6IklyYXFpIERpbmFyIiwicmF0ZSI6Mjg0OTkwLjM1NX0s 139 | eyJjb2RlIjoiSVNLIiwibmFtZSI6IkljZWxhbmRpYyBLcsOzbmEiLCJyYXRl 140 | IjozMTk4Ny4wNjgzfSx7ImNvZGUiOiJKRVAiLCJuYW1lIjoiSmVyc2V5IFBv 141 | dW5kIiwicmF0ZSI6MTU0LjM1MDMzN30seyJjb2RlIjoiSk1EIiwibmFtZSI6 142 | IkphbWFpY2FuIERvbGxhciIsInJhdGUiOjI4MzYxLjAyODZ9LHsiY29kZSI6 143 | IkpPRCIsIm5hbWUiOiJKb3JkYW5pYW4gRGluYXIiLCJyYXRlIjoxNzIuOTQ2 144 | ODI3fSx7ImNvZGUiOiJLRVMiLCJuYW1lIjoiS2VueWFuIFNoaWxsaW5nIiwi 145 | cmF0ZSI6MjQwNzIuNTgzMzgxfSx7ImNvZGUiOiJLR1MiLCJuYW1lIjoiS3ly 146 | Z3lzdGFuaSBTb20iLCJyYXRlIjoxNDgyMC4yNDc2MDV9LHsiY29kZSI6IktI 147 | UiIsIm5hbWUiOiJDYW1ib2RpYW4gUmllbCIsInJhdGUiOjk2OTM4Mi40NDl9 148 | LHsiY29kZSI6IktNRiIsIm5hbWUiOiJDb21vcmlhbiBGcmFuYyIsInJhdGUi 149 | OjEwNjk2Ni4wNDMxNTd9LHsiY29kZSI6IktXRCIsIm5hbWUiOiJLdXdhaXRp 150 | IERpbmFyIiwicmF0ZSI6NzMuNzI0NTA3fSx7ImNvZGUiOiJLWUQiLCJuYW1l 151 | IjoiQ2F5bWFuIElzbGFuZHMgRG9sbGFyIiwicmF0ZSI6MjAxLjQzNTExNn0s 152 | eyJjb2RlIjoiS1pUIiwibmFtZSI6IkthemFraHN0YW5pIFRlbmdlIiwicmF0 153 | ZSI6NDUxMjcuNzY3Nzh9LHsiY29kZSI6IkxBSyIsIm5hbWUiOiJMYW90aWFu 154 | IEtpcCIsInJhdGUiOjE5NDc5MjQuNjQ4fSx7ImNvZGUiOiJMQlAiLCJuYW1l 155 | IjoiTGViYW5lc2UgUG91bmQiLCJyYXRlIjozNjU0MzcuMzg2fSx7ImNvZGUi 156 | OiJMS1IiLCJuYW1lIjoiU3JpIExhbmthbiBSdXBlZSIsInJhdGUiOjMyNTky 157 | LjU4ODg0fSx7ImNvZGUiOiJMUkQiLCJuYW1lIjoiTGliZXJpYW4gRG9sbGFy 158 | IiwicmF0ZSI6MjA4ODIuMTQxNTc3fSx7ImNvZGUiOiJMU0wiLCJuYW1lIjoi 159 | TGVzb3RobyBMb3RpIiwicmF0ZSI6Mjk2OC45MTUwODN9LHsiY29kZSI6IkxU 160 | TCIsIm5hbWUiOiJMaXRodWFuaWFuIExpdGFzIiwicmF0ZSI6NzM2LjQ1NzU3 161 | OX0seyJjb2RlIjoiTFZMIiwibmFtZSI6IkxhdHZpYW4gTGF0cyIsInJhdGUi 162 | OjE1MS4yMDA4NDh9LHsiY29kZSI6IkxZRCIsIm5hbWUiOiJMaWJ5YW4gRGlu 163 | YXIiLCJyYXRlIjozMzMuNzAzODZ9LHsiY29kZSI6Ik1BRCIsIm5hbWUiOiJN 164 | b3JvY2NhbiBEaXJoYW0iLCJyYXRlIjoyMzU0LjMzNjY0OX0seyJjb2RlIjoi 165 | TURMIiwibmFtZSI6Ik1vbGRvdmFuIExldSIsInJhdGUiOjQ2MTUuMTQwMDM3 166 | fSx7ImNvZGUiOiJNR0EiLCJuYW1lIjoiTWFsYWdhc3kgQXJpYXJ5IiwicmF0 167 | ZSI6Nzc1MDAwLjM0MX0seyJjb2RlIjoiTUtEIiwibmFtZSI6Ik1hY2Vkb25p 168 | YW4gRGVuYXIiLCJyYXRlIjoxMzI1Mi40ODUwNjl9LHsiY29kZSI6Ik1NSyIs 169 | Im5hbWUiOiJNeWFubWEgS3lhdCIsInJhdGUiOjI2NjkwMi45MDJ9LHsiY29k 170 | ZSI6Ik1OVCIsIm5hbWUiOiJNb25nb2xpYW4gVHVncmlrIiwicmF0ZSI6NDY3 171 | NTc0LjcwNX0seyJjb2RlIjoiTU9QIiwibmFtZSI6Ik1hY2FuZXNlIFBhdGFj 172 | YSIsInJhdGUiOjE5NDIuNjI5MDkxfSx7ImNvZGUiOiJNUk8iLCJuYW1lIjoi 173 | TWF1cml0YW5pYW4gT3VndWl5YSIsInJhdGUiOjc4Mjc3LjUwMTQ2N30seyJj 174 | b2RlIjoiTVVSIiwibmFtZSI6Ik1hdXJpdGlhbiBSdXBlZSIsInJhdGUiOjg1 175 | NzAuMjIwNDI5fSx7ImNvZGUiOiJNVlIiLCJuYW1lIjoiTWFsZGl2aWFuIFJ1 176 | Zml5YWEiLCJyYXRlIjozNzEzLjU2NjExOX0seyJjb2RlIjoiTVdLIiwibmFt 177 | ZSI6Ik1hbGF3aWFuIEt3YWNoYSIsInJhdGUiOjEwNjQ1NC42MjA2MDd9LHsi 178 | Y29kZSI6Ik1YTiIsIm5hbWUiOiJNZXhpY2FuIFBlc28iLCJyYXRlIjozNzQz 179 | Ljg1ODUxMX0seyJjb2RlIjoiTVlSIiwibmFtZSI6Ik1hbGF5c2lhbiBSaW5n 180 | Z2l0IiwicmF0ZSI6OTExLjE1MDg2NX0seyJjb2RlIjoiTVpOIiwibmFtZSI6 181 | Ik1vemFtYmljYW4gTWV0aWNhbCIsInJhdGUiOjkzMTkuNDUwODUyfSx7ImNv 182 | ZGUiOiJOQUQiLCJuYW1lIjoiTmFtaWJpYW4gRG9sbGFyIiwicmF0ZSI6Mjk2 183 | OC4xNzQyNDJ9LHsiY29kZSI6Ik5HTiIsIm5hbWUiOiJOaWdlcmlhbiBOYWly 184 | YSIsInJhdGUiOjQ4NjI0LjUzMjg3NX0seyJjb2RlIjoiTklPIiwibmFtZSI6 185 | Ik5pY2FyYWd1YW4gQ8OzcmRvYmEiLCJyYXRlIjo2NTkyLjQ2MTM3N30seyJj 186 | b2RlIjoiTk9LIiwibmFtZSI6Ik5vcndlZ2lhbiBLcm9uZSIsInJhdGUiOjE4 187 | ODMuNzkyOTY0fSx7ImNvZGUiOiJOUFIiLCJuYW1lIjoiTmVwYWxlc2UgUnVw 188 | ZWUiLCJyYXRlIjoyNDg4OC43NDQ0NjR9LHsiY29kZSI6Ik9NUiIsIm5hbWUi 189 | OiJPbWFuaSBSaWFsIiwicmF0ZSI6OTMuOTkwMDI3fSx7ImNvZGUiOiJQQUIi 190 | LCJuYW1lIjoiUGFuYW1hbmlhbiBCYWxib2EiLCJyYXRlIjoyNDQuMjZ9LHsi 191 | Y29kZSI6IlBFTiIsIm5hbWUiOiJQZXJ1dmlhbiBOdWV2byBTb2wiLCJyYXRl 192 | Ijo3NzIuOTI3MzA2fSx7ImNvZGUiOiJQR0siLCJuYW1lIjoiUGFwdWEgTmV3 193 | IEd1aW5lYW4gS2luYSIsInJhdGUiOjY2NS43OTE2OTV9LHsiY29kZSI6IlBI 194 | UCIsIm5hbWUiOiJQaGlsaXBwaW5lIFBlc28iLCJyYXRlIjoxMDk4NC44MTE4 195 | Njh9LHsiY29kZSI6IlBLUiIsIm5hbWUiOiJQYWtpc3RhbmkgUnVwZWUiLCJy 196 | YXRlIjoyNDg1NC42NzYzfSx7ImNvZGUiOiJQTE4iLCJuYW1lIjoiUG9saXNo 197 | IFpsb3R5IiwicmF0ZSI6ODk2Ljg5MTY5OX0seyJjb2RlIjoiUFlHIiwibmFt 198 | ZSI6IlBhcmFndWF5YW4gR3VhcmFuaSIsInJhdGUiOjEyNjEyMzYuNTF9LHsi 199 | Y29kZSI6IlFBUiIsIm5hbWUiOiJRYXRhcmkgUmlhbCIsInJhdGUiOjg4OS4y 200 | ODIyNjd9LHsiY29kZSI6IlJPTiIsIm5hbWUiOiJSb21hbmlhbiBMZXUiLCJy 201 | YXRlIjo5NjQuNjk1ODMyfSx7ImNvZGUiOiJSU0QiLCJuYW1lIjoiU2VyYmlh 202 | biBEaW5hciIsInJhdGUiOjI1OTUxLjU4MDc4OX0seyJjb2RlIjoiUlVCIiwi 203 | bmFtZSI6IlJ1c3NpYW4gUnVibGUiLCJyYXRlIjoxMzE4Ni41ODM3MjF9LHsi 204 | Y29kZSI6IlJXRiIsIm5hbWUiOiJSd2FuZGFuIEZyYW5jIiwicmF0ZSI6MTcz 205 | ODQwLjI0OTE4MX0seyJjb2RlIjoiU0FSIiwibmFtZSI6IlNhdWRpIFJpeWFs 206 | IiwicmF0ZSI6OTE1LjgzMDY0Mn0seyJjb2RlIjoiU0JEIiwibmFtZSI6IlNv 207 | bG9tb24gSXNsYW5kcyBEb2xsYXIiLCJyYXRlIjoxOTIwLjIwOTY4N30seyJj 208 | b2RlIjoiU0NSIiwibmFtZSI6IlNleWNoZWxsb2lzIFJ1cGVlIiwicmF0ZSI6 209 | MzE3Ni4zMTYyNDl9LHsiY29kZSI6IlNERyIsIm5hbWUiOiJTdWRhbmVzZSBQ 210 | b3VuZCIsInJhdGUiOjE0NTguMTU0Nzd9LHsiY29kZSI6IlNHRCIsIm5hbWUi 211 | OiJTaW5nYXBvcmUgRG9sbGFyIiwicmF0ZSI6MzI1Ljc4ODg1OX0seyJjb2Rl 212 | IjoiU0hQIiwibmFtZSI6IlNhaW50IEhlbGVuYSBQb3VuZCIsInJhdGUiOjE1 213 | NC4zNTAzMzd9LHsiY29kZSI6IlNMTCIsIm5hbWUiOiJTaWVycmEgTGVvbmVh 214 | biBMZW9uZSIsInJhdGUiOjEwMzA3NzcuMn0seyJjb2RlIjoiU09TIiwibmFt 215 | ZSI6IlNvbWFsaSBTaGlsbGluZyIsInJhdGUiOjE3MDE3NC4zMTM1MTl9LHsi 216 | Y29kZSI6IlNSRCIsIm5hbWUiOiJTdXJpbmFtZXNlIERvbGxhciIsInJhdGUi 217 | Ojc5Ny40MjkyNzF9LHsiY29kZSI6IlNURCIsIm5hbWUiOiJTw6NvIFRvbcOp 218 | IGFuZCBQcsOtbmNpcGUgRG9icmEiLCJyYXRlIjo1MzAwOTcyLjA0NDJ9LHsi 219 | Y29kZSI6IlNWQyIsIm5hbWUiOiJTYWx2YWRvcmFuIENvbMOzbiIsInJhdGUi 220 | OjIxMjMuMDM0NjQyfSx7ImNvZGUiOiJTWVAiLCJuYW1lIjoiU3lyaWFuIFBv 221 | dW5kIiwicmF0ZSI6NDYxMjUuNzg1MzE3fSx7ImNvZGUiOiJTWkwiLCJuYW1l 222 | IjoiU3dhemkgTGlsYW5nZW5pIiwicmF0ZSI6Mjk2OC4zNTc0Mzd9LHsiY29k 223 | ZSI6IlRIQiIsIm5hbWUiOiJUaGFpIEJhaHQiLCJyYXRlIjo4MTYzLjMzNTI5 224 | N30seyJjb2RlIjoiVEpTIiwibmFtZSI6IlRhamlraXN0YW5pIFNvbW9uaSIs 225 | InJhdGUiOjE1MjguOTYzNTQ1fSx7ImNvZGUiOiJUTVQiLCJuYW1lIjoiVHVy 226 | a21lbmlzdGFuaSBNYW5hdCIsInJhdGUiOjg1NC44MDAwODN9LHsiY29kZSI6 227 | IlRORCIsIm5hbWUiOiJUdW5pc2lhbiBEaW5hciIsInJhdGUiOjQ3MC43Njg0 228 | MDV9LHsiY29kZSI6IlRPUCIsIm5hbWUiOiJUb25nYW4gUGHKu2FuZ2EiLCJy 229 | YXRlIjo1MTguMzE2Nzg5fSx7ImNvZGUiOiJUUlkiLCJuYW1lIjoiVHVya2lz 230 | aCBMaXJhIiwicmF0ZSI6NjU1LjAzNDcyOX0seyJjb2RlIjoiVFREIiwibmFt 231 | ZSI6IlRyaW5pZGFkIGFuZCBUb2JhZ28gRG9sbGFyIiwicmF0ZSI6MTU0OC4z 232 | NTQzN30seyJjb2RlIjoiVFdEIiwibmFtZSI6Ik5ldyBUYWl3YW4gRG9sbGFy 233 | IiwicmF0ZSI6NzQ1Ni43OTM3MDZ9LHsiY29kZSI6IlRaUyIsIm5hbWUiOiJU 234 | YW56YW5pYW4gU2hpbGxpbmciLCJyYXRlIjo1NTg4MDUuODE1fSx7ImNvZGUi 235 | OiJVQUgiLCJuYW1lIjoiVWtyYWluaWFuIEhyeXZuaWEiLCJyYXRlIjo1MzIw 236 | LjMxMjU1MX0seyJjb2RlIjoiVUdYIiwibmFtZSI6IlVnYW5kYW4gU2hpbGxp 237 | bmciLCJyYXRlIjo4MDQyMjYuMDV9LHsiY29kZSI6IlVZVSIsIm5hbWUiOiJV 238 | cnVndWF5YW4gUGVzbyIsInJhdGUiOjY1MzEuNjU4OTU2fSx7ImNvZGUiOiJV 239 | WlMiLCJuYW1lIjoiVXpiZWtpc3RhbiBTb20iLCJyYXRlIjo2MjIwNzAuMjkw 240 | ODA5fSx7ImNvZGUiOiJWRUYiLCJuYW1lIjoiVmVuZXp1ZWxhbiBCb2zDrXZh 241 | ciBGdWVydGUiLCJyYXRlIjoxNTM5LjY2MzU5OX0seyJjb2RlIjoiVk5EIiwi 242 | bmFtZSI6IlZpZXRuYW1lc2UgRG9uZyIsInJhdGUiOjUyNDE1NzUuMzR9LHsi 243 | Y29kZSI6IlZVViIsIm5hbWUiOiJWYW51YXR1IFZhdHUiLCJyYXRlIjoyNTYx 244 | Ny45ODg4fSx7ImNvZGUiOiJXU1QiLCJuYW1lIjoiU2Ftb2FuIFRhbGEiLCJy 245 | YXRlIjo2MDAuMTQ2ODJ9LHsiY29kZSI6IlhBRiIsIm5hbWUiOiJDRkEgRnJh 246 | bmMgQkVBQyIsInJhdGUiOjE0MTIzOS4wNjQzNDR9LHsiY29kZSI6IlhBRyIs 247 | Im5hbWUiOiJTaWx2ZXIgKHRyb3kgb3VuY2UpIiwicmF0ZSI6MTUuMTA1MDM4 248 | fSx7ImNvZGUiOiJYQVUiLCJuYW1lIjoiR29sZCAodHJveSBvdW5jZSkiLCJy 249 | YXRlIjowLjIwNTE3OH0seyJjb2RlIjoiWENEIiwibmFtZSI6IkVhc3QgQ2Fy 250 | aWJiZWFuIERvbGxhciIsInJhdGUiOjY1OS4xNzQ2OTJ9LHsiY29kZSI6IlhP 251 | RiIsIm5hbWUiOiJDRkEgRnJhbmMgQkNFQU8iLCJyYXRlIjoxNDE0NDcuOTA2 252 | NjQ0fSx7ImNvZGUiOiJYUEYiLCJuYW1lIjoiQ0ZQIEZyYW5jIiwicmF0ZSI6 253 | MjU3MDEuMDM3Mn0seyJjb2RlIjoiWUVSIiwibmFtZSI6IlllbWVuaSBSaWFs 254 | IiwicmF0ZSI6NTI0ODcuNTY1ODR9LHsiY29kZSI6IlpBUiIsIm5hbWUiOiJT 255 | b3V0aCBBZnJpY2FuIFJhbmQiLCJyYXRlIjoyOTY3LjkwMjYyNX0seyJjb2Rl 256 | IjoiWk1XIiwibmFtZSI6IlphbWJpYW4gS3dhY2hhIiwicmF0ZSI6MTgwOS4z 257 | Mzk1ODV9LHsiY29kZSI6IlpXTCIsIm5hbWUiOiJaaW1iYWJ3ZWFuIERvbGxh 258 | ciIsInJhdGUiOjc4NzQ2LjMwODk1Mn1d 259 | http_version: 260 | recorded_at: Mon, 22 Jun 2015 14:27:31 GMT 261 | recorded_with: VCR 2.9.3 262 | -------------------------------------------------------------------------------- /spec/fixtures/vcr/exchange_rate_coinbase_adapter.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: get 5 | uri: https://coinbase.com/api/v1/currencies/exchange_rates 6 | body: 7 | encoding: US-ASCII 8 | string: '' 9 | headers: 10 | Accept-Encoding: 11 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 12 | Accept: 13 | - "*/*" 14 | User-Agent: 15 | - Ruby 16 | response: 17 | status: 18 | code: 200 19 | message: OK 20 | headers: 21 | Server: 22 | - cloudflare-nginx 23 | Date: 24 | - Mon, 22 Jun 2015 14:10:31 GMT 25 | Content-Type: 26 | - application/json; charset=utf-8 27 | Transfer-Encoding: 28 | - chunked 29 | Connection: 30 | - keep-alive 31 | Set-Cookie: 32 | - __cfduid=dd3dc941aa9f284c15f25c6747d904a671434982230; expires=Tue, 21-Jun-16 33 | 14:10:30 GMT; path=/; domain=.coinbase.com; HttpOnly 34 | - _coinbase=c3VpcW5PRDNsdDJoN2c1N1dpQ0J3QStyeWxiTzRBaVZNWUE0dnpHeVpRSHYyR2x6R0FIUE42dEpJWFlUMlFwVkV5cDBYTDhudUNXZDdGQXQzdFpSaFA3cmord2czRS9QVzdWTFNxS1BoNVJ4ZWFzSk12RGtoSHA5UTI4ZHN0b28ybDV6cTdGMVNEYmFkcGRnelM4SnFRPT0tLUVPUXV3NkpBR1c1eHNQOUtiUXA1L1E9PQ%3D%3D--60ffc34663824a4e6ea279a7d31396cb33df7e6b; 35 | path=/; secure; HttpOnly 36 | Strict-Transport-Security: 37 | - max-age=15552000; includeSubDomains; preload 38 | X-Frame-Options: 39 | - SAMEORIGIN 40 | X-Xss-Protection: 41 | - 1; mode=block 42 | X-Content-Type-Options: 43 | - nosniff 44 | X-Download-Options: 45 | - noopen 46 | Etag: 47 | - W/"985a89d88eda26f75b506b23c0ada2ec" 48 | Cache-Control: 49 | - private, no-cache, no-store, must-revalidate 50 | X-Request-Id: 51 | - 0b176614-c577-4ca6-bb22-40bdc7e7d071 52 | Pragma: 53 | - no-cache 54 | Expires: 55 | - Sat, 01 Jan 2000 00:00:00 GMT 56 | X-Powered-By: 57 | - Proof-of-Work 58 | Vary: 59 | - Accept-Encoding 60 | Via: 61 | - 1.1 vegur 62 | Cf-Ray: 63 | - 1fa8897f84c60725-AMS 64 | body: 65 | encoding: ASCII-8BIT 66 | string: | 67 | {"usd_to_mzn":"38.146185","bbd_to_usd":"0.5","btc_to_awg":"441.09553","ttd_to_usd":"0.157694","idr_to_usd":"0.000075","btc_to_top":"522.474162","usd_to_gel":"2.244233","btc_to_bsd":"246.17","btc_to_pkr":"24904.03422","sek_to_btc":"0.0005","cop_to_btc":"0.000002","omr_to_usd":"2.598921","usd_to_shp":"0.632073","uzs_to_usd":"0.000393","usd_to_pln":"3.669885","btc_to_ang":"440.233935","ugx_to_btc":"0.000001","usd_to_tzs":"2287.75","btc_to_uzs":"626934.59219","ltl_to_btc":"0.001347","sbd_to_usd":"0.127205","usd_to_nok":"7.72472","sos_to_usd":"0.001435","btc_to_btn":"15592.715512","btc_to_ern":"3692.919255","bgn_to_usd":"0.580526","btc_to_bif":"383655.945","pyg_to_btc":"0.000001","sos_to_btc":"0.000006","usd_to_dzd":"98.3538","fjd_to_usd":"0.482981","usd_to_crc":"537.58498","bsd_to_btc":"0.004062","btc_to_azn":"258.462007","usd_to_cny":"6.171224","btc_to_bam":"424.041857","usd_to_uzs":"2546.75465","rub_to_btc":"0.000075","pln_to_usd":"0.272488","btc_to_hkd":"1908.886862","btc_to_zar":"2991.673239","btc_to_jod":"174.278021","clf_to_btc":"0.165135","kgs_to_btc":"0.000067","awg_to_btc":"0.002267","usd_to_ils":"3.83238","rub_to_usd":"0.018541","usd_to_ang":"1.788333","btc_to_shp":"155.59741","usd_to_huf":"274.35075","usd_to_sos":"696.703333","btc_to_zwl":"79362.068594","usd_to_nzd":"1.4544","tjs_to_btc":"0.000649","iqd_to_btc":"0.000003","ghs_to_btc":"0.000931","usd_to_mmk":"1092.7","btc_to_mmk":"268989.959","btc_to_all":"30516.771763","bwp_to_btc":"0.000413","gip_to_btc":"0.006426","kwd_to_usd":"3.3132","btc_to_srd":"803.825548","btc_to_aoa":"29500.27429","btn_to_btc":"0.000064","btc_to_mad":"2373.589603","bzd_to_usd":"0.501857","usd_to_mad":"9.642075","gel_to_usd":"0.445587","php_to_btc":"0.00009","usd_to_lvl":"0.619166","pgk_to_usd":"0.365627","sdg_to_btc":"0.00068","btc_to_bmd":"246.17","kmf_to_usd":"0.002284","btc_to_aed":"904.147946","uyu_to_usd":"0.037396","usd_to_zwl":"322.387247","try_to_usd":"0.372981","nzd_to_btc":"0.002793","btc_to_cop":"628705.8715","usd_to_jep":"0.632073","thb_to_btc":"0.000122","wst_to_usd":"0.407","mop_to_usd":"0.125669","usd_to_xau":"0.00084","usd_to_khr":"3993.0","aoa_to_btc":"0.000034","usd_to_clf":"0.024598","usd_to_std":"21697.83","btc_to_kzt":"45490.49281","usd_to_xaf":"578.320325","btc_to_bnd":"328.698493","btc_to_mnt":"471230.9225","kes_to_btc":"0.000041","myr_to_btc":"0.001089","clf_to_usd":"40.653712","btc_to_gyd":"50417.462275","bam_to_btc":"0.002358","mga_to_btc":"0.000001","xau_to_usd":"1190.47619","mzn_to_btc":"0.000106","clp_to_usd":"0.001581","ars_to_btc":"0.000447","btc_to_htg":"11871.827161","nio_to_btc":"0.000151","usd_to_egp":"7.624156","usd_to_mop":"7.95742","chf_to_usd":"1.086802","tnd_to_usd":"0.518659","shp_to_usd":"1.582096","xag_to_btc":"0.065696","mur_to_usd":"0.028509","btc_to_dop":"10979.374013","cuc_to_btc":"0.004062","dzd_to_usd":"0.010167","czk_to_btc":"0.000169","btc_to_aud":"317.317315","usd_to_hkd":"7.754344","usd_to_afn":"57.874212","khr_to_usd":"0.00025","eur_to_btc":"0.004611","nok_to_usd":"0.129455","btc_to_xdr":"174.146566","gbp_to_btc":"0.006426","btc_to_gbp":"155.59741","usd_to_dkk":"6.569319","yer_to_btc":"0.000019","awg_to_usd":"0.558088","gtq_to_usd":"0.131098","pyg_to_usd":"0.000194","xag_to_usd":"16.173379","btc_to_dzd":"24211.754946","btc_to_sar":"923.046171","kpw_to_btc":"0.000005","bhd_to_usd":"2.655528","usd_to_gip":"0.632073","ngn_to_usd":"0.005027","sbd_to_btc":"0.000517","mkd_to_usd":"0.018417","usd_to_mxn":"15.320688","vuv_to_btc":"0.000039","bmd_to_btc":"0.004062","kes_to_usd":"0.01015","std_to_usd":"0.000046","dkk_to_btc":"0.000618","usd_to_sll":"4215.5","kzt_to_btc":"0.000022","cuc_to_usd":"1.0","btc_to_lvl":"152.420094","cny_to_btc":"0.000658","mvr_to_usd":"0.065789","ern_to_btc":"0.000271","usd_to_uah":"21.7814","btc_to_xcd":"664.329132","usd_to_bif":"1558.5","usd_to_vuv":"104.88","usd_to_eek":"13.784075","usd_to_cop":"2553.95","usd_to_mwk":"436.71375","btc_to_kyd":"203.116098","zwl_to_usd":"0.003102","bbd_to_btc":"0.002031","btc_to_mwk":"107505.823838","btc_to_kes":"24254.357865","usd_to_xof":"579.156275","usd_to_btn":"63.34125","bmd_to_usd":"1.0","btc_to_std":"5341354.8111","btc_to_djf":"43645.325575","usd_to_cup":"0.994328","btc_to_hrk":"1642.006334","fjd_to_btc":"0.001962","btc_to_gtq":"1877.752758","dkk_to_usd":"0.152223","ngn_to_btc":"0.00002","rsd_to_btc":"0.000038","usd_to_cve":"97.14236235","amd_to_btc":"0.000009","usd_to_lak":"8052.25","vnd_to_btc":"0.0","gtq_to_btc":"0.000533","all_to_usd":"0.008067","usd_to_bsd":"1.0","btc_to_mkd":"13366.569431","gip_to_usd":"1.582096","jmd_to_usd":"0.008612","irr_to_usd":"0.000034","clp_to_btc":"0.000006","btc_to_tmm":"975.966266","btc_to_ugx":"811930.2025","pkr_to_btc":"0.00004","btc_to_wst":"604.83969","gmd_to_usd":"0.023341","bwp_to_usd":"0.101646","btc_to_pab":"246.17","usd_to_bhd":"0.376573","btc_to_isk":"32238.4232","btc_to_kwd":"74.299768","szl_to_btc":"0.000334","usd_to_kyd":"0.825105","btc_to_fkp":"155.59741","usd_to_clp":"632.40125","btc_to_sdg":"1469.581481","usd_to_ltl":"3.014956","btc_to_twd":"7545.794853","lbp_to_usd":"0.000664","btc_to_syp":"46486.467582","amd_to_usd":"0.002112","usd_to_sar":"3.749629","mzn_to_usd":"0.026215","vef_to_btc":"0.000644","mvr_to_btc":"0.000267","nad_to_btc":"0.000334","jep_to_usd":"1.582096","usd_to_wst":"2.457","btc_to_kmf":"107780.909704","lsl_to_btc":"0.000334","mmk_to_usd":"0.000915","zmk_to_btc":"0.000001","zmw_to_usd":"0.134997","gyd_to_btc":"0.00002","usd_to_ghs":"4.362988","btc_to_bzd":"490.518342","usd_to_cdf":"917.5","usd_to_tmt":"3.50035","usd_to_gtq":"7.62787","btc_to_xag":"15.220691","btc_to_yer":"52898.48662","usd_to_fjd":"2.070475","btc_to_thb":"8218.961488","pen_to_usd":"0.316014","omr_to_btc":"0.010557","krw_to_usd":"0.000918","djf_to_usd":"0.00564","inr_to_usd":"0.01585","aud_to_btc":"0.003151","gel_to_btc":"0.00181","usd_to_kmf":"437.831213","usd_to_pkr":"101.166","usd_to_pyg":"5163.5","usd_to_zmk":"5253.075255","usd_to_xag":"0.06183","sar_to_btc":"0.001083","usd_to_gmd":"42.84385","hkd_to_usd":"0.12896","usd_to_irr":"29148.0849","rwf_to_btc":"0.000006","jod_to_btc":"0.005738","usd_to_brl":"3.099213","usd_to_zar":"12.152875","usd_to_srd":"3.265327","bnd_to_btc":"0.003042","btc_to_gnf":"1793784.478612","usd_to_bwp":"9.838083","usd_to_omr":"0.384775","vef_to_usd":"0.158645","btc_to_zmw":"1823.520768","usd_to_jpy":"123.151","usd_to_pgk":"2.735026","mmk_to_btc":"0.000004","btc_to_cdf":"225860.975","btc_to_ron":"972.565482","ron_to_usd":"0.253114","usd_to_btc":"0.004062","usd_to_szl":"12.15365","usd_to_bgn":"1.722576","btc_to_khr":"982956.81","crc_to_usd":"0.00186","rwf_to_usd":"0.001405","xof_to_btc":"0.000007","btc_to_brl":"762.933264","btc_to_hnl":"5364.142768","brl_to_usd":"0.322663","btc_to_afn":"14246.894768","usd_to_mkd":"54.298125","pen_to_btc":"0.001284","btc_to_tnd":"474.628069","btc_to_php":"11051.752916","sll_to_usd":"0.000237","lkr_to_usd":"0.007498","shp_to_btc":"0.006426","gnf_to_btc":"0.000001","usd_to_mtl":"0.683738","mnt_to_btc":"0.000002","btc_to_pln":"903.41559","cad_to_btc":"0.003317","usd_to_kwd":"0.301823","usd_to_thb":"33.38734","cve_to_btc":"0.000042","btc_to_usd":"246.17","btc_to_egp":"1876.838483","btc_to_byr":"3766162.153558","mad_to_btc":"0.000421","btc_to_tjs":"1540.919332","jpy_to_btc":"0.000033","btc_to_bob":"1689.634567","usd_to_bdt":"77.51594","tmt_to_btc":"0.00116","lyd_to_btc":"0.002973","htg_to_btc":"0.000084","gnf_to_usd":"0.000137","usd_to_aud":"1.289017","pab_to_usd":"1.0","cdf_to_usd":"0.00109","usd_to_lkr":"133.363","usd_to_zmw":"7.407567","usd_to_bam":"1.722557","thb_to_usd":"0.029951","egp_to_usd":"0.131162","std_to_btc":"0.0","btc_to_chf":"226.508648","kpw_to_usd":"0.001111","usd_to_eur":"0.88099","sll_to_btc":"0.000001","btc_to_gmd":"10546.870555","npr_to_usd":"0.009814","hrk_to_usd":"0.14992","usd_to_tmm":"2.85","xpf_to_btc":"0.000039","jmd_to_btc":"0.000035","dop_to_btc":"0.000091","all_to_btc":"0.000033","btc_to_lak":"1982222.3825","eur_to_usd":"1.135087","btc_to_jep":"155.59741","aed_to_btc":"0.001106","btc_to_jpy":"30316.08167","uyu_to_btc":"0.000152","nad_to_usd":"0.082285","pln_to_btc":"0.001107","ang_to_btc":"0.002271","etb_to_usd":"0.048445","zwl_to_btc":"0.000013","twd_to_usd":"0.032623","btc_to_bgn":"424.046534","btc_to_lbp":"370896.133415","pgk_to_btc":"0.001485","ils_to_usd":"0.260934","crc_to_btc":"0.000008","usd_to_djf":"177.2975","btc_to_nio":"6644.08522","lvl_to_usd":"1.615076","btc_to_mur":"8634.823115","azn_to_btc":"0.003869","lak_to_btc":"0.000001","usd_to_vnd":"21548.333333","xof_to_usd":"0.001727","btc_to_sos":"171507.459485","zmw_to_btc":"0.000548","czk_to_usd":"0.041718","usd_to_amd":"473.42","pkr_to_usd":"0.009885","usd_to_nad":"12.1529","bdt_to_usd":"0.012901","sdg_to_usd":"0.16751","btc_to_eek":"3393.225743","btc_to_bhd":"92.700975","usd_to_bbd":"2.0","aoa_to_usd":"0.008345","huf_to_btc":"0.000015","btc_to_zmk":"1293149.535523","qar_to_usd":"0.274674","kwd_to_btc":"0.013458","qar_to_btc":"0.001116","mxn_to_usd":"0.065271","btc_to_sbd":"1935.224837","usd_to_inr":"63.09158","fkp_to_usd":"1.582096","btc_to_cny":"1519.170212","btc_to_gip":"155.59741","mxn_to_btc":"0.000265","btc_to_kpw":"221530.8447","btc_to_jmd":"28583.414125","usd_to_kpw":"899.91","usd_to_usd":"1.0","btc_to_omr":"94.720062","btc_to_pyg":"1271098.795","usd_to_pen":"3.164413","mur_to_btc":"0.000116","btc_to_kgs":"14936.135073","btc_to_crc":"132337.294527","btc_to_try":"660.006141","btc_to_pen":"778.983548","btc_to_sll":"1037729.635","kmf_to_btc":"0.000009","chf_to_btc":"0.004415","btc_to_fjd":"509.688831","usd_to_isk":"130.96","btc_to_rwf":"175202.88155","btc_to_eur":"216.873308","scr_to_usd":"0.077347","btc_to_czk":"5900.827339","btc_to_ltl":"742.191719","lkr_to_btc":"0.00003","btc_to_tzs":"563175.4175","kzt_to_usd":"0.005411","bgn_to_btc":"0.002358","btc_to_svc":"2140.528155","usd_to_ngn":"198.92625","btc_to_qar":"896.226196","usd_to_mvr":"15.2","nio_to_usd":"0.037051","btc_to_uah":"5361.927238","cny_to_usd":"0.162042","usd_to_uyu":"26.74075","sgd_to_usd":"0.749602","usd_to_byr":"15299.02975","djf_to_btc":"0.000023","mdl_to_usd":"0.052925","usd_to_ern":"15.0015","usd_to_rub":"53.934","usd_to_awg":"1.791833","usd_to_htg":"48.226133","nok_to_btc":"0.000526","btc_to_cuc":"246.17","nzd_to_usd":"0.687569","gbp_to_usd":"1.582096","bdt_to_btc":"0.000052","mtl_to_usd":"1.462549","lak_to_usd":"0.000124","eek_to_usd":"0.072547","mdl_to_btc":"0.000215","khr_to_btc":"0.000001","byr_to_usd":"0.000065","ang_to_usd":"0.55918","ars_to_usd":"0.110103","btc_to_inr":"15531.254249","usd_to_cad":"1.224461","ugx_to_usd":"0.000303","tjs_to_usd":"0.159755","myr_to_usd":"0.268093","btc_to_ttd":"1561.057515","hrk_to_btc":"0.000609","usd_to_try":"2.681099","btc_to_mxn":"3771.493765","zar_to_usd":"0.082285","btc_to_dkk":"1617.169258","btc_to_tmt":"861.681159","btc_to_xau":"0.206783","svc_to_usd":"0.115004","wst_to_btc":"0.001653","huf_to_usd":"0.003645","cop_to_usd":"0.000392","krw_to_btc":"0.000004","usd_to_bob":"6.86369","usd_to_bnd":"1.33525","usd_to_aed":"3.67286","btc_to_xaf":"142365.114405","bif_to_usd":"0.000642","irr_to_btc":"0.0","usd_to_mur":"35.076667","usd_to_iqd":"1163.375","svc_to_btc":"0.000467","xau_to_btc":"4.835714","mwk_to_usd":"0.00229","cup_to_btc":"0.004085","usd_to_kgs":"60.674067","gmd_to_btc":"0.000095","cad_to_usd":"0.816686","rsd_to_usd":"0.009408","usd_to_sek":"8.128259","top_to_usd":"0.471162","hkd_to_btc":"0.000524","tmm_to_usd":"0.350877","btc_to_mga":"781060.4845","mtl_to_btc":"0.005941","iqd_to_usd":"0.00086","usd_to_vef":"6.30338","btc_to_nok":"1901.594322","lrd_to_usd":"0.011697","tmt_to_usd":"0.285686","usd_to_sdg":"5.969783","ttd_to_btc":"0.000641","usd_to_npr":"101.894475","xdr_to_usd":"1.413579","usd_to_top":"2.122412","btc_to_ghs":"1074.036756","usd_to_kes":"98.526863","lyd_to_usd":"0.731966","btc_to_btc":"0.999943","lbp_to_btc":"0.000003","azn_to_usd":"0.952442","kyd_to_usd":"1.211967","isk_to_usd":"0.007636","btc_to_sgd":"328.400873","pab_to_btc":"0.004062","isk_to_btc":"0.000031","eek_to_btc":"0.000295","btc_to_szl":"2991.864021","sek_to_usd":"0.123028","usd_to_sgd":"1.334041","xdr_to_btc":"0.005742","ltl_to_usd":"0.33168","bam_to_usd":"0.580532","htg_to_usd":"0.020736","kgs_to_usd":"0.016482","usd_to_bmd":"1.0","twd_to_btc":"0.000133","btc_to_huf":"67536.924127","usd_to_sbd":"7.861335","btc_to_lsl":"2992.163609","inr_to_btc":"0.000064","lrd_to_btc":"0.000048","usd_to_gbp":"0.632073","mad_to_usd":"0.103712","usd_to_tjs":"6.259574","btc_to_rsd":"26166.677076","bsd_to_usd":"1.0","aud_to_usd":"0.775785","btc_to_mop":"1958.878081","usd_to_nio":"26.989825","btc_to_sek":"2000.933518","btc_to_xof":"142570.900217","btc_to_cup":"244.773724","xaf_to_usd":"0.001729","btc_to_amd":"116541.8014","btc_to_nzd":"358.029648","usd_to_rsd":"106.29515","fkp_to_btc":"0.006426","usd_to_mdl":"18.8945","lsl_to_usd":"0.082272","usd_to_dop":"44.60078","btc_to_ils":"943.416985","xcd_to_btc":"0.001505","tmm_to_btc":"0.001025","usd_to_fkp":"0.632073","bnd_to_usd":"0.748923","btc_to_clf":"6.05529","usd_to_xpf":"105.22","usd_to_hrk":"6.670213","usd_to_lsl":"12.154867","usd_to_tnd":"1.92805","btc_to_mro":"78889.595252","usd_to_mro":"320.46795","try_to_btc":"0.001515","usd_to_php":"44.8948","btc_to_clp":"155678.215712","xcd_to_usd":"0.370554","vuv_to_usd":"0.009535","jod_to_usd":"1.412513","usd_to_gyd":"204.8075","btc_to_lkr":"32829.96971","tzs_to_btc":"0.000002","usd_to_chf":"0.920131","brl_to_btc":"0.001311","egp_to_btc":"0.000533","jep_to_btc":"0.006426","srd_to_btc":"0.001244","usd_to_kzt":"184.793","szl_to_usd":"0.08228","afn_to_btc":"0.00007","btc_to_cve":"23913.53534","btc_to_mdl":"4651.259065","syp_to_usd":"0.005296","usd_to_lbp":"1506.666667","syp_to_btc":"0.000022","mro_to_btc":"0.000013","aed_to_usd":"0.272267","vnd_to_usd":"0.000046","bif_to_btc":"0.000003","btc_to_uyu":"6582.770428","bob_to_btc":"0.000592","gyd_to_usd":"0.004883","btc_to_mtl":"168.315783","kyd_to_btc":"0.004923","usd_to_svc":"8.695325","mro_to_usd":"0.00312","btc_to_nad":"2991.679393","usd_to_scr":"12.928707","btc_to_bbd":"492.34","mnt_to_usd":"0.000522","btn_to_usd":"0.015788","uah_to_usd":"0.045911","zmk_to_usd":"0.00019","btc_to_ars":"2235.817608","btc_to_iqd":"286388.02375","usd_to_krw":"1089.276667","usd_to_xdr":"0.707424","usd_to_mga":"3172.85","yer_to_usd":"0.004654","btc_to_ngn":"48969.674963","btc_to_vnd":"5304553.216585","tnd_to_btc":"0.002107","zar_to_btc":"0.000334","bzd_to_btc":"0.002039","btc_to_vef":"1551.703055","dzd_to_btc":"0.000041","ern_to_usd":"0.06666","scr_to_btc":"0.000314","btc_to_vuv":"25818.3096","sar_to_usd":"0.266693","usd_to_jod":"0.707958","btc_to_etb":"5081.416523","btc_to_gel":"552.462838","usd_to_lrd":"85.49145","btc_to_lyd":"336.313269","ils_to_btc":"0.00106","ron_to_btc":"0.001028","usd_to_rwf":"711.715","usd_to_etb":"20.6419","btc_to_xpf":"25902.0074","usd_to_cuc":"1.0","btc_to_pgk":"673.28135","btc_to_krw":"268147.237115","usd_to_ron":"3.950788","mga_to_usd":"0.000315","usd_to_twd":"30.65278","usd_to_lyd":"1.366183","btc_to_lrd":"21045.430247","btc_to_myr":"918.226408","usd_to_gnf":"7286.77125","xaf_to_btc":"0.000007","btc_to_bwp":"2421.840892","tzs_to_usd":"0.000437","dop_to_usd":"0.022421","byr_to_btc":"0.0","usd_to_ars":"9.082413","idr_to_btc":"0.0","usd_to_ugx":"3298.25","usd_to_jmd":"116.1125","btc_to_scr":"3182.659802","usd_to_myr":"3.73005","btc_to_mzn":"9390.446361","usd_to_czk":"23.970538","uah_to_btc":"0.000186","btc_to_idr":"3279107.485","cve_to_usd":"0.010294","usd_to_all":"123.96625","usd_to_xcd":"2.69866","xpf_to_usd":"0.009504","hnl_to_btc":"0.000186","ghs_to_usd":"0.229201","btc_to_bdt":"19082.09895","usd_to_azn":"1.049933","usd_to_ttd":"6.34138","btc_to_rub":"13276.93278","btc_to_irr":"7175384.059833","mop_to_btc":"0.00051","usd_to_hnl":"21.7904","php_to_usd":"0.022274","npr_to_btc":"0.00004","usd_to_yer":"214.886","jpy_to_usd":"0.00812","cup_to_usd":"1.005704","usd_to_mnt":"1914.25","btc_to_mvr":"3741.784","hnl_to_usd":"0.045892","mkd_to_btc":"0.000075","usd_to_idr":"13320.5","btc_to_cad":"301.425564","bhd_to_btc":"0.010787","bob_to_usd":"0.145694","usd_to_bzd":"1.9926","usd_to_qar":"3.64068","sgd_to_btc":"0.003045","afn_to_usd":"0.017279","usd_to_pab":"1.0","lvl_to_btc":"0.00656","usd_to_syp":"188.838882","btc_to_npr":"25083.362911","mwk_to_btc":"0.000009","srd_to_usd":"0.306248","uzs_to_btc":"0.000002","usd_to_aoa":"119.837","top_to_btc":"0.001914","etb_to_btc":"0.000197","cdf_to_btc":"0.000004"} 68 | http_version: 69 | recorded_at: Mon, 22 Jun 2015 14:10:31 GMT 70 | recorded_with: VCR 2.9.3 71 | -------------------------------------------------------------------------------- /spec/fixtures/vcr/exchange_rate_average_rate_adapter.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: get 5 | uri: https://www.bitstamp.net/api/ticker/ 6 | body: 7 | encoding: US-ASCII 8 | string: '' 9 | headers: 10 | Accept-Encoding: 11 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 12 | Accept: 13 | - "*/*" 14 | User-Agent: 15 | - Ruby 16 | response: 17 | status: 18 | code: 200 19 | message: OK 20 | headers: 21 | Etag: 22 | - '"668cb-9e-5191bd5b14582"' 23 | Last-Modified: 24 | - Mon, 22 Jun 2015 14:10:23 GMT 25 | Content-Type: 26 | - application/json 27 | Content-Length: 28 | - '158' 29 | Connection: 30 | - keep-alive 31 | Cache-Control: 32 | - max-age=10, public 33 | Expires: 34 | - Mon, 22 Jun 2015 14:10:39 GMT 35 | Date: 36 | - Mon, 22 Jun 2015 14:10:29 GMT 37 | Set-Cookie: 38 | - incap_ses_324_99025=ADo4COcCjHKRv/PQbRR/BFUXiFUAAAAAfI5t+lvdbzGK+emYUtLrJg==; 39 | path=/; Domain=.bitstamp.net 40 | - visid_incap_99025=6+IrklV2RSeknYLkirutf1UXiFUAAAAAQUIPAAAAAABZi80yAdGOU7JNknm80lr8; 41 | expires=Tue, 20 Jun 2017 19:47:19 GMT; path=/; Domain=.bitstamp.net 42 | X-Iinfo: 43 | - 10-52782343-0 0CNN RT(1434982228882 146) q(0 -1 -1 1) r(0 -1) 44 | X-Cdn: 45 | - Incapsula 46 | body: 47 | encoding: UTF-8 48 | string: '{"high": "246.82", "last": "245.20", "timestamp": "1434982220", "bid": 49 | "245.13", "vwap": "244.3", "volume": "4244.46970928", "low": "241.06", "ask": 50 | "245.20"}' 51 | http_version: 52 | recorded_at: Mon, 22 Jun 2015 14:10:29 GMT 53 | - request: 54 | method: get 55 | uri: https://bitpay.com/api/rates 56 | body: 57 | encoding: US-ASCII 58 | string: '' 59 | headers: 60 | Accept-Encoding: 61 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 62 | Accept: 63 | - "*/*" 64 | User-Agent: 65 | - Ruby 66 | response: 67 | status: 68 | code: 200 69 | message: OK 70 | headers: 71 | Server: 72 | - cloudflare-nginx 73 | Date: 74 | - Mon, 22 Jun 2015 14:10:30 GMT 75 | Content-Type: 76 | - application/json 77 | Transfer-Encoding: 78 | - chunked 79 | Connection: 80 | - keep-alive 81 | Set-Cookie: 82 | - __cfduid=dadf01e54b0b3d9278c2f9e1d47ec81341434982230; expires=Tue, 21-Jun-16 83 | 14:10:30 GMT; path=/; domain=.bitpay.com; HttpOnly 84 | Strict-Transport-Security: 85 | - max-age=31536000 86 | Access-Control-Allow-Origin: 87 | - "*" 88 | Cache-Control: 89 | - public, max-age=1800 90 | Expires: 91 | - Mon, 22 Jun 2015 14:40:30 GMT 92 | Cf-Cache-Status: 93 | - HIT 94 | Vary: 95 | - Accept-Encoding 96 | Cf-Ray: 97 | - 1fa889798e490731-AMS 98 | body: 99 | encoding: ASCII-8BIT 100 | string: !binary |- 101 | W3siY29kZSI6IkJUQyIsIm5hbWUiOiJCaXRjb2luIiwicmF0ZSI6MX0seyJj 102 | b2RlIjoiVVNEIiwibmFtZSI6IlVTIERvbGxhciIsInJhdGUiOjI0NS4xM30s 103 | eyJjb2RlIjoiRVVSIiwibmFtZSI6IkV1cm96b25lIEV1cm8iLCJyYXRlIjoy 104 | MTUuMDA5MTMyfSx7ImNvZGUiOiJHQlAiLCJuYW1lIjoiUG91bmQgU3Rlcmxp 105 | bmciLCJyYXRlIjoxNTQuMzUwMzM3fSx7ImNvZGUiOiJKUFkiLCJuYW1lIjoi 106 | SmFwYW5lc2UgWWVuIiwicmF0ZSI6MzAwMzkuMzM5MDZ9LHsiY29kZSI6IkNB 107 | RCIsIm5hbWUiOiJDYW5hZGlhbiBEb2xsYXIiLCJyYXRlIjoyOTkuMDYyNDE4 108 | fSx7ImNvZGUiOiJBVUQiLCJuYW1lIjoiQXVzdHJhbGlhbiBEb2xsYXIiLCJy 109 | YXRlIjozMTQuMzkxMTk4fSx7ImNvZGUiOiJDTlkiLCJuYW1lIjoiQ2hpbmVz 110 | ZSBZdWFuIiwicmF0ZSI6MTUxMS4yNTcxMzh9LHsiY29kZSI6IkNIRiIsIm5h 111 | bWUiOiJTd2lzcyBGcmFuYyIsInJhdGUiOjIyMi44Mzk2MTl9LHsiY29kZSI6 112 | IlNFSyIsIm5hbWUiOiJTd2VkaXNoIEtyb25hIiwicmF0ZSI6MTk4NC4yMDU1 113 | NjN9LHsiY29kZSI6Ik5aRCIsIm5hbWUiOiJOZXcgWmVhbGFuZCBEb2xsYXIi 114 | LCJyYXRlIjozNTQuNjIwODJ9LHsiY29kZSI6IktSVyIsIm5hbWUiOiJTb3V0 115 | aCBLb3JlYW4gV29uIiwicmF0ZSI6MjY1OTU2LjgwMTY4MX0seyJjb2RlIjoi 116 | QUVEIiwibmFtZSI6IlVBRSBEaXJoYW0iLCJyYXRlIjo4OTcuMTE4MTI4fSx7 117 | ImNvZGUiOiJBRk4iLCJuYW1lIjoiQWZnaGFuIEFmZ2hhbmkiLCJyYXRlIjox 118 | Mzk4MC44MzE3NX0seyJjb2RlIjoiQUxMIiwibmFtZSI6IkFsYmFuaWFuIExl 119 | ayIsInJhdGUiOjMwMjc1LjExMTAyNX0seyJjb2RlIjoiQU1EIiwibmFtZSI6 120 | IkFybWVuaWFuIERyYW0iLCJyYXRlIjoxMTU2MDguMjU4fSx7ImNvZGUiOiJB 121 | TkciLCJuYW1lIjoiTmV0aGVybGFuZHMgQW50aWxsZWFuIEd1aWxkZXIiLCJy 122 | YXRlIjo0MzYuODEwMTU4fSx7ImNvZGUiOiJBT0EiLCJuYW1lIjoiQW5nb2xh 123 | biBLd2FuemEiLCJyYXRlIjoyOTI3MC4xNjQzMn0seyJjb2RlIjoiQVJTIiwi 124 | bmFtZSI6IkFyZ2VudGluZSBQZXNvIiwicmF0ZSI6MzEyMS42NDI4fSx7ImNv 125 | ZGUiOiJBV0ciLCJuYW1lIjoiQXJ1YmFuIEZsb3JpbiIsInJhdGUiOjQzNy42 126 | MzI1ODF9LHsiY29kZSI6IkFaTiIsIm5hbWUiOiJBemVyYmFpamFuaSBNYW5h 127 | dCIsInJhdGUiOjI1Ni40NTY2MzV9LHsiY29kZSI6IkJBTSIsIm5hbWUiOiJC 128 | b3NuaWEtSGVyemVnb3ZpbmEgQ29udmVydGlibGUgTWFyayIsInJhdGUiOjQy 129 | MC43NTE3NzN9LHsiY29kZSI6IkJCRCIsIm5hbWUiOiJCYXJiYWRpYW4gRG9s 130 | bGFyIiwicmF0ZSI6NDg4LjUyfSx7ImNvZGUiOiJCRFQiLCJuYW1lIjoiQmFu 131 | Z2xhZGVzaGkgVGFrYSIsInJhdGUiOjE4OTIyLjkzNDU2fSx7ImNvZGUiOiJC 132 | R04iLCJuYW1lIjoiQnVsZ2FyaWFuIExldiIsInJhdGUiOjQyMC42NTg3MX0s 133 | eyJjb2RlIjoiQkhEIiwibmFtZSI6IkJhaHJhaW5pIERpbmFyIiwicmF0ZSI6 134 | OTEuNjAyMTQxfSx7ImNvZGUiOiJCSUYiLCJuYW1lIjoiQnVydW5kaWFuIEZy 135 | YW5jIiwicmF0ZSI6MzgxMTA2LjY2NX0seyJjb2RlIjoiQk1EIiwibmFtZSI6 136 | IkJlcm11ZGFuIERvbGxhciIsInJhdGUiOjI0NC4yNn0seyJjb2RlIjoiQk5E 137 | IiwibmFtZSI6IkJydW5laSBEb2xsYXIiLCJyYXRlIjozMjYuMTIzNzM5fSx7 138 | ImNvZGUiOiJCT0IiLCJuYW1lIjoiQm9saXZpYW4gQm9saXZpYW5vIiwicmF0 139 | ZSI6MTY3Ni40NDY3NTZ9LHsiY29kZSI6IkJSTCIsIm5hbWUiOiJCcmF6aWxp 140 | YW4gUmVhbCIsInJhdGUiOjc1NC43Mzg5NzR9LHsiY29kZSI6IkJTRCIsIm5h 141 | bWUiOiJCYWhhbWlhbiBEb2xsYXIiLCJyYXRlIjoyNDQuMjZ9LHsiY29kZSI6 142 | IkJUTiIsIm5hbWUiOiJCaHV0YW5lc2UgTmd1bHRydW0iLCJyYXRlIjoxNTQ2 143 | OS41OTY0NX0seyJjb2RlIjoiQldQIiwibmFtZSI6IkJvdHN3YW5hbiBQdWxh 144 | IiwicmF0ZSI6MjQwMy4wOTkwMDZ9LHsiY29kZSI6IkJZUiIsIm5hbWUiOiJC 145 | ZWxhcnVzaWFuIFJ1YmxlIiwicmF0ZSI6MzczNjk0MS4wMDY3MzV9LHsiY29k 146 | ZSI6IkJaRCIsIm5hbWUiOiJCZWxpemUgRG9sbGFyIiwicmF0ZSI6NDg5LjI2 147 | NDk5M30seyJjb2RlIjoiQ0RGIiwibmFtZSI6IkNvbmdvbGVzZSBGcmFuYyIs 148 | InJhdGUiOjIyMzg2NC4yOX0seyJjb2RlIjoiQ0xGIiwibmFtZSI6IkNoaWxl 149 | YW4gVW5pdCBvZiBBY2NvdW50IChVRikiLCJyYXRlIjo2LjAwODMwN30seyJj 150 | b2RlIjoiQ0xQIiwibmFtZSI6IkNoaWxlYW4gUGVzbyIsInJhdGUiOjE1NDE5 151 | OC41OTAwNzV9LHsiY29kZSI6IkNPUCIsIm5hbWUiOiJDb2xvbWJpYW4gUGVz 152 | byIsInJhdGUiOjYyMzgyNy44Mjd9LHsiY29kZSI6IkNSQyIsIm5hbWUiOiJD 153 | b3N0YSBSaWNhbiBDb2zDs24iLCJyYXRlIjoxMzAxNTUuMTU3NDE1fSx7ImNv 154 | ZGUiOiJDVkUiLCJuYW1lIjoiQ2FwZSBWZXJkZWFuIEVzY3VkbyIsInJhdGUi 155 | OjIzNzA3Ljk4MTk2NH0seyJjb2RlIjoiQ1pLIiwibmFtZSI6IkN6ZWNoIEtv 156 | cnVuYSIsInJhdGUiOjU4NTIuNTMzODR9LHsiY29kZSI6IkRKRiIsIm5hbWUi 157 | OiJEamlib3V0aWFuIEZyYW5jIiwicmF0ZSI6NDMzMDYuMDc2N30seyJjb2Rl 158 | IjoiREtLIiwibmFtZSI6IkRhbmlzaCBLcm9uZSIsInJhdGUiOjE2MDQuMTUx 159 | NjU4fSx7ImNvZGUiOiJET1AiLCJuYW1lIjoiRG9taW5pY2FuIFBlc28iLCJy 160 | YXRlIjoxMDk3Ni41NDk3NzR9LHsiY29kZSI6IkRaRCIsIm5hbWUiOiJBbGdl 161 | cmlhbiBEaW5hciIsInJhdGUiOjI0MDI3LjM4Nn0seyJjb2RlIjoiRUVLIiwi 162 | bmFtZSI6IkVzdG9uaWFuIEtyb29uIiwicmF0ZSI6MzM2Ni4xNTMxNjd9LHsi 163 | Y29kZSI6IkVHUCIsIm5hbWUiOiJFZ3lwdGlhbiBQb3VuZCIsInJhdGUiOjE4 164 | NjEuNjY4ODd9LHsiY29kZSI6IkVUQiIsIm5hbWUiOiJFdGhpb3BpYW4gQmly 165 | ciIsInJhdGUiOjUwNDEuOTM1NTM2fSx7ImNvZGUiOiJGSkQiLCJuYW1lIjoi 166 | RmlqaWFuIERvbGxhciIsInJhdGUiOjUwNC45NTI1OTJ9LHsiY29kZSI6IkZL 167 | UCIsIm5hbWUiOiJGYWxrbGFuZCBJc2xhbmRzIFBvdW5kIiwicmF0ZSI6MTU0 168 | LjM1MDMzN30seyJjb2RlIjoiR0VMIiwibmFtZSI6Ikdlb3JnaWFuIExhcmki 169 | LCJyYXRlIjo1NDcuMzk0NzIxfSx7ImNvZGUiOiJHSFMiLCJuYW1lIjoiR2hh 170 | bmFpYW4gQ2VkaSIsInJhdGUiOjEwNjkuMjMzMDA2fSx7ImNvZGUiOiJHSVAi 171 | LCJuYW1lIjoiR2licmFsdGFyIFBvdW5kIiwicmF0ZSI6MTU0LjM1MDMzN30s 172 | eyJjb2RlIjoiR01EIiwibmFtZSI6IkdhbWJpYW4gRGFsYXNpIiwicmF0ZSI6 173 | MTA0NjQuOTIyNzc4fSx7ImNvZGUiOiJHTkYiLCJuYW1lIjoiR3VpbmVhbiBG 174 | cmFuYyIsInJhdGUiOjE3Nzk4NjYuNzQ1NTI1fSx7ImNvZGUiOiJHVFEiLCJu 175 | YW1lIjoiR3VhdGVtYWxhbiBRdWV0emFsIiwicmF0ZSI6MTg2Mi4zMDQxOX0s 176 | eyJjb2RlIjoiR1lEIiwibmFtZSI6Ikd1eWFuYWVzZSBEb2xsYXIiLCJyYXRl 177 | Ijo1MDAyNi4yNzk5NX0seyJjb2RlIjoiSEtEIiwibmFtZSI6IkhvbmcgS29u 178 | ZyBEb2xsYXIiLCJyYXRlIjoxODkzLjYzNzg2M30seyJjb2RlIjoiSE5MIiwi 179 | bmFtZSI6IkhvbmR1cmFuIExlbXBpcmEiLCJyYXRlIjo1MzIyLjQ2MjAzOX0s 180 | eyJjb2RlIjoiSFJLIiwibmFtZSI6IkNyb2F0aWFuIEt1bmEiLCJyYXRlIjox 181 | NjI3Ljk3Nzc1Nn0seyJjb2RlIjoiSFRHIiwibmFtZSI6IkhhaXRpYW4gR291 182 | cmRlIiwicmF0ZSI6MTE3NzkuNTM2MjA0fSx7ImNvZGUiOiJIVUYiLCJuYW1l 183 | IjoiSHVuZ2FyaWFuIEZvcmludCIsInJhdGUiOjY2OTgxLjc3MTA0NX0seyJj 184 | b2RlIjoiSURSIiwibmFtZSI6IkluZG9uZXNpYW4gUnVwaWFoIiwicmF0ZSI6 185 | MzI1MzY2NS4zM30seyJjb2RlIjoiSUxTIiwibmFtZSI6IklzcmFlbGkgU2hl 186 | a2VsIiwicmF0ZSI6OTM2LjMxMDg2Nn0seyJjb2RlIjoiSU5SIiwibmFtZSI6 187 | IkluZGlhbiBSdXBlZSIsInJhdGUiOjE1NTExLjU2NjQyNH0seyJjb2RlIjoi 188 | SVFEIiwibmFtZSI6IklyYXFpIERpbmFyIiwicmF0ZSI6Mjg0OTkwLjM1NX0s 189 | eyJjb2RlIjoiSVNLIiwibmFtZSI6IkljZWxhbmRpYyBLcsOzbmEiLCJyYXRl 190 | IjozMTk4Ny4wNjgzfSx7ImNvZGUiOiJKRVAiLCJuYW1lIjoiSmVyc2V5IFBv 191 | dW5kIiwicmF0ZSI6MTU0LjM1MDMzN30seyJjb2RlIjoiSk1EIiwibmFtZSI6 192 | IkphbWFpY2FuIERvbGxhciIsInJhdGUiOjI4MzYxLjAyODZ9LHsiY29kZSI6 193 | IkpPRCIsIm5hbWUiOiJKb3JkYW5pYW4gRGluYXIiLCJyYXRlIjoxNzIuOTQ2 194 | ODI3fSx7ImNvZGUiOiJLRVMiLCJuYW1lIjoiS2VueWFuIFNoaWxsaW5nIiwi 195 | cmF0ZSI6MjQwNzIuNTgzMzgxfSx7ImNvZGUiOiJLR1MiLCJuYW1lIjoiS3ly 196 | Z3lzdGFuaSBTb20iLCJyYXRlIjoxNDgyMC4yNDc2MDV9LHsiY29kZSI6IktI 197 | UiIsIm5hbWUiOiJDYW1ib2RpYW4gUmllbCIsInJhdGUiOjk2OTM4Mi40NDl9 198 | LHsiY29kZSI6IktNRiIsIm5hbWUiOiJDb21vcmlhbiBGcmFuYyIsInJhdGUi 199 | OjEwNjk2Ni4wNDMxNTd9LHsiY29kZSI6IktXRCIsIm5hbWUiOiJLdXdhaXRp 200 | IERpbmFyIiwicmF0ZSI6NzMuNzI0NTA3fSx7ImNvZGUiOiJLWUQiLCJuYW1l 201 | IjoiQ2F5bWFuIElzbGFuZHMgRG9sbGFyIiwicmF0ZSI6MjAxLjQzNTExNn0s 202 | eyJjb2RlIjoiS1pUIiwibmFtZSI6IkthemFraHN0YW5pIFRlbmdlIiwicmF0 203 | ZSI6NDUxMjcuNzY3Nzh9LHsiY29kZSI6IkxBSyIsIm5hbWUiOiJMYW90aWFu 204 | IEtpcCIsInJhdGUiOjE5NDc5MjQuNjQ4fSx7ImNvZGUiOiJMQlAiLCJuYW1l 205 | IjoiTGViYW5lc2UgUG91bmQiLCJyYXRlIjozNjU0MzcuMzg2fSx7ImNvZGUi 206 | OiJMS1IiLCJuYW1lIjoiU3JpIExhbmthbiBSdXBlZSIsInJhdGUiOjMyNTky 207 | LjU4ODg0fSx7ImNvZGUiOiJMUkQiLCJuYW1lIjoiTGliZXJpYW4gRG9sbGFy 208 | IiwicmF0ZSI6MjA4ODIuMTQxNTc3fSx7ImNvZGUiOiJMU0wiLCJuYW1lIjoi 209 | TGVzb3RobyBMb3RpIiwicmF0ZSI6Mjk2OC45MTUwODN9LHsiY29kZSI6IkxU 210 | TCIsIm5hbWUiOiJMaXRodWFuaWFuIExpdGFzIiwicmF0ZSI6NzM2LjQ1NzU3 211 | OX0seyJjb2RlIjoiTFZMIiwibmFtZSI6IkxhdHZpYW4gTGF0cyIsInJhdGUi 212 | OjE1MS4yMDA4NDh9LHsiY29kZSI6IkxZRCIsIm5hbWUiOiJMaWJ5YW4gRGlu 213 | YXIiLCJyYXRlIjozMzMuNzAzODZ9LHsiY29kZSI6Ik1BRCIsIm5hbWUiOiJN 214 | b3JvY2NhbiBEaXJoYW0iLCJyYXRlIjoyMzU0LjMzNjY0OX0seyJjb2RlIjoi 215 | TURMIiwibmFtZSI6Ik1vbGRvdmFuIExldSIsInJhdGUiOjQ2MTUuMTQwMDM3 216 | fSx7ImNvZGUiOiJNR0EiLCJuYW1lIjoiTWFsYWdhc3kgQXJpYXJ5IiwicmF0 217 | ZSI6Nzc1MDAwLjM0MX0seyJjb2RlIjoiTUtEIiwibmFtZSI6Ik1hY2Vkb25p 218 | YW4gRGVuYXIiLCJyYXRlIjoxMzI1Mi40ODUwNjl9LHsiY29kZSI6Ik1NSyIs 219 | Im5hbWUiOiJNeWFubWEgS3lhdCIsInJhdGUiOjI2NjkwMi45MDJ9LHsiY29k 220 | ZSI6Ik1OVCIsIm5hbWUiOiJNb25nb2xpYW4gVHVncmlrIiwicmF0ZSI6NDY3 221 | NTc0LjcwNX0seyJjb2RlIjoiTU9QIiwibmFtZSI6Ik1hY2FuZXNlIFBhdGFj 222 | YSIsInJhdGUiOjE5NDIuNjI5MDkxfSx7ImNvZGUiOiJNUk8iLCJuYW1lIjoi 223 | TWF1cml0YW5pYW4gT3VndWl5YSIsInJhdGUiOjc4Mjc3LjUwMTQ2N30seyJj 224 | b2RlIjoiTVVSIiwibmFtZSI6Ik1hdXJpdGlhbiBSdXBlZSIsInJhdGUiOjg1 225 | NzAuMjIwNDI5fSx7ImNvZGUiOiJNVlIiLCJuYW1lIjoiTWFsZGl2aWFuIFJ1 226 | Zml5YWEiLCJyYXRlIjozNzEzLjU2NjExOX0seyJjb2RlIjoiTVdLIiwibmFt 227 | ZSI6Ik1hbGF3aWFuIEt3YWNoYSIsInJhdGUiOjEwNjQ1NC42MjA2MDd9LHsi 228 | Y29kZSI6Ik1YTiIsIm5hbWUiOiJNZXhpY2FuIFBlc28iLCJyYXRlIjozNzQz 229 | Ljg1ODUxMX0seyJjb2RlIjoiTVlSIiwibmFtZSI6Ik1hbGF5c2lhbiBSaW5n 230 | Z2l0IiwicmF0ZSI6OTExLjE1MDg2NX0seyJjb2RlIjoiTVpOIiwibmFtZSI6 231 | Ik1vemFtYmljYW4gTWV0aWNhbCIsInJhdGUiOjkzMTkuNDUwODUyfSx7ImNv 232 | ZGUiOiJOQUQiLCJuYW1lIjoiTmFtaWJpYW4gRG9sbGFyIiwicmF0ZSI6Mjk2 233 | OC4xNzQyNDJ9LHsiY29kZSI6Ik5HTiIsIm5hbWUiOiJOaWdlcmlhbiBOYWly 234 | YSIsInJhdGUiOjQ4NjI0LjUzMjg3NX0seyJjb2RlIjoiTklPIiwibmFtZSI6 235 | Ik5pY2FyYWd1YW4gQ8OzcmRvYmEiLCJyYXRlIjo2NTkyLjQ2MTM3N30seyJj 236 | b2RlIjoiTk9LIiwibmFtZSI6Ik5vcndlZ2lhbiBLcm9uZSIsInJhdGUiOjE4 237 | ODMuNzkyOTY0fSx7ImNvZGUiOiJOUFIiLCJuYW1lIjoiTmVwYWxlc2UgUnVw 238 | ZWUiLCJyYXRlIjoyNDg4OC43NDQ0NjR9LHsiY29kZSI6Ik9NUiIsIm5hbWUi 239 | OiJPbWFuaSBSaWFsIiwicmF0ZSI6OTMuOTkwMDI3fSx7ImNvZGUiOiJQQUIi 240 | LCJuYW1lIjoiUGFuYW1hbmlhbiBCYWxib2EiLCJyYXRlIjoyNDQuMjZ9LHsi 241 | Y29kZSI6IlBFTiIsIm5hbWUiOiJQZXJ1dmlhbiBOdWV2byBTb2wiLCJyYXRl 242 | Ijo3NzIuOTI3MzA2fSx7ImNvZGUiOiJQR0siLCJuYW1lIjoiUGFwdWEgTmV3 243 | IEd1aW5lYW4gS2luYSIsInJhdGUiOjY2NS43OTE2OTV9LHsiY29kZSI6IlBI 244 | UCIsIm5hbWUiOiJQaGlsaXBwaW5lIFBlc28iLCJyYXRlIjoxMDk4NC44MTE4 245 | Njh9LHsiY29kZSI6IlBLUiIsIm5hbWUiOiJQYWtpc3RhbmkgUnVwZWUiLCJy 246 | YXRlIjoyNDg1NC42NzYzfSx7ImNvZGUiOiJQTE4iLCJuYW1lIjoiUG9saXNo 247 | IFpsb3R5IiwicmF0ZSI6ODk2Ljg5MTY5OX0seyJjb2RlIjoiUFlHIiwibmFt 248 | ZSI6IlBhcmFndWF5YW4gR3VhcmFuaSIsInJhdGUiOjEyNjEyMzYuNTF9LHsi 249 | Y29kZSI6IlFBUiIsIm5hbWUiOiJRYXRhcmkgUmlhbCIsInJhdGUiOjg4OS4y 250 | ODIyNjd9LHsiY29kZSI6IlJPTiIsIm5hbWUiOiJSb21hbmlhbiBMZXUiLCJy 251 | YXRlIjo5NjQuNjk1ODMyfSx7ImNvZGUiOiJSU0QiLCJuYW1lIjoiU2VyYmlh 252 | biBEaW5hciIsInJhdGUiOjI1OTUxLjU4MDc4OX0seyJjb2RlIjoiUlVCIiwi 253 | bmFtZSI6IlJ1c3NpYW4gUnVibGUiLCJyYXRlIjoxMzE4Ni41ODM3MjF9LHsi 254 | Y29kZSI6IlJXRiIsIm5hbWUiOiJSd2FuZGFuIEZyYW5jIiwicmF0ZSI6MTcz 255 | ODQwLjI0OTE4MX0seyJjb2RlIjoiU0FSIiwibmFtZSI6IlNhdWRpIFJpeWFs 256 | IiwicmF0ZSI6OTE1LjgzMDY0Mn0seyJjb2RlIjoiU0JEIiwibmFtZSI6IlNv 257 | bG9tb24gSXNsYW5kcyBEb2xsYXIiLCJyYXRlIjoxOTIwLjIwOTY4N30seyJj 258 | b2RlIjoiU0NSIiwibmFtZSI6IlNleWNoZWxsb2lzIFJ1cGVlIiwicmF0ZSI6 259 | MzE3Ni4zMTYyNDl9LHsiY29kZSI6IlNERyIsIm5hbWUiOiJTdWRhbmVzZSBQ 260 | b3VuZCIsInJhdGUiOjE0NTguMTU0Nzd9LHsiY29kZSI6IlNHRCIsIm5hbWUi 261 | OiJTaW5nYXBvcmUgRG9sbGFyIiwicmF0ZSI6MzI1Ljc4ODg1OX0seyJjb2Rl 262 | IjoiU0hQIiwibmFtZSI6IlNhaW50IEhlbGVuYSBQb3VuZCIsInJhdGUiOjE1 263 | NC4zNTAzMzd9LHsiY29kZSI6IlNMTCIsIm5hbWUiOiJTaWVycmEgTGVvbmVh 264 | biBMZW9uZSIsInJhdGUiOjEwMzA3NzcuMn0seyJjb2RlIjoiU09TIiwibmFt 265 | ZSI6IlNvbWFsaSBTaGlsbGluZyIsInJhdGUiOjE3MDE3NC4zMTM1MTl9LHsi 266 | Y29kZSI6IlNSRCIsIm5hbWUiOiJTdXJpbmFtZXNlIERvbGxhciIsInJhdGUi 267 | Ojc5Ny40MjkyNzF9LHsiY29kZSI6IlNURCIsIm5hbWUiOiJTw6NvIFRvbcOp 268 | IGFuZCBQcsOtbmNpcGUgRG9icmEiLCJyYXRlIjo1MzAwOTcyLjA0NDJ9LHsi 269 | Y29kZSI6IlNWQyIsIm5hbWUiOiJTYWx2YWRvcmFuIENvbMOzbiIsInJhdGUi 270 | OjIxMjMuMDM0NjQyfSx7ImNvZGUiOiJTWVAiLCJuYW1lIjoiU3lyaWFuIFBv 271 | dW5kIiwicmF0ZSI6NDYxMjUuNzg1MzE3fSx7ImNvZGUiOiJTWkwiLCJuYW1l 272 | IjoiU3dhemkgTGlsYW5nZW5pIiwicmF0ZSI6Mjk2OC4zNTc0Mzd9LHsiY29k 273 | ZSI6IlRIQiIsIm5hbWUiOiJUaGFpIEJhaHQiLCJyYXRlIjo4MTYzLjMzNTI5 274 | N30seyJjb2RlIjoiVEpTIiwibmFtZSI6IlRhamlraXN0YW5pIFNvbW9uaSIs 275 | InJhdGUiOjE1MjguOTYzNTQ1fSx7ImNvZGUiOiJUTVQiLCJuYW1lIjoiVHVy 276 | a21lbmlzdGFuaSBNYW5hdCIsInJhdGUiOjg1NC44MDAwODN9LHsiY29kZSI6 277 | IlRORCIsIm5hbWUiOiJUdW5pc2lhbiBEaW5hciIsInJhdGUiOjQ3MC43Njg0 278 | MDV9LHsiY29kZSI6IlRPUCIsIm5hbWUiOiJUb25nYW4gUGHKu2FuZ2EiLCJy 279 | YXRlIjo1MTguMzE2Nzg5fSx7ImNvZGUiOiJUUlkiLCJuYW1lIjoiVHVya2lz 280 | aCBMaXJhIiwicmF0ZSI6NjU1LjAzNDcyOX0seyJjb2RlIjoiVFREIiwibmFt 281 | ZSI6IlRyaW5pZGFkIGFuZCBUb2JhZ28gRG9sbGFyIiwicmF0ZSI6MTU0OC4z 282 | NTQzN30seyJjb2RlIjoiVFdEIiwibmFtZSI6Ik5ldyBUYWl3YW4gRG9sbGFy 283 | IiwicmF0ZSI6NzQ1Ni43OTM3MDZ9LHsiY29kZSI6IlRaUyIsIm5hbWUiOiJU 284 | YW56YW5pYW4gU2hpbGxpbmciLCJyYXRlIjo1NTg4MDUuODE1fSx7ImNvZGUi 285 | OiJVQUgiLCJuYW1lIjoiVWtyYWluaWFuIEhyeXZuaWEiLCJyYXRlIjo1MzIw 286 | LjMxMjU1MX0seyJjb2RlIjoiVUdYIiwibmFtZSI6IlVnYW5kYW4gU2hpbGxp 287 | bmciLCJyYXRlIjo4MDQyMjYuMDV9LHsiY29kZSI6IlVZVSIsIm5hbWUiOiJV 288 | cnVndWF5YW4gUGVzbyIsInJhdGUiOjY1MzEuNjU4OTU2fSx7ImNvZGUiOiJV 289 | WlMiLCJuYW1lIjoiVXpiZWtpc3RhbiBTb20iLCJyYXRlIjo2MjIwNzAuMjkw 290 | ODA5fSx7ImNvZGUiOiJWRUYiLCJuYW1lIjoiVmVuZXp1ZWxhbiBCb2zDrXZh 291 | ciBGdWVydGUiLCJyYXRlIjoxNTM5LjY2MzU5OX0seyJjb2RlIjoiVk5EIiwi 292 | bmFtZSI6IlZpZXRuYW1lc2UgRG9uZyIsInJhdGUiOjUyNDE1NzUuMzR9LHsi 293 | Y29kZSI6IlZVViIsIm5hbWUiOiJWYW51YXR1IFZhdHUiLCJyYXRlIjoyNTYx 294 | Ny45ODg4fSx7ImNvZGUiOiJXU1QiLCJuYW1lIjoiU2Ftb2FuIFRhbGEiLCJy 295 | YXRlIjo2MDAuMTQ2ODJ9LHsiY29kZSI6IlhBRiIsIm5hbWUiOiJDRkEgRnJh 296 | bmMgQkVBQyIsInJhdGUiOjE0MTIzOS4wNjQzNDR9LHsiY29kZSI6IlhBRyIs 297 | Im5hbWUiOiJTaWx2ZXIgKHRyb3kgb3VuY2UpIiwicmF0ZSI6MTUuMTA1MDM4 298 | fSx7ImNvZGUiOiJYQVUiLCJuYW1lIjoiR29sZCAodHJveSBvdW5jZSkiLCJy 299 | YXRlIjowLjIwNTE3OH0seyJjb2RlIjoiWENEIiwibmFtZSI6IkVhc3QgQ2Fy 300 | aWJiZWFuIERvbGxhciIsInJhdGUiOjY1OS4xNzQ2OTJ9LHsiY29kZSI6IlhP 301 | RiIsIm5hbWUiOiJDRkEgRnJhbmMgQkNFQU8iLCJyYXRlIjoxNDE0NDcuOTA2 302 | NjQ0fSx7ImNvZGUiOiJYUEYiLCJuYW1lIjoiQ0ZQIEZyYW5jIiwicmF0ZSI6 303 | MjU3MDEuMDM3Mn0seyJjb2RlIjoiWUVSIiwibmFtZSI6IlllbWVuaSBSaWFs 304 | IiwicmF0ZSI6NTI0ODcuNTY1ODR9LHsiY29kZSI6IlpBUiIsIm5hbWUiOiJT 305 | b3V0aCBBZnJpY2FuIFJhbmQiLCJyYXRlIjoyOTY3LjkwMjYyNX0seyJjb2Rl 306 | IjoiWk1XIiwibmFtZSI6IlphbWJpYW4gS3dhY2hhIiwicmF0ZSI6MTgwOS4z 307 | Mzk1ODV9LHsiY29kZSI6IlpXTCIsIm5hbWUiOiJaaW1iYWJ3ZWFuIERvbGxh 308 | ciIsInJhdGUiOjc4NzQ2LjMwODk1Mn1d 309 | http_version: 310 | recorded_at: Mon, 22 Jun 2015 14:10:30 GMT 311 | recorded_with: VCR 2.9.3 312 | -------------------------------------------------------------------------------- /spec/fixtures/vcr/blockchain_insight_adapter.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: post 5 | uri: https://insight.mycelium.com/api/tx/ae0d040f48d75fdc46d9035236a1782164857d6f0cca1f864640281115898560 6 | body: 7 | encoding: UTF-8 8 | string: Content-Type=application%2Fjson 9 | headers: 10 | Accept-Encoding: 11 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 12 | Accept: 13 | - "*/*" 14 | User-Agent: 15 | - Ruby 16 | response: 17 | status: 18 | code: 404 19 | message: Not Found 20 | headers: 21 | Server: 22 | - nginx/1.6.3 23 | Date: 24 | - Wed, 08 Jul 2015 11:04:21 GMT 25 | Content-Type: 26 | - application/json; charset=utf-8 27 | Content-Length: 28 | - '115' 29 | Connection: 30 | - keep-alive 31 | X-Powered-By: 32 | - Express 33 | Access-Control-Allow-Origin: 34 | - "*" 35 | Access-Control-Allow-Methods: 36 | - GET, POST, OPTIONS, PUT, DELETE 37 | Access-Control-Allow-Headers: 38 | - X-Requested-With,Content-Type,Authorization 39 | Access-Control-Expose-Headers: 40 | - X-Email-Needs-Validation,X-Quota-Per-Item,X-Quota-Items-Limit,X-RateLimit-Limit,X-RateLimit-Remaining 41 | Vary: 42 | - Accept-Encoding 43 | body: 44 | encoding: UTF-8 45 | string: '{"status":404,"url":"/api/tx/ae0d040f48d75fdc46d9035236a1782164857d6f0cca1f864640281115898560","error":"Not 46 | found"}' 47 | http_version: 48 | recorded_at: Wed, 08 Jul 2015 11:03:14 GMT 49 | - request: 50 | method: get 51 | uri: https://insight.mycelium.com/api/tx/ae0d040f48d75fdc46d9035236a1782164857d6f0cca1f864640281115898560 52 | body: 53 | encoding: UTF-8 54 | string: Content-Type=application%2Fjson 55 | headers: 56 | Accept-Encoding: 57 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 58 | Accept: 59 | - "*/*" 60 | User-Agent: 61 | - Ruby 62 | response: 63 | status: 64 | code: 404 65 | message: Not Found 66 | headers: 67 | Server: 68 | - nginx/1.6.3 69 | Date: 70 | - Wed, 08 Jul 2015 11:36:56 GMT 71 | Content-Type: 72 | - text/html; charset=utf-8 73 | Content-Length: 74 | - '9' 75 | Connection: 76 | - keep-alive 77 | X-Powered-By: 78 | - Express 79 | Access-Control-Allow-Origin: 80 | - "*" 81 | Access-Control-Allow-Methods: 82 | - GET, POST, OPTIONS, PUT, DELETE 83 | Access-Control-Allow-Headers: 84 | - X-Requested-With,Content-Type,Authorization 85 | Access-Control-Expose-Headers: 86 | - X-Email-Needs-Validation,X-Quota-Per-Item,X-Quota-Items-Limit,X-RateLimit-Limit,X-RateLimit-Remaining 87 | Etag: 88 | - '"1890952009"' 89 | Vary: 90 | - Accept-Encoding 91 | body: 92 | encoding: UTF-8 93 | string: Not found 94 | http_version: 95 | recorded_at: Wed, 08 Jul 2015 11:35:49 GMT 96 | - request: 97 | method: get 98 | uri: https://insight.mycelium.com/api/tx/b168b57a9ae38c0671c5eef3be6c8305782bd1351e75028dac491185388d5424 99 | body: 100 | encoding: UTF-8 101 | string: Content-Type=application%2Fjson 102 | headers: 103 | Accept-Encoding: 104 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 105 | Accept: 106 | - "*/*" 107 | User-Agent: 108 | - Ruby 109 | response: 110 | status: 111 | code: 200 112 | message: OK 113 | headers: 114 | Server: 115 | - nginx/1.6.3 116 | Date: 117 | - Wed, 08 Jul 2015 11:52:10 GMT 118 | Content-Type: 119 | - application/json; charset=utf-8 120 | Transfer-Encoding: 121 | - chunked 122 | Connection: 123 | - keep-alive 124 | X-Powered-By: 125 | - Express 126 | Access-Control-Allow-Origin: 127 | - "*" 128 | Access-Control-Allow-Methods: 129 | - GET, POST, OPTIONS, PUT, DELETE 130 | Access-Control-Allow-Headers: 131 | - X-Requested-With,Content-Type,Authorization 132 | Access-Control-Expose-Headers: 133 | - X-Email-Needs-Validation,X-Quota-Per-Item,X-Quota-Items-Limit,X-RateLimit-Limit,X-RateLimit-Remaining 134 | Etag: 135 | - '"1743052601"' 136 | Vary: 137 | - Accept-Encoding 138 | body: 139 | encoding: ASCII-8BIT 140 | string: '{"txid":"b168b57a9ae38c0671c5eef3be6c8305782bd1351e75028dac491185388d5424","version":1,"locktime":0,"vin":[{"txid":"a48637c382c65858b569409c74147d25a606db1ca4f341eaec40f4ba2e239691","vout":0,"scriptSig":{"asm":"3044022060877aa4485463fa72e87762bd163231155f4f0165ea05eb38bac961665ccb5f0220449180c81b7a1db604b537eecf5a5ce10810603ecda59d3190a8fc095e19e3f401 141 | 04a09f26dab2886c732b17fc041b04b0974ffebe1c0a2574e51f8503a9949882d398fce64c051b6b2859165d403d3d74daced1fe168d4baf7e585dadf9466d1c58","hex":"473044022060877aa4485463fa72e87762bd163231155f4f0165ea05eb38bac961665ccb5f0220449180c81b7a1db604b537eecf5a5ce10810603ecda59d3190a8fc095e19e3f4014104a09f26dab2886c732b17fc041b04b0974ffebe1c0a2574e51f8503a9949882d398fce64c051b6b2859165d403d3d74daced1fe168d4baf7e585dadf9466d1c58"},"sequence":4294967295,"n":0,"addr":"1FCzPT4Ve59ojviH8R2MMWCsVEEq8xrc53","valueSat":500000000,"value":5,"doubleSpentTxID":null}],"vout":[{"value":"1.87000000","n":0,"scriptPubKey":{"asm":"OP_DUP 142 | OP_HASH160 7aa6944d611413e062e97a8668154e39b27b5efc OP_EQUALVERIFY OP_CHECKSIG","hex":"76a9147aa6944d611413e062e97a8668154e39b27b5efc88ac","reqSigs":1,"type":"pubkeyhash","addresses":["1CBWzY7PEnUtT4b36bth4UZuNmby9pTT7A"]}},{"value":"3.12900000","n":1,"scriptPubKey":{"asm":"OP_DUP 143 | OP_HASH160 9bd63da8b392b1066f182a8a618e520107a83352 OP_EQUALVERIFY OP_CHECKSIG","hex":"76a9149bd63da8b392b1066f182a8a618e520107a8335288ac","reqSigs":1,"type":"pubkeyhash","addresses":["1FCzPT4Ve59ojviH8R2MMWCsVEEq8xrc53"]}}],"valueOut":4.999,"size":257,"valueIn":5,"fees":0.001}' 144 | http_version: 145 | recorded_at: Wed, 08 Jul 2015 11:51:02 GMT 146 | - request: 147 | method: get 148 | uri: https://insight.mycelium.com/api/addr/1CBWzY7PEnUtT4b36bth4UZuNmby9pTT7A 149 | body: 150 | encoding: UTF-8 151 | string: Content-Type=application%2Fjson 152 | headers: 153 | Accept-Encoding: 154 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 155 | Accept: 156 | - "*/*" 157 | User-Agent: 158 | - Ruby 159 | response: 160 | status: 161 | code: 200 162 | message: OK 163 | headers: 164 | Server: 165 | - nginx/1.6.3 166 | Date: 167 | - Wed, 08 Jul 2015 12:59:50 GMT 168 | Content-Type: 169 | - application/json; charset=utf-8 170 | Content-Length: 171 | - '623' 172 | Connection: 173 | - keep-alive 174 | X-Powered-By: 175 | - Express 176 | Access-Control-Allow-Origin: 177 | - "*" 178 | Access-Control-Allow-Methods: 179 | - GET, POST, OPTIONS, PUT, DELETE 180 | Access-Control-Allow-Headers: 181 | - X-Requested-With,Content-Type,Authorization 182 | Access-Control-Expose-Headers: 183 | - X-Email-Needs-Validation,X-Quota-Per-Item,X-Quota-Items-Limit,X-RateLimit-Limit,X-RateLimit-Remaining 184 | Etag: 185 | - '"-938702409"' 186 | Vary: 187 | - Accept-Encoding 188 | body: 189 | encoding: UTF-8 190 | string: '{"addrStr":"1CBWzY7PEnUtT4b36bth4UZuNmby9pTT7A","balance":3.74,"balanceSat":374000000,"totalReceived":4.49,"totalReceivedSat":449000000,"totalSent":0.75,"totalSentSat":75000000,"unconfirmedBalance":0,"unconfirmedBalanceSat":0,"unconfirmedTxApperances":0,"txApperances":5,"transactions":["b168b57a9ae38c0671c5eef3be6c8305782bd1351e75028dac491185388d5424","1a00e2945c31a73da1e97cf06c0adbdbdecb92a17834a4efd75b78444da997d5","09597b18dca4e6b32c0956f0ff23704cf3a9f98ec0915d74a5ae50a3bf39b915","35c5a73d05bcd5b74a379eb9f01dc3203dbc313cd43958a4aaff39a9f03ff663","90db27ed495ca7330b1ce622384fb9d9ca664580672b3f3bde6f32b3e67ec4e3"]}' 191 | http_version: 192 | recorded_at: Wed, 08 Jul 2015 12:58:42 GMT 193 | - request: 194 | method: get 195 | uri: https://insight.mycelium.com/api/tx/b168b57a9ae38c0671c5eef3be6c8305782bd1351e75028dac491185388d5424 196 | body: 197 | encoding: UTF-8 198 | string: Content-Type=application%2Fjson 199 | headers: 200 | Accept-Encoding: 201 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 202 | Accept: 203 | - "*/*" 204 | User-Agent: 205 | - Ruby 206 | response: 207 | status: 208 | code: 200 209 | message: OK 210 | headers: 211 | Server: 212 | - nginx/1.6.3 213 | Date: 214 | - Wed, 08 Jul 2015 14:21:53 GMT 215 | Content-Type: 216 | - application/json; charset=utf-8 217 | Transfer-Encoding: 218 | - chunked 219 | Connection: 220 | - keep-alive 221 | X-Powered-By: 222 | - Express 223 | Access-Control-Allow-Origin: 224 | - "*" 225 | Access-Control-Allow-Methods: 226 | - GET, POST, OPTIONS, PUT, DELETE 227 | Access-Control-Allow-Headers: 228 | - X-Requested-With,Content-Type,Authorization 229 | Access-Control-Expose-Headers: 230 | - X-Email-Needs-Validation,X-Quota-Per-Item,X-Quota-Items-Limit,X-RateLimit-Limit,X-RateLimit-Remaining 231 | Etag: 232 | - '"-1732622975"' 233 | Vary: 234 | - Accept-Encoding 235 | body: 236 | encoding: ASCII-8BIT 237 | string: '{"txid":"b168b57a9ae38c0671c5eef3be6c8305782bd1351e75028dac491185388d5424","version":1,"locktime":0,"vin":[{"txid":"a48637c382c65858b569409c74147d25a606db1ca4f341eaec40f4ba2e239691","vout":0,"scriptSig":{"asm":"3044022060877aa4485463fa72e87762bd163231155f4f0165ea05eb38bac961665ccb5f0220449180c81b7a1db604b537eecf5a5ce10810603ecda59d3190a8fc095e19e3f401 238 | 04a09f26dab2886c732b17fc041b04b0974ffebe1c0a2574e51f8503a9949882d398fce64c051b6b2859165d403d3d74daced1fe168d4baf7e585dadf9466d1c58","hex":"473044022060877aa4485463fa72e87762bd163231155f4f0165ea05eb38bac961665ccb5f0220449180c81b7a1db604b537eecf5a5ce10810603ecda59d3190a8fc095e19e3f4014104a09f26dab2886c732b17fc041b04b0974ffebe1c0a2574e51f8503a9949882d398fce64c051b6b2859165d403d3d74daced1fe168d4baf7e585dadf9466d1c58"},"sequence":4294967295,"n":0,"addr":"1FCzPT4Ve59ojviH8R2MMWCsVEEq8xrc53","valueSat":500000000,"value":5,"doubleSpentTxID":null}],"vout":[{"value":"1.87000000","n":0,"scriptPubKey":{"asm":"OP_DUP 239 | OP_HASH160 7aa6944d611413e062e97a8668154e39b27b5efc OP_EQUALVERIFY OP_CHECKSIG","hex":"76a9147aa6944d611413e062e97a8668154e39b27b5efc88ac","reqSigs":1,"type":"pubkeyhash","addresses":["1CBWzY7PEnUtT4b36bth4UZuNmby9pTT7A"]}},{"value":"3.12900000","n":1,"scriptPubKey":{"asm":"OP_DUP 240 | OP_HASH160 9bd63da8b392b1066f182a8a618e520107a83352 OP_EQUALVERIFY OP_CHECKSIG","hex":"76a9149bd63da8b392b1066f182a8a618e520107a8335288ac","reqSigs":1,"type":"pubkeyhash","addresses":["1FCzPT4Ve59ojviH8R2MMWCsVEEq8xrc53"]},"spentTxId":"b3a947af37d36ddd0a1cd00fa913794373861d78ae937f76113d51d0b53834eb","spentIndex":0,"spentTs":1436359326}],"blockhash":"00000000000000000b26638ad830025fbb9f7246eae7a6cbaeb00dc3a6786238","confirmations":13,"time":1436357480,"blocktime":1436357480,"valueOut":4.999,"size":257,"valueIn":5,"fees":0.001}' 241 | http_version: 242 | recorded_at: Wed, 08 Jul 2015 14:20:45 GMT 243 | - request: 244 | method: get 245 | uri: https://insight.mycelium.com/api/addr/1CBWzY7PEnUtT4b36bth4UZuNmby9pTT7A 246 | body: 247 | encoding: UTF-8 248 | string: Content-Type=application%2Fjson 249 | headers: 250 | Accept-Encoding: 251 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 252 | Accept: 253 | - "*/*" 254 | User-Agent: 255 | - Ruby 256 | response: 257 | status: 258 | code: 200 259 | message: OK 260 | headers: 261 | Server: 262 | - nginx/1.6.3 263 | Date: 264 | - Wed, 08 Jul 2015 14:34:52 GMT 265 | Content-Type: 266 | - application/json; charset=utf-8 267 | Content-Length: 268 | - '623' 269 | Connection: 270 | - keep-alive 271 | X-Powered-By: 272 | - Express 273 | Access-Control-Allow-Origin: 274 | - "*" 275 | Access-Control-Allow-Methods: 276 | - GET, POST, OPTIONS, PUT, DELETE 277 | Access-Control-Allow-Headers: 278 | - X-Requested-With,Content-Type,Authorization 279 | Access-Control-Expose-Headers: 280 | - X-Email-Needs-Validation,X-Quota-Per-Item,X-Quota-Items-Limit,X-RateLimit-Limit,X-RateLimit-Remaining 281 | Etag: 282 | - '"-938702409"' 283 | Vary: 284 | - Accept-Encoding 285 | body: 286 | encoding: UTF-8 287 | string: '{"addrStr":"1CBWzY7PEnUtT4b36bth4UZuNmby9pTT7A","balance":3.74,"balanceSat":374000000,"totalReceived":4.49,"totalReceivedSat":449000000,"totalSent":0.75,"totalSentSat":75000000,"unconfirmedBalance":0,"unconfirmedBalanceSat":0,"unconfirmedTxApperances":0,"txApperances":5,"transactions":["b168b57a9ae38c0671c5eef3be6c8305782bd1351e75028dac491185388d5424","1a00e2945c31a73da1e97cf06c0adbdbdecb92a17834a4efd75b78444da997d5","09597b18dca4e6b32c0956f0ff23704cf3a9f98ec0915d74a5ae50a3bf39b915","35c5a73d05bcd5b74a379eb9f01dc3203dbc313cd43958a4aaff39a9f03ff663","90db27ed495ca7330b1ce622384fb9d9ca664580672b3f3bde6f32b3e67ec4e3"]}' 288 | http_version: 289 | recorded_at: Wed, 08 Jul 2015 14:33:44 GMT 290 | - request: 291 | method: get 292 | uri: https://insight.mycelium.com/api/tx/b168b57a9ae38c0671c5eef3be6c8305782bd1351e75028dac491185388d5424 293 | body: 294 | encoding: UTF-8 295 | string: Content-Type=application%2Fjson 296 | headers: 297 | Accept-Encoding: 298 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 299 | Accept: 300 | - "*/*" 301 | User-Agent: 302 | - Ruby 303 | response: 304 | status: 305 | code: 200 306 | message: OK 307 | headers: 308 | Server: 309 | - nginx/1.6.3 310 | Date: 311 | - Wed, 08 Jul 2015 14:34:53 GMT 312 | Content-Type: 313 | - application/json; charset=utf-8 314 | Transfer-Encoding: 315 | - chunked 316 | Connection: 317 | - keep-alive 318 | X-Powered-By: 319 | - Express 320 | Access-Control-Allow-Origin: 321 | - "*" 322 | Access-Control-Allow-Methods: 323 | - GET, POST, OPTIONS, PUT, DELETE 324 | Access-Control-Allow-Headers: 325 | - X-Requested-With,Content-Type,Authorization 326 | Access-Control-Expose-Headers: 327 | - X-Email-Needs-Validation,X-Quota-Per-Item,X-Quota-Items-Limit,X-RateLimit-Limit,X-RateLimit-Remaining 328 | Etag: 329 | - '"-1732622975"' 330 | Vary: 331 | - Accept-Encoding 332 | body: 333 | encoding: ASCII-8BIT 334 | string: '{"txid":"b168b57a9ae38c0671c5eef3be6c8305782bd1351e75028dac491185388d5424","version":1,"locktime":0,"vin":[{"txid":"a48637c382c65858b569409c74147d25a606db1ca4f341eaec40f4ba2e239691","vout":0,"scriptSig":{"asm":"3044022060877aa4485463fa72e87762bd163231155f4f0165ea05eb38bac961665ccb5f0220449180c81b7a1db604b537eecf5a5ce10810603ecda59d3190a8fc095e19e3f401 335 | 04a09f26dab2886c732b17fc041b04b0974ffebe1c0a2574e51f8503a9949882d398fce64c051b6b2859165d403d3d74daced1fe168d4baf7e585dadf9466d1c58","hex":"473044022060877aa4485463fa72e87762bd163231155f4f0165ea05eb38bac961665ccb5f0220449180c81b7a1db604b537eecf5a5ce10810603ecda59d3190a8fc095e19e3f4014104a09f26dab2886c732b17fc041b04b0974ffebe1c0a2574e51f8503a9949882d398fce64c051b6b2859165d403d3d74daced1fe168d4baf7e585dadf9466d1c58"},"sequence":4294967295,"n":0,"addr":"1FCzPT4Ve59ojviH8R2MMWCsVEEq8xrc53","valueSat":500000000,"value":5,"doubleSpentTxID":null}],"vout":[{"value":"1.87000000","n":0,"scriptPubKey":{"asm":"OP_DUP 336 | OP_HASH160 7aa6944d611413e062e97a8668154e39b27b5efc OP_EQUALVERIFY OP_CHECKSIG","hex":"76a9147aa6944d611413e062e97a8668154e39b27b5efc88ac","reqSigs":1,"type":"pubkeyhash","addresses":["1CBWzY7PEnUtT4b36bth4UZuNmby9pTT7A"]}},{"value":"3.12900000","n":1,"scriptPubKey":{"asm":"OP_DUP 337 | OP_HASH160 9bd63da8b392b1066f182a8a618e520107a83352 OP_EQUALVERIFY OP_CHECKSIG","hex":"76a9149bd63da8b392b1066f182a8a618e520107a8335288ac","reqSigs":1,"type":"pubkeyhash","addresses":["1FCzPT4Ve59ojviH8R2MMWCsVEEq8xrc53"]},"spentTxId":"b3a947af37d36ddd0a1cd00fa913794373861d78ae937f76113d51d0b53834eb","spentIndex":0,"spentTs":1436359326}],"blockhash":"00000000000000000b26638ad830025fbb9f7246eae7a6cbaeb00dc3a6786238","confirmations":13,"time":1436357480,"blocktime":1436357480,"valueOut":4.999,"size":257,"valueIn":5,"fees":0.001}' 338 | http_version: 339 | recorded_at: Wed, 08 Jul 2015 14:33:45 GMT 340 | - request: 341 | method: get 342 | uri: https://insight.mycelium.com/api/tx/b168b57a9ae38c0671c5eef3be6c8305782bd1351e75028dac491185388d5424 343 | body: 344 | encoding: UTF-8 345 | string: Content-Type=application%2Fjson 346 | headers: 347 | Accept-Encoding: 348 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 349 | Accept: 350 | - "*/*" 351 | User-Agent: 352 | - Ruby 353 | response: 354 | status: 355 | code: 200 356 | message: OK 357 | headers: 358 | Server: 359 | - nginx/1.6.3 360 | Date: 361 | - Wed, 08 Jul 2015 14:51:10 GMT 362 | Content-Type: 363 | - application/json; charset=utf-8 364 | Transfer-Encoding: 365 | - chunked 366 | Connection: 367 | - keep-alive 368 | X-Powered-By: 369 | - Express 370 | Access-Control-Allow-Origin: 371 | - "*" 372 | Access-Control-Allow-Methods: 373 | - GET, POST, OPTIONS, PUT, DELETE 374 | Access-Control-Allow-Headers: 375 | - X-Requested-With,Content-Type,Authorization 376 | Access-Control-Expose-Headers: 377 | - X-Email-Needs-Validation,X-Quota-Per-Item,X-Quota-Items-Limit,X-RateLimit-Limit,X-RateLimit-Remaining 378 | Etag: 379 | - '"-1732622975"' 380 | Vary: 381 | - Accept-Encoding 382 | body: 383 | encoding: ASCII-8BIT 384 | string: '{"txid":"b168b57a9ae38c0671c5eef3be6c8305782bd1351e75028dac491185388d5424","version":1,"locktime":0,"vin":[{"txid":"a48637c382c65858b569409c74147d25a606db1ca4f341eaec40f4ba2e239691","vout":0,"scriptSig":{"asm":"3044022060877aa4485463fa72e87762bd163231155f4f0165ea05eb38bac961665ccb5f0220449180c81b7a1db604b537eecf5a5ce10810603ecda59d3190a8fc095e19e3f401 385 | 04a09f26dab2886c732b17fc041b04b0974ffebe1c0a2574e51f8503a9949882d398fce64c051b6b2859165d403d3d74daced1fe168d4baf7e585dadf9466d1c58","hex":"473044022060877aa4485463fa72e87762bd163231155f4f0165ea05eb38bac961665ccb5f0220449180c81b7a1db604b537eecf5a5ce10810603ecda59d3190a8fc095e19e3f4014104a09f26dab2886c732b17fc041b04b0974ffebe1c0a2574e51f8503a9949882d398fce64c051b6b2859165d403d3d74daced1fe168d4baf7e585dadf9466d1c58"},"sequence":4294967295,"n":0,"addr":"1FCzPT4Ve59ojviH8R2MMWCsVEEq8xrc53","valueSat":500000000,"value":5,"doubleSpentTxID":null}],"vout":[{"value":"1.87000000","n":0,"scriptPubKey":{"asm":"OP_DUP 386 | OP_HASH160 7aa6944d611413e062e97a8668154e39b27b5efc OP_EQUALVERIFY OP_CHECKSIG","hex":"76a9147aa6944d611413e062e97a8668154e39b27b5efc88ac","reqSigs":1,"type":"pubkeyhash","addresses":["1CBWzY7PEnUtT4b36bth4UZuNmby9pTT7A"]}},{"value":"3.12900000","n":1,"scriptPubKey":{"asm":"OP_DUP 387 | OP_HASH160 9bd63da8b392b1066f182a8a618e520107a83352 OP_EQUALVERIFY OP_CHECKSIG","hex":"76a9149bd63da8b392b1066f182a8a618e520107a8335288ac","reqSigs":1,"type":"pubkeyhash","addresses":["1FCzPT4Ve59ojviH8R2MMWCsVEEq8xrc53"]},"spentTxId":"b3a947af37d36ddd0a1cd00fa913794373861d78ae937f76113d51d0b53834eb","spentIndex":0,"spentTs":1436359326}],"blockhash":"00000000000000000b26638ad830025fbb9f7246eae7a6cbaeb00dc3a6786238","confirmations":13,"time":1436357480,"blocktime":1436357480,"valueOut":4.999,"size":257,"valueIn":5,"fees":0.001}' 388 | http_version: 389 | recorded_at: Wed, 08 Jul 2015 14:50:02 GMT 390 | - request: 391 | method: get 392 | uri: https://insight.mycelium.com/api/tx/b168b57a9ae38c0671c5eef3be6c8305782bd1351e75028dac491185388d5424 393 | body: 394 | encoding: UTF-8 395 | string: Content-Type=application%2Fjson 396 | headers: 397 | Accept-Encoding: 398 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 399 | Accept: 400 | - "*/*" 401 | User-Agent: 402 | - Ruby 403 | response: 404 | status: 405 | code: 200 406 | message: OK 407 | headers: 408 | Server: 409 | - nginx/1.6.3 410 | Date: 411 | - Wed, 08 Jul 2015 14:51:11 GMT 412 | Content-Type: 413 | - application/json; charset=utf-8 414 | Transfer-Encoding: 415 | - chunked 416 | Connection: 417 | - keep-alive 418 | X-Powered-By: 419 | - Express 420 | Access-Control-Allow-Origin: 421 | - "*" 422 | Access-Control-Allow-Methods: 423 | - GET, POST, OPTIONS, PUT, DELETE 424 | Access-Control-Allow-Headers: 425 | - X-Requested-With,Content-Type,Authorization 426 | Access-Control-Expose-Headers: 427 | - X-Email-Needs-Validation,X-Quota-Per-Item,X-Quota-Items-Limit,X-RateLimit-Limit,X-RateLimit-Remaining 428 | Etag: 429 | - '"-1732622975"' 430 | Vary: 431 | - Accept-Encoding 432 | body: 433 | encoding: ASCII-8BIT 434 | string: '{"txid":"b168b57a9ae38c0671c5eef3be6c8305782bd1351e75028dac491185388d5424","version":1,"locktime":0,"vin":[{"txid":"a48637c382c65858b569409c74147d25a606db1ca4f341eaec40f4ba2e239691","vout":0,"scriptSig":{"asm":"3044022060877aa4485463fa72e87762bd163231155f4f0165ea05eb38bac961665ccb5f0220449180c81b7a1db604b537eecf5a5ce10810603ecda59d3190a8fc095e19e3f401 435 | 04a09f26dab2886c732b17fc041b04b0974ffebe1c0a2574e51f8503a9949882d398fce64c051b6b2859165d403d3d74daced1fe168d4baf7e585dadf9466d1c58","hex":"473044022060877aa4485463fa72e87762bd163231155f4f0165ea05eb38bac961665ccb5f0220449180c81b7a1db604b537eecf5a5ce10810603ecda59d3190a8fc095e19e3f4014104a09f26dab2886c732b17fc041b04b0974ffebe1c0a2574e51f8503a9949882d398fce64c051b6b2859165d403d3d74daced1fe168d4baf7e585dadf9466d1c58"},"sequence":4294967295,"n":0,"addr":"1FCzPT4Ve59ojviH8R2MMWCsVEEq8xrc53","valueSat":500000000,"value":5,"doubleSpentTxID":null}],"vout":[{"value":"1.87000000","n":0,"scriptPubKey":{"asm":"OP_DUP 436 | OP_HASH160 7aa6944d611413e062e97a8668154e39b27b5efc OP_EQUALVERIFY OP_CHECKSIG","hex":"76a9147aa6944d611413e062e97a8668154e39b27b5efc88ac","reqSigs":1,"type":"pubkeyhash","addresses":["1CBWzY7PEnUtT4b36bth4UZuNmby9pTT7A"]}},{"value":"3.12900000","n":1,"scriptPubKey":{"asm":"OP_DUP 437 | OP_HASH160 9bd63da8b392b1066f182a8a618e520107a83352 OP_EQUALVERIFY OP_CHECKSIG","hex":"76a9149bd63da8b392b1066f182a8a618e520107a8335288ac","reqSigs":1,"type":"pubkeyhash","addresses":["1FCzPT4Ve59ojviH8R2MMWCsVEEq8xrc53"]},"spentTxId":"b3a947af37d36ddd0a1cd00fa913794373861d78ae937f76113d51d0b53834eb","spentIndex":0,"spentTs":1436359326}],"blockhash":"00000000000000000b26638ad830025fbb9f7246eae7a6cbaeb00dc3a6786238","confirmations":13,"time":1436357480,"blocktime":1436357480,"valueOut":4.999,"size":257,"valueIn":5,"fees":0.001}' 438 | http_version: 439 | recorded_at: Wed, 08 Jul 2015 14:50:03 GMT 440 | - request: 441 | method: get 442 | uri: https://insight.mycelium.com/api/addr/1CBWzY7PEnUtT4b36bth4UZuNmby9pTT7A 443 | body: 444 | encoding: UTF-8 445 | string: Content-Type=application%2Fjson 446 | headers: 447 | Accept-Encoding: 448 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 449 | Accept: 450 | - "*/*" 451 | User-Agent: 452 | - Ruby 453 | response: 454 | status: 455 | code: 200 456 | message: OK 457 | headers: 458 | Server: 459 | - nginx/1.6.3 460 | Date: 461 | - Wed, 08 Jul 2015 14:51:11 GMT 462 | Content-Type: 463 | - application/json; charset=utf-8 464 | Content-Length: 465 | - '623' 466 | Connection: 467 | - keep-alive 468 | X-Powered-By: 469 | - Express 470 | Access-Control-Allow-Origin: 471 | - "*" 472 | Access-Control-Allow-Methods: 473 | - GET, POST, OPTIONS, PUT, DELETE 474 | Access-Control-Allow-Headers: 475 | - X-Requested-With,Content-Type,Authorization 476 | Access-Control-Expose-Headers: 477 | - X-Email-Needs-Validation,X-Quota-Per-Item,X-Quota-Items-Limit,X-RateLimit-Limit,X-RateLimit-Remaining 478 | Etag: 479 | - '"-938702409"' 480 | Vary: 481 | - Accept-Encoding 482 | body: 483 | encoding: UTF-8 484 | string: '{"addrStr":"1CBWzY7PEnUtT4b36bth4UZuNmby9pTT7A","balance":3.74,"balanceSat":374000000,"totalReceived":4.49,"totalReceivedSat":449000000,"totalSent":0.75,"totalSentSat":75000000,"unconfirmedBalance":0,"unconfirmedBalanceSat":0,"unconfirmedTxApperances":0,"txApperances":5,"transactions":["b168b57a9ae38c0671c5eef3be6c8305782bd1351e75028dac491185388d5424","1a00e2945c31a73da1e97cf06c0adbdbdecb92a17834a4efd75b78444da997d5","09597b18dca4e6b32c0956f0ff23704cf3a9f98ec0915d74a5ae50a3bf39b915","35c5a73d05bcd5b74a379eb9f01dc3203dbc313cd43958a4aaff39a9f03ff663","90db27ed495ca7330b1ce622384fb9d9ca664580672b3f3bde6f32b3e67ec4e3"]}' 485 | http_version: 486 | recorded_at: Wed, 08 Jul 2015 14:50:03 GMT 487 | - request: 488 | method: get 489 | uri: https://insight.mycelium.com/api/tx/b168b57a9ae38c0671c5eef3be6c8305782bd1351e75028dac491185388d5424 490 | body: 491 | encoding: UTF-8 492 | string: Content-Type=application%2Fjson 493 | headers: 494 | Accept-Encoding: 495 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 496 | Accept: 497 | - "*/*" 498 | User-Agent: 499 | - Ruby 500 | response: 501 | status: 502 | code: 200 503 | message: OK 504 | headers: 505 | Server: 506 | - nginx/1.6.3 507 | Date: 508 | - Wed, 08 Jul 2015 14:52:19 GMT 509 | Content-Type: 510 | - application/json; charset=utf-8 511 | Transfer-Encoding: 512 | - chunked 513 | Connection: 514 | - keep-alive 515 | X-Powered-By: 516 | - Express 517 | Access-Control-Allow-Origin: 518 | - "*" 519 | Access-Control-Allow-Methods: 520 | - GET, POST, OPTIONS, PUT, DELETE 521 | Access-Control-Allow-Headers: 522 | - X-Requested-With,Content-Type,Authorization 523 | Access-Control-Expose-Headers: 524 | - X-Email-Needs-Validation,X-Quota-Per-Item,X-Quota-Items-Limit,X-RateLimit-Limit,X-RateLimit-Remaining 525 | Etag: 526 | - '"-1732622975"' 527 | Vary: 528 | - Accept-Encoding 529 | body: 530 | encoding: ASCII-8BIT 531 | string: '{"txid":"b168b57a9ae38c0671c5eef3be6c8305782bd1351e75028dac491185388d5424","version":1,"locktime":0,"vin":[{"txid":"a48637c382c65858b569409c74147d25a606db1ca4f341eaec40f4ba2e239691","vout":0,"scriptSig":{"asm":"3044022060877aa4485463fa72e87762bd163231155f4f0165ea05eb38bac961665ccb5f0220449180c81b7a1db604b537eecf5a5ce10810603ecda59d3190a8fc095e19e3f401 532 | 04a09f26dab2886c732b17fc041b04b0974ffebe1c0a2574e51f8503a9949882d398fce64c051b6b2859165d403d3d74daced1fe168d4baf7e585dadf9466d1c58","hex":"473044022060877aa4485463fa72e87762bd163231155f4f0165ea05eb38bac961665ccb5f0220449180c81b7a1db604b537eecf5a5ce10810603ecda59d3190a8fc095e19e3f4014104a09f26dab2886c732b17fc041b04b0974ffebe1c0a2574e51f8503a9949882d398fce64c051b6b2859165d403d3d74daced1fe168d4baf7e585dadf9466d1c58"},"sequence":4294967295,"n":0,"addr":"1FCzPT4Ve59ojviH8R2MMWCsVEEq8xrc53","valueSat":500000000,"value":5,"doubleSpentTxID":null}],"vout":[{"value":"1.87000000","n":0,"scriptPubKey":{"asm":"OP_DUP 533 | OP_HASH160 7aa6944d611413e062e97a8668154e39b27b5efc OP_EQUALVERIFY OP_CHECKSIG","hex":"76a9147aa6944d611413e062e97a8668154e39b27b5efc88ac","reqSigs":1,"type":"pubkeyhash","addresses":["1CBWzY7PEnUtT4b36bth4UZuNmby9pTT7A"]}},{"value":"3.12900000","n":1,"scriptPubKey":{"asm":"OP_DUP 534 | OP_HASH160 9bd63da8b392b1066f182a8a618e520107a83352 OP_EQUALVERIFY OP_CHECKSIG","hex":"76a9149bd63da8b392b1066f182a8a618e520107a8335288ac","reqSigs":1,"type":"pubkeyhash","addresses":["1FCzPT4Ve59ojviH8R2MMWCsVEEq8xrc53"]},"spentTxId":"b3a947af37d36ddd0a1cd00fa913794373861d78ae937f76113d51d0b53834eb","spentIndex":0,"spentTs":1436359326}],"blockhash":"00000000000000000b26638ad830025fbb9f7246eae7a6cbaeb00dc3a6786238","confirmations":13,"time":1436357480,"blocktime":1436357480,"valueOut":4.999,"size":257,"valueIn":5,"fees":0.001}' 535 | http_version: 536 | recorded_at: Wed, 08 Jul 2015 14:51:11 GMT 537 | - request: 538 | method: get 539 | uri: https://insight.mycelium.com/api/addr/16iKJsRM3LrA4k7NeTQbCB9ZDpV64Fkm6 540 | body: 541 | encoding: UTF-8 542 | string: Content-Type=application%2Fjson 543 | headers: 544 | Accept-Encoding: 545 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 546 | Accept: 547 | - "*/*" 548 | User-Agent: 549 | - Ruby 550 | response: 551 | status: 552 | code: 200 553 | message: OK 554 | headers: 555 | Server: 556 | - nginx/1.6.3 557 | Date: 558 | - Wed, 08 Jul 2015 14:53:43 GMT 559 | Content-Type: 560 | - application/json; charset=utf-8 561 | Content-Length: 562 | - '256' 563 | Connection: 564 | - keep-alive 565 | X-Powered-By: 566 | - Express 567 | Access-Control-Allow-Origin: 568 | - "*" 569 | Access-Control-Allow-Methods: 570 | - GET, POST, OPTIONS, PUT, DELETE 571 | Access-Control-Allow-Headers: 572 | - X-Requested-With,Content-Type,Authorization 573 | Access-Control-Expose-Headers: 574 | - X-Email-Needs-Validation,X-Quota-Per-Item,X-Quota-Items-Limit,X-RateLimit-Limit,X-RateLimit-Remaining 575 | Etag: 576 | - '"2057968110"' 577 | Vary: 578 | - Accept-Encoding 579 | body: 580 | encoding: UTF-8 581 | string: '{"addrStr":"16iKJsRM3LrA4k7NeTQbCB9ZDpV64Fkm6","balance":0,"balanceSat":0,"totalReceived":0,"totalReceivedSat":0,"totalSent":0,"totalSentSat":0,"unconfirmedBalance":0,"unconfirmedBalanceSat":0,"unconfirmedTxApperances":0,"txApperances":0,"transactions":[]}' 582 | http_version: 583 | recorded_at: Wed, 08 Jul 2015 14:52:35 GMT 584 | - request: 585 | method: get 586 | uri: https://insight.mycelium.com/wrong_api/tx/b168b57a9ae38c0671c5eef3be6c8305782bd1351e75028dac491185388d5424 587 | body: 588 | encoding: UTF-8 589 | string: Content-Type=application%2Fjson 590 | headers: 591 | Accept-Encoding: 592 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 593 | Accept: 594 | - "*/*" 595 | User-Agent: 596 | - Ruby 597 | response: 598 | status: 599 | code: 200 600 | message: OK 601 | headers: 602 | Server: 603 | - nginx/1.6.3 604 | Date: 605 | - Wed, 08 Jul 2015 15:15:53 GMT 606 | Content-Type: 607 | - text/html; charset=UTF-8 608 | Transfer-Encoding: 609 | - chunked 610 | Connection: 611 | - keep-alive 612 | X-Powered-By: 613 | - Express 614 | Access-Control-Allow-Origin: 615 | - "*" 616 | Access-Control-Allow-Methods: 617 | - GET, POST, OPTIONS, PUT, DELETE 618 | Access-Control-Allow-Headers: 619 | - X-Requested-With,Content-Type,Authorization 620 | Access-Control-Expose-Headers: 621 | - X-Email-Needs-Validation,X-Quota-Per-Item,X-Quota-Items-Limit,X-RateLimit-Limit,X-RateLimit-Remaining 622 | Accept-Ranges: 623 | - bytes 624 | Etag: 625 | - '"3181-1435065629000"' 626 | Cache-Control: 627 | - public, max-age=0 628 | Last-Modified: 629 | - Tue, 23 Jun 2015 13:20:29 GMT 630 | Vary: 631 | - Accept-Encoding 632 | body: 633 | encoding: ASCII-8BIT 634 | string: "\n\n\n\t\n\t\n\t\n\t\n\tInsight\n\t\n\t\n\t\n\t\n\t\n\n\n 645 | \
\n \n
\n\t
\n\t\t\n\t\t
\n\t
\n\t
\n
\n 661 | \ \n 669 | \ insight 670 | API v{{version}}\n
\n
\n\t\n\t\n\t\n\t\n\n\n" 673 | http_version: 674 | recorded_at: Wed, 08 Jul 2015 15:14:45 GMT 675 | - request: 676 | method: get 677 | uri: https://insight.mycelium.com/api/a-404-requesttid 678 | body: 679 | encoding: UTF-8 680 | string: Content-Type=application%2Fjson 681 | headers: 682 | Accept-Encoding: 683 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 684 | Accept: 685 | - "*/*" 686 | User-Agent: 687 | - Ruby 688 | response: 689 | status: 690 | code: 200 691 | message: OK 692 | headers: 693 | Server: 694 | - nginx/1.6.3 695 | Date: 696 | - Wed, 08 Jul 2015 15:19:17 GMT 697 | Content-Type: 698 | - text/html; charset=UTF-8 699 | Transfer-Encoding: 700 | - chunked 701 | Connection: 702 | - keep-alive 703 | X-Powered-By: 704 | - Express 705 | Access-Control-Allow-Origin: 706 | - "*" 707 | Access-Control-Allow-Methods: 708 | - GET, POST, OPTIONS, PUT, DELETE 709 | Access-Control-Allow-Headers: 710 | - X-Requested-With,Content-Type,Authorization 711 | Access-Control-Expose-Headers: 712 | - X-Email-Needs-Validation,X-Quota-Per-Item,X-Quota-Items-Limit,X-RateLimit-Limit,X-RateLimit-Remaining 713 | Accept-Ranges: 714 | - bytes 715 | Etag: 716 | - '"3181-1435065629000"' 717 | Cache-Control: 718 | - public, max-age=0 719 | Last-Modified: 720 | - Tue, 23 Jun 2015 13:20:29 GMT 721 | Vary: 722 | - Accept-Encoding 723 | body: 724 | encoding: ASCII-8BIT 725 | string: "\n\n\n\t\n\t\n\t\n\t\n\tInsight\n\t\n\t\n\t\n\t\n\t\n\n\n 736 | \
\n \n
\n\t
\n\t\t\n\t\t
\n\t
\n\t
\n
\n 752 | \ \n 760 | \ insight 761 | API v{{version}}\n
\n
\n\t\n\t\n\t\n\t\n\n\n" 764 | http_version: 765 | recorded_at: Wed, 08 Jul 2015 15:18:08 GMT 766 | - request: 767 | method: get 768 | uri: https://insight.mycelium.com/api/addr/wrong_address 769 | body: 770 | encoding: UTF-8 771 | string: Content-Type=application%2Fjson 772 | headers: 773 | Accept-Encoding: 774 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 775 | Accept: 776 | - "*/*" 777 | User-Agent: 778 | - Ruby 779 | response: 780 | status: 781 | code: 400 782 | message: Bad Request 783 | headers: 784 | Server: 785 | - nginx/1.6.3 786 | Date: 787 | - Thu, 09 Jul 2015 14:57:53 GMT 788 | Content-Type: 789 | - text/html; charset=utf-8 790 | Content-Length: 791 | - '72' 792 | Connection: 793 | - keep-alive 794 | X-Powered-By: 795 | - Express 796 | Access-Control-Allow-Origin: 797 | - "*" 798 | Access-Control-Allow-Methods: 799 | - GET, POST, OPTIONS, PUT, DELETE 800 | Access-Control-Allow-Headers: 801 | - X-Requested-With,Content-Type,Authorization 802 | Access-Control-Expose-Headers: 803 | - X-Email-Needs-Validation,X-Quota-Per-Item,X-Quota-Items-Limit,X-RateLimit-Limit,X-RateLimit-Remaining 804 | Etag: 805 | - '"-1523394150"' 806 | Vary: 807 | - Accept-Encoding 808 | body: 809 | encoding: UTF-8 810 | string: Invalid address:Unspecified operation for type undefined for add. Code:1 811 | http_version: 812 | recorded_at: Thu, 09 Jul 2015 14:57:53 GMT 813 | - request: 814 | method: get 815 | uri: https://insight.mycelium.com/api/addr/12X3JTpcGPS1GXmuJn9gT3gspP6YFsFT6W 816 | body: 817 | encoding: UTF-8 818 | string: Content-Type=application%2Fjson 819 | headers: 820 | Accept-Encoding: 821 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 822 | Accept: 823 | - "*/*" 824 | User-Agent: 825 | - Ruby 826 | response: 827 | status: 828 | code: 200 829 | message: OK 830 | headers: 831 | Server: 832 | - nginx/1.6.3 833 | Date: 834 | - Fri, 10 Jul 2015 08:44:02 GMT 835 | Content-Type: 836 | - application/json; charset=utf-8 837 | Content-Length: 838 | - '257' 839 | Connection: 840 | - keep-alive 841 | X-Powered-By: 842 | - Express 843 | Access-Control-Allow-Origin: 844 | - "*" 845 | Access-Control-Allow-Methods: 846 | - GET, POST, OPTIONS, PUT, DELETE 847 | Access-Control-Allow-Headers: 848 | - X-Requested-With,Content-Type,Authorization 849 | Access-Control-Expose-Headers: 850 | - X-Email-Needs-Validation,X-Quota-Per-Item,X-Quota-Items-Limit,X-RateLimit-Limit,X-RateLimit-Remaining 851 | Etag: 852 | - '"1131642747"' 853 | Vary: 854 | - Accept-Encoding 855 | body: 856 | encoding: UTF-8 857 | string: '{"addrStr":"12X3JTpcGPS1GXmuJn9gT3gspP6YFsFT6W","balance":0,"balanceSat":0,"totalReceived":0,"totalReceivedSat":0,"totalSent":0,"totalSentSat":0,"unconfirmedBalance":0,"unconfirmedBalanceSat":0,"unconfirmedTxApperances":0,"txApperances":0,"transactions":[]}' 858 | http_version: 859 | recorded_at: Fri, 10 Jul 2015 08:44:01 GMT 860 | recorded_with: VCR 2.9.3 861 | --------------------------------------------------------------------------------