├── .gitignore ├── .rspec ├── .rubocop.yml ├── .travis.yml ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── bin ├── console └── setup ├── cryptocoin_payable.gemspec ├── features ├── coin_payments.feature ├── default.feature ├── pricing_processor.feature ├── step_definitions │ ├── coin_payment_steps.rb │ ├── currency_conversion_steps.rb │ ├── model_step.rb │ ├── processor_steps.rb │ └── widget_steps.rb └── support │ └── env.rb ├── gemfiles ├── rails_4_0.gemfile ├── rails_4_1.gemfile ├── rails_4_2.gemfile ├── rails_5_0.gemfile ├── rails_5_1.gemfile ├── rails_5_2.gemfile └── rails_master.gemfile ├── lib ├── cryptocoin_payable.rb ├── cryptocoin_payable │ ├── adapters.rb │ ├── adapters │ │ ├── base.rb │ │ ├── bitcoin.rb │ │ ├── bitcoin_cash.rb │ │ └── ethereum.rb │ ├── coin_payment.rb │ ├── coin_payment_transaction.rb │ ├── commands │ │ ├── payment_processor.rb │ │ └── pricing_processor.rb │ ├── config.rb │ ├── currency_conversion.rb │ ├── errors.rb │ ├── orm │ │ └── activerecord.rb │ └── version.rb ├── generators │ └── cryptocoin_payable │ │ ├── install_generator.rb │ │ └── templates │ │ ├── create_coin_payment_transactions.rb │ │ ├── create_coin_payments.rb │ │ └── create_currency_conversions.rb └── tasks │ ├── delete_currency_conversions.rake │ ├── process_payments.rake │ └── process_prices.rake └── spec ├── acceptance ├── adapters │ ├── bitcoin_cash_spec.rb │ ├── bitcoin_spec.rb │ └── ethereum_spec.rb ├── coin_payment_spec.rb └── commands │ ├── payment_processor_spec.rb │ └── pricing_processor_spec.rb ├── dummy ├── README.rdoc ├── Rakefile ├── app │ ├── assets │ │ ├── images │ │ │ └── .keep │ │ ├── javascripts │ │ │ └── application.js │ │ └── stylesheets │ │ │ └── application.css │ ├── controllers │ │ ├── application_controller.rb │ │ └── concerns │ │ │ └── .keep │ ├── helpers │ │ └── application_helper.rb │ ├── mailers │ │ └── .keep │ ├── models │ │ ├── .keep │ │ ├── concerns │ │ │ └── .keep │ │ └── widget.rb │ └── views │ │ └── layouts │ │ └── application.html.erb ├── bin │ ├── bundle │ ├── rails │ └── rake ├── config.ru ├── config │ ├── application.rb │ ├── boot.rb │ ├── database.yml │ ├── environment.rb │ ├── environments │ │ ├── development.rb │ │ ├── production.rb │ │ └── test.rb │ ├── initializers │ │ ├── backtrace_silencers.rb │ │ ├── cryptocoin_payable.rb │ │ ├── filter_parameter_logging.rb │ │ ├── inflections.rb │ │ ├── mime_types.rb │ │ ├── secret_token.rb │ │ ├── session_store.rb │ │ └── wrap_parameters.rb │ ├── locales │ │ └── en.yml │ └── routes.rb ├── db │ ├── migrate │ │ ├── 20140510023211_create_widgets.rb │ │ ├── 20171227225132_create_coin_payments.rb │ │ ├── 20171227225134_create_currency_conversions.rb │ │ └── 20181015141952_create_coin_payment_transactions.rb │ └── schema.rb ├── lib │ └── assets │ │ └── .keep ├── log │ └── .keep ├── public │ ├── 404.html │ ├── 422.html │ ├── 500.html │ └── favicon.ico └── test │ ├── fixtures │ └── widgets.yml │ └── models │ └── widget_test.rb ├── fixtures └── vcr_cassettes │ ├── CryptocoinPayable_Adapters_Bitcoin │ ├── gets_an_empty_result_when_no_transactions_found.yml │ ├── gets_transactions_for_a_given_address.yml │ ├── gives_zero_instead_of_null_for_zero-value_transactions.yml │ ├── handles_nil.yml │ ├── raises_an_error_when_an_invalid_address_is_passed.yml │ ├── returns_zero_estimated_value_for_zero-value_transactions.yml │ └── when_the_Block_Explorer_API_fails │ │ └── falls_back_to_using_the_BlockCypher_API.yml │ ├── CryptocoinPayable_Adapters_BitcoinCash │ ├── gets_an_empty_result_when_no_transactions_found.yml │ └── gets_transactions_for_a_given_address.yml │ ├── CryptocoinPayable_Adapters_Ethereum │ ├── gets_an_empty_result_when_no_transactions_found.yml │ ├── gets_transactions_for_a_given_address.yml │ └── raises_an_error_when_an_invalid_address_is_passed.yml │ └── CryptocoinPayable_PricingProcessor │ └── when_updating_stale_payments │ ├── can_update.yml │ └── can_update_without_errors.yml └── spec_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | spec/dummy/log/* 2 | *.gem 3 | /Gemfile.lock 4 | /spec/dummy/log/development.log 5 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --require spec_helper 2 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | Layout/MultilineMethodCallIndentation: 2 | EnforcedStyle: indented 3 | 4 | Layout/MultilineAssignmentLayout: 5 | EnforcedStyle: same_line 6 | 7 | Layout/EndAlignment: 8 | EnforcedStyleAlignWith: variable 9 | 10 | Layout/AlignParameters: 11 | EnforcedStyle: with_fixed_indentation 12 | 13 | Layout/IndentHash: 14 | EnforcedStyle: consistent 15 | 16 | Style/Documentation: 17 | Enabled: false 18 | 19 | Metrics/MethodLength: 20 | Enabled: false 21 | 22 | Metrics/LineLength: 23 | Max: 120 24 | 25 | Metrics/AbcSize: 26 | Max: 21 27 | 28 | Metrics/ClassLength: 29 | Max: 200 30 | 31 | Metrics/BlockLength: 32 | Exclude: 33 | - 'Rakefile' 34 | - '**/*.rake' 35 | - 'test/**/*.rb' 36 | - 'spec/**/*.rb' 37 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | 3 | rvm: 4 | - 2.2 5 | - 2.3 6 | - 2.4 7 | - 2.5 8 | 9 | gemfile: 10 | - gemfiles/rails_4_0.gemfile 11 | - gemfiles/rails_4_1.gemfile 12 | - gemfiles/rails_4_2.gemfile 13 | - gemfiles/rails_5_0.gemfile 14 | - gemfiles/rails_5_1.gemfile 15 | - gemfiles/rails_5_2.gemfile 16 | - gemfiles/rails_master.gemfile 17 | 18 | sudo: false 19 | 20 | before_install: 21 | - gem install bundler -v 1.17.3 22 | 23 | before_script: 24 | - psql -c 'create database cryptocoin_payable_test;' -U postgres 25 | - bundle exec rake -f spec/dummy/Rakefile db:schema:load RAILS_ENV=test 26 | 27 | notifications: 28 | email: false 29 | 30 | script: 31 | - bundle exec cucumber features 32 | - bundle exec rspec 33 | - bundle exec rubocop 34 | 35 | matrix: 36 | exclude: 37 | - rvm: 2.4 38 | gemfile: gemfiles/rails_4_0.gemfile 39 | - rvm: 2.4 40 | gemfile: gemfiles/rails_4_1.gemfile 41 | - rvm: 2.5 42 | gemfile: gemfiles/rails_4_0.gemfile 43 | - rvm: 2.5 44 | gemfile: gemfiles/rails_4_1.gemfile 45 | allow_failures: 46 | - gemfile: gemfiles/rails_4_0.gemfile 47 | - gemfile: gemfiles/rails_master.gemfile 48 | 49 | addons: 50 | postgresql: '9.5' 51 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in cryptocoin_payable.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Jonathan Salis 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Gem Version](https://badge.fury.io/rb/cryptocoin_payable.svg)](https://badge.fury.io/rb/cryptocoin_payable) 2 | [![Build Status](https://travis-ci.com/Sailias/cryptocoin_payable.svg?branch=master)](https://travis-ci.com/Sailias/cryptocoin_payable) 3 | 4 | # Cryptocoin Payable 5 | 6 | Forked from [Bitcoin Payable](https://github.com/Sailias/bitcoin_payable) 7 | 8 | A rails gem that enables any model to have cryptocurrency payments. 9 | The polymorphic table coin_payments creates payments with unique addresses based on a BIP32 deterministic seed using https://github.com/GemHQ/money-tree and uses external APIs to check for payments: 10 | 11 | - https://etherscan.io 12 | - https://blockexplorer.com 13 | - https://www.blockcypher.com 14 | 15 | Supported coins are: 16 | 17 | - Bitcoin 18 | - Bitcoin Cash 19 | - Ethereum 20 | 21 | Payments have the following states: 22 | 23 | - `pending` 24 | - `partial_payment` 25 | - `paid_in_full` 26 | - `comped` (useful for refunding payments) 27 | - `confirmed` (enters state after n blockchain confirmations, see `confirmations` config option) 28 | - `expired` (useful for auto-expiring incomplete payments, see `expire_payments_after` config option) 29 | 30 | No private keys needed, No bitcoind blockchain indexing on new servers, just address and payments. 31 | 32 | **Donations appreciated** 33 | 34 | - `14xXZ6SFjwYZHATiywBE2durFknLePYqHS` (Maros Hluska) 35 | 36 | ## Installation 37 | 38 | Add this line to your application's Gemfile: 39 | 40 | ```rb 41 | gem 'cryptocoin_payable' 42 | ``` 43 | 44 | And then execute: 45 | 46 | ```sh 47 | bundle 48 | rails g cryptocoin_payable:install 49 | bundle exec rake db:migrate 50 | populate cryptocoin_payable.rb (see below) 51 | bundle exec rake cryptocoin_payable:process_prices (see below) 52 | ``` 53 | 54 | ## Uninstall 55 | 56 | ```sh 57 | rails d cryptocoin_payable:install 58 | ``` 59 | 60 | ## Run Tests 61 | 62 | ```sh 63 | cucumber features 64 | rspec 65 | rubocop 66 | ``` 67 | 68 | ## Usage 69 | 70 | ### Configuration 71 | 72 | config/initializers/cryptocoin_payable.rb 73 | 74 | ```rb 75 | CryptocoinPayable.configure do |config| 76 | # config.currency = :usd 77 | # config.testnet = true 78 | 79 | config.request_delay = 0.5 80 | config.expire_payments_after = 15.minutes 81 | 82 | config.configure_btc do |btc_config| 83 | # btc_config.confirmations = 3 84 | # btc_config.node_path = '' 85 | 86 | btc_config.master_public_key = 'tpub...' 87 | end 88 | 89 | config.configure_bch do |bch_config| 90 | # bch_config.confirmations = 3 91 | # btc_config.node_path = '' 92 | 93 | bch_config.master_public_key = 'tpub...' 94 | end 95 | 96 | config.configure_eth do |eth_config| 97 | # eth_config.confirmations = 12 98 | # eth_config.node_path = '' 99 | 100 | eth_config.master_public_key = 'tpub...' 101 | end 102 | end 103 | ``` 104 | 105 | In order to use the bitcoin network and issue real addresses, `CryptocoinPayable.config.testnet` must be set to false: 106 | 107 | ```rb 108 | CryptocoinPayable.config.testnet = false 109 | ``` 110 | 111 | Consider adding a request delay (in seconds) to prevent API rate limit errors: 112 | 113 | ```rb 114 | CryptocoinPayable.config.request_delay = 0.5 115 | ``` 116 | 117 | #### Node Path 118 | 119 | The derivation path for the node that will be creating your addresses. 120 | 121 | #### Master Public Key 122 | 123 | A BIP32 MPK in "Extended Key" format used when configuring bitcoin payments (see `btc_config.master_public_key` above). 124 | 125 | Public net starts with: xpub 126 | Testnet starts with: tpub 127 | 128 | * Obtain your BIP32 MPK from http://bip32.org/ 129 | 130 | ### Adding it to your model 131 | 132 | ```rb 133 | class Product < ActiveRecord::Base 134 | has_coin_payments 135 | end 136 | ``` 137 | 138 | ### Creating a payment from your application 139 | 140 | ```rb 141 | def create_payment(amount_in_cents) 142 | self.coin_payments.create!(reason: 'sale', price: amount_in_cents, coin_type: :btc) 143 | end 144 | ``` 145 | 146 | ### Update payments with the current price of BTC based on your currency 147 | 148 | CryptocoinPayable also supports local currency conversions and BTC exchange rates. 149 | 150 | The `process_prices` rake task connects to api.coinbase.com to get the latest BTC price for your specified currency. As a fallback, it will connect to api.gemini.com to get the latest price. 151 | It then updates all payments that havent received an update in the last 30 minutes with the new value owing in BTC. 152 | This *honors* the price of a payment for 30 minutes at a time. 153 | 154 | ```sh 155 | rake cryptocoin_payable:process_prices 156 | ``` 157 | 158 | ### Processing payments 159 | 160 | All payments are calculated against the dollar amount of the payment. So a `bitcoin_payment` for $49.99 will have it's value calculated in BTC. 161 | It will stay at that price for 30 minutes. When a payment is made, a transaction is created that stores the BTC in satoshis paid and the exchange rate is was paid at. 162 | This is very valuable for accounting later. (capital gains of all payments received) 163 | 164 | If a partial payment is made, the BTC value is recalculated for the remaining *dollar* amount with the latest exchange rate. 165 | This means that if someone pays 0.01 for a 0.5 payment, that 0.01 is converted into dollars at the time of processing and the 166 | remaining amount is calculated in dollars and the remaining amount in BTC is issued. (If BTC bombs, that value could be greater than 0.5 now) 167 | 168 | This prevents people from gaming the payments by paying very little BTC in hopes the price will rise. 169 | Payments are not recalculated based on the current value of BTC, but in dollars. 170 | 171 | To run the payment processor: 172 | 173 | ```sh 174 | rake cryptocoin_payable:process_payments 175 | ``` 176 | 177 | ### Notify your application when a payment is made 178 | 179 | Use the `coin_payment_paid` and `coin_payment_confirmed` methods 180 | 181 | ```rb 182 | def Product < ActiveRecord::Base 183 | has_coin_payments 184 | 185 | def create_payment(amount_in_cents) 186 | self.coin_payments.create!(reason: 'sale', price: amount_in_cents, type: :btc) 187 | end 188 | 189 | # Runs when the payment is first detected on the network. 190 | def coin_payment_paid(payment) 191 | self.notify! 192 | end 193 | 194 | # Runs when enough confirmations have occurred. 195 | def coin_payment_confirmed(payment) 196 | self.ship! 197 | end 198 | end 199 | ``` 200 | 201 | ### Delete old CurrencyConversion data 202 | 203 | Every time the payment processor is run, several rows are inserted into the 204 | database to record the value of the coin at a given instance in time. Over time, 205 | your application will accumulate historical currency conversion data and you may 206 | want to clear it out: 207 | 208 | ```sh 209 | rake cryptocoin_payable:delete_currency_conversions 210 | ``` 211 | 212 | By default, it will delete any data older than 1 month. You can configure this 213 | using an env variable: 214 | 215 | ```sh 216 | DELETE_BEFORE=2017-12-15 rake cryptocoin_payable:delete_currency_conversions 217 | ``` 218 | 219 | ### Comp a payment 220 | 221 | This will bypass the payment, set the state to comped and call back to your app that the payment has been processed. 222 | 223 | ```rb 224 | @coin_payment.comp 225 | ``` 226 | 227 | ### Expire a payment 228 | 229 | ```rb 230 | @coin_payment.expire 231 | ``` 232 | 233 | Payments will auto-expire if you set the `expire_payments_after` option. The 234 | exact timing is not precise because payment expiry is evaluated whenever 235 | payment_processor runs. 236 | 237 | ### View all the transactions in the payment 238 | 239 | ```rb 240 | coin_payment = @product.coin_payments.first 241 | coin_payment.transactions.find_each do |transaction| 242 | puts transaction.attributes 243 | end 244 | ``` 245 | 246 | ## Contributing 247 | 248 | 1. Fork it 249 | 2. Create your feature branch (`git checkout -b my-new-feature`) 250 | 3. Commit your changes (`git commit -am 'Add some feature'`) 251 | 4. Push to the branch (`git push origin my-new-feature`) 252 | 5. Create new Pull Request 253 | 254 | ## Contributors 255 | 256 | * andersonlewin 257 | * krtschmr 258 | * mhluska 259 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/gem_tasks' 2 | 3 | import './lib/tasks/delete_currency_conversions.rake' 4 | import './lib/tasks/process_payments.rake' 5 | import './lib/tasks/process_prices.rake' 6 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'bundler/setup' 4 | require 'cryptocoin_payable' 5 | require 'cryptocoin_payable/orm/activerecord' 6 | 7 | # You can add fixtures and/or initialization code here to make experimenting 8 | # with your gem easier. You can also use a different console, if you like. 9 | 10 | # (If you use this, don't forget to add pry to your Gemfile!) 11 | # require "pry" 12 | # Pry.start 13 | 14 | require 'irb' 15 | IRB.start(__FILE__) 16 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /cryptocoin_payable.gemspec: -------------------------------------------------------------------------------- 1 | lib = File.expand_path('lib', __dir__) 2 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 3 | require 'cryptocoin_payable/version' 4 | 5 | # rubocop:disable Metrics/BlockLength 6 | Gem::Specification.new do |spec| 7 | spec.name = 'cryptocoin_payable' 8 | spec.version = CryptocoinPayable::VERSION 9 | spec.authors = ['Jonathan Salis', 'Maros Hluska'] 10 | spec.email = ['jsalis@bitcoinsultants.ca', 'mhluska@gmail.com'] 11 | spec.description = 'Cryptocurrency payment processor' 12 | spec.summary = 'Cryptocurrency payment processor' 13 | spec.homepage = '' 14 | spec.license = 'MIT' 15 | 16 | spec.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR) 17 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 18 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) 19 | spec.require_paths = ['lib'] 20 | spec.required_rubygems_version = '>= 1.3.6' 21 | spec.required_ruby_version = '>= 2.2.0' 22 | 23 | spec.add_development_dependency 'bundler', '~> 1.15' 24 | spec.add_development_dependency 'cucumber' 25 | spec.add_development_dependency 'cucumber-rails' 26 | spec.add_development_dependency 'database_cleaner', '~> 1.7' 27 | spec.add_development_dependency 'pg', '~> 0.21.0' 28 | spec.add_development_dependency 'rails', '>= 4.0.0' 29 | spec.add_development_dependency 'rake', '~> 12.3' 30 | spec.add_development_dependency 'rspec-benchmark', '~> 0.4' 31 | spec.add_development_dependency 'rspec-rails', '~> 3.7' 32 | spec.add_development_dependency 'rspec-retry', '~> 0.6' 33 | spec.add_development_dependency 'rubocop', '~> 0.59' 34 | spec.add_development_dependency 'timecop', '~> 0.9' 35 | spec.add_development_dependency 'vcr', '~> 4.0' 36 | spec.add_development_dependency 'webmock', '~> 3.4' 37 | 38 | spec.add_dependency 'activerecord-import', '~> 0.27' 39 | spec.add_dependency 'cash-addr', '~> 0.2' 40 | spec.add_dependency 'eth', '0.4.8' 41 | spec.add_dependency 'money-tree', '0.10.0' 42 | spec.add_dependency 'state_machines-activerecord', '~> 0.5' 43 | end 44 | # rubocop:enable Metrics/BlockLength 45 | -------------------------------------------------------------------------------- /features/coin_payments.feature: -------------------------------------------------------------------------------- 1 | Feature: CoinPayment creation, validation and state 2 | 3 | Background: 4 | Given a saved widget 5 | And a new coin_payment 6 | Then the coin_payment field coin_type is set to btc 7 | Then the coin_payment field price is set to 10000 8 | And the coin_payment field reason is set to New 9 | When the coin_payment is saved 10 | 11 | Scenario: A saved widget can create a payment 12 | Then the coin_payment should have an address 13 | And the coin_payment should have the state pending 14 | 15 | Scenario: When the payment processor is run the payment status should be pending 16 | When the payment_processor is run 17 | Then the coin_payment should have the state pending 18 | 19 | Scenario: When a payment is made for 1/2 the amount, the status should be partial payment 20 | When the coin_amount_due is set 21 | And a payment is made for 50 percent 22 | When the payment_processor is run 23 | Then the coin_payment should have the state partial_payment 24 | And the amount paid percentage should be 50% 25 | 26 | Scenario: When a payment is made for 1/2 the amount, the status should be partial payment 27 | When the coin_amount_due is set 28 | And a payment is made for 100 percent 29 | When the payment_processor is run 30 | Then the coin_payment should have the state paid_in_full 31 | And the amount paid percentage should be 100% 32 | 33 | Scenario: When the price bombs the payment is still honoured at the conversion rate 34 | When the coin_amount_due is set 35 | And the currency_conversion is 1 36 | And a payment is made for 50 percent 37 | When the payment_processor is run 38 | Then the coin_payment should have the state partial_payment 39 | And the amount paid percentage should be 50% 40 | 41 | Scenario: When a partial payment is made and another payment made it should complete 42 | Scenario: When a payment is made for 1/2 the amount, the status should be partial payment 43 | When the coin_amount_due is set 44 | And a payment is made for 50 percent 45 | When the payment_processor is run 46 | Then the coin_payment should have the state partial_payment 47 | And the amount paid percentage should be 50% 48 | Then a payment is made for 50 percent 49 | When the payment_processor is run 50 | Then the coin_payment should have the state paid_in_full 51 | And the amount paid percentage should be 100% 52 | -------------------------------------------------------------------------------- /features/default.feature: -------------------------------------------------------------------------------- 1 | Feature: Testing an empty widget 2 | 3 | Scenario: An unsaved widget should respond to coin_payments 4 | Given an unsaved widget 5 | Then the widget should have 0 coin_payments 6 | -------------------------------------------------------------------------------- /features/pricing_processor.feature: -------------------------------------------------------------------------------- 1 | Feature: Pricing Processor tests 2 | 3 | Scenario: The test framework should set up 3 currency conversions 4 | Given there should be 3 currency_conversions 5 | -------------------------------------------------------------------------------- /features/step_definitions/coin_payment_steps.rb: -------------------------------------------------------------------------------- 1 | Given(/^the coin_payment field (\S*) is set to (.*)/) do |field, value| 2 | @coin_payment.send("#{field}=", value) 3 | end 4 | 5 | Given(/^the coin_payment is saved$/) do 6 | @coin_payment.save 7 | expect(@coin_payment.reload.new_record?).to be(false) 8 | end 9 | 10 | Given(/^the coin_payment should have an address$/) do 11 | expect(@coin_payment.address).to_not be(nil) 12 | end 13 | 14 | Given(/^the coin_payment should have the state (\S+)$/) do |state| 15 | expect(@coin_payment.reload.state).to eq(state) 16 | end 17 | 18 | Given(/^the coin_amount_due is set$/) do 19 | @coin_amount_due = @coin_payment.calculate_coin_amount_due 20 | end 21 | 22 | Given(/^a payment is made for (\d+) percent$/) do |percentage| 23 | CryptocoinPayable::Adapters::Bitcoin.any_instance.stub(:fetch_transactions).and_return( 24 | [{ 25 | transaction_hash: SecureRandom.uuid, 26 | block_hash: '00000000000000606aa74093ed91d657192a3772732ee4d99a7b7be8075eafa2', 27 | block_time: Time.iso8601('2017-12-26T21:38:44.000+00:00'), 28 | estimated_time: Time.iso8601('2017-12-26T21:30:19.858+00:00'), 29 | estimated_value: @coin_amount_due * (percentage.to_f / 100.0), 30 | confirmations: 1 31 | }] 32 | ) 33 | end 34 | 35 | Given(/^the amount paid percentage should be greater than (\d+)%$/) do |percentage| 36 | expect(@coin_payment.currency_amount_paid / @coin_payment.price.to_f).to be >= (percentage.to_f / 100) 37 | end 38 | 39 | Given(/^the amount paid percentage should be less than (\d+)%$/) do |percentage| 40 | expect(@coin_payment.currency_amount_paid / @coin_payment.price).to be < (percentage.to_f / 100) 41 | end 42 | 43 | Given(/^the amount paid percentage should be (\d+)%$/) do |percentage| 44 | expect(@coin_payment.currency_amount_paid / @coin_payment.price.to_f).to eq(percentage.to_f / 100) 45 | end 46 | -------------------------------------------------------------------------------- /features/step_definitions/currency_conversion_steps.rb: -------------------------------------------------------------------------------- 1 | Given(/^there should be (\d+) currency_conversions?$/) do |n| 2 | expect(@currency_conversions).to_not be_nil 3 | expect(@currency_conversions.count).to eq(n.to_i) 4 | end 5 | 6 | Given(/^the currency_conversion is (\d+)$/) do |conversion_rate| 7 | CryptocoinPayable::CurrencyConversion.create!( 8 | currency: 1, 9 | price: conversion_rate.to_i 10 | ) 11 | @currency_conversions = CryptocoinPayable::CurrencyConversion.all 12 | end 13 | -------------------------------------------------------------------------------- /features/step_definitions/model_step.rb: -------------------------------------------------------------------------------- 1 | Given(/^an unsaved widget$/) do 2 | @widget = Widget.new 3 | end 4 | 5 | Given(/^a saved widget$/) do 6 | @widget = Widget.create 7 | end 8 | 9 | Given(/^a new coin_payment$/) do 10 | @coin_payment = @widget.coin_payments.new 11 | end 12 | -------------------------------------------------------------------------------- /features/step_definitions/processor_steps.rb: -------------------------------------------------------------------------------- 1 | When(/^the payment_processor is run$/) do 2 | CryptocoinPayable::PaymentProcessor.perform 3 | end 4 | 5 | When(/^the pricing processor is run$/) do 6 | CryptocoinPayable::PricingProcessor.perform 7 | end 8 | -------------------------------------------------------------------------------- /features/step_definitions/widget_steps.rb: -------------------------------------------------------------------------------- 1 | Given(/^the widget should have (\d+) coin_payments$/) do |n| 2 | expect(@widget.coin_payments.count).to eq(n.to_i) 3 | end 4 | -------------------------------------------------------------------------------- /features/support/env.rb: -------------------------------------------------------------------------------- 1 | ENV['RAILS_ENV'] ||= 'test' 2 | ENV['RAILS_ROOT'] ||= File.dirname(__FILE__) + '../../../spec/dummy' 3 | 4 | require 'cucumber/rails' 5 | require 'cucumber/rspec/doubles' 6 | 7 | # ActiveRecord::Base.logger = Logger.new(STDOUT) if defined?(ActiveRecord::Base) 8 | 9 | # Remove/comment out the lines below if your app doesn't have a database. 10 | # For some databases (like MongoDB and CouchDB) you may need to use :truncation instead. 11 | begin 12 | DatabaseCleaner.strategy = :transaction 13 | rescue NameError 14 | raise 'You need to add database_cleaner to your Gemfile (in the :test group) if you wish to use it.' 15 | end 16 | 17 | Before do 18 | 3.times do 19 | CryptocoinPayable::CurrencyConversion.create!( 20 | coin_type: :btc, 21 | currency: rand(85...99), 22 | price: rand(10_000...15_000) * 100, # cents in fiat 23 | ) 24 | end 25 | @currency_conversions = CryptocoinPayable::CurrencyConversion.all 26 | end 27 | -------------------------------------------------------------------------------- /gemfiles/rails_4_0.gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'rails', '~> 4.0.0' 4 | 5 | gemspec path: '../' 6 | -------------------------------------------------------------------------------- /gemfiles/rails_4_1.gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'rails', '~> 4.1.0' 4 | 5 | gemspec path: '../' 6 | -------------------------------------------------------------------------------- /gemfiles/rails_4_2.gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'rails', '~> 4.2.0' 4 | 5 | gemspec path: '../' 6 | -------------------------------------------------------------------------------- /gemfiles/rails_5_0.gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'rails', '~> 5.0.0' 4 | 5 | gemspec path: '../' 6 | -------------------------------------------------------------------------------- /gemfiles/rails_5_1.gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'rails', '~> 5.1.0' 4 | 5 | gemspec path: '../' 6 | -------------------------------------------------------------------------------- /gemfiles/rails_5_2.gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'rails', '~> 5.2.0' 4 | 5 | gemspec path: '../' 6 | -------------------------------------------------------------------------------- /gemfiles/rails_master.gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'rails', github: 'rails/rails', branch: 'master' 4 | 5 | gemspec path: '../' 6 | -------------------------------------------------------------------------------- /lib/cryptocoin_payable.rb: -------------------------------------------------------------------------------- 1 | module CryptocoinPayable 2 | end 3 | 4 | if defined?(Rails) 5 | module CryptocoinPayable 6 | class Railtie < Rails::Railtie 7 | initializer 'cryptocoin_payable.active_record' do 8 | ActiveSupport.on_load(:active_record) do 9 | require 'cryptocoin_payable/orm/activerecord' 10 | end 11 | end 12 | 13 | rake_tasks do 14 | path = File.expand_path(__dir__) 15 | Dir.glob("#{path}/tasks/**/*.rake").each { |f| load f } 16 | end 17 | end 18 | end 19 | end 20 | 21 | require 'cryptocoin_payable/config' 22 | require 'cryptocoin_payable/errors' 23 | require 'cryptocoin_payable/version' 24 | require 'cryptocoin_payable/adapters' 25 | -------------------------------------------------------------------------------- /lib/cryptocoin_payable/adapters.rb: -------------------------------------------------------------------------------- 1 | module CryptocoinPayable 2 | module Adapters 3 | def self.for(coin_type) 4 | case coin_type.to_sym 5 | when :bch 6 | bitcoin_cash_adapter 7 | when :btc 8 | bitcoin_adapter 9 | when :eth 10 | ethereum_adapter 11 | else 12 | raise "Invalid coin type #{coin_type}" 13 | end 14 | end 15 | 16 | def self.bitcoin_cash_adapter 17 | @bitcoin_cash_adapter ||= BitcoinCash.new 18 | end 19 | 20 | def self.bitcoin_adapter 21 | @bitcoin_adapter ||= Bitcoin.new 22 | end 23 | 24 | def self.ethereum_adapter 25 | @ethereum_adapter ||= Ethereum.new 26 | end 27 | end 28 | end 29 | 30 | require 'cryptocoin_payable/adapters/base' 31 | require 'cryptocoin_payable/adapters/bitcoin' 32 | require 'cryptocoin_payable/adapters/bitcoin_cash' 33 | require 'cryptocoin_payable/adapters/ethereum' 34 | -------------------------------------------------------------------------------- /lib/cryptocoin_payable/adapters/base.rb: -------------------------------------------------------------------------------- 1 | module CryptocoinPayable 2 | module Adapters 3 | class Base 4 | # Implement these in a subclass: 5 | 6 | # Returns the amount of cents in the main unit. E.g. 10^18 Wei in Ether. 7 | # def self.subunit_in_main 8 | # 1_000_000_000_000_000_000 9 | # end 10 | 11 | # Returns the currency symbol (used for querying for ticker data). 12 | # def self.coin_symbol 13 | # 'ETH' 14 | # end 15 | 16 | # Queries an API like etherscan.io and returns a list of transactions 17 | # which conform to the following shape: 18 | # { 19 | # transaction_hash: string, 20 | # block_hash: string, 21 | # block_time: nil | string, 22 | # estimated_time: nil | string, 23 | # estimated_value: integer, 24 | # confirmations: integer, 25 | # } 26 | # `block_time` and `estimated_time` are optional strings conforming to 27 | # date format ISO 8601. 28 | # 29 | # Can optionally raise ApiLimitedReached if needed. 30 | # 31 | # def self.fetch_transactions(address) 32 | # end 33 | 34 | # Uses a predefined seed to generate HD addresses based on an index/id 35 | # passed into the method. 36 | # def self.create_address(id) 37 | # end 38 | 39 | def convert_subunit_to_main(subunit) 40 | subunit / self.class.subunit_in_main.to_f 41 | end 42 | 43 | def convert_main_to_subunit(main) 44 | (main * self.class.subunit_in_main).to_i 45 | end 46 | 47 | def fetch_rate 48 | currency = CryptocoinPayable.configuration.currency.to_s.upcase 49 | symbol = self.class.coin_symbol 50 | amount = 51 | begin 52 | response = get_request("https://api.coinbase.com/v2/prices/#{symbol}-#{currency}/spot") 53 | JSON.parse(response.body)['data']['amount'].to_f 54 | rescue StandardError 55 | response = get_request("https://api.gemini.com/v1/pubticker/#{symbol}#{currency}") 56 | JSON.parse(response.body)['last'].to_f 57 | end 58 | 59 | (amount * 100).to_i 60 | end 61 | 62 | def create_address(id) 63 | raise MissingMasterPublicKey, 'master_public_key is required' unless coin_config.master_public_key 64 | 65 | master = MoneyTree::Node.from_bip32(coin_config.master_public_key) 66 | master.node_for_path(coin_config.node_path + id.to_s) 67 | end 68 | 69 | protected 70 | 71 | def coin_config 72 | @coin_config ||= CryptocoinPayable.configuration.send(self.class.coin_symbol.downcase) 73 | end 74 | 75 | def adapter_api_key 76 | @adapter_api_key ||= coin_config && coin_config.adapter_api_key 77 | end 78 | 79 | def parse_timestamp(timestamp) 80 | timestamp.nil? ? nil : Time.strptime(timestamp.to_s, '%s') 81 | end 82 | 83 | def parse_time(time) 84 | time.nil? ? nil : Time.iso8601(time) 85 | end 86 | 87 | private 88 | 89 | def get_request(url) 90 | uri = URI.parse(url) 91 | http = Net::HTTP.new(uri.host, uri.port) 92 | http.use_ssl = uri.scheme == 'https' 93 | request = Net::HTTP::Get.new(uri.request_uri) 94 | result = http.request(request) 95 | 96 | request_delay = CryptocoinPayable.configuration.request_delay 97 | sleep request_delay if request_delay 98 | 99 | result 100 | end 101 | end 102 | end 103 | end 104 | -------------------------------------------------------------------------------- /lib/cryptocoin_payable/adapters/bitcoin.rb: -------------------------------------------------------------------------------- 1 | module CryptocoinPayable 2 | module Adapters 3 | class Bitcoin < Base 4 | # Satoshi in Bitcoin 5 | def self.subunit_in_main 6 | 100_000_000 7 | end 8 | 9 | def self.coin_symbol 10 | 'BTC' 11 | end 12 | 13 | def fetch_transactions(address) 14 | fetch_block_explorer_transactions(address) 15 | rescue StandardError 16 | fetch_block_cypher_transactions(address) 17 | end 18 | 19 | def create_address(id) 20 | super.to_address(network: network) 21 | end 22 | 23 | private 24 | 25 | def prefix 26 | CryptocoinPayable.configuration.testnet ? 'testnet.' : '' 27 | end 28 | 29 | def network 30 | CryptocoinPayable.configuration.testnet ? :bitcoin_testnet : :bitcoin 31 | end 32 | 33 | def parse_total_tx_value_block_explorer(output_transactions, address) 34 | output_transactions 35 | .reject { |out| out['scriptPubKey']['addresses'].nil? } 36 | .select { |out| out['scriptPubKey']['addresses'].include?(address) } 37 | .map { |out| (out['value'].to_f * self.class.subunit_in_main).to_i } 38 | .inject(:+) || 0 39 | end 40 | 41 | def fetch_block_explorer_transactions(address) 42 | url = "https://#{prefix}blockexplorer.com/api/txs/?address=#{address}" 43 | parse_block_explorer_transactions(get_request(url).body, address) 44 | end 45 | 46 | def parse_block_explorer_transactions(response, address) 47 | json = JSON.parse(response) 48 | json['txs'].map { |tx| convert_block_explorer_transactions(tx, address) } 49 | rescue JSON::ParserError 50 | raise ApiError, response 51 | end 52 | 53 | def convert_block_explorer_transactions(transaction, address) 54 | { 55 | transaction_hash: transaction['txid'], 56 | block_hash: transaction['blockhash'], 57 | block_time: parse_timestamp(transaction['blocktime']), 58 | estimated_time: parse_timestamp(transaction['time']), 59 | estimated_value: parse_total_tx_value_block_explorer(transaction['vout'], address), 60 | confirmations: transaction['confirmations'] 61 | } 62 | end 63 | 64 | def parse_total_tx_value_block_cypher(output_transactions, address) 65 | output_transactions 66 | .map { |out| out['addresses'].join.eql?(address) ? out['value'] : 0 } 67 | .inject(:+) || 0 68 | end 69 | 70 | def fetch_block_cypher_transactions(address) 71 | url = "https://api.blockcypher.com/v1/btc/main/addrs/#{address}/full" 72 | parse_block_cypher_transactions(get_request(url).body, address) 73 | end 74 | 75 | def parse_block_cypher_transactions(response, address) 76 | json = JSON.parse(response) 77 | raise ApiError, json['error'] if json['error'] 78 | 79 | json['txs'].map { |tx| convert_block_cypher_transactions(tx, address) } 80 | rescue JSON::ParserError 81 | raise ApiError, response 82 | end 83 | 84 | def convert_block_cypher_transactions(transaction, address) 85 | { 86 | transaction_hash: transaction['hash'], 87 | block_hash: transaction['block_hash'], 88 | block_time: parse_time(transaction['confirmed']), 89 | estimated_time: parse_time(transaction['received']), 90 | estimated_value: parse_total_tx_value_block_cypher(transaction['outputs'], address), 91 | confirmations: transaction['confirmations'].to_i 92 | } 93 | end 94 | end 95 | end 96 | end 97 | -------------------------------------------------------------------------------- /lib/cryptocoin_payable/adapters/bitcoin_cash.rb: -------------------------------------------------------------------------------- 1 | require 'cash_addr' 2 | 3 | module CryptocoinPayable 4 | module Adapters 5 | class BitcoinCash < Bitcoin 6 | def self.coin_symbol 7 | 'BCH' 8 | end 9 | 10 | def fetch_transactions(address) 11 | raise NetworkNotSupported if CryptocoinPayable.configuration.testnet 12 | 13 | url = "https://#{prefix}blockexplorer.com/api/txs/?address=#{legacy_address(address)}" 14 | parse_block_explorer_transactions(get_request(url).body, address) 15 | end 16 | 17 | def create_address(id) 18 | CashAddr::Converter.to_cash_address(super) 19 | end 20 | 21 | private 22 | 23 | def legacy_address(address) 24 | CashAddr::Converter.to_legacy_address(address) 25 | rescue CashAddr::InvalidAddress 26 | raise ApiError 27 | end 28 | 29 | def prefix 30 | CryptocoinPayable.configuration.testnet ? 'bchtest.' : 'bitcoincash.' 31 | end 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/cryptocoin_payable/adapters/ethereum.rb: -------------------------------------------------------------------------------- 1 | require 'eth' 2 | 3 | module CryptocoinPayable 4 | module Adapters 5 | class Ethereum < Base 6 | # Wei in Ether 7 | def self.subunit_in_main 8 | 1_000_000_000_000_000_000 9 | end 10 | 11 | def self.coin_symbol 12 | 'ETH' 13 | end 14 | 15 | def fetch_transactions(address) 16 | url = "https://#{subdomain}.etherscan.io/api?module=account&action=txlist&address=#{address}&tag=latest" 17 | url += '?apiKey=' + adapter_api_key if adapter_api_key 18 | 19 | response = get_request(url) 20 | json = JSON.parse(response.body) 21 | 22 | raise ApiError, json['message'] if json['status'] == '0' && json['message'] == 'NOTOK' 23 | 24 | json['result'].map { |tx| convert_transactions(tx, address) } 25 | end 26 | 27 | def create_address(id) 28 | Eth::Utils.public_key_to_address(super.public_key.uncompressed.to_hex) 29 | end 30 | 31 | private 32 | 33 | def subdomain 34 | @subdomain ||= CryptocoinPayable.configuration.testnet ? 'rinkeby' : 'api' 35 | end 36 | 37 | # Example response: 38 | # { 39 | # status: "1", 40 | # message: "OK", 41 | # result: [ 42 | # { 43 | # blockNumber: "4790248", 44 | # timeStamp: "1514144760", 45 | # hash: "0x52345400e42a15ba883fb0e314d050a7e7e376a30fc59dfcd7b841007d5d710c", 46 | # nonce: "215964", 47 | # block_hash: "0xe6ed0d98586cae04be57e515ca7773c020b441de60a467cd2773877a8996916f", 48 | # transactionIndex: "4", 49 | # from: "0xd24400ae8bfebb18ca49be86258a3c749cf46853", 50 | # to: "0x911f9d574d1ca099cae5ab606aa9207fe238579f", 51 | # value: "10000000000000000", 52 | # gas: "90000", 53 | # gasPrice: "28000000000", 54 | # isError: "0", 55 | # txreceipt_status: "1", 56 | # input: "0x", 57 | # contractAddress: "", 58 | # cumulativeGasUsed: "156270", 59 | # gasUsed: "21000", 60 | # confirmations: "154" 61 | # } 62 | # ] 63 | # } 64 | def convert_transactions(transaction, _address) 65 | { 66 | transaction_hash: transaction['hash'], 67 | block_hash: transaction['block_hash'], 68 | block_time: nil, # Not supported 69 | estimated_time: parse_timestamp(transaction['timeStamp']), 70 | estimated_value: transaction['value'].to_i, # Units here are 'Wei' 71 | confirmations: transaction['confirmations'].to_i 72 | } 73 | end 74 | end 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /lib/cryptocoin_payable/coin_payment.rb: -------------------------------------------------------------------------------- 1 | require 'money-tree' 2 | require 'state_machines-activerecord' 3 | 4 | module CryptocoinPayable 5 | class CoinPayment < ActiveRecord::Base 6 | belongs_to :payable, polymorphic: true 7 | has_many :transactions, class_name: 'CryptocoinPayable::CoinPaymentTransaction' 8 | 9 | validates :reason, presence: true 10 | validates :price, presence: true 11 | validates :coin_type, presence: true 12 | 13 | before_create :populate_currency_and_amount_due 14 | after_create :populate_address 15 | 16 | scope :unconfirmed, -> { where(state: %i[pending partial_payment paid_in_full]) } 17 | scope :unpaid, -> { where(state: %i[pending partial_payment]) } 18 | scope :stale, -> { where('updated_at < ? OR coin_amount_due = 0', 30.minutes.ago) } 19 | 20 | # TODO: Duplicated in `CurrencyConversion`. 21 | enum coin_type: %i[ 22 | btc 23 | eth 24 | bch 25 | ] 26 | 27 | state_machine :state, initial: :pending do 28 | state :pending 29 | state :partial_payment 30 | state :paid_in_full 31 | state :confirmed 32 | state :comped 33 | state :expired 34 | 35 | after_transition on: :pay, do: :notify_payable_paid 36 | after_transition on: :comp, do: :notify_payable_paid 37 | after_transition on: :confirm, do: :notify_payable_confirmed 38 | after_transition on: :expire, do: :notify_payable_expired 39 | 40 | event :pay do 41 | transition %i[pending partial_payment] => :paid_in_full 42 | end 43 | 44 | event :partially_pay do 45 | transition pending: :partial_payment 46 | end 47 | 48 | event :comp do 49 | transition %i[pending partial_payment] => :comped 50 | end 51 | 52 | event :confirm do 53 | transition paid_in_full: :confirmed 54 | end 55 | 56 | event :expire do 57 | transition [:pending] => :expired 58 | end 59 | end 60 | 61 | def coin_amount_paid 62 | transactions.sum { |tx| adapter.convert_subunit_to_main(tx.estimated_value) } 63 | end 64 | 65 | def coin_amount_paid_subunit 66 | transactions.sum(&:estimated_value) 67 | end 68 | 69 | # @returns cents in fiat currency. 70 | def currency_amount_paid 71 | cents = transactions.inject(0) do |sum, tx| 72 | sum + (adapter.convert_subunit_to_main(tx.estimated_value) * tx.coin_conversion) 73 | end 74 | 75 | # Round to 0 decimal places so there aren't any partial cents. 76 | cents.round(0) 77 | end 78 | 79 | def currency_amount_due 80 | price - currency_amount_paid 81 | end 82 | 83 | def calculate_coin_amount_due 84 | adapter.convert_main_to_subunit(currency_amount_due / coin_conversion.to_f).ceil 85 | end 86 | 87 | def coin_conversion 88 | @coin_conversion ||= CurrencyConversion.where(coin_type: coin_type).last.price 89 | end 90 | 91 | def update_coin_amount_due(rate: coin_conversion) 92 | update!( 93 | coin_amount_due: calculate_coin_amount_due, 94 | coin_conversion: rate 95 | ) 96 | end 97 | 98 | def transactions_confirmed? 99 | transactions.all? do |t| 100 | t.confirmations >= CryptocoinPayable.configuration.send(coin_type).confirmations 101 | end 102 | end 103 | 104 | def adapter 105 | @adapter ||= Adapters.for(coin_type) 106 | end 107 | 108 | private 109 | 110 | def populate_currency_and_amount_due 111 | self.currency ||= CryptocoinPayable.configuration.currency 112 | self.coin_amount_due = calculate_coin_amount_due 113 | self.coin_conversion = coin_conversion 114 | end 115 | 116 | def populate_address 117 | update(address: adapter.create_address(id)) 118 | end 119 | 120 | def notify_payable_event(event_name) 121 | method_name = :"coin_payment_#{event_name}" 122 | payable.send(method_name, self) if payable.respond_to?(method_name) 123 | 124 | payable.coin_payment_event(self, event_name) if payable.respond_to?(:coin_payment_event) 125 | end 126 | 127 | def notify_payable_paid 128 | notify_payable_event(:paid) 129 | end 130 | 131 | def notify_payable_confirmed 132 | notify_payable_event(:confirmed) 133 | end 134 | 135 | def notify_payable_expired 136 | notify_payable_event(:expired) 137 | end 138 | end 139 | end 140 | -------------------------------------------------------------------------------- /lib/cryptocoin_payable/coin_payment_transaction.rb: -------------------------------------------------------------------------------- 1 | module CryptocoinPayable 2 | class CoinPaymentTransaction < ActiveRecord::Base 3 | belongs_to :coin_payment 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /lib/cryptocoin_payable/commands/payment_processor.rb: -------------------------------------------------------------------------------- 1 | require 'activerecord-import' 2 | 3 | module CryptocoinPayable 4 | class PaymentProcessor 5 | def self.perform 6 | new.perform 7 | end 8 | 9 | def self.update_transactions_for(payment) 10 | new.update_transactions_for(payment) 11 | end 12 | 13 | def perform 14 | CoinPayment.unconfirmed.find_each do |payment| 15 | # Check for completed payment first, in case it's 0 and we don't need to 16 | # make an API call. 17 | update_payment_state(payment) 18 | 19 | next if payment.confirmed? 20 | 21 | begin 22 | update_transactions_for(payment) 23 | rescue StandardError => error 24 | STDERR.puts 'PaymentProcessor: Unknown error encountered, skipping transaction' 25 | STDERR.puts error 26 | next 27 | end 28 | 29 | # Check for payments after the response comes back. 30 | update_payment_state(payment) 31 | 32 | # If the payment has not moved out of the pending state after loading 33 | # new transactions, we expire it. 34 | update_payment_expired_state(payment) if payment.pending? 35 | end 36 | end 37 | 38 | def update_transactions_for(payment) 39 | transactions = Adapters.for(payment.coin_type).fetch_transactions(payment.address) 40 | 41 | payment.transaction do 42 | if supports_bulk_insert? 43 | update_via_bulk_insert(payment, transactions) 44 | else 45 | update_via_many_insert(payment, transactions) 46 | end 47 | end 48 | 49 | transactions 50 | end 51 | 52 | private 53 | 54 | def supports_bulk_insert? 55 | # TODO: Remove this once this is fixed: https://github.com/zdennis/activerecord-import/issues/559 56 | return false if Gem.loaded_specs['rails'].version < Gem::Version.create('4.2') 57 | 58 | ActiveRecord::Base.connection.supports_on_duplicate_key_update? 59 | end 60 | 61 | def update_via_bulk_insert(payment, transactions) 62 | transactions.each do |t| 63 | t[:coin_conversion] = payment.coin_conversion 64 | t[:coin_payment_id] = payment.id 65 | end 66 | 67 | CoinPaymentTransaction.import( 68 | transactions, 69 | on_duplicate_key_update: { 70 | conflict_target: [:transaction_hash], 71 | columns: [:confirmations] 72 | } 73 | ) 74 | payment.reload 75 | payment.update_coin_amount_due 76 | end 77 | 78 | def update_via_many_insert(payment, transactions) 79 | transactions.each do |tx| 80 | transaction = payment.transactions.find_by_transaction_hash(tx[:transaction_hash]) 81 | if transaction 82 | transaction.update(confirmations: tx[:confirmations]) 83 | else 84 | tx[:coin_conversion] = payment.coin_conversion 85 | payment.transactions.create!(tx) 86 | payment.update_coin_amount_due 87 | end 88 | end 89 | end 90 | 91 | def update_payment_state(payment) 92 | if payment.currency_amount_paid >= payment.price 93 | payment.pay 94 | payment.confirm if payment.transactions_confirmed? 95 | elsif payment.currency_amount_paid > 0 96 | payment.partially_pay 97 | end 98 | end 99 | 100 | def update_payment_expired_state(payment) 101 | expire_after = CryptocoinPayable.configuration.expire_payments_after 102 | payment.expire if expire_after.present? && (Time.now - payment.created_at) >= expire_after 103 | end 104 | end 105 | end 106 | -------------------------------------------------------------------------------- /lib/cryptocoin_payable/commands/pricing_processor.rb: -------------------------------------------------------------------------------- 1 | module CryptocoinPayable 2 | class PricingProcessor 3 | def self.perform 4 | new.perform 5 | end 6 | 7 | def self.delete_currency_conversions(time_ago) 8 | new.delete_currency_conversions(time_ago) 9 | end 10 | 11 | def perform 12 | rates = CurrencyConversion.coin_types.map do |coin_pair| 13 | coin_type = coin_pair[0].to_sym 14 | [ 15 | coin_type, 16 | CurrencyConversion.create!( 17 | # TODO: Store three previous price ranges, defaulting to 100 for now. 18 | currency: 100, 19 | price: Adapters.for(coin_type).fetch_rate, 20 | coin_type: coin_type 21 | ) 22 | ] 23 | end.to_h 24 | 25 | # Loop through all unpaid payments and update them with the new price if 26 | # it has been 30 mins since they have been updated. 27 | CoinPayment.unpaid.stale.find_each do |payment| 28 | payment.update_coin_amount_due(rate: rates[payment.coin_type.to_sym].price) 29 | end 30 | end 31 | 32 | def delete_currency_conversions(time_ago) 33 | # Makes sure to keep at least one record in the db since other areas of 34 | # the gem assume the existence of at least one record. 35 | last_id = CurrencyConversion.last.id 36 | time = time_ago || 1.month.ago 37 | CurrencyConversion.where('created_at < ? AND id != ?', time, last_id).delete_all 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/cryptocoin_payable/config.rb: -------------------------------------------------------------------------------- 1 | module CryptocoinPayable 2 | class << self 3 | attr_accessor :configuration 4 | end 5 | 6 | def self.configure 7 | @configuration ||= Configuration.new 8 | yield(configuration) 9 | end 10 | 11 | class Configuration 12 | attr_accessor :testnet, :expire_payments_after, :request_delay, :btc, :bch, :eth 13 | attr_writer :currency 14 | 15 | def currency 16 | @currency ||= :usd 17 | end 18 | 19 | def configure_btc 20 | @btc ||= BtcConfiguration.new 21 | yield(@btc) 22 | end 23 | 24 | def configure_bch 25 | @bch ||= BchConfiguration.new 26 | yield(@bch) 27 | end 28 | 29 | def configure_eth 30 | @eth ||= EthConfiguration.new 31 | yield(@eth) 32 | 33 | Eth.configure do |config| 34 | config.chain_id = CryptocoinPayable.configuration.testnet ? 4 : 1 35 | end 36 | end 37 | 38 | class CoinConfiguration 39 | attr_accessor :master_public_key, :confirmations, :adapter_api_key 40 | attr_writer :node_path 41 | 42 | def node_path 43 | @node_path ||= '' 44 | end 45 | end 46 | 47 | class BtcConfiguration < CoinConfiguration 48 | def confirmations 49 | @confirmations ||= 3 50 | end 51 | end 52 | 53 | class BchConfiguration < CoinConfiguration 54 | def confirmations 55 | @confirmations ||= 3 56 | end 57 | end 58 | 59 | class EthConfiguration < CoinConfiguration 60 | def confirmations 61 | @confirmations ||= 12 62 | end 63 | end 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /lib/cryptocoin_payable/currency_conversion.rb: -------------------------------------------------------------------------------- 1 | module CryptocoinPayable 2 | class CurrencyConversion < ActiveRecord::Base 3 | validates :price, presence: true 4 | 5 | # TODO: Duplicated in `CoinPayment`. 6 | enum coin_type: %i[ 7 | btc 8 | eth 9 | bch 10 | ] 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/cryptocoin_payable/errors.rb: -------------------------------------------------------------------------------- 1 | module CryptocoinPayable 2 | class ApiError < StandardError; end 3 | class ApiLimitReached < ApiError; end 4 | class ConfigError < StandardError; end 5 | class MissingMasterPublicKey < ConfigError; end 6 | class NetworkNotSupported < ConfigError; end 7 | end 8 | -------------------------------------------------------------------------------- /lib/cryptocoin_payable/orm/activerecord.rb: -------------------------------------------------------------------------------- 1 | require 'active_record' 2 | require 'cryptocoin_payable/coin_payment_transaction' 3 | require 'cryptocoin_payable/coin_payment' 4 | require 'cryptocoin_payable/currency_conversion' 5 | require 'cryptocoin_payable/commands/pricing_processor' 6 | require 'cryptocoin_payable/commands/payment_processor' 7 | 8 | module ActiveRecord 9 | class Base 10 | def self.has_coin_payments # rubocop:disable Naming/PredicateName 11 | has_many :coin_payments, class_name: 'CryptocoinPayable::CoinPayment', as: 'payable' 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/cryptocoin_payable/version.rb: -------------------------------------------------------------------------------- 1 | module CryptocoinPayable 2 | VERSION = '1.4.5'.freeze 3 | end 4 | -------------------------------------------------------------------------------- /lib/generators/cryptocoin_payable/install_generator.rb: -------------------------------------------------------------------------------- 1 | require 'rails/generators' 2 | require 'rails/generators/active_record' 3 | 4 | module CryptocoinPayable 5 | class InstallGenerator < Rails::Generators::Base 6 | include Rails::Generators::Migration 7 | 8 | source_root File.expand_path('templates', __dir__) 9 | 10 | desc 'Generates (but does not run) a migration to add a coin payment tables.' 11 | 12 | def create_migration_file 13 | migration_template 'create_coin_payments.rb', 14 | 'db/migrate/create_coin_payments.rb' 15 | 16 | migration_template 'create_coin_payment_transactions.rb', 17 | 'db/migrate/create_coin_payment_transactions.rb' 18 | 19 | migration_template 'create_currency_conversions.rb', 20 | 'db/migrate/create_currency_conversions.rb' 21 | end 22 | 23 | def self.next_migration_number(dirname) 24 | ActiveRecord::Generators::Base.next_migration_number(dirname) 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/generators/cryptocoin_payable/templates/create_coin_payment_transactions.rb: -------------------------------------------------------------------------------- 1 | class CreateCoinPaymentTransactions < ActiveRecord::Migration[5.1] 2 | def change 3 | create_table :coin_payment_transactions do |t| 4 | t.decimal :estimated_value, precision: 24, scale: 0 5 | t.string :transaction_hash, index: { unique: true } 6 | t.string :block_hash 7 | t.datetime :block_time 8 | t.datetime :estimated_time 9 | t.integer :coin_payment_id 10 | t.decimal :coin_conversion, precision: 24, scale: 0 11 | t.integer :confirmations, default: 0 12 | end 13 | 14 | add_index :coin_payment_transactions, :coin_payment_id 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/generators/cryptocoin_payable/templates/create_coin_payments.rb: -------------------------------------------------------------------------------- 1 | class CreateCoinPayments < ActiveRecord::Migration[5.1] 2 | def change 3 | create_table :coin_payments do |t| 4 | t.string :payable_type 5 | t.integer :coin_type 6 | t.integer :payable_id 7 | t.string :currency 8 | t.string :reason 9 | t.integer :price, limit: 8 10 | t.decimal :coin_amount_due, default: 0, precision: 24, scale: 0 11 | t.string :address 12 | t.string :state, default: 'pending' 13 | t.datetime :created_at 14 | t.datetime :updated_at 15 | t.decimal :coin_conversion, precision: 24, scale: 0 16 | end 17 | add_index :coin_payments, %i[payable_type payable_id] 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/generators/cryptocoin_payable/templates/create_currency_conversions.rb: -------------------------------------------------------------------------------- 1 | class CreateCurrencyConversions < ActiveRecord::Migration[5.1] 2 | def change 3 | create_table :currency_conversions do |t| 4 | t.integer :currency 5 | t.decimal :price, precision: 24, scale: 0 6 | t.integer :coin_type 7 | t.datetime :created_at 8 | t.datetime :updated_at 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/tasks/delete_currency_conversions.rake: -------------------------------------------------------------------------------- 1 | namespace :cryptocoin_payable do 2 | desc 'Delete old CurrencyConversion data (will delete last month by default)' 3 | task delete_currency_conversions: :environment do 4 | CryptocoinPayable::PricingProcessor.delete_currency_conversions(ENV['DELETE_BEFORE']) 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /lib/tasks/process_payments.rake: -------------------------------------------------------------------------------- 1 | namespace :cryptocoin_payable do 2 | desc 'Get transactions from external API and process payments' 3 | task process_payments: :environment do 4 | CryptocoinPayable::PaymentProcessor.perform 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /lib/tasks/process_prices.rake: -------------------------------------------------------------------------------- 1 | namespace :cryptocoin_payable do 2 | desc 'Process the prices and update the payments' 3 | task process_prices: :environment do 4 | CryptocoinPayable::PricingProcessor.perform 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /spec/acceptance/adapters/bitcoin_cash_spec.rb: -------------------------------------------------------------------------------- 1 | require 'active_record' 2 | require 'cryptocoin_payable' 3 | 4 | describe CryptocoinPayable::Adapters::BitcoinCash, :vcr do 5 | it 'gets transactions for a given address' do 6 | response = subject.fetch_transactions('bitcoincash:qzmdvhfdxv7j79hs6080hdm47szxfxqpccemzq6n52') 7 | 8 | expect(response).to eq( 9 | [ 10 | { 11 | transaction_hash: '10d9d3927a21d90c573a5fbbb347f409af37219ceb93f7475d6c4cca4231d29f', 12 | block_hash: '0000000000000000015493ab50fde669130f9b64f0918031a5b6dcc44f14698f', 13 | block_time: Time.iso8601('2018-10-12T07:28:21.000000000+00:00'), 14 | estimated_time: Time.iso8601('2018-10-12T07:28:21.000000000+00:00'), 15 | estimated_value: 4_128_450, 16 | confirmations: 2 17 | } 18 | ] 19 | ) 20 | end 21 | 22 | it 'gets an empty result when no transactions found' do 23 | response = subject.fetch_transactions('bitcoincash:qqu5af4540fw6eg3cqr3t8ndhplpd0xf0vmqpw59ef') 24 | expect(response).to eq([]) 25 | end 26 | 27 | it 'raises an error when an invalid address is passed' do 28 | expect { subject.fetch_transactions('foo') }.to raise_error CryptocoinPayable::ApiError 29 | end 30 | 31 | it 'creates BIP32 addresses' do 32 | 3.times do 33 | expect(subject.create_address(0)).to eq('bitcoincash:qpfspf58t6vcsvq7xeumpuudqhvj38s5sus4uutspg') 34 | expect(subject.create_address(1)).to eq('bitcoincash:qz94rwzlgccnkvaaea5klmtmad32l8gndgrcfwaryc') 35 | expect(subject.create_address(2)).to eq('bitcoincash:qrgpwhl6x5qvf8ratcdl992r5afuv6286ujfa82xrh') 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /spec/acceptance/adapters/bitcoin_spec.rb: -------------------------------------------------------------------------------- 1 | require 'active_record' 2 | require 'cryptocoin_payable' 3 | 4 | describe CryptocoinPayable::Adapters::Bitcoin, :vcr do 5 | def expect_transaction_result(response) 6 | expect(response).to match_array( 7 | [ 8 | { 9 | transaction_hash: '5bdeaf7829148d7e0e1e7b5233512a2c5ae54ef7ccbc8e68b2f85b7e49c917a0', 10 | block_hash: '0000000000000000048e8ea3fdd2c3a59ddcbcf7575f82cb96ce9fd17da9f2f4', 11 | block_time: Time.iso8601('2016-09-13T15:41:00.000000000+00:00'), 12 | estimated_time: be_within(1.day).of(Time.iso8601('2016-09-13T15:41:00.000000000+00:00')), 13 | estimated_value: 499_000_000, 14 | confirmations: 116_077 15 | }, 16 | { 17 | transaction_hash: 'e7bcdb13d9c903973bd8a740054d4c056a559bae67d4e8f6d0a42b4bab552623', 18 | block_hash: '000000000000000001af27feb303ad97af81a5882157f166781784c639f8e896', 19 | block_time: Time.iso8601('2016-09-13T15:22:42.000000000+00:00'), 20 | estimated_time: be_within(1.day).of(Time.iso8601('2016-09-13T15:22:42.000000000+00:00')), 21 | estimated_value: 1_000_000, 22 | confirmations: 116_080 23 | } 24 | ] 25 | ) 26 | end 27 | 28 | it 'gets transactions for a given address' do 29 | response = subject.fetch_transactions('3HR9xYD7MybbE7JLVTjwijYse48BtfEKni') 30 | expect_transaction_result(response) 31 | end 32 | 33 | it 'returns zero estimated value for zero-value transactions' do 34 | response = subject.fetch_transactions('1PKKkNRPPfPjrPiufHzuLFX2gMAVJbcN8H') 35 | expect(response.first[:estimated_value]).not_to be_nil 36 | expect(response.first[:estimated_value]).to be_zero 37 | end 38 | 39 | it 'gets an empty result when no transactions found' do 40 | response = subject.fetch_transactions('1twtr17A65VAPhJDJRxhoMSpLBTR5Xy44') 41 | expect(response).to eq([]) 42 | end 43 | 44 | it 'raises an error when an invalid address is passed' do 45 | expect { subject.fetch_transactions('foo') }.to raise_error CryptocoinPayable::ApiError 46 | end 47 | 48 | context 'when the Block Explorer API fails' do 49 | before do 50 | stub_request(:any, %r{blockexplorer.com/api}) 51 | .to_return(body: '502 Gateway Error', headers: { 'Content-Type' => 'text/html' }) 52 | 53 | allow(subject).to receive(:fetch_block_cypher_transactions).and_call_original 54 | end 55 | 56 | it 'falls back to using the BlockCypher API' do 57 | response = subject.fetch_transactions('3HR9xYD7MybbE7JLVTjwijYse48BtfEKni') 58 | expect(subject).to have_received(:fetch_block_cypher_transactions).once 59 | expect_transaction_result(response) 60 | end 61 | end 62 | 63 | it 'creates BIP32 addresses' do 64 | 3.times do 65 | expect(subject.create_address(0)).to eq('1D5qJDG6No5xcHovLmyNU1b3vq7xkEzTRH') 66 | expect(subject.create_address(1)).to eq('17A91ZXzkJQkSrbNWcY8ywDC7D9aW9roKo') 67 | expect(subject.create_address(2)).to eq('16Ak3B8ahHWbrZvukMUe8PUDLR5HScM6LK') 68 | end 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /spec/acceptance/adapters/ethereum_spec.rb: -------------------------------------------------------------------------------- 1 | require 'active_record' 2 | require 'cryptocoin_payable' 3 | 4 | describe CryptocoinPayable::Adapters::Ethereum, :vcr do 5 | def expect_transaction_result(response) 6 | expect(response).to match_array( 7 | [ 8 | { 9 | transaction_hash: '0xa88b799514e9621962e3d0de25e7e0bc7a123e33085f322c7acdb99cc2585c6d', 10 | block_hash: '0x752c50e426f65820f5bf6fd49acbb08d79464f8e7e8ea5b77e2299b69fd6398b', 11 | block_time: nil, 12 | estimated_time: be_within(1.day).of(Time.iso8601('2018-07-05T12:58:33.000000000+07:00')), 13 | estimated_value: 33_753_640_000_000_000, 14 | confirmations: 569_771 15 | }, 16 | { 17 | transaction_hash: '0xb325a8cf241f332bca92c7f715987e4d34be9a6b3bb78d2425c83086b4aced26', 18 | block_hash: '0x1c2b73a16fd8c4d25feeccaa2f0bf5c82b8f415f1beaf4d34aaf870daf89689d', 19 | block_time: nil, 20 | estimated_time: be_within(1.day).of(Time.iso8601('2018-07-05T13:35:07.000000000+07:00')), 21 | estimated_value: 2_190_144_444_444_444, 22 | confirmations: 569_629 23 | }, 24 | { 25 | transaction_hash: '0xcd874917be5ad177e7ebd88b5c4a7d4283796e00e43345da5b63fb4f78130b37', 26 | block_hash: '0x4ce71d11146445f123680ea9beba7db968b04dc675caddf60248c9d9d6f5739e', 27 | block_time: nil, 28 | estimated_time: be_within(1.day).of(Time.iso8601('2018-07-05T13:55:53.000000000+07:00')), 29 | estimated_value: 1_007_518_888_888_888, 30 | confirmations: 569_549 31 | }, 32 | { 33 | transaction_hash: '0x799ec2aaafbddbc2e746334f96f59f6127dec62e5693480576db351aaf840bfb', 34 | block_hash: '0xc1361b19b2266e2259ac433b9e18b4fbc81339304988bbc62dd93aa24fac6449', 35 | block_time: nil, 36 | estimated_time: be_within(1.day).of(Time.iso8601('2018-08-26T16:05:44.000000000+07:00')), 37 | estimated_value: 15_678_420_000_000_000, 38 | confirmations: 261_969 39 | } 40 | ] 41 | ) 42 | end 43 | 44 | it 'gets transactions for a given address' do 45 | response = subject.fetch_transactions('0xfc8cfb26c31931572e65e450f7fa498bcc11651c') 46 | expect_transaction_result(response) 47 | end 48 | 49 | it 'gets an empty result when no transactions found' do 50 | response = subject.fetch_transactions('0x772fDD41BFB34C9903B253322baccdbE2C10851e') 51 | expect(response).to eq([]) 52 | end 53 | 54 | it 'raises an error when an invalid address is passed' do 55 | expect { subject.fetch_transactions('foo') }.to raise_error CryptocoinPayable::ApiError 56 | end 57 | 58 | it 'creates BIP32 addresses' do 59 | 3.times do 60 | expect(subject.create_address(0)).to eq('0xcDe321aCfa5B779dCD174850C3FB6E5Ff15cDEAf') 61 | expect(subject.create_address(1)).to eq('0x0CA6E0C53EEb559c0D8803076D4F02b72f0FAE9C') 62 | expect(subject.create_address(2)).to eq('0xD87D2476c93411242778fe0ef6e758De19ed19E8') 63 | end 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /spec/acceptance/coin_payment_spec.rb: -------------------------------------------------------------------------------- 1 | require 'active_record' 2 | require 'cryptocoin_payable' 3 | require 'cryptocoin_payable/orm/activerecord' 4 | 5 | describe CryptocoinPayable::CoinPayment do 6 | context 'when creating a Bitcoin Cash payment' do 7 | subject { CryptocoinPayable::CoinPayment.new(coin_type: :bch, reason: 'test', price: 1) } 8 | 9 | it 'can save a payment' do 10 | expect { subject.save! }.not_to raise_error 11 | end 12 | 13 | it 'can update the coin amount due' do 14 | subject.update_coin_amount_due 15 | expect(subject.coin_amount_due).to eq(100_000_000) 16 | expect(subject.coin_conversion).to eq(1) 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/acceptance/commands/payment_processor_spec.rb: -------------------------------------------------------------------------------- 1 | require 'digest' 2 | require 'active_record' 3 | require 'cryptocoin_payable/orm/activerecord' 4 | 5 | describe CryptocoinPayable::PaymentProcessor do 6 | let(:adapter) { CryptocoinPayable::Adapters.bitcoin_adapter } 7 | 8 | def build_fake_transactions_data(count: 10, confirmations: 10) 9 | transactions = [] 10 | count.times do |i| 11 | transactions << { 12 | transaction_hash: Digest::SHA2.new(256).hexdigest(i.to_s), 13 | block_hash: '0000000000000000048e8ea3fdd2c3a59ddcbcf7575f82cb96ce9fd17da9f2f4', 14 | block_time: Time.iso8601('2016-09-13T15:41:00.000000000+00:00'), 15 | estimated_time: Time.iso8601('2016-09-13T15:41:00.000000000+00:00'), 16 | estimated_value: 499_000_000, 17 | confirmations: confirmations 18 | } 19 | end 20 | transactions 21 | end 22 | 23 | def create_payment 24 | payment = CryptocoinPayable::CoinPayment.create!(reason: 'test', price: 1, coin_type: :btc) 25 | payment.update(address: '3HR9xYD7MybbE7JLVTjwijYse48BtfEKni') 26 | payment 27 | end 28 | 29 | context 'when using bulk insert' do 30 | before do 31 | # TODO: Remove this once this is fixed: https://github.com/zdennis/activerecord-import/issues/559 32 | skip if Gem.loaded_specs['rails'].version < Gem::Version.create('4.2') 33 | end 34 | 35 | context 'when testing performance' do 36 | before { GC.disable } 37 | after { GC.enable } 38 | 39 | before do 40 | allow(adapter).to receive(:fetch_transactions) { build_fake_transactions_data(count: 300) } 41 | end 42 | 43 | it 'should insert 300 transactions in under 400ms', retry: 3 do 44 | payment = create_payment 45 | expect { subject.update_transactions_for(payment) }.to perform_under(400).ms 46 | end 47 | end 48 | 49 | it 'should update the confirmation count' do 50 | payment = create_payment 51 | 52 | expect(payment.transactions.size).to eq(0) 53 | 54 | allow(adapter).to receive(:fetch_transactions) { build_fake_transactions_data(confirmations: 5) } 55 | subject.update_transactions_for(payment) 56 | 57 | expect(payment.transactions.size).to eq(10) 58 | 59 | allow(adapter).to receive(:fetch_transactions) { build_fake_transactions_data(confirmations: 10) } 60 | subject.update_transactions_for(payment) 61 | 62 | expect(payment.transactions.size).to eq(10) 63 | expect(payment.transactions.last.confirmations).to eq(10) 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /spec/acceptance/commands/pricing_processor_spec.rb: -------------------------------------------------------------------------------- 1 | require 'timecop' 2 | 3 | describe CryptocoinPayable::PricingProcessor, vcr: true do 4 | context 'when updating stale payments' do 5 | before do 6 | # Ensure we have a stale payment. 7 | Timecop.freeze(3.days.from_now) 8 | 9 | CryptocoinPayable::CoinPayment.create!( 10 | state: :pending, 11 | coin_type: :btc, 12 | price: 1, 13 | reason: 'test' 14 | ) 15 | end 16 | 17 | after do 18 | Timecop.return 19 | end 20 | 21 | it 'can update without errors' do 22 | subject.perform 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /spec/dummy/README.rdoc: -------------------------------------------------------------------------------- 1 | == README 2 | 3 | This README would normally document whatever steps are necessary to get the 4 | application up and running. 5 | 6 | Things you may want to cover: 7 | 8 | * Ruby version 9 | 10 | * System dependencies 11 | 12 | * Configuration 13 | 14 | * Database creation 15 | 16 | * Database initialization 17 | 18 | * How to run the test suite 19 | 20 | * Services (job queues, cache servers, search engines, etc.) 21 | 22 | * Deployment instructions 23 | 24 | * ... 25 | 26 | 27 | Please feel free to use a different markup language if you do not plan to run 28 | rake doc:app. 29 | -------------------------------------------------------------------------------- /spec/dummy/Rakefile: -------------------------------------------------------------------------------- 1 | # Add your own tasks in files placed in lib/tasks ending in .rake, 2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 3 | 4 | require File.expand_path('config/application', __dir__) 5 | 6 | Dummy::Application.load_tasks 7 | -------------------------------------------------------------------------------- /spec/dummy/app/assets/images/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sailias/cryptocoin_payable/0c505b79a28bf825626ff0cbfbed6b6d1575a816/spec/dummy/app/assets/images/.keep -------------------------------------------------------------------------------- /spec/dummy/app/assets/javascripts/application.js: -------------------------------------------------------------------------------- 1 | // This is a manifest file that'll be compiled into application.js, which will include all the files 2 | // listed below. 3 | // 4 | // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts, 5 | // or vendor/assets/javascripts of plugins, if any, can be referenced here using a relative path. 6 | // 7 | // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the 8 | // compiled file. 9 | // 10 | // Read Sprockets README (https://github.com/sstephenson/sprockets#sprockets-directives) for details 11 | // about supported directives. 12 | // 13 | //= require_tree . 14 | -------------------------------------------------------------------------------- /spec/dummy/app/assets/stylesheets/application.css: -------------------------------------------------------------------------------- 1 | /* 2 | * This is a manifest file that'll be compiled into application.css, which will include all the files 3 | * listed below. 4 | * 5 | * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets, 6 | * or vendor/assets/stylesheets of plugins, if any, can be referenced here using a relative path. 7 | * 8 | * You're free to add application-wide styles to this file and they'll appear at the top of the 9 | * compiled file, but it's generally better to create a new file per style scope. 10 | * 11 | *= require_self 12 | *= require_tree . 13 | */ 14 | -------------------------------------------------------------------------------- /spec/dummy/app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | # Prevent CSRF attacks by raising an exception. 3 | # For APIs, you may want to use :null_session instead. 4 | protect_from_forgery with: :exception 5 | end 6 | -------------------------------------------------------------------------------- /spec/dummy/app/controllers/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sailias/cryptocoin_payable/0c505b79a28bf825626ff0cbfbed6b6d1575a816/spec/dummy/app/controllers/concerns/.keep -------------------------------------------------------------------------------- /spec/dummy/app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | module ApplicationHelper 2 | end 3 | -------------------------------------------------------------------------------- /spec/dummy/app/mailers/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sailias/cryptocoin_payable/0c505b79a28bf825626ff0cbfbed6b6d1575a816/spec/dummy/app/mailers/.keep -------------------------------------------------------------------------------- /spec/dummy/app/models/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sailias/cryptocoin_payable/0c505b79a28bf825626ff0cbfbed6b6d1575a816/spec/dummy/app/models/.keep -------------------------------------------------------------------------------- /spec/dummy/app/models/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sailias/cryptocoin_payable/0c505b79a28bf825626ff0cbfbed6b6d1575a816/spec/dummy/app/models/concerns/.keep -------------------------------------------------------------------------------- /spec/dummy/app/models/widget.rb: -------------------------------------------------------------------------------- 1 | class Widget < ActiveRecord::Base 2 | has_coin_payments 3 | end 4 | -------------------------------------------------------------------------------- /spec/dummy/app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Dummy 5 | <%= stylesheet_link_tag "application", media: "all", "data-turbolinks-track" => true %> 6 | <%= javascript_include_tag "application", "data-turbolinks-track" => true %> 7 | <%= csrf_meta_tags %> 8 | 9 | 10 | 11 | <%= yield %> 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /spec/dummy/bin/bundle: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) 3 | load Gem.bin_path('bundler', 'bundle') 4 | -------------------------------------------------------------------------------- /spec/dummy/bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | APP_PATH = File.expand_path('../config/application', __dir__) 3 | require_relative '../config/boot' 4 | require 'rails/commands' 5 | -------------------------------------------------------------------------------- /spec/dummy/bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require_relative '../config/boot' 3 | require 'rake' 4 | Rake.application.run 5 | -------------------------------------------------------------------------------- /spec/dummy/config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require File.expand_path('config/environment', __dir__) 4 | run Rails.application 5 | -------------------------------------------------------------------------------- /spec/dummy/config/application.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('boot', __dir__) 2 | 3 | require 'rails/all' 4 | 5 | Bundler.require(*Rails.groups) 6 | 7 | require 'cryptocoin_payable' 8 | 9 | module Dummy 10 | class Application < Rails::Application 11 | # Settings in config/environments/* take precedence over those specified here. 12 | # Application configuration should go into files in config/initializers 13 | # -- all .rb files in that directory are automatically loaded. 14 | 15 | # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. 16 | # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC. 17 | # config.time_zone = 'Central Time (US & Canada)' 18 | 19 | # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. 20 | # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] 21 | # config.i18n.default_locale = :de 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /spec/dummy/config/boot.rb: -------------------------------------------------------------------------------- 1 | # Set up gems listed in the Gemfile. 2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../../Gemfile', __dir__) 3 | 4 | require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE']) 5 | $LOAD_PATH.unshift File.expand_path('../../../lib', __dir__) 6 | -------------------------------------------------------------------------------- /spec/dummy/config/database.yml: -------------------------------------------------------------------------------- 1 | test: 2 | adapter: postgresql 3 | encoding: unicode 4 | pool: 5 5 | database: cryptocoin_payable_test 6 | -------------------------------------------------------------------------------- /spec/dummy/config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the Rails application. 2 | require File.expand_path('application', __dir__) 3 | 4 | # Initialize the Rails application. 5 | Dummy::Application.initialize! 6 | -------------------------------------------------------------------------------- /spec/dummy/config/environments/development.rb: -------------------------------------------------------------------------------- 1 | Dummy::Application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # In the development environment your application's code is reloaded on 5 | # every request. This slows down response time but is perfect for development 6 | # since you don't have to restart the web server when you make code changes. 7 | config.cache_classes = false 8 | 9 | # Do not eager load code on boot. 10 | config.eager_load = false 11 | 12 | # Show full error reports and disable caching. 13 | config.consider_all_requests_local = true 14 | config.action_controller.perform_caching = false 15 | 16 | # Don't care if the mailer can't send. 17 | config.action_mailer.raise_delivery_errors = false 18 | 19 | # Print deprecation notices to the Rails logger. 20 | config.active_support.deprecation = :log 21 | 22 | # Raise an error on page load if there are pending migrations 23 | config.active_record.migration_error = :page_load 24 | 25 | # Debug mode disables concatenation and preprocessing of assets. 26 | # This option may cause significant delays in view rendering with a large 27 | # number of complex assets. 28 | config.assets.debug = true 29 | end 30 | -------------------------------------------------------------------------------- /spec/dummy/config/environments/production.rb: -------------------------------------------------------------------------------- 1 | Dummy::Application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # Code is not reloaded between requests. 5 | config.cache_classes = true 6 | 7 | # Eager load code on boot. This eager loads most of Rails and 8 | # your application in memory, allowing both thread web servers 9 | # and those relying on copy on write to perform better. 10 | # Rake tasks automatically ignore this option for performance. 11 | config.eager_load = true 12 | 13 | # Full error reports are disabled and caching is turned on. 14 | config.consider_all_requests_local = false 15 | config.action_controller.perform_caching = true 16 | 17 | # Enable Rack::Cache to put a simple HTTP cache in front of your application 18 | # Add `rack-cache` to your Gemfile before enabling this. 19 | # For large-scale production use, consider using a caching reverse proxy like nginx, varnish or squid. 20 | # config.action_dispatch.rack_cache = true 21 | 22 | # Disable Rails's static asset server (Apache or nginx will already do this). 23 | config.serve_static_assets = false 24 | 25 | # Compress JavaScripts and CSS. 26 | config.assets.js_compressor = :uglifier 27 | # config.assets.css_compressor = :sass 28 | 29 | # Do not fallback to assets pipeline if a precompiled asset is missed. 30 | config.assets.compile = false 31 | 32 | # Generate digests for assets URLs. 33 | config.assets.digest = true 34 | 35 | # Version of your assets, change this if you want to expire all your assets. 36 | config.assets.version = '1.0' 37 | 38 | # Specifies the header that your server uses for sending files. 39 | # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for apache 40 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for nginx 41 | 42 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. 43 | # config.force_ssl = true 44 | 45 | # Set to :debug to see everything in the log. 46 | config.log_level = :info 47 | 48 | # Prepend all log lines with the following tags. 49 | # config.log_tags = [ :subdomain, :uuid ] 50 | 51 | # Use a different logger for distributed setups. 52 | # config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new) 53 | 54 | # Use a different cache store in production. 55 | # config.cache_store = :mem_cache_store 56 | 57 | # Enable serving of images, stylesheets, and JavaScripts from an asset server. 58 | # config.action_controller.asset_host = "http://assets.example.com" 59 | 60 | # Precompile additional assets. 61 | # application.js, application.css, and all non-JS/CSS in app/assets folder are already added. 62 | # config.assets.precompile += %w( search.js ) 63 | 64 | # Ignore bad email addresses and do not raise email delivery errors. 65 | # Set this to true and configure the email server for immediate delivery to raise delivery errors. 66 | # config.action_mailer.raise_delivery_errors = false 67 | 68 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 69 | # the I18n.default_locale when a translation can not be found). 70 | config.i18n.fallbacks = true 71 | 72 | # Send deprecation notices to registered listeners. 73 | config.active_support.deprecation = :notify 74 | 75 | # Disable automatic flushing of the log to improve performance. 76 | # config.autoflush_log = false 77 | 78 | # Use default logging formatter so that PID and timestamp are not suppressed. 79 | config.log_formatter = Logger::Formatter.new 80 | end 81 | -------------------------------------------------------------------------------- /spec/dummy/config/environments/test.rb: -------------------------------------------------------------------------------- 1 | Dummy::Application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # The test environment is used exclusively to run your application's 5 | # test suite. You never need to work with it otherwise. Remember that 6 | # your test database is "scratch space" for the test suite and is wiped 7 | # and recreated between test runs. Don't rely on the data there! 8 | config.cache_classes = true 9 | 10 | # Do not eager load code on boot. This avoids loading your whole application 11 | # just for the purpose of running a single test. If you are using a tool that 12 | # preloads Rails for running tests, you may have to set it to true. 13 | config.eager_load = false 14 | 15 | # Configure static asset server for tests with Cache-Control for performance. 16 | config.serve_static_assets = true 17 | config.static_cache_control = 'public, max-age=3600' 18 | 19 | # Show full error reports and disable caching. 20 | config.consider_all_requests_local = true 21 | config.action_controller.perform_caching = false 22 | 23 | # Raise exceptions instead of rendering exception templates. 24 | config.action_dispatch.show_exceptions = false 25 | 26 | # Disable request forgery protection in test environment. 27 | config.action_controller.allow_forgery_protection = false 28 | 29 | # Tell Action Mailer not to deliver emails to the real world. 30 | # The :test delivery method accumulates sent emails in the 31 | # ActionMailer::Base.deliveries array. 32 | config.action_mailer.delivery_method = :test 33 | 34 | # Print deprecation notices to the stderr. 35 | config.active_support.deprecation = :stderr 36 | end 37 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/backtrace_silencers.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. 4 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } 5 | 6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. 7 | # Rails.backtrace_cleaner.remove_silencers! 8 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/cryptocoin_payable.rb: -------------------------------------------------------------------------------- 1 | CryptocoinPayable.configure do |config| 2 | config.currency = :usd 3 | config.testnet = true 4 | config.expire_payments_after = 15.minutes 5 | config.request_delay = 0.5 6 | 7 | config.configure_btc do |btc_config| 8 | btc_config.node_path = 'm/0/' 9 | # rubocop:disable Metrics/LineLength 10 | btc_config.master_public_key = 'tpubD6NzVbkrYhZ4X3cxCktWVsVvMDd35JbNdhzZxb1aeDCG7LfN6KbcDQsqiyJHMEQGJURRgdxGbFBBF32Brwb2LsfpE2jQfCZKwzNBBMosjfm' 11 | # rubocop:enable Metrics/LineLength 12 | end 13 | 14 | config.configure_eth do |eth_config| 15 | # Will default to 4 if `config.testnet` is true, otherwise 1 but can be 16 | # overriden. 17 | # 18 | # 1: Frontier, Homestead, Metropolis, the Ethereum public main network 19 | # 4: Rinkeby, the public Geth Ethereum testnet 20 | # See https://ethereum.stackexchange.com/a/17101/26695 21 | # eth_config.chain_id = 1 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/filter_parameter_logging.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Configure sensitive parameters which will be filtered from the log file. 4 | Rails.application.config.filter_parameters += [:password] 5 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new inflection rules using the following format. Inflections 4 | # are locale specific, and you may define rules for as many different 5 | # locales as you wish. All of these examples are active by default: 6 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 7 | # inflect.plural /^(ox)$/i, '\1en' 8 | # inflect.singular /^(ox)en/i, '\1' 9 | # inflect.irregular 'person', 'people' 10 | # inflect.uncountable %w( fish sheep ) 11 | # end 12 | 13 | # These inflection rules are supported but not enabled by default: 14 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 15 | # inflect.acronym 'RESTful' 16 | # end 17 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/mime_types.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new mime types for use in respond_to blocks: 4 | # Mime::Type.register "text/richtext", :rtf 5 | # Mime::Type.register_alias "text/html", :iphone 6 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/secret_token.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Your secret key is used for verifying the integrity of signed cookies. 4 | # If you change this key, all old signed cookies will become invalid! 5 | 6 | # Make sure the secret is at least 30 characters and all random, 7 | # no regular words or you'll be exposed to dictionary attacks. 8 | # You can use `rake secret` to generate a secure secret key. 9 | 10 | # Make sure your secret_key_base is kept private 11 | # if you're sharing your code publicly. 12 | # rubocop:disable Metrics/LineLength 13 | Dummy::Application.config.secret_key_base = '138c9dde72de8eb2544bc525694ea35b9cf95cb3703796f4c1863593c46838206cf9a0fb1872729d14c1fcf59cf22690c3777e9f66e15a30603a75f78da01498' 14 | # rubocop:enable Metrics/LineLength 15 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/session_store.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | Dummy::Application.config.session_store :cookie_store, key: '_dummy_session' 4 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/wrap_parameters.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # This file contains settings for ActionController::ParamsWrapper which 4 | # is enabled by default. 5 | 6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. 7 | ActiveSupport.on_load(:action_controller) do 8 | wrap_parameters format: [:json] if respond_to?(:wrap_parameters) 9 | end 10 | 11 | # To enable root element in JSON for ActiveRecord objects. 12 | # ActiveSupport.on_load(:active_record) do 13 | # self.include_root_in_json = true 14 | # end 15 | -------------------------------------------------------------------------------- /spec/dummy/config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # Files in the config/locales directory are used for internationalization 2 | # and are automatically loaded by Rails. If you want to use locales other 3 | # than English, add the necessary files in this directory. 4 | # 5 | # To use the locales, use `I18n.t`: 6 | # 7 | # I18n.t 'hello' 8 | # 9 | # In views, this is aliased to just `t`: 10 | # 11 | # <%= t('hello') %> 12 | # 13 | # To use a different locale, set it with `I18n.locale`: 14 | # 15 | # I18n.locale = :es 16 | # 17 | # This would use the information in config/locales/es.yml. 18 | # 19 | # To learn more, please read the Rails Internationalization guide 20 | # available at http://guides.rubyonrails.org/i18n.html. 21 | 22 | en: 23 | hello: "Hello world" 24 | -------------------------------------------------------------------------------- /spec/dummy/config/routes.rb: -------------------------------------------------------------------------------- 1 | Dummy::Application.routes.draw do 2 | # The priority is based upon order of creation: first created -> highest priority. 3 | # See how all your routes lay out with "rake routes". 4 | 5 | # You can have the root of your site routed with "root" 6 | # root 'welcome#index' 7 | 8 | # Example of regular route: 9 | # get 'products/:id' => 'catalog#view' 10 | 11 | # Example of named route that can be invoked with purchase_url(id: product.id) 12 | # get 'products/:id/purchase' => 'catalog#purchase', as: :purchase 13 | 14 | # Example resource route (maps HTTP verbs to controller actions automatically): 15 | # resources :products 16 | 17 | # Example resource route with options: 18 | # resources :products do 19 | # member do 20 | # get 'short' 21 | # post 'toggle' 22 | # end 23 | # 24 | # collection do 25 | # get 'sold' 26 | # end 27 | # end 28 | 29 | # Example resource route with sub-resources: 30 | # resources :products do 31 | # resources :comments, :sales 32 | # resource :seller 33 | # end 34 | 35 | # Example resource route with more complex sub-resources: 36 | # resources :products do 37 | # resources :comments 38 | # resources :sales do 39 | # get 'recent', on: :collection 40 | # end 41 | # end 42 | 43 | # Example resource route with concerns: 44 | # concern :toggleable do 45 | # post 'toggle' 46 | # end 47 | # resources :posts, concerns: :toggleable 48 | # resources :photos, concerns: :toggleable 49 | 50 | # Example resource route within a namespace: 51 | # namespace :admin do 52 | # # Directs /admin/products/* to Admin::ProductsController 53 | # # (app/controllers/admin/products_controller.rb) 54 | # resources :products 55 | # end 56 | end 57 | -------------------------------------------------------------------------------- /spec/dummy/db/migrate/20140510023211_create_widgets.rb: -------------------------------------------------------------------------------- 1 | class CreateWidgets < ActiveRecord::Migration[5.1] 2 | def change 3 | create_table :widgets, &:timestamps 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /spec/dummy/db/migrate/20171227225132_create_coin_payments.rb: -------------------------------------------------------------------------------- 1 | class CreateCoinPayments < ActiveRecord::Migration[5.1] 2 | def change 3 | create_table :coin_payments do |t| 4 | t.string :payable_type 5 | t.integer :coin_type 6 | t.integer :payable_id 7 | t.string :currency 8 | t.string :reason 9 | t.integer :price, limit: 8 10 | t.decimal :coin_amount_due, default: 0, precision: 24, scale: 0 11 | t.string :address 12 | t.string :state, default: 'pending' 13 | t.datetime :created_at 14 | t.datetime :updated_at 15 | t.decimal :coin_conversion, precision: 24, scale: 0 16 | end 17 | add_index :coin_payments, %i[payable_type payable_id] 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/dummy/db/migrate/20171227225134_create_currency_conversions.rb: -------------------------------------------------------------------------------- 1 | class CreateCurrencyConversions < ActiveRecord::Migration[5.1] 2 | def change 3 | create_table :currency_conversions do |t| 4 | t.integer :currency 5 | t.decimal :price, precision: 24, scale: 0 6 | t.integer :coin_type 7 | t.datetime :created_at 8 | t.datetime :updated_at 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/dummy/db/migrate/20181015141952_create_coin_payment_transactions.rb: -------------------------------------------------------------------------------- 1 | class CreateCoinPaymentTransactions < ActiveRecord::Migration[5.1] 2 | def change 3 | create_table :coin_payment_transactions do |t| 4 | t.decimal :estimated_value, precision: 24, scale: 0 5 | t.string :transaction_hash, index: { unique: true } 6 | t.string :block_hash 7 | t.datetime :block_time 8 | t.datetime :estimated_time 9 | t.integer :coin_payment_id 10 | t.decimal :coin_conversion, precision: 24, scale: 0 11 | t.integer :confirmations, default: 0 12 | end 13 | 14 | add_index :coin_payment_transactions, :coin_payment_id 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /spec/dummy/db/schema.rb: -------------------------------------------------------------------------------- 1 | # This file is auto-generated from the current state of the database. Instead 2 | # of editing this file, please use the migrations feature of Active Record to 3 | # incrementally modify your database, and then regenerate this schema definition. 4 | # 5 | # Note that this schema.rb definition is the authoritative source for your 6 | # database schema. If you need to create the application database on another 7 | # system, you should be using db:schema:load, not running all the migrations 8 | # from scratch. The latter is a flawed and unsustainable approach (the more migrations 9 | # you'll amass, the slower it'll run and the greater likelihood for issues). 10 | # 11 | # It's strongly recommended that you check this file into your version control system. 12 | 13 | ActiveRecord::Schema.define(version: 20_181_015_141_952) do 14 | create_table 'coin_payment_transactions', force: :cascade do |t| 15 | t.decimal 'estimated_value', precision: 24 16 | t.string 'transaction_hash' 17 | t.string 'block_hash' 18 | t.datetime 'block_time' 19 | t.datetime 'estimated_time' 20 | t.integer 'coin_payment_id' 21 | t.decimal 'coin_conversion', precision: 24 22 | t.integer 'confirmations', default: 0 23 | t.index ['coin_payment_id'], name: 'index_coin_payment_transactions_on_coin_payment_id' 24 | t.index ['transaction_hash'], name: 'index_coin_payment_transactions_on_transaction_hash', unique: true 25 | end 26 | 27 | create_table 'coin_payments', force: :cascade do |t| 28 | t.string 'payable_type' 29 | t.integer 'coin_type' 30 | t.integer 'payable_id' 31 | t.string 'currency' 32 | t.string 'reason' 33 | t.integer 'price', limit: 8 34 | t.decimal 'coin_amount_due', precision: 24, default: '0' 35 | t.string 'address' 36 | t.string 'state', default: 'pending' 37 | t.datetime 'created_at' 38 | t.datetime 'updated_at' 39 | t.decimal 'coin_conversion', precision: 24 40 | t.index %w[payable_type payable_id], name: 'index_coin_payments_on_payable_type_and_payable_id' 41 | end 42 | 43 | create_table 'currency_conversions', force: :cascade do |t| 44 | t.integer 'currency' 45 | t.decimal 'price', precision: 24 46 | t.integer 'coin_type' 47 | t.datetime 'created_at' 48 | t.datetime 'updated_at' 49 | end 50 | 51 | create_table 'widgets', force: :cascade do |t| 52 | t.datetime 'created_at', null: false 53 | t.datetime 'updated_at', null: false 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /spec/dummy/lib/assets/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sailias/cryptocoin_payable/0c505b79a28bf825626ff0cbfbed6b6d1575a816/spec/dummy/lib/assets/.keep -------------------------------------------------------------------------------- /spec/dummy/log/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sailias/cryptocoin_payable/0c505b79a28bf825626ff0cbfbed6b6d1575a816/spec/dummy/log/.keep -------------------------------------------------------------------------------- /spec/dummy/public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The page you were looking for doesn't exist (404) 5 | 48 | 49 | 50 | 51 | 52 |
53 |

The page you were looking for doesn't exist.

54 |

You may have mistyped the address or the page may have moved.

55 |
56 |

If you are the application owner check the logs for more information.

57 | 58 | 59 | -------------------------------------------------------------------------------- /spec/dummy/public/422.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The change you wanted was rejected (422) 5 | 48 | 49 | 50 | 51 | 52 |
53 |

The change you wanted was rejected.

54 |

Maybe you tried to change something you didn't have access to.

55 |
56 |

If you are the application owner check the logs for more information.

57 | 58 | 59 | -------------------------------------------------------------------------------- /spec/dummy/public/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | We're sorry, but something went wrong (500) 5 | 48 | 49 | 50 | 51 | 52 |
53 |

We're sorry, but something went wrong.

54 |
55 |

If you are the application owner check the logs for more information.

56 | 57 | 58 | -------------------------------------------------------------------------------- /spec/dummy/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sailias/cryptocoin_payable/0c505b79a28bf825626ff0cbfbed6b6d1575a816/spec/dummy/public/favicon.ico -------------------------------------------------------------------------------- /spec/dummy/test/fixtures/widgets.yml: -------------------------------------------------------------------------------- 1 | # Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html 2 | 3 | # This model initially had no columns defined. If you add columns to the 4 | # model remove the '{}' from the fixture names and add the columns immediately 5 | # below each fixture, per the syntax in the comments below 6 | # 7 | one: {} 8 | # column: value 9 | # 10 | two: {} 11 | # column: value 12 | -------------------------------------------------------------------------------- /spec/dummy/test/models/widget_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class WidgetTest < ActiveSupport::TestCase 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /spec/fixtures/vcr_cassettes/CryptocoinPayable_Adapters_Bitcoin/gets_an_empty_result_when_no_transactions_found.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: get 5 | uri: https://blockexplorer.com/api/txs/?address=1twtr17A65VAPhJDJRxhoMSpLBTR5Xy44 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 | Date: 22 | - Sun, 14 Oct 2018 10:35:50 GMT 23 | Content-Type: 24 | - application/json; charset=utf-8 25 | Content-Length: 26 | - '25' 27 | Connection: 28 | - keep-alive 29 | Set-Cookie: 30 | - __cfduid=d47ff8371481605e635f17b28a5748d871539513349; expires=Mon, 14-Oct-19 31 | 10:35:49 GMT; path=/; domain=.blockexplorer.com; HttpOnly; Secure 32 | X-Powered-By: 33 | - Express 34 | X-Ratelimit-Limit: 35 | - '10800' 36 | X-Ratelimit-Remaining: 37 | - '10788' 38 | Access-Control-Allow-Origin: 39 | - "*" 40 | Access-Control-Allow-Methods: 41 | - GET, HEAD, PUT, POST, OPTIONS 42 | Access-Control-Allow-Headers: 43 | - Origin, X-Requested-With, Content-Type, Accept, Content-Length, Cache-Control, 44 | cf-connecting-ip 45 | Cache-Control: 46 | - public, max-age=30 47 | X-Content-Type-Options: 48 | - nosniff 49 | - nosniff 50 | Etag: 51 | - W/"19-ateSR7OFd51Y/7Qm9D9BXg" 52 | Vary: 53 | - Accept-Encoding 54 | Cf-Cache-Status: 55 | - MISS 56 | Expect-Ct: 57 | - max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct" 58 | Server: 59 | - cloudflare 60 | Cf-Ray: 61 | - 469966c21862c338-SIN 62 | body: 63 | encoding: UTF-8 64 | string: '{"pagesTotal":0,"txs":[]}' 65 | http_version: 66 | recorded_at: Sun, 14 Oct 2018 10:35:52 GMT 67 | recorded_with: VCR 4.0.0 68 | -------------------------------------------------------------------------------- /spec/fixtures/vcr_cassettes/CryptocoinPayable_Adapters_Bitcoin/gets_transactions_for_a_given_address.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: get 5 | uri: https://blockexplorer.com/api/txs/?address=3HR9xYD7MybbE7JLVTjwijYse48BtfEKni 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 | Date: 22 | - Sun, 14 Oct 2018 10:35:49 GMT 23 | Content-Type: 24 | - application/json; charset=utf-8 25 | Transfer-Encoding: 26 | - chunked 27 | Connection: 28 | - keep-alive 29 | Set-Cookie: 30 | - __cfduid=dfbf39137c18550c63fcc8c85bee8b7e41539513348; expires=Mon, 14-Oct-19 31 | 10:35:48 GMT; path=/; domain=.blockexplorer.com; HttpOnly; Secure 32 | X-Powered-By: 33 | - Express 34 | X-Ratelimit-Limit: 35 | - '10800' 36 | X-Ratelimit-Remaining: 37 | - '10789' 38 | Access-Control-Allow-Origin: 39 | - "*" 40 | Access-Control-Allow-Methods: 41 | - GET, HEAD, PUT, POST, OPTIONS 42 | Access-Control-Allow-Headers: 43 | - Origin, X-Requested-With, Content-Type, Accept, Content-Length, Cache-Control, 44 | cf-connecting-ip 45 | Cache-Control: 46 | - public, max-age=30 47 | X-Content-Type-Options: 48 | - nosniff 49 | - nosniff 50 | Etag: 51 | - W/"d6f-oVIhbGDsjzojZRPbIeFCTg" 52 | Vary: 53 | - Accept-Encoding 54 | Cf-Cache-Status: 55 | - EXPIRED 56 | Expect-Ct: 57 | - max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct" 58 | Server: 59 | - cloudflare 60 | Cf-Ray: 61 | - 469966babf93c347-SIN 62 | body: 63 | encoding: ASCII-8BIT 64 | string: '{"pagesTotal":1,"txs":[{"txid":"5bdeaf7829148d7e0e1e7b5233512a2c5ae54ef7ccbc8e68b2f85b7e49c917a0","version":1,"locktime":0,"vin":[{"txid":"252dde616fbbd25ebaf37ee8df82c38d2135a87100cc1f0315797b1b42f52f01","vout":0,"scriptSig":{"asm":"304402206b40298326b6c4ab181c4c61b43569490e91773997c0ec53309ab01851098c1002202731263d747aba117b65aaf7d5ee0ad5e88c2e3b4dc3e47d6ecc414e3bbb38e8[ALL] 65 | 02027fcba75d05e735e734d9287c3b22b90465e30ee87ff064303b8175ddeb48fa","hex":"47304402206b40298326b6c4ab181c4c61b43569490e91773997c0ec53309ab01851098c1002202731263d747aba117b65aaf7d5ee0ad5e88c2e3b4dc3e47d6ecc414e3bbb38e8012102027fcba75d05e735e734d9287c3b22b90465e30ee87ff064303b8175ddeb48fa"},"sequence":4294967295,"n":0,"addr":"1CJzCcrY9WarJ5nZCcfbNu2eAnuHqSGqJP","valueSat":826658803,"value":8.26658803,"doubleSpentTxID":null}],"vout":[{"value":"4.99000000","n":0,"scriptPubKey":{"hex":"a914ac82105dbe13eb1b2b606b8fa4e58ced5b317ceb87","asm":"OP_HASH160 66 | ac82105dbe13eb1b2b606b8fa4e58ced5b317ceb OP_EQUAL","addresses":["3HR9xYD7MybbE7JLVTjwijYse48BtfEKni"],"type":"scripthash"},"spentTxId":null,"spentIndex":null,"spentHeight":null},{"value":"3.27645063","n":1,"scriptPubKey":{"hex":"76a9147dfed19339f4a44a5a9888448ebb270656e2919488ac","asm":"OP_DUP 67 | OP_HASH160 7dfed19339f4a44a5a9888448ebb270656e29194 OP_EQUALVERIFY OP_CHECKSIG","addresses":["1CVCj6B2Q4DJ1rFEhaXDdcEcaQQho37yrT"],"type":"pubkeyhash"},"spentTxId":"08aee92c75baa331b66aaf8ad27fdaea80053f3fdb090112ce332983cb63de00","spentIndex":3,"spentHeight":432992}],"blockhash":"0000000000000000048e8ea3fdd2c3a59ddcbcf7575f82cb96ce9fd17da9f2f4","blockheight":429632,"confirmations":116077,"time":1473781260,"blocktime":1473781260,"valueOut":8.26645063,"size":223,"valueIn":8.26658803,"fees":0.0001374},{"txid":"e7bcdb13d9c903973bd8a740054d4c056a559bae67d4e8f6d0a42b4bab552623","version":1,"locktime":0,"vin":[{"txid":"e5c2d3a1ffa653121e38d051274dcbfde19ec53ed13d5e4eba54fdf77e8fbf60","vout":0,"scriptSig":{"asm":"304402201d1d8197bd319e43ca5931023dad0bd4fbcd5d941f7bbf51e5ea6390bdf59c3702202303a697a6f722a90981e3fd1d7f0f27d397de3e1ee601e0f681ec933b809ccd[ALL] 68 | 028ee20d4ea0b72c78caf36b803a79e5a315c61f6d76193130699040cded33ad90","hex":"47304402201d1d8197bd319e43ca5931023dad0bd4fbcd5d941f7bbf51e5ea6390bdf59c3702202303a697a6f722a90981e3fd1d7f0f27d397de3e1ee601e0f681ec933b809ccd0121028ee20d4ea0b72c78caf36b803a79e5a315c61f6d76193130699040cded33ad90"},"sequence":4294967295,"n":0,"addr":"1GWPE3nEHHLY6C5U4c6qj3wxZvx4hiVDxZ","valueSat":465006260,"value":4.6500626,"doubleSpentTxID":null}],"vout":[{"value":"4.63992520","n":0,"scriptPubKey":{"hex":"76a9141a984f5195f48bcd9995ec4bb5ed5a386a10dfdf88ac","asm":"OP_DUP 69 | OP_HASH160 1a984f5195f48bcd9995ec4bb5ed5a386a10dfdf OP_EQUALVERIFY OP_CHECKSIG","addresses":["13Rd2fKcsUQ9eChqMZGprGeo8XAMAsQhMV"],"type":"pubkeyhash"},"spentTxId":"5c9864d21dca5bd4715a5489fad9e51dbdcde1588e03d58ddbbbcabc0de54e32","spentIndex":0,"spentHeight":429716},{"value":"0.01000000","n":1,"scriptPubKey":{"hex":"a914ac82105dbe13eb1b2b606b8fa4e58ced5b317ceb87","asm":"OP_HASH160 70 | ac82105dbe13eb1b2b606b8fa4e58ced5b317ceb OP_EQUAL","addresses":["3HR9xYD7MybbE7JLVTjwijYse48BtfEKni"],"type":"scripthash"},"spentTxId":null,"spentIndex":null,"spentHeight":null}],"blockhash":"000000000000000001af27feb303ad97af81a5882157f166781784c639f8e896","blockheight":429629,"confirmations":116080,"time":1473780162,"blocktime":1473780162,"valueOut":4.6499252,"size":223,"valueIn":4.6500626,"fees":0.0001374}]}' 71 | http_version: 72 | recorded_at: Sun, 14 Oct 2018 10:35:51 GMT 73 | recorded_with: VCR 4.0.0 74 | -------------------------------------------------------------------------------- /spec/fixtures/vcr_cassettes/CryptocoinPayable_Adapters_Bitcoin/gives_zero_instead_of_null_for_zero-value_transactions.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: get 5 | uri: https://blockexplorer.com/api/txs/?address=1PKKkNRPPfPjrPiufHzuLFX2gMAVJbcN8H 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 | Date: 22 | - Thu, 25 Oct 2018 05:50:00 GMT 23 | Content-Type: 24 | - application/json; charset=utf-8 25 | Transfer-Encoding: 26 | - chunked 27 | Connection: 28 | - keep-alive 29 | Set-Cookie: 30 | - __cfduid=db74b4fab7aae788fb96e5f61e12942101540446599; expires=Fri, 25-Oct-19 31 | 05:49:59 GMT; path=/; domain=.blockexplorer.com; HttpOnly; Secure 32 | X-Powered-By: 33 | - Express 34 | X-Ratelimit-Limit: 35 | - '10800' 36 | X-Ratelimit-Remaining: 37 | - '10797' 38 | Access-Control-Allow-Origin: 39 | - "*" 40 | Access-Control-Allow-Methods: 41 | - GET, HEAD, PUT, POST, OPTIONS 42 | Access-Control-Allow-Headers: 43 | - Origin, X-Requested-With, Content-Type, Accept, Content-Length, Cache-Control, 44 | cf-connecting-ip 45 | Cache-Control: 46 | - public, max-age=30 47 | X-Content-Type-Options: 48 | - nosniff 49 | - nosniff 50 | Etag: 51 | - W/"1068-cX1BciCIIz8/pqQ99oJLfQ" 52 | Vary: 53 | - Accept-Encoding 54 | Cf-Cache-Status: 55 | - EXPIRED 56 | Expect-Ct: 57 | - max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct" 58 | Server: 59 | - cloudflare 60 | Cf-Ray: 61 | - 46f2672bf943838f-BKK 62 | body: 63 | encoding: ASCII-8BIT 64 | string: '{"pagesTotal":1,"txs":[{"txid":"1c085fb7180d35736dae4d1526f18c71fb551173c7471a70251bff88d38d559d","version":1,"locktime":0,"vin":[{"txid":"5dd89fb2f9a2adfe6f4918db6a203b7c8c91c25c7cb0cd9bfb8a250e3c25abdd","vout":0,"scriptSig":{"asm":"3045022100b029679930b80ce4c067b9c903571c6a935983d7beecaeb902ad1fffa9dba21502204e542e193b74acf68ab17cb13f821d339244aea9cb339bede4b195b3c3b35c6c[ALL] 65 | 028005d0d0b1c96d549f0eb5e18fb9f7b35ed5a28fb2f3c55842ab9dc9a9218976","hex":"483045022100b029679930b80ce4c067b9c903571c6a935983d7beecaeb902ad1fffa9dba21502204e542e193b74acf68ab17cb13f821d339244aea9cb339bede4b195b3c3b35c6c0121028005d0d0b1c96d549f0eb5e18fb9f7b35ed5a28fb2f3c55842ab9dc9a9218976"},"sequence":4294967295,"n":0,"addr":"1PKKkNRPPfPjrPiufHzuLFX2gMAVJbcN8H","valueSat":55965,"value":0.00055965,"doubleSpentTxID":null},{"txid":"5dd89fb2f9a2adfe6f4918db6a203b7c8c91c25c7cb0cd9bfb8a250e3c25abdd","vout":1,"scriptSig":{"asm":"3044022048fad34851bc49c267386186df590ac12390172669f5612b2c28dc09d517223c02203fe2ecf934c10cfaa3d27d1e739ec3413fd16612ba16eaa15ba7a2cfbf0b3a79[ALL] 66 | 034994bba249dc54ed3651595dab6c5c6e3c177032495e00a3fe53874adf565a34","hex":"473044022048fad34851bc49c267386186df590ac12390172669f5612b2c28dc09d517223c02203fe2ecf934c10cfaa3d27d1e739ec3413fd16612ba16eaa15ba7a2cfbf0b3a790121034994bba249dc54ed3651595dab6c5c6e3c177032495e00a3fe53874adf565a34"},"sequence":4294967295,"n":1,"addr":"15Gmaq5M8cMD4mAy1Gh6dVtnoDX3XAazwf","valueSat":503191,"value":0.00503191,"doubleSpentTxID":null}],"vout":[{"value":"0.00062184","n":0,"scriptPubKey":{"hex":"a91484fd6a85eb6cb335e0b8641717f3cf7f392f897587","asm":"OP_HASH160 67 | 84fd6a85eb6cb335e0b8641717f3cf7f392f8975 OP_EQUAL","addresses":["3DpChXQfjya6zQdUcgpA5tQFfqv9KgRXMP"],"type":"scripthash"},"spentTxId":"0a4c5507fe17f62d698f4ad30843909320770fd5b7159a925a0df289ed77c38e","spentIndex":1,"spentHeight":547179},{"value":"0.00495855","n":1,"scriptPubKey":{"hex":"76a9142edbf57347f7f56a2975c4c5c147680a781e32a988ac","asm":"OP_DUP 68 | OP_HASH160 2edbf57347f7f56a2975c4c5c147680a781e32a9 OP_EQUALVERIFY OP_CHECKSIG","addresses":["15Gmaq5M8cMD4mAy1Gh6dVtnoDX3XAazwf"],"type":"pubkeyhash"},"spentTxId":null,"spentIndex":null,"spentHeight":null}],"blockhash":"00000000000000000008d05a3b4530691dcfe048fa046bc1c95cbf7ae6c60ff0","blockheight":547177,"confirmations":46,"time":1540413667,"blocktime":1540413667,"valueOut":0.00558039,"size":371,"valueIn":0.00559156,"fees":0.00001117},{"txid":"5dd89fb2f9a2adfe6f4918db6a203b7c8c91c25c7cb0cd9bfb8a250e3c25abdd","version":1,"locktime":0,"vin":[{"txid":"040d0d19654c2fa3e5dbea6654849339261c0bc5f4540173bf87ad0c9d1b2726","vout":1,"scriptSig":{"asm":"3045022100d74af226278b3eaa850f12ccbbdc005796da6172b4b4664d1769586a361982a60220227b54175f2989cbceb4795997381c4b7a3f99d579431bf74294fcde8317ac8c[ALL] 69 | 034994bba249dc54ed3651595dab6c5c6e3c177032495e00a3fe53874adf565a34","hex":"483045022100d74af226278b3eaa850f12ccbbdc005796da6172b4b4664d1769586a361982a60220227b54175f2989cbceb4795997381c4b7a3f99d579431bf74294fcde8317ac8c0121034994bba249dc54ed3651595dab6c5c6e3c177032495e00a3fe53874adf565a34"},"sequence":4294967295,"n":0,"addr":"15Gmaq5M8cMD4mAy1Gh6dVtnoDX3XAazwf","valueSat":559835,"value":0.00559835,"doubleSpentTxID":null}],"vout":[{"value":"0.00055965","n":0,"scriptPubKey":{"hex":"76a914f4c9e785dbe753f59b98c082d793b2e1312c558788ac","asm":"OP_DUP 70 | OP_HASH160 f4c9e785dbe753f59b98c082d793b2e1312c5587 OP_EQUALVERIFY OP_CHECKSIG","addresses":["1PKKkNRPPfPjrPiufHzuLFX2gMAVJbcN8H"],"type":"pubkeyhash"},"spentTxId":"1c085fb7180d35736dae4d1526f18c71fb551173c7471a70251bff88d38d559d","spentIndex":0,"spentHeight":547177},{"value":"0.00503191","n":1,"scriptPubKey":{"hex":"76a9142edbf57347f7f56a2975c4c5c147680a781e32a988ac","asm":"OP_DUP 71 | OP_HASH160 2edbf57347f7f56a2975c4c5c147680a781e32a9 OP_EQUALVERIFY OP_CHECKSIG","addresses":["15Gmaq5M8cMD4mAy1Gh6dVtnoDX3XAazwf"],"type":"pubkeyhash"},"spentTxId":"1c085fb7180d35736dae4d1526f18c71fb551173c7471a70251bff88d38d559d","spentIndex":1,"spentHeight":547177}],"blockhash":"0000000000000000001e7b7b2bf28d3e3813e0a37354f95288fe05fa18a0bc8a","blockheight":547126,"confirmations":97,"time":1540380059,"blocktime":1540380059,"valueOut":0.00559156,"size":226,"valueIn":0.00559835,"fees":0.00000679}]}' 72 | http_version: 73 | recorded_at: Thu, 25 Oct 2018 05:50:23 GMT 74 | recorded_with: VCR 4.0.0 75 | -------------------------------------------------------------------------------- /spec/fixtures/vcr_cassettes/CryptocoinPayable_Adapters_Bitcoin/handles_nil.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: get 5 | uri: https://blockexplorer.com/api/txs/?address=1PKKkNRPPfPjrPiufHzuLFX2gMAVJbcN8H 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 | Date: 22 | - Thu, 25 Oct 2018 05:42:00 GMT 23 | Content-Type: 24 | - application/json; charset=utf-8 25 | Transfer-Encoding: 26 | - chunked 27 | Connection: 28 | - keep-alive 29 | Set-Cookie: 30 | - __cfduid=d723480bb1497f5e9445df42588704d991540446119; expires=Fri, 25-Oct-19 31 | 05:41:59 GMT; path=/; domain=.blockexplorer.com; HttpOnly; Secure 32 | X-Powered-By: 33 | - Express 34 | X-Ratelimit-Limit: 35 | - '10800' 36 | X-Ratelimit-Remaining: 37 | - '10799' 38 | Access-Control-Allow-Origin: 39 | - "*" 40 | Access-Control-Allow-Methods: 41 | - GET, HEAD, PUT, POST, OPTIONS 42 | Access-Control-Allow-Headers: 43 | - Origin, X-Requested-With, Content-Type, Accept, Content-Length, Cache-Control, 44 | cf-connecting-ip 45 | Cache-Control: 46 | - public, max-age=30 47 | X-Content-Type-Options: 48 | - nosniff 49 | - nosniff 50 | Etag: 51 | - W/"1068-qj/jkrI8Gp8RPKM+4Iwz6Q" 52 | Vary: 53 | - Accept-Encoding 54 | Cf-Cache-Status: 55 | - REVALIDATED 56 | Expect-Ct: 57 | - max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct" 58 | Server: 59 | - cloudflare 60 | Cf-Ray: 61 | - 46f25b76ad0d83ad-BKK 62 | body: 63 | encoding: ASCII-8BIT 64 | string: '{"pagesTotal":1,"txs":[{"txid":"1c085fb7180d35736dae4d1526f18c71fb551173c7471a70251bff88d38d559d","version":1,"locktime":0,"vin":[{"txid":"5dd89fb2f9a2adfe6f4918db6a203b7c8c91c25c7cb0cd9bfb8a250e3c25abdd","vout":0,"scriptSig":{"asm":"3045022100b029679930b80ce4c067b9c903571c6a935983d7beecaeb902ad1fffa9dba21502204e542e193b74acf68ab17cb13f821d339244aea9cb339bede4b195b3c3b35c6c[ALL] 65 | 028005d0d0b1c96d549f0eb5e18fb9f7b35ed5a28fb2f3c55842ab9dc9a9218976","hex":"483045022100b029679930b80ce4c067b9c903571c6a935983d7beecaeb902ad1fffa9dba21502204e542e193b74acf68ab17cb13f821d339244aea9cb339bede4b195b3c3b35c6c0121028005d0d0b1c96d549f0eb5e18fb9f7b35ed5a28fb2f3c55842ab9dc9a9218976"},"sequence":4294967295,"n":0,"addr":"1PKKkNRPPfPjrPiufHzuLFX2gMAVJbcN8H","valueSat":55965,"value":0.00055965,"doubleSpentTxID":null},{"txid":"5dd89fb2f9a2adfe6f4918db6a203b7c8c91c25c7cb0cd9bfb8a250e3c25abdd","vout":1,"scriptSig":{"asm":"3044022048fad34851bc49c267386186df590ac12390172669f5612b2c28dc09d517223c02203fe2ecf934c10cfaa3d27d1e739ec3413fd16612ba16eaa15ba7a2cfbf0b3a79[ALL] 66 | 034994bba249dc54ed3651595dab6c5c6e3c177032495e00a3fe53874adf565a34","hex":"473044022048fad34851bc49c267386186df590ac12390172669f5612b2c28dc09d517223c02203fe2ecf934c10cfaa3d27d1e739ec3413fd16612ba16eaa15ba7a2cfbf0b3a790121034994bba249dc54ed3651595dab6c5c6e3c177032495e00a3fe53874adf565a34"},"sequence":4294967295,"n":1,"addr":"15Gmaq5M8cMD4mAy1Gh6dVtnoDX3XAazwf","valueSat":503191,"value":0.00503191,"doubleSpentTxID":null}],"vout":[{"value":"0.00062184","n":0,"scriptPubKey":{"hex":"a91484fd6a85eb6cb335e0b8641717f3cf7f392f897587","asm":"OP_HASH160 67 | 84fd6a85eb6cb335e0b8641717f3cf7f392f8975 OP_EQUAL","addresses":["3DpChXQfjya6zQdUcgpA5tQFfqv9KgRXMP"],"type":"scripthash"},"spentTxId":"0a4c5507fe17f62d698f4ad30843909320770fd5b7159a925a0df289ed77c38e","spentIndex":1,"spentHeight":547179},{"value":"0.00495855","n":1,"scriptPubKey":{"hex":"76a9142edbf57347f7f56a2975c4c5c147680a781e32a988ac","asm":"OP_DUP 68 | OP_HASH160 2edbf57347f7f56a2975c4c5c147680a781e32a9 OP_EQUALVERIFY OP_CHECKSIG","addresses":["15Gmaq5M8cMD4mAy1Gh6dVtnoDX3XAazwf"],"type":"pubkeyhash"},"spentTxId":null,"spentIndex":null,"spentHeight":null}],"blockhash":"00000000000000000008d05a3b4530691dcfe048fa046bc1c95cbf7ae6c60ff0","blockheight":547177,"confirmations":45,"time":1540413667,"blocktime":1540413667,"valueOut":0.00558039,"size":371,"valueIn":0.00559156,"fees":0.00001117},{"txid":"5dd89fb2f9a2adfe6f4918db6a203b7c8c91c25c7cb0cd9bfb8a250e3c25abdd","version":1,"locktime":0,"vin":[{"txid":"040d0d19654c2fa3e5dbea6654849339261c0bc5f4540173bf87ad0c9d1b2726","vout":1,"scriptSig":{"asm":"3045022100d74af226278b3eaa850f12ccbbdc005796da6172b4b4664d1769586a361982a60220227b54175f2989cbceb4795997381c4b7a3f99d579431bf74294fcde8317ac8c[ALL] 69 | 034994bba249dc54ed3651595dab6c5c6e3c177032495e00a3fe53874adf565a34","hex":"483045022100d74af226278b3eaa850f12ccbbdc005796da6172b4b4664d1769586a361982a60220227b54175f2989cbceb4795997381c4b7a3f99d579431bf74294fcde8317ac8c0121034994bba249dc54ed3651595dab6c5c6e3c177032495e00a3fe53874adf565a34"},"sequence":4294967295,"n":0,"addr":"15Gmaq5M8cMD4mAy1Gh6dVtnoDX3XAazwf","valueSat":559835,"value":0.00559835,"doubleSpentTxID":null}],"vout":[{"value":"0.00055965","n":0,"scriptPubKey":{"hex":"76a914f4c9e785dbe753f59b98c082d793b2e1312c558788ac","asm":"OP_DUP 70 | OP_HASH160 f4c9e785dbe753f59b98c082d793b2e1312c5587 OP_EQUALVERIFY OP_CHECKSIG","addresses":["1PKKkNRPPfPjrPiufHzuLFX2gMAVJbcN8H"],"type":"pubkeyhash"},"spentTxId":"1c085fb7180d35736dae4d1526f18c71fb551173c7471a70251bff88d38d559d","spentIndex":0,"spentHeight":547177},{"value":"0.00503191","n":1,"scriptPubKey":{"hex":"76a9142edbf57347f7f56a2975c4c5c147680a781e32a988ac","asm":"OP_DUP 71 | OP_HASH160 2edbf57347f7f56a2975c4c5c147680a781e32a9 OP_EQUALVERIFY OP_CHECKSIG","addresses":["15Gmaq5M8cMD4mAy1Gh6dVtnoDX3XAazwf"],"type":"pubkeyhash"},"spentTxId":"1c085fb7180d35736dae4d1526f18c71fb551173c7471a70251bff88d38d559d","spentIndex":1,"spentHeight":547177}],"blockhash":"0000000000000000001e7b7b2bf28d3e3813e0a37354f95288fe05fa18a0bc8a","blockheight":547126,"confirmations":96,"time":1540380059,"blocktime":1540380059,"valueOut":0.00559156,"size":226,"valueIn":0.00559835,"fees":0.00000679}]}' 72 | http_version: 73 | recorded_at: Thu, 25 Oct 2018 05:42:23 GMT 74 | recorded_with: VCR 4.0.0 75 | -------------------------------------------------------------------------------- /spec/fixtures/vcr_cassettes/CryptocoinPayable_Adapters_Bitcoin/raises_an_error_when_an_invalid_address_is_passed.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: get 5 | uri: https://blockexplorer.com/api/txs/?address=foo 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: 400 19 | message: Bad Request 20 | headers: 21 | Date: 22 | - Sun, 14 Oct 2018 10:35:51 GMT 23 | Content-Type: 24 | - text/html; charset=utf-8 25 | Transfer-Encoding: 26 | - chunked 27 | Connection: 28 | - keep-alive 29 | Set-Cookie: 30 | - __cfduid=da94483646f7f50cf4fccfb76ee0e929e1539513350; expires=Mon, 14-Oct-19 31 | 10:35:50 GMT; path=/; domain=.blockexplorer.com; HttpOnly; Secure 32 | X-Powered-By: 33 | - Express 34 | X-Ratelimit-Limit: 35 | - '10800' 36 | X-Ratelimit-Remaining: 37 | - '10787' 38 | Access-Control-Allow-Origin: 39 | - "*" 40 | Access-Control-Allow-Methods: 41 | - GET, HEAD, PUT, POST, OPTIONS 42 | Access-Control-Allow-Headers: 43 | - Origin, X-Requested-With, Content-Type, Accept, Content-Length, Cache-Control, 44 | cf-connecting-ip 45 | Cache-Control: 46 | - public, max-age=30 47 | Vary: 48 | - Accept-Encoding 49 | Cf-Cache-Status: 50 | - EXPIRED 51 | Expect-Ct: 52 | - max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct" 53 | Server: 54 | - cloudflare 55 | Cf-Ray: 56 | - 469966c91983c365-SIN 57 | body: 58 | encoding: UTF-8 59 | string: Invalid address. Code:-5 60 | http_version: 61 | recorded_at: Sun, 14 Oct 2018 10:35:53 GMT 62 | - request: 63 | method: get 64 | uri: https://api.blockcypher.com/v1/btc/main/addrs/foo/full 65 | body: 66 | encoding: US-ASCII 67 | string: '' 68 | headers: 69 | Accept-Encoding: 70 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 71 | Accept: 72 | - "*/*" 73 | User-Agent: 74 | - Ruby 75 | response: 76 | status: 77 | code: 404 78 | message: Not Found 79 | headers: 80 | Server: 81 | - nginx/1.10.3 (Ubuntu) 82 | Date: 83 | - Sun, 14 Oct 2018 10:35:52 GMT 84 | Content-Type: 85 | - application/json 86 | Content-Length: 87 | - '33' 88 | Connection: 89 | - keep-alive 90 | Access-Control-Allow-Headers: 91 | - Origin, X-Requested-With, Content-Type, Accept 92 | Access-Control-Allow-Methods: 93 | - GET, POST, PUT, DELETE 94 | Access-Control-Allow-Origin: 95 | - "*" 96 | X-Ratelimit-Remaining: 97 | - '98' 98 | body: 99 | encoding: UTF-8 100 | string: '{"error": "Wallet foo not found"}' 101 | http_version: 102 | recorded_at: Sun, 14 Oct 2018 10:35:54 GMT 103 | recorded_with: VCR 4.0.0 104 | -------------------------------------------------------------------------------- /spec/fixtures/vcr_cassettes/CryptocoinPayable_Adapters_Bitcoin/returns_zero_estimated_value_for_zero-value_transactions.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: get 5 | uri: https://blockexplorer.com/api/txs/?address=1PKKkNRPPfPjrPiufHzuLFX2gMAVJbcN8H 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 | Date: 22 | - Thu, 08 Nov 2018 17:34:48 GMT 23 | Content-Type: 24 | - application/json; charset=utf-8 25 | Transfer-Encoding: 26 | - chunked 27 | Connection: 28 | - keep-alive 29 | Set-Cookie: 30 | - BE=f6fe53688c017ff7; path=/ 31 | - __cfduid=dd04ba3e33bea3f20633eebd13320dc081541698486; expires=Fri, 08-Nov-19 32 | 17:34:46 GMT; path=/; domain=.blockexplorer.com; HttpOnly; Secure 33 | X-Powered-By: 34 | - Express 35 | Access-Control-Allow-Origin: 36 | - "*" 37 | Access-Control-Allow-Methods: 38 | - GET, HEAD, PUT, POST, OPTIONS 39 | Access-Control-Allow-Headers: 40 | - Origin, X-Requested-With, Content-Type, Accept, Content-Length, Cache-Control, 41 | cf-connecting-ip 42 | Cache-Control: 43 | - public, max-age=30 44 | X-Content-Type-Options: 45 | - nosniff 46 | Etag: 47 | - W/"10a9-4+a9umI+STIlOj3adv7w8ZfOGog" 48 | Vary: 49 | - Accept-Encoding 50 | Cf-Cache-Status: 51 | - BYPASS 52 | Expect-Ct: 53 | - max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct" 54 | Server: 55 | - cloudflare 56 | Cf-Ray: 57 | - 4769cad68b06a49d-PNH 58 | body: 59 | encoding: ASCII-8BIT 60 | string: '{"pagesTotal":1,"txs":[{"txid":"1c085fb7180d35736dae4d1526f18c71fb551173c7471a70251bff88d38d559d","version":1,"locktime":0,"vin":[{"txid":"5dd89fb2f9a2adfe6f4918db6a203b7c8c91c25c7cb0cd9bfb8a250e3c25abdd","vout":0,"sequence":4294967295,"n":0,"scriptSig":{"hex":"483045022100b029679930b80ce4c067b9c903571c6a935983d7beecaeb902ad1fffa9dba21502204e542e193b74acf68ab17cb13f821d339244aea9cb339bede4b195b3c3b35c6c0121028005d0d0b1c96d549f0eb5e18fb9f7b35ed5a28fb2f3c55842ab9dc9a9218976","asm":"3045022100b029679930b80ce4c067b9c903571c6a935983d7beecaeb902ad1fffa9dba21502204e542e193b74acf68ab17cb13f821d339244aea9cb339bede4b195b3c3b35c6c[ALL] 61 | 028005d0d0b1c96d549f0eb5e18fb9f7b35ed5a28fb2f3c55842ab9dc9a9218976"},"addr":"1PKKkNRPPfPjrPiufHzuLFX2gMAVJbcN8H","valueSat":55965,"value":0.00055965,"doubleSpentTxID":null},{"txid":"5dd89fb2f9a2adfe6f4918db6a203b7c8c91c25c7cb0cd9bfb8a250e3c25abdd","vout":1,"sequence":4294967295,"n":1,"scriptSig":{"hex":"473044022048fad34851bc49c267386186df590ac12390172669f5612b2c28dc09d517223c02203fe2ecf934c10cfaa3d27d1e739ec3413fd16612ba16eaa15ba7a2cfbf0b3a790121034994bba249dc54ed3651595dab6c5c6e3c177032495e00a3fe53874adf565a34","asm":"3044022048fad34851bc49c267386186df590ac12390172669f5612b2c28dc09d517223c02203fe2ecf934c10cfaa3d27d1e739ec3413fd16612ba16eaa15ba7a2cfbf0b3a79[ALL] 62 | 034994bba249dc54ed3651595dab6c5c6e3c177032495e00a3fe53874adf565a34"},"addr":"15Gmaq5M8cMD4mAy1Gh6dVtnoDX3XAazwf","valueSat":503191,"value":0.00503191,"doubleSpentTxID":null}],"vout":[{"value":"0.00062184","n":0,"scriptPubKey":{"hex":"a91484fd6a85eb6cb335e0b8641717f3cf7f392f897587","asm":"OP_HASH160 63 | 84fd6a85eb6cb335e0b8641717f3cf7f392f8975 OP_EQUAL","addresses":["3DpChXQfjya6zQdUcgpA5tQFfqv9KgRXMP"],"type":"scripthash"},"spentTxId":"0a4c5507fe17f62d698f4ad30843909320770fd5b7159a925a0df289ed77c38e","spentIndex":1,"spentHeight":547179},{"value":"0.00495855","n":1,"scriptPubKey":{"hex":"76a9142edbf57347f7f56a2975c4c5c147680a781e32a988ac","asm":"OP_DUP 64 | OP_HASH160 2edbf57347f7f56a2975c4c5c147680a781e32a9 OP_EQUALVERIFY OP_CHECKSIG","addresses":["15Gmaq5M8cMD4mAy1Gh6dVtnoDX3XAazwf"],"type":"pubkeyhash"},"spentTxId":"6f4f788586ba84ec1bea20b0063fff671c39f85a246b161188980fd13d8203f3","spentIndex":0,"spentHeight":547444}],"blockhash":"00000000000000000008d05a3b4530691dcfe048fa046bc1c95cbf7ae6c60ff0","blockheight":547177,"confirmations":2057,"time":1540413667,"blocktime":1540413667,"valueOut":0.00558039,"size":371,"valueIn":0.00559156,"fees":0.00001117},{"txid":"5dd89fb2f9a2adfe6f4918db6a203b7c8c91c25c7cb0cd9bfb8a250e3c25abdd","version":1,"locktime":0,"vin":[{"txid":"040d0d19654c2fa3e5dbea6654849339261c0bc5f4540173bf87ad0c9d1b2726","vout":1,"sequence":4294967295,"n":0,"scriptSig":{"hex":"483045022100d74af226278b3eaa850f12ccbbdc005796da6172b4b4664d1769586a361982a60220227b54175f2989cbceb4795997381c4b7a3f99d579431bf74294fcde8317ac8c0121034994bba249dc54ed3651595dab6c5c6e3c177032495e00a3fe53874adf565a34","asm":"3045022100d74af226278b3eaa850f12ccbbdc005796da6172b4b4664d1769586a361982a60220227b54175f2989cbceb4795997381c4b7a3f99d579431bf74294fcde8317ac8c[ALL] 65 | 034994bba249dc54ed3651595dab6c5c6e3c177032495e00a3fe53874adf565a34"},"addr":"15Gmaq5M8cMD4mAy1Gh6dVtnoDX3XAazwf","valueSat":559835,"value":0.00559835,"doubleSpentTxID":null}],"vout":[{"value":"0.00055965","n":0,"scriptPubKey":{"hex":"76a914f4c9e785dbe753f59b98c082d793b2e1312c558788ac","asm":"OP_DUP 66 | OP_HASH160 f4c9e785dbe753f59b98c082d793b2e1312c5587 OP_EQUALVERIFY OP_CHECKSIG","addresses":["1PKKkNRPPfPjrPiufHzuLFX2gMAVJbcN8H"],"type":"pubkeyhash"},"spentTxId":"1c085fb7180d35736dae4d1526f18c71fb551173c7471a70251bff88d38d559d","spentIndex":0,"spentHeight":547177},{"value":"0.00503191","n":1,"scriptPubKey":{"hex":"76a9142edbf57347f7f56a2975c4c5c147680a781e32a988ac","asm":"OP_DUP 67 | OP_HASH160 2edbf57347f7f56a2975c4c5c147680a781e32a9 OP_EQUALVERIFY OP_CHECKSIG","addresses":["15Gmaq5M8cMD4mAy1Gh6dVtnoDX3XAazwf"],"type":"pubkeyhash"},"spentTxId":"1c085fb7180d35736dae4d1526f18c71fb551173c7471a70251bff88d38d559d","spentIndex":1,"spentHeight":547177}],"blockhash":"0000000000000000001e7b7b2bf28d3e3813e0a37354f95288fe05fa18a0bc8a","blockheight":547126,"confirmations":2108,"time":1540380059,"blocktime":1540380059,"valueOut":0.00559156,"size":226,"valueIn":0.00559835,"fees":0.00000679}]}' 68 | http_version: 69 | recorded_at: Thu, 08 Nov 2018 17:34:53 GMT 70 | recorded_with: VCR 4.0.0 71 | -------------------------------------------------------------------------------- /spec/fixtures/vcr_cassettes/CryptocoinPayable_Adapters_Bitcoin/when_the_Block_Explorer_API_fails/falls_back_to_using_the_BlockCypher_API.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: get 5 | uri: https://api.blockcypher.com/v1/btc/main/addrs/3HR9xYD7MybbE7JLVTjwijYse48BtfEKni/full 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 | - nginx/1.10.3 (Ubuntu) 23 | Date: 24 | - Sun, 14 Oct 2018 10:46:00 GMT 25 | Content-Type: 26 | - application/json 27 | Transfer-Encoding: 28 | - chunked 29 | Connection: 30 | - keep-alive 31 | Access-Control-Allow-Headers: 32 | - Origin, X-Requested-With, Content-Type, Accept 33 | Access-Control-Allow-Methods: 34 | - GET, POST, PUT, DELETE 35 | Access-Control-Allow-Origin: 36 | - "*" 37 | X-Ratelimit-Remaining: 38 | - '97' 39 | body: 40 | encoding: ASCII-8BIT 41 | string: |- 42 | { 43 | "address": "3HR9xYD7MybbE7JLVTjwijYse48BtfEKni", 44 | "total_received": 500000000, 45 | "total_sent": 0, 46 | "balance": 500000000, 47 | "unconfirmed_balance": 0, 48 | "final_balance": 500000000, 49 | "n_tx": 2, 50 | "unconfirmed_n_tx": 0, 51 | "final_n_tx": 2, 52 | "txs": [ 53 | { 54 | "block_hash": "0000000000000000048e8ea3fdd2c3a59ddcbcf7575f82cb96ce9fd17da9f2f4", 55 | "block_height": 429632, 56 | "block_index": 893, 57 | "hash": "5bdeaf7829148d7e0e1e7b5233512a2c5ae54ef7ccbc8e68b2f85b7e49c917a0", 58 | "addresses": [ 59 | "1CJzCcrY9WarJ5nZCcfbNu2eAnuHqSGqJP", 60 | "1CVCj6B2Q4DJ1rFEhaXDdcEcaQQho37yrT", 61 | "3HR9xYD7MybbE7JLVTjwijYse48BtfEKni" 62 | ], 63 | "total": 826645063, 64 | "fees": 13740, 65 | "size": 223, 66 | "preference": "high", 67 | "relayed_by": "79.208.245.182:8333", 68 | "confirmed": "2016-09-13T15:41:00Z", 69 | "received": "2016-09-13T15:33:29.126Z", 70 | "ver": 1, 71 | "double_spend": false, 72 | "vin_sz": 1, 73 | "vout_sz": 2, 74 | "confirmations": 116077, 75 | "confidence": 1, 76 | "inputs": [ 77 | { 78 | "prev_hash": "252dde616fbbd25ebaf37ee8df82c38d2135a87100cc1f0315797b1b42f52f01", 79 | "output_index": 0, 80 | "script": "47304402206b40298326b6c4ab181c4c61b43569490e91773997c0ec53309ab01851098c1002202731263d747aba117b65aaf7d5ee0ad5e88c2e3b4dc3e47d6ecc414e3bbb38e8012102027fcba75d05e735e734d9287c3b22b90465e30ee87ff064303b8175ddeb48fa", 81 | "output_value": 826658803, 82 | "sequence": 4294967295, 83 | "addresses": [ 84 | "1CJzCcrY9WarJ5nZCcfbNu2eAnuHqSGqJP" 85 | ], 86 | "script_type": "pay-to-pubkey-hash", 87 | "age": 426962 88 | } 89 | ], 90 | "outputs": [ 91 | { 92 | "value": 499000000, 93 | "script": "a914ac82105dbe13eb1b2b606b8fa4e58ced5b317ceb87", 94 | "addresses": [ 95 | "3HR9xYD7MybbE7JLVTjwijYse48BtfEKni" 96 | ], 97 | "script_type": "pay-to-script-hash" 98 | }, 99 | { 100 | "value": 327645063, 101 | "script": "76a9147dfed19339f4a44a5a9888448ebb270656e2919488ac", 102 | "spent_by": "08aee92c75baa331b66aaf8ad27fdaea80053f3fdb090112ce332983cb63de00", 103 | "addresses": [ 104 | "1CVCj6B2Q4DJ1rFEhaXDdcEcaQQho37yrT" 105 | ], 106 | "script_type": "pay-to-pubkey-hash" 107 | } 108 | ] 109 | }, 110 | { 111 | "block_hash": "000000000000000001af27feb303ad97af81a5882157f166781784c639f8e896", 112 | "block_height": 429629, 113 | "block_index": 255, 114 | "hash": "e7bcdb13d9c903973bd8a740054d4c056a559bae67d4e8f6d0a42b4bab552623", 115 | "addresses": [ 116 | "13Rd2fKcsUQ9eChqMZGprGeo8XAMAsQhMV", 117 | "1GWPE3nEHHLY6C5U4c6qj3wxZvx4hiVDxZ", 118 | "3HR9xYD7MybbE7JLVTjwijYse48BtfEKni" 119 | ], 120 | "total": 464992520, 121 | "fees": 13740, 122 | "size": 223, 123 | "preference": "high", 124 | "relayed_by": "71.206.238.16:8333", 125 | "confirmed": "2016-09-13T15:22:42Z", 126 | "received": "2016-09-13T15:20:15.538Z", 127 | "ver": 1, 128 | "double_spend": false, 129 | "vin_sz": 1, 130 | "vout_sz": 2, 131 | "confirmations": 116080, 132 | "confidence": 1, 133 | "inputs": [ 134 | { 135 | "prev_hash": "e5c2d3a1ffa653121e38d051274dcbfde19ec53ed13d5e4eba54fdf77e8fbf60", 136 | "output_index": 0, 137 | "script": "47304402201d1d8197bd319e43ca5931023dad0bd4fbcd5d941f7bbf51e5ea6390bdf59c3702202303a697a6f722a90981e3fd1d7f0f27d397de3e1ee601e0f681ec933b809ccd0121028ee20d4ea0b72c78caf36b803a79e5a315c61f6d76193130699040cded33ad90", 138 | "output_value": 465006260, 139 | "sequence": 4294967295, 140 | "addresses": [ 141 | "1GWPE3nEHHLY6C5U4c6qj3wxZvx4hiVDxZ" 142 | ], 143 | "script_type": "pay-to-pubkey-hash", 144 | "age": 428716 145 | } 146 | ], 147 | "outputs": [ 148 | { 149 | "value": 463992520, 150 | "script": "76a9141a984f5195f48bcd9995ec4bb5ed5a386a10dfdf88ac", 151 | "spent_by": "5c9864d21dca5bd4715a5489fad9e51dbdcde1588e03d58ddbbbcabc0de54e32", 152 | "addresses": [ 153 | "13Rd2fKcsUQ9eChqMZGprGeo8XAMAsQhMV" 154 | ], 155 | "script_type": "pay-to-pubkey-hash" 156 | }, 157 | { 158 | "value": 1000000, 159 | "script": "a914ac82105dbe13eb1b2b606b8fa4e58ced5b317ceb87", 160 | "addresses": [ 161 | "3HR9xYD7MybbE7JLVTjwijYse48BtfEKni" 162 | ], 163 | "script_type": "pay-to-script-hash" 164 | } 165 | ] 166 | } 167 | ] 168 | } 169 | http_version: 170 | recorded_at: Sun, 14 Oct 2018 10:46:02 GMT 171 | recorded_with: VCR 4.0.0 172 | -------------------------------------------------------------------------------- /spec/fixtures/vcr_cassettes/CryptocoinPayable_Adapters_BitcoinCash/gets_an_empty_result_when_no_transactions_found.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: get 5 | uri: https://bitcoincash.blockexplorer.com/api/txs/?address=16E1jiUfQevixSoe7uE4fscNj4PjJkd1o9 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 | Date: 22 | - Fri, 12 Oct 2018 07:32:41 GMT 23 | Content-Type: 24 | - application/json; charset=utf-8 25 | Content-Length: 26 | - '25' 27 | Connection: 28 | - keep-alive 29 | Set-Cookie: 30 | - __cfduid=daacb5c42d20ef981bf9b41e8b10381d41539329560; expires=Sat, 12-Oct-19 31 | 07:32:40 GMT; path=/; domain=.blockexplorer.com; HttpOnly; Secure 32 | X-Powered-By: 33 | - Express 34 | X-Ratelimit-Limit: 35 | - '10800' 36 | X-Ratelimit-Remaining: 37 | - '10701' 38 | Access-Control-Allow-Origin: 39 | - "*" 40 | Access-Control-Allow-Methods: 41 | - GET, HEAD, PUT, POST, OPTIONS 42 | Access-Control-Allow-Headers: 43 | - Origin, X-Requested-With, Content-Type, Accept, Content-Length, Cache-Control, 44 | cf-connecting-ip 45 | X-Content-Type-Options: 46 | - nosniff 47 | Etag: 48 | - W/"19-yV2jHjCT6452sEiXtqmAbfkYdac" 49 | Vary: 50 | - Accept-Encoding 51 | Expect-Ct: 52 | - max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct" 53 | Server: 54 | - cloudflare 55 | Cf-Ray: 56 | - 4687dfb7d85f318c-SIN 57 | body: 58 | encoding: UTF-8 59 | string: '{"pagesTotal":0,"txs":[]}' 60 | http_version: 61 | recorded_at: Fri, 12 Oct 2018 07:32:42 GMT 62 | recorded_with: VCR 4.0.0 63 | -------------------------------------------------------------------------------- /spec/fixtures/vcr_cassettes/CryptocoinPayable_Adapters_BitcoinCash/gets_transactions_for_a_given_address.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: get 5 | uri: https://bitcoincash.blockexplorer.com/api/txs/?address=1HfknBcT93RE12YC4SBfm4NNBkARsNc8Wa 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 | Date: 22 | - Fri, 12 Oct 2018 07:32:39 GMT 23 | Content-Type: 24 | - application/json; charset=utf-8 25 | Transfer-Encoding: 26 | - chunked 27 | Connection: 28 | - keep-alive 29 | Set-Cookie: 30 | - __cfduid=dda29ada292f7048f8a3ebdd480e7fd041539329558; expires=Sat, 12-Oct-19 31 | 07:32:38 GMT; path=/; domain=.blockexplorer.com; HttpOnly; Secure 32 | X-Powered-By: 33 | - Express 34 | X-Ratelimit-Limit: 35 | - '10800' 36 | X-Ratelimit-Remaining: 37 | - '10702' 38 | Access-Control-Allow-Origin: 39 | - "*" 40 | Access-Control-Allow-Methods: 41 | - GET, HEAD, PUT, POST, OPTIONS 42 | Access-Control-Allow-Headers: 43 | - Origin, X-Requested-With, Content-Type, Accept, Content-Length, Cache-Control, 44 | cf-connecting-ip 45 | X-Content-Type-Options: 46 | - nosniff 47 | Etag: 48 | - W/"84c-f7pBa6wjN3QCqsDu9o519TbOLmo" 49 | Vary: 50 | - Accept-Encoding 51 | Expect-Ct: 52 | - max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct" 53 | Server: 54 | - cloudflare 55 | Cf-Ray: 56 | - 4687dfaf2b563186-SIN 57 | body: 58 | encoding: ASCII-8BIT 59 | string: '{"pagesTotal":1,"txs":[{"txid":"10d9d3927a21d90c573a5fbbb347f409af37219ceb93f7475d6c4cca4231d29f","version":1,"locktime":0,"vin":[{"txid":"0214539ca7f81cd38a046c7083b592d88e10a74e139c1c66f87904d62db8c149","vout":0,"sequence":4294967295,"n":0,"scriptSig":{"hex":"483045022100e89cbd22d6ffbfeb30c68420d9c5bd39c2ddb9514fc545704778dc42c3ee003b0220696c92a97b3b1c32a4ab140e8f6c447c9b5cb8596eafdc3dac45d6c1f87dc38a412102bec87afd168c0701f5cac29cda799f45eb0d70f52ce902e337b958c9c986005b","asm":"3045022100e89cbd22d6ffbfeb30c68420d9c5bd39c2ddb9514fc545704778dc42c3ee003b0220696c92a97b3b1c32a4ab140e8f6c447c9b5cb8596eafdc3dac45d6c1f87dc38a[ALL|FORKID] 60 | 02bec87afd168c0701f5cac29cda799f45eb0d70f52ce902e337b958c9c986005b"},"addr":"bitcoincash:qpth77f5fzj3hmmtpw42tt00c9t3and9guumguzfj2","valueSat":4049279,"value":0.04049279,"doubleSpentTxID":null},{"txid":"bf91d7c20551e78665be98c7344ed87281af543d0bfa3997e6dfa2bf4524f4d2","vout":0,"sequence":4294967295,"n":1,"scriptSig":{"hex":"483045022100dcbb116b70e597e1344394e34bc11936f8bf6e7833cb00a6af425b846c31c72e022034f18e40c530a7f9e4af3498dfb83d72b99913ab5ffcdc31e657a6eef84e42ba41210287ad2b715b610834654606551bdf1045ec163cff2df03e3d49f3c6231aa9cf33","asm":"3045022100dcbb116b70e597e1344394e34bc11936f8bf6e7833cb00a6af425b846c31c72e022034f18e40c530a7f9e4af3498dfb83d72b99913ab5ffcdc31e657a6eef84e42ba[ALL|FORKID] 61 | 0287ad2b715b610834654606551bdf1045ec163cff2df03e3d49f3c6231aa9cf33"},"addr":"bitcoincash:qpe0ccjkmwyku7mk7kqtphlq7x3p6ykedyg8tlttqq","valueSat":79513,"value":0.00079513,"doubleSpentTxID":null}],"vout":[{"value":"0.04128450","n":0,"scriptPubKey":{"hex":"76a914b6d65d2d333d2f16f0d3cefbb775f404649801c688ac","asm":"OP_DUP 62 | OP_HASH160 b6d65d2d333d2f16f0d3cefbb775f404649801c6 OP_EQUALVERIFY OP_CHECKSIG","addresses":["bitcoincash:qzmdvhfdxv7j79hs6080hdm47szxfxqpccemzq6n52"],"type":"pubkeyhash"},"spentTxId":null,"spentIndex":null,"spentHeight":null}],"blockhash":"0000000000000000015493ab50fde669130f9b64f0918031a5b6dcc44f14698f","blockheight":551829,"confirmations":2,"time":1539329301,"blocktime":1539329301,"valueOut":0.0412845,"size":340,"valueIn":0.04128792,"fees":0.00000342}]}' 63 | http_version: 64 | recorded_at: Fri, 12 Oct 2018 07:32:41 GMT 65 | recorded_with: VCR 4.0.0 66 | -------------------------------------------------------------------------------- /spec/fixtures/vcr_cassettes/CryptocoinPayable_Adapters_Ethereum/gets_an_empty_result_when_no_transactions_found.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: get 5 | uri: https://api.etherscan.io/api?action=txlist&address=0x772fDD41BFB34C9903B253322baccdbE2C10851e&module=account&tag=latest 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 | Cache-Control: 22 | - private 23 | Content-Type: 24 | - application/json; charset=utf-8 25 | Server: 26 | - Microsoft-IIS/10.0 27 | Access-Control-Allow-Origin: 28 | - "*" 29 | Access-Control-Allow-Headers: 30 | - Content-Type 31 | Access-Control-Allow-Methods: 32 | - GET, POST, OPTIONS 33 | X-Frame-Options: 34 | - SAMEORIGIN 35 | Date: 36 | - Thu, 11 Oct 2018 06:20:39 GMT 37 | Content-Length: 38 | - '60' 39 | body: 40 | encoding: UTF-8 41 | string: '{"status":"0","message":"No transactions found","result":[]}' 42 | http_version: 43 | recorded_at: Thu, 11 Oct 2018 06:20:56 GMT 44 | recorded_with: VCR 4.0.0 45 | -------------------------------------------------------------------------------- /spec/fixtures/vcr_cassettes/CryptocoinPayable_Adapters_Ethereum/gets_transactions_for_a_given_address.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: get 5 | uri: https://api.etherscan.io/api?action=txlist&address=0xfc8cfb26c31931572e65e450f7fa498bcc11651c&module=account&tag=latest 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 | Cache-Control: 22 | - private 23 | Content-Type: 24 | - application/json; charset=utf-8 25 | Server: 26 | - Microsoft-IIS/10.0 27 | Access-Control-Allow-Origin: 28 | - "*" 29 | Access-Control-Allow-Headers: 30 | - Content-Type 31 | Access-Control-Allow-Methods: 32 | - GET, POST, OPTIONS 33 | X-Frame-Options: 34 | - SAMEORIGIN 35 | Date: 36 | - Mon, 08 Oct 2018 18:29:04 GMT 37 | Content-Length: 38 | - '2269' 39 | body: 40 | encoding: UTF-8 41 | string: '{"status":"1","message":"OK","result":[{"blockNumber":"5908320","block_hash":"0x752c50e426f65820f5bf6fd49acbb08d79464f8e7e8ea5b77e2299b69fd6398b","timeStamp":"1530770313","hash":"0xa88b799514e9621962e3d0de25e7e0bc7a123e33085f322c7acdb99cc2585c6d","nonce":"342313","transactionIndex":"12","from":"0xeed16856d551569d134530ee3967ec79995e2051","to":"0xfc8cfb26c31931572e65e450f7fa498bcc11651c","value":"33753640000000000","gas":"50000","gasPrice":"93000000000","input":"0x","contractAddress":"","cumulativeGasUsed":"422633","txreceipt_status":"1","gasUsed":"21000","confirmations":"569771","isError":"0"},{"blockNumber":"5908462","block_hash":"0x1c2b73a16fd8c4d25feeccaa2f0bf5c82b8f415f1beaf4d34aaf870daf89689d","timeStamp":"1530772507","hash":"0xb325a8cf241f332bca92c7f715987e4d34be9a6b3bb78d2425c83086b4aced26","nonce":"0","transactionIndex":"36","from":"0xfc8cfb26c31931572e65e450f7fa498bcc11651c","to":"0x6e5eaac372b4abad8957d68af4f53bcf245c0100","value":"2190144444444444","gas":"21000","gasPrice":"43000000000","input":"0x","contractAddress":"","cumulativeGasUsed":"6094102","txreceipt_status":"1","gasUsed":"21000","confirmations":"569629","isError":"0"},{"blockNumber":"5908542","block_hash":"0x4ce71d11146445f123680ea9beba7db968b04dc675caddf60248c9d9d6f5739e","timeStamp":"1530773753","hash":"0xcd874917be5ad177e7ebd88b5c4a7d4283796e00e43345da5b63fb4f78130b37","nonce":"1","transactionIndex":"27","from":"0xfc8cfb26c31931572e65e450f7fa498bcc11651c","to":"0xa80de67c85669132f25678220b492c301217872e","value":"1007518888888888","gas":"21000","gasPrice":"49000000000","input":"0x","contractAddress":"","cumulativeGasUsed":"805592","txreceipt_status":"1","gasUsed":"21000","confirmations":"569549","isError":"0"},{"blockNumber":"6216122","block_hash":"0xc1361b19b2266e2259ac433b9e18b4fbc81339304988bbc62dd93aa24fac6449","timeStamp":"1535274344","hash":"0x799ec2aaafbddbc2e746334f96f59f6127dec62e5693480576db351aaf840bfb","nonce":"0","transactionIndex":"54","from":"0xd2b296fdf2eb4a441721b5a97dc2d9cca44f7a52","to":"0xfc8cfb26c31931572e65e450f7fa498bcc11651c","value":"15678420000000000","gas":"21000","gasPrice":"39000000000","input":"0x","contractAddress":"","cumulativeGasUsed":"7209675","txreceipt_status":"1","gasUsed":"21000","confirmations":"261969","isError":"0"}]}' 42 | http_version: 43 | recorded_at: Mon, 08 Oct 2018 18:29:13 GMT 44 | recorded_with: VCR 4.0.0 45 | -------------------------------------------------------------------------------- /spec/fixtures/vcr_cassettes/CryptocoinPayable_Adapters_Ethereum/raises_an_error_when_an_invalid_address_is_passed.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: get 5 | uri: https://api.etherscan.io/api?action=txlist&address=foo&module=account&tag=latest 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 | Cache-Control: 22 | - private 23 | Content-Type: 24 | - application/json; charset=utf-8 25 | Server: 26 | - Microsoft-IIS/10.0 27 | Access-Control-Allow-Origin: 28 | - "*" 29 | Access-Control-Allow-Headers: 30 | - Content-Type 31 | Access-Control-Allow-Methods: 32 | - GET, POST, OPTIONS 33 | X-Frame-Options: 34 | - SAMEORIGIN 35 | Date: 36 | - Thu, 11 Oct 2018 06:23:53 GMT 37 | Content-Length: 38 | - '73' 39 | body: 40 | encoding: UTF-8 41 | string: '{"status":"0","message":"NOTOK","result":"Error! Invalid address format"}' 42 | http_version: 43 | recorded_at: Thu, 11 Oct 2018 06:24:09 GMT 44 | recorded_with: VCR 4.0.0 45 | -------------------------------------------------------------------------------- /spec/fixtures/vcr_cassettes/CryptocoinPayable_PricingProcessor/when_updating_stale_payments/can_update.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: get 5 | uri: https://api.coinbase.com/v2/prices/BCH-USD/spot 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 | Date: 22 | - Thu, 08 Nov 2018 17:53:56 GMT 23 | Content-Type: 24 | - application/json; charset=utf-8 25 | Content-Length: 26 | - '81' 27 | Connection: 28 | - keep-alive 29 | Set-Cookie: 30 | - __cfduid=d5f901b0febd4880997885e4cdc0882561541699635; expires=Fri, 08-Nov-19 31 | 17:53:55 GMT; path=/; domain=.coinbase.com; HttpOnly 32 | Cache-Control: 33 | - public, max-age=30 34 | Content-Disposition: 35 | - attachment; filename=response.json 36 | Content-Security-Policy: 37 | - 'default-src ''self'' https://www.coinbase.com; child-src ''self'' https://www.coinbase.com 38 | https://*.online-metrix.net https://*.wpstn.com https://netverify.com https://platform.twitter.com 39 | https://www.google.com/recaptcha/ https://cdn.plaid.com/link/ https://*.doubleclick.net/ 40 | blob: https://coinbase.ada.support; connect-src ''self'' https://www.coinbase.com 41 | https://api.coinbase.com https://api.mixpanel.com https://*.online-metrix.net 42 | https://api.cloudinary.com https://ott9.wpstn.com/live https://api.amplitude.com/ 43 | static.coinbase.com wss://ws.coinbase.com wss://ws.coinbase.com:443 https://www.coinbase.com/api 44 | https://coinbase.ada.support/api/; font-src ''self'' https://www.coinbase.com 45 | https://assets.coinbase.com/ static.coinbase.com; img-src ''self'' data: https://www.coinbase.com 46 | https://images.coinbase.com https://exceptions.coinbase.com https://coinbase-uploads.s3.amazonaws.com 47 | https://s3.amazonaws.com/app-public/ https://maps.gstatic.com https://ssl.google-analytics.com 48 | https://www.google.com https://maps.googleapis.com https://csi.gstatic.com 49 | https://www.google-analytics.com https://res.cloudinary.com https://secure.gravatar.com 50 | https://i2.wp.com https://*.online-metrix.net https://assets.coinbase.com/ 51 | https://hexagon-analytics.com https://api.mixpanel.com https://cb-brand.s3.amazonaws.com 52 | https://googleads.g.doubleclick.net https://stats.g.doubleclick.net/r/collect 53 | blob: static.coinbase.com https://d124s1zbdqkqqe.cloudfront.net https://www.facebook.com/tr; 54 | media-src ''self'' https://www.coinbase.com blob:; object-src ''self'' data: 55 | blob: https://www.coinbase.com https://cdn.siftscience.com https://*.online-metrix.net 56 | https://www.gstatic.com https://www.google.com/recaptcha/api/; script-src 57 | ''self'' ''unsafe-inline'' ''unsafe-eval'' https://www.coinbase.com https://cdn.siftscience.com 58 | https://*.newrelic.com https://bam.nr-data.net https://*.google-analytics.com 59 | https://www.google.com https://www.gstatic.com https://*.online-metrix.net 60 | https://code.jquery.com https://chart.googleapis.com https://maps.googleapis.com 61 | https://maps.gstatic.com https://netverify.com https://ajax.cloudflare.com 62 | https://cdn.plaid.com/link/v2/stable/ https://www.googletagmanager.com/gtag/js 63 | https://www.googletagmanager.com/gtm.js https://www.googleadservices.com https://googleads.g.doubleclick.net 64 | https://assets.coinbase.com/ static.coinbase.com; style-src ''self'' ''unsafe-inline'' 65 | https://www.coinbase.com https://assets.coinbase.com/ static.coinbase.com; 66 | report-uri /csp-report' 67 | Etag: 68 | - W/"b660661171f1c14502e61d57793828bd" 69 | Expect-Ct: 70 | - enforce; max-age=86400; report-uri="https://coinbase.report-uri.io/r/default/ct/reportOnly" 71 | Referrer-Policy: 72 | - strict-origin-when-cross-origin 73 | Strict-Transport-Security: 74 | - max-age=31536000; includeSubDomains; preload 75 | Vary: 76 | - Origin,Accept-Encoding 77 | X-Content-Type-Options: 78 | - nosniff 79 | X-Download-Options: 80 | - noopen 81 | X-Frame-Options: 82 | - DENY 83 | X-Permitted-Cross-Domain-Policies: 84 | - none 85 | X-Powered-By: 86 | - Proof-of-Work 87 | X-Request-Id: 88 | - 875e27b8-fda6-465f-b629-d783863ee485 89 | X-Xss-Protection: 90 | - 1; mode=block 91 | Cf-Cache-Status: 92 | - HIT 93 | Expires: 94 | - Thu, 08 Nov 2018 17:54:26 GMT 95 | Server: 96 | - cloudflare 97 | Cf-Ray: 98 | - 4769e6e35bd0a491-PNH 99 | body: 100 | encoding: ASCII-8BIT 101 | string: '{"data":{"base":"BCH","currency":"USD","amount":"587.97"}}' 102 | http_version: 103 | recorded_at: Sun, 11 Nov 2018 17:54:00 GMT 104 | - request: 105 | method: get 106 | uri: https://api.coinbase.com/v2/prices/BTC-USD/spot 107 | body: 108 | encoding: US-ASCII 109 | string: '' 110 | headers: 111 | Accept-Encoding: 112 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 113 | Accept: 114 | - "*/*" 115 | User-Agent: 116 | - Ruby 117 | response: 118 | status: 119 | code: 200 120 | message: OK 121 | headers: 122 | Date: 123 | - Thu, 08 Nov 2018 17:53:57 GMT 124 | Content-Type: 125 | - application/json; charset=utf-8 126 | Content-Length: 127 | - '82' 128 | Connection: 129 | - keep-alive 130 | Set-Cookie: 131 | - __cfduid=d04b2b46c8cd0d0c16375884efe5462451541699636; expires=Fri, 08-Nov-19 132 | 17:53:56 GMT; path=/; domain=.coinbase.com; HttpOnly 133 | Cache-Control: 134 | - public, max-age=30 135 | Content-Disposition: 136 | - attachment; filename=response.json 137 | Content-Security-Policy: 138 | - 'default-src ''self'' https://www.coinbase.com; child-src ''self'' https://www.coinbase.com 139 | https://*.online-metrix.net https://*.wpstn.com https://netverify.com https://platform.twitter.com 140 | https://www.google.com/recaptcha/ https://cdn.plaid.com/link/ https://*.doubleclick.net/ 141 | blob: https://coinbase.ada.support; connect-src ''self'' https://www.coinbase.com 142 | https://api.coinbase.com https://api.mixpanel.com https://*.online-metrix.net 143 | https://api.cloudinary.com https://ott9.wpstn.com/live https://api.amplitude.com/ 144 | static.coinbase.com wss://ws.coinbase.com wss://ws.coinbase.com:443 https://www.coinbase.com/api 145 | https://coinbase.ada.support/api/; font-src ''self'' https://www.coinbase.com 146 | https://assets.coinbase.com/ static.coinbase.com; img-src ''self'' data: https://www.coinbase.com 147 | https://images.coinbase.com https://exceptions.coinbase.com https://coinbase-uploads.s3.amazonaws.com 148 | https://s3.amazonaws.com/app-public/ https://maps.gstatic.com https://ssl.google-analytics.com 149 | https://www.google.com https://maps.googleapis.com https://csi.gstatic.com 150 | https://www.google-analytics.com https://res.cloudinary.com https://secure.gravatar.com 151 | https://i2.wp.com https://*.online-metrix.net https://assets.coinbase.com/ 152 | https://hexagon-analytics.com https://api.mixpanel.com https://cb-brand.s3.amazonaws.com 153 | https://googleads.g.doubleclick.net https://stats.g.doubleclick.net/r/collect 154 | blob: static.coinbase.com https://d124s1zbdqkqqe.cloudfront.net https://www.facebook.com/tr; 155 | media-src ''self'' https://www.coinbase.com blob:; object-src ''self'' data: 156 | blob: https://www.coinbase.com https://cdn.siftscience.com https://*.online-metrix.net 157 | https://www.gstatic.com https://www.google.com/recaptcha/api/; script-src 158 | ''self'' ''unsafe-inline'' ''unsafe-eval'' https://www.coinbase.com https://cdn.siftscience.com 159 | https://*.newrelic.com https://bam.nr-data.net https://*.google-analytics.com 160 | https://www.google.com https://www.gstatic.com https://*.online-metrix.net 161 | https://code.jquery.com https://chart.googleapis.com https://maps.googleapis.com 162 | https://maps.gstatic.com https://netverify.com https://ajax.cloudflare.com 163 | https://cdn.plaid.com/link/v2/stable/ https://www.googletagmanager.com/gtag/js 164 | https://www.googletagmanager.com/gtm.js https://www.googleadservices.com https://googleads.g.doubleclick.net 165 | https://assets.coinbase.com/ static.coinbase.com; style-src ''self'' ''unsafe-inline'' 166 | https://www.coinbase.com https://assets.coinbase.com/ static.coinbase.com; 167 | report-uri /csp-report' 168 | Etag: 169 | - W/"4f5f2ad82a3963f92662806d45ab3edf" 170 | Expect-Ct: 171 | - enforce; max-age=86400; report-uri="https://coinbase.report-uri.io/r/default/ct/reportOnly" 172 | Referrer-Policy: 173 | - strict-origin-when-cross-origin 174 | Strict-Transport-Security: 175 | - max-age=31536000; includeSubDomains; preload 176 | Vary: 177 | - Origin,Accept-Encoding 178 | X-Content-Type-Options: 179 | - nosniff 180 | X-Download-Options: 181 | - noopen 182 | X-Frame-Options: 183 | - DENY 184 | X-Permitted-Cross-Domain-Policies: 185 | - none 186 | X-Powered-By: 187 | - Proof-of-Work 188 | X-Request-Id: 189 | - d16c1c0c-26ea-4fc7-a1ef-b02abfa8bf80 190 | X-Xss-Protection: 191 | - 1; mode=block 192 | Cf-Cache-Status: 193 | - HIT 194 | Expires: 195 | - Thu, 08 Nov 2018 17:54:27 GMT 196 | Server: 197 | - cloudflare 198 | Cf-Ray: 199 | - 4769e6ea49b4a485-PNH 200 | body: 201 | encoding: ASCII-8BIT 202 | string: '{"data":{"base":"BTC","currency":"USD","amount":"6424.00"}}' 203 | http_version: 204 | recorded_at: Sun, 11 Nov 2018 17:54:00 GMT 205 | - request: 206 | method: get 207 | uri: https://api.coinbase.com/v2/prices/ETH-USD/spot 208 | body: 209 | encoding: US-ASCII 210 | string: '' 211 | headers: 212 | Accept-Encoding: 213 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 214 | Accept: 215 | - "*/*" 216 | User-Agent: 217 | - Ruby 218 | response: 219 | status: 220 | code: 200 221 | message: OK 222 | headers: 223 | Date: 224 | - Thu, 08 Nov 2018 17:53:59 GMT 225 | Content-Type: 226 | - application/json; charset=utf-8 227 | Content-Length: 228 | - '81' 229 | Connection: 230 | - keep-alive 231 | Set-Cookie: 232 | - __cfduid=d233c228c665e49647bb663088827d07f1541699638; expires=Fri, 08-Nov-19 233 | 17:53:58 GMT; path=/; domain=.coinbase.com; HttpOnly 234 | Cache-Control: 235 | - public, max-age=30 236 | Content-Disposition: 237 | - attachment; filename=response.json 238 | Content-Security-Policy: 239 | - 'default-src ''self'' https://www.coinbase.com; child-src ''self'' https://www.coinbase.com 240 | https://*.online-metrix.net https://*.wpstn.com https://netverify.com https://platform.twitter.com 241 | https://www.google.com/recaptcha/ https://cdn.plaid.com/link/ https://*.doubleclick.net/ 242 | blob: https://coinbase.ada.support; connect-src ''self'' https://www.coinbase.com 243 | https://api.coinbase.com https://api.mixpanel.com https://*.online-metrix.net 244 | https://api.cloudinary.com https://ott9.wpstn.com/live https://api.amplitude.com/ 245 | static.coinbase.com wss://ws.coinbase.com wss://ws.coinbase.com:443 https://www.coinbase.com/api 246 | https://coinbase.ada.support/api/; font-src ''self'' https://www.coinbase.com 247 | https://assets.coinbase.com/ static.coinbase.com; img-src ''self'' data: https://www.coinbase.com 248 | https://images.coinbase.com https://exceptions.coinbase.com https://coinbase-uploads.s3.amazonaws.com 249 | https://s3.amazonaws.com/app-public/ https://maps.gstatic.com https://ssl.google-analytics.com 250 | https://www.google.com https://maps.googleapis.com https://csi.gstatic.com 251 | https://www.google-analytics.com https://res.cloudinary.com https://secure.gravatar.com 252 | https://i2.wp.com https://*.online-metrix.net https://assets.coinbase.com/ 253 | https://hexagon-analytics.com https://api.mixpanel.com https://cb-brand.s3.amazonaws.com 254 | https://googleads.g.doubleclick.net https://stats.g.doubleclick.net/r/collect 255 | blob: static.coinbase.com https://d124s1zbdqkqqe.cloudfront.net https://www.facebook.com/tr; 256 | media-src ''self'' https://www.coinbase.com blob:; object-src ''self'' data: 257 | blob: https://www.coinbase.com https://cdn.siftscience.com https://*.online-metrix.net 258 | https://www.gstatic.com https://www.google.com/recaptcha/api/; script-src 259 | ''self'' ''unsafe-inline'' ''unsafe-eval'' https://www.coinbase.com https://cdn.siftscience.com 260 | https://*.newrelic.com https://bam.nr-data.net https://*.google-analytics.com 261 | https://www.google.com https://www.gstatic.com https://*.online-metrix.net 262 | https://code.jquery.com https://chart.googleapis.com https://maps.googleapis.com 263 | https://maps.gstatic.com https://netverify.com https://ajax.cloudflare.com 264 | https://cdn.plaid.com/link/v2/stable/ https://www.googletagmanager.com/gtag/js 265 | https://www.googletagmanager.com/gtm.js https://www.googleadservices.com https://googleads.g.doubleclick.net 266 | https://assets.coinbase.com/ static.coinbase.com; style-src ''self'' ''unsafe-inline'' 267 | https://www.coinbase.com https://assets.coinbase.com/ static.coinbase.com; 268 | report-uri /csp-report' 269 | Etag: 270 | - W/"3f88f9ab53d68d44e3364543ddfdb274" 271 | Expect-Ct: 272 | - enforce; max-age=86400; report-uri="https://coinbase.report-uri.io/r/default/ct/reportOnly" 273 | Referrer-Policy: 274 | - strict-origin-when-cross-origin 275 | Strict-Transport-Security: 276 | - max-age=31536000; includeSubDomains; preload 277 | Vary: 278 | - Origin,Accept-Encoding 279 | X-Content-Type-Options: 280 | - nosniff 281 | X-Download-Options: 282 | - noopen 283 | X-Frame-Options: 284 | - DENY 285 | X-Permitted-Cross-Domain-Policies: 286 | - none 287 | X-Powered-By: 288 | - Proof-of-Work 289 | X-Request-Id: 290 | - bc27a504-585e-4550-98f5-a71b699a954b 291 | X-Xss-Protection: 292 | - 1; mode=block 293 | Cf-Cache-Status: 294 | - HIT 295 | Expires: 296 | - Thu, 08 Nov 2018 17:54:29 GMT 297 | Server: 298 | - cloudflare 299 | Cf-Ray: 300 | - 4769e6f1aa45a4af-PNH 301 | body: 302 | encoding: ASCII-8BIT 303 | string: '{"data":{"base":"ETH","currency":"USD","amount":"211.81"}}' 304 | http_version: 305 | recorded_at: Sun, 11 Nov 2018 17:54:00 GMT 306 | recorded_with: VCR 4.0.0 307 | -------------------------------------------------------------------------------- /spec/fixtures/vcr_cassettes/CryptocoinPayable_PricingProcessor/when_updating_stale_payments/can_update_without_errors.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: get 5 | uri: https://api.coinbase.com/v2/prices/BTC-USD/spot 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 | Date: 22 | - Thu, 03 Jan 2019 06:16:59 GMT 23 | Content-Type: 24 | - application/json; charset=utf-8 25 | Content-Length: 26 | - '83' 27 | Connection: 28 | - keep-alive 29 | Set-Cookie: 30 | - __cfduid=dd4af8527655294ea5b85a44c50ba5bc11546496219; expires=Fri, 03-Jan-20 31 | 06:16:59 GMT; path=/; domain=.coinbase.com; HttpOnly 32 | Cache-Control: 33 | - public, max-age=30 34 | Content-Disposition: 35 | - attachment; filename=response.json 36 | Content-Security-Policy: 37 | - 'default-src ''self'' https://www.coinbase.com; child-src ''self'' https://www.coinbase.com 38 | https://fast.wistia.net https://*.online-metrix.net https://*.wpstn.com https://netverify.com 39 | https://platform.twitter.com https://www.google.com/recaptcha/ https://cdn.plaid.com/link/ 40 | https://*.doubleclick.net/ blob: https://coinbase.ada.support; connect-src 41 | ''self'' https://www.coinbase.com https://api.coinbase.com https://api.mixpanel.com 42 | https://*.online-metrix.net https://api.cloudinary.com https://ott9.wpstn.com/live 43 | https://api.amplitude.com/ https://d3907m2cqladbn.cloudfront.net/ https://exceptions.coinbase.com 44 | static.coinbase.com wss://ws.coinbase.com wss://ws.coinbase.com:443 https://www.coinbase.com/api 45 | https://coinbase.ada.support/api/; font-src ''self'' https://www.coinbase.com 46 | https://assets.coinbase.com/ static.coinbase.com; img-src ''self'' data: https://www.coinbase.com 47 | https://images.coinbase.com https://d3907m2cqladbn.cloudfront.net/ https://dynamic-assets.coinbase.com 48 | https://exceptions.coinbase.com https://coinbase-uploads.s3.amazonaws.com 49 | https://s3.amazonaws.com/app-public/ https://maps.gstatic.com https://ssl.google-analytics.com 50 | https://www.google.com https://maps.googleapis.com https://csi.gstatic.com 51 | https://www.google-analytics.com https://res.cloudinary.com https://secure.gravatar.com 52 | https://i2.wp.com https://*.online-metrix.net https://assets.coinbase.com/ 53 | https://hexagon-analytics.com https://api.mixpanel.com https://cb-brand.s3.amazonaws.com 54 | https://googleads.g.doubleclick.net https://stats.g.doubleclick.net/r/collect 55 | blob: static.coinbase.com https://d124s1zbdqkqqe.cloudfront.net https://www.facebook.com/tr; 56 | media-src ''self'' https://www.coinbase.com blob:; object-src ''self'' data: 57 | blob: https://www.coinbase.com https://cdn.siftscience.com https://*.online-metrix.net 58 | https://www.gstatic.com https://www.google.com/recaptcha/api/; script-src 59 | ''self'' ''unsafe-inline'' ''unsafe-eval'' https://www.coinbase.com https://fast.wistia.com/assets/external/E-v1.js 60 | https://cdn.siftscience.com https://*.newrelic.com https://bam.nr-data.net 61 | https://*.google-analytics.com https://www.google.com https://www.gstatic.com 62 | https://*.online-metrix.net https://code.jquery.com https://chart.googleapis.com 63 | https://maps.googleapis.com https://maps.gstatic.com https://netverify.com 64 | https://ajax.cloudflare.com https://cdn.plaid.com/link/v2/stable/ https://www.googletagmanager.com/gtag/js 65 | https://www.googletagmanager.com/gtm.js https://www.googleadservices.com https://googleads.g.doubleclick.net 66 | https://assets.coinbase.com/ static.coinbase.com; style-src ''self'' ''unsafe-inline'' 67 | https://www.coinbase.com https://assets.coinbase.com/ static.coinbase.com; 68 | report-uri /csp-report' 69 | Etag: 70 | - W/"f23beca76975a48cdfd807f7b2325631" 71 | Expect-Ct: 72 | - enforce; max-age=86400; report-uri="https://coinbase.report-uri.io/r/default/ct/reportOnly" 73 | Referrer-Policy: 74 | - strict-origin-when-cross-origin 75 | Strict-Transport-Security: 76 | - max-age=31536000; includeSubDomains; preload 77 | Vary: 78 | - Origin,Accept-Encoding 79 | X-Content-Type-Options: 80 | - nosniff 81 | X-Download-Options: 82 | - noopen 83 | X-Frame-Options: 84 | - DENY 85 | X-Permitted-Cross-Domain-Policies: 86 | - none 87 | X-Powered-By: 88 | - Proof-of-Work 89 | X-Request-Id: 90 | - 3b5ee3d5-750e-43dd-8bf6-21de7799db6d 91 | X-Xss-Protection: 92 | - 1; mode=block 93 | Cf-Cache-Status: 94 | - HIT 95 | Expires: 96 | - Thu, 03 Jan 2019 06:17:29 GMT 97 | Accept-Ranges: 98 | - bytes 99 | Server: 100 | - cloudflare 101 | Cf-Ray: 102 | - 493356f9eea43307-HKG 103 | body: 104 | encoding: ASCII-8BIT 105 | string: '{"data":{"base":"BTC","currency":"USD","amount":"3847.715"}}' 106 | http_version: 107 | recorded_at: Sun, 06 Jan 2019 06:17:09 GMT 108 | - request: 109 | method: get 110 | uri: https://api.coinbase.com/v2/prices/ETH-USD/spot 111 | body: 112 | encoding: US-ASCII 113 | string: '' 114 | headers: 115 | Accept-Encoding: 116 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 117 | Accept: 118 | - "*/*" 119 | User-Agent: 120 | - Ruby 121 | response: 122 | status: 123 | code: 200 124 | message: OK 125 | headers: 126 | Date: 127 | - Thu, 03 Jan 2019 06:16:59 GMT 128 | Content-Type: 129 | - application/json; charset=utf-8 130 | Transfer-Encoding: 131 | - chunked 132 | Connection: 133 | - keep-alive 134 | Set-Cookie: 135 | - __cfduid=d0d42a5c4acef107d2c3e931dc70afc351546496219; expires=Fri, 03-Jan-20 136 | 06:16:59 GMT; path=/; domain=.coinbase.com; HttpOnly 137 | Cache-Control: 138 | - public, max-age=30 139 | Content-Disposition: 140 | - attachment; filename=response.json 141 | Content-Security-Policy: 142 | - 'default-src ''self'' https://www.coinbase.com; child-src ''self'' https://www.coinbase.com 143 | https://fast.wistia.net https://*.online-metrix.net https://*.wpstn.com https://netverify.com 144 | https://platform.twitter.com https://www.google.com/recaptcha/ https://cdn.plaid.com/link/ 145 | https://*.doubleclick.net/ blob: https://coinbase.ada.support; connect-src 146 | ''self'' https://www.coinbase.com https://api.coinbase.com https://api.mixpanel.com 147 | https://*.online-metrix.net https://api.cloudinary.com https://ott9.wpstn.com/live 148 | https://api.amplitude.com/ https://d3907m2cqladbn.cloudfront.net/ https://exceptions.coinbase.com 149 | static.coinbase.com wss://ws.coinbase.com wss://ws.coinbase.com:443 https://www.coinbase.com/api 150 | https://coinbase.ada.support/api/; font-src ''self'' https://www.coinbase.com 151 | https://assets.coinbase.com/ static.coinbase.com; img-src ''self'' data: https://www.coinbase.com 152 | https://images.coinbase.com https://d3907m2cqladbn.cloudfront.net/ https://dynamic-assets.coinbase.com 153 | https://exceptions.coinbase.com https://coinbase-uploads.s3.amazonaws.com 154 | https://s3.amazonaws.com/app-public/ https://maps.gstatic.com https://ssl.google-analytics.com 155 | https://www.google.com https://maps.googleapis.com https://csi.gstatic.com 156 | https://www.google-analytics.com https://res.cloudinary.com https://secure.gravatar.com 157 | https://i2.wp.com https://*.online-metrix.net https://assets.coinbase.com/ 158 | https://hexagon-analytics.com https://api.mixpanel.com https://cb-brand.s3.amazonaws.com 159 | https://googleads.g.doubleclick.net https://stats.g.doubleclick.net/r/collect 160 | blob: static.coinbase.com https://d124s1zbdqkqqe.cloudfront.net https://www.facebook.com/tr; 161 | media-src ''self'' https://www.coinbase.com blob:; object-src ''self'' data: 162 | blob: https://www.coinbase.com https://cdn.siftscience.com https://*.online-metrix.net 163 | https://www.gstatic.com https://www.google.com/recaptcha/api/; script-src 164 | ''self'' ''unsafe-inline'' ''unsafe-eval'' https://www.coinbase.com https://fast.wistia.com/assets/external/E-v1.js 165 | https://cdn.siftscience.com https://*.newrelic.com https://bam.nr-data.net 166 | https://*.google-analytics.com https://www.google.com https://www.gstatic.com 167 | https://*.online-metrix.net https://code.jquery.com https://chart.googleapis.com 168 | https://maps.googleapis.com https://maps.gstatic.com https://netverify.com 169 | https://ajax.cloudflare.com https://cdn.plaid.com/link/v2/stable/ https://www.googletagmanager.com/gtag/js 170 | https://www.googletagmanager.com/gtm.js https://www.googleadservices.com https://googleads.g.doubleclick.net 171 | https://assets.coinbase.com/ static.coinbase.com; style-src ''self'' ''unsafe-inline'' 172 | https://www.coinbase.com https://assets.coinbase.com/ static.coinbase.com; 173 | report-uri /csp-report' 174 | Etag: 175 | - W/"18049f93c854cad2425f2a4ef68cc599" 176 | Expect-Ct: 177 | - enforce; max-age=86400; report-uri="https://coinbase.report-uri.io/r/default/ct/reportOnly" 178 | Referrer-Policy: 179 | - strict-origin-when-cross-origin 180 | Strict-Transport-Security: 181 | - max-age=31536000; includeSubDomains; preload 182 | Vary: 183 | - Origin,Accept-Encoding 184 | X-Content-Type-Options: 185 | - nosniff 186 | X-Download-Options: 187 | - noopen 188 | X-Frame-Options: 189 | - DENY 190 | X-Permitted-Cross-Domain-Policies: 191 | - none 192 | X-Powered-By: 193 | - Proof-of-Work 194 | X-Request-Id: 195 | - c3f896ac-5980-4b8c-b4de-3bcc88f40ef7 196 | X-Xss-Protection: 197 | - 1; mode=block 198 | Cf-Cache-Status: 199 | - HIT 200 | Expires: 201 | - Thu, 03 Jan 2019 06:17:29 GMT 202 | Server: 203 | - cloudflare 204 | Cf-Ray: 205 | - 493356fbe9578496-HKG 206 | body: 207 | encoding: ASCII-8BIT 208 | string: '{"data":{"base":"ETH","currency":"USD","amount":"152.97"}}' 209 | http_version: 210 | recorded_at: Sun, 06 Jan 2019 06:17:09 GMT 211 | - request: 212 | method: get 213 | uri: https://api.coinbase.com/v2/prices/BCH-USD/spot 214 | body: 215 | encoding: US-ASCII 216 | string: '' 217 | headers: 218 | Accept-Encoding: 219 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 220 | Accept: 221 | - "*/*" 222 | User-Agent: 223 | - Ruby 224 | response: 225 | status: 226 | code: 200 227 | message: OK 228 | headers: 229 | Date: 230 | - Thu, 03 Jan 2019 06:16:59 GMT 231 | Content-Type: 232 | - application/json; charset=utf-8 233 | Content-Length: 234 | - '81' 235 | Connection: 236 | - keep-alive 237 | Set-Cookie: 238 | - __cfduid=dba0bc81102dac71ede146df16e0035391546496219; expires=Fri, 03-Jan-20 239 | 06:16:59 GMT; path=/; domain=.coinbase.com; HttpOnly 240 | Cache-Control: 241 | - public, max-age=30 242 | Content-Disposition: 243 | - attachment; filename=response.json 244 | Content-Security-Policy: 245 | - 'default-src ''self'' https://www.coinbase.com; child-src ''self'' https://www.coinbase.com 246 | https://fast.wistia.net https://*.online-metrix.net https://*.wpstn.com https://netverify.com 247 | https://platform.twitter.com https://www.google.com/recaptcha/ https://cdn.plaid.com/link/ 248 | https://*.doubleclick.net/ blob: https://coinbase.ada.support; connect-src 249 | ''self'' https://www.coinbase.com https://api.coinbase.com https://api.mixpanel.com 250 | https://*.online-metrix.net https://api.cloudinary.com https://ott9.wpstn.com/live 251 | https://api.amplitude.com/ https://d3907m2cqladbn.cloudfront.net/ https://exceptions.coinbase.com 252 | static.coinbase.com wss://ws.coinbase.com wss://ws.coinbase.com:443 https://www.coinbase.com/api 253 | https://coinbase.ada.support/api/; font-src ''self'' https://www.coinbase.com 254 | https://assets.coinbase.com/ static.coinbase.com; img-src ''self'' data: https://www.coinbase.com 255 | https://images.coinbase.com https://d3907m2cqladbn.cloudfront.net/ https://dynamic-assets.coinbase.com 256 | https://exceptions.coinbase.com https://coinbase-uploads.s3.amazonaws.com 257 | https://s3.amazonaws.com/app-public/ https://maps.gstatic.com https://ssl.google-analytics.com 258 | https://www.google.com https://maps.googleapis.com https://csi.gstatic.com 259 | https://www.google-analytics.com https://res.cloudinary.com https://secure.gravatar.com 260 | https://i2.wp.com https://*.online-metrix.net https://assets.coinbase.com/ 261 | https://hexagon-analytics.com https://api.mixpanel.com https://cb-brand.s3.amazonaws.com 262 | https://googleads.g.doubleclick.net https://stats.g.doubleclick.net/r/collect 263 | blob: static.coinbase.com https://d124s1zbdqkqqe.cloudfront.net https://www.facebook.com/tr; 264 | media-src ''self'' https://www.coinbase.com blob:; object-src ''self'' data: 265 | blob: https://www.coinbase.com https://cdn.siftscience.com https://*.online-metrix.net 266 | https://www.gstatic.com https://www.google.com/recaptcha/api/; script-src 267 | ''self'' ''unsafe-inline'' ''unsafe-eval'' https://www.coinbase.com https://fast.wistia.com/assets/external/E-v1.js 268 | https://cdn.siftscience.com https://*.newrelic.com https://bam.nr-data.net 269 | https://*.google-analytics.com https://www.google.com https://www.gstatic.com 270 | https://*.online-metrix.net https://code.jquery.com https://chart.googleapis.com 271 | https://maps.googleapis.com https://maps.gstatic.com https://netverify.com 272 | https://ajax.cloudflare.com https://cdn.plaid.com/link/v2/stable/ https://www.googletagmanager.com/gtag/js 273 | https://www.googletagmanager.com/gtm.js https://www.googleadservices.com https://googleads.g.doubleclick.net 274 | https://assets.coinbase.com/ static.coinbase.com; style-src ''self'' ''unsafe-inline'' 275 | https://www.coinbase.com https://assets.coinbase.com/ static.coinbase.com; 276 | report-uri /csp-report' 277 | Etag: 278 | - W/"77a093734a450f62c9c7c6605cb9010d" 279 | Expect-Ct: 280 | - enforce; max-age=86400; report-uri="https://coinbase.report-uri.io/r/default/ct/reportOnly" 281 | Referrer-Policy: 282 | - strict-origin-when-cross-origin 283 | Strict-Transport-Security: 284 | - max-age=31536000; includeSubDomains; preload 285 | Vary: 286 | - Origin,Accept-Encoding 287 | X-Content-Type-Options: 288 | - nosniff 289 | X-Download-Options: 290 | - noopen 291 | X-Frame-Options: 292 | - DENY 293 | X-Permitted-Cross-Domain-Policies: 294 | - none 295 | X-Powered-By: 296 | - Proof-of-Work 297 | X-Request-Id: 298 | - b10f3d9b-69f2-49fa-8262-426756be5d6a 299 | X-Xss-Protection: 300 | - 1; mode=block 301 | Cf-Cache-Status: 302 | - HIT 303 | Expires: 304 | - Thu, 03 Jan 2019 06:17:29 GMT 305 | Accept-Ranges: 306 | - bytes 307 | Server: 308 | - cloudflare 309 | Cf-Ray: 310 | - 493356fe290332e3-HKG 311 | body: 312 | encoding: ASCII-8BIT 313 | string: '{"data":{"base":"BCH","currency":"USD","amount":"167.82"}}' 314 | http_version: 315 | recorded_at: Sun, 06 Jan 2019 06:17:09 GMT 316 | recorded_with: VCR 4.0.0 317 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'vcr' 2 | require 'webmock/rspec' 3 | require 'active_support/time' 4 | require 'rspec-benchmark' 5 | require 'rspec/retry' 6 | 7 | # ActiveRecord::Base.logger = Logger.new(STDOUT) if defined?(ActiveRecord::Base) 8 | 9 | VCR.configure do |config| 10 | config.cassette_library_dir = 'spec/fixtures/vcr_cassettes' 11 | config.hook_into :webmock 12 | config.configure_rspec_metadata! 13 | end 14 | 15 | def create_tables 16 | ENV['RAILS_ENV'] = 'test' 17 | load 'spec/dummy/db/schema.rb' 18 | end 19 | 20 | def drop_tables 21 | %i[ 22 | coin_payment_transactions 23 | coin_payments 24 | currency_conversions 25 | widgets 26 | ].each do |table_name| 27 | ActiveRecord::Base.connection.drop_table(table_name) 28 | end 29 | end 30 | 31 | # This file was generated by the `rspec --init` command. Conventionally, all 32 | # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. 33 | # The generated `.rspec` file contains `--require spec_helper` which will cause 34 | # this file to always be loaded, without a need to explicitly require it in any 35 | # files. 36 | # 37 | # Given that it is always loaded, you are encouraged to keep this file as 38 | # light-weight as possible. Requiring heavyweight dependencies from this file 39 | # will add to the boot time of your test suite on EVERY test run, even for an 40 | # individual file that may not need all of that loaded. Instead, consider making 41 | # a separate helper file that requires the additional dependencies and performs 42 | # the additional setup, and require it from the spec files that actually need 43 | # it. 44 | # 45 | # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration 46 | RSpec.configure do |config| 47 | # rspec-expectations config goes here. You can use an alternate 48 | # assertion/expectation library such as wrong or the stdlib/minitest 49 | # assertions if you prefer. 50 | config.expect_with :rspec do |expectations| 51 | # This option will default to `true` in RSpec 4. It makes the `description` 52 | # and `failure_message` of custom matchers include text for helper methods 53 | # defined using `chain`, e.g.: 54 | # be_bigger_than(2).and_smaller_than(4).description 55 | # # => "be bigger than 2 and smaller than 4" 56 | # ...rather than: 57 | # # => "be bigger than 2" 58 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true 59 | end 60 | 61 | # rspec-mocks config goes here. You can use an alternate test double 62 | # library (such as bogus or mocha) by changing the `mock_with` option here. 63 | config.mock_with :rspec do |mocks| 64 | # Prevents you from mocking or stubbing a method that does not exist on 65 | # a real object. This is generally recommended, and will default to 66 | # `true` in RSpec 4. 67 | mocks.verify_partial_doubles = true 68 | end 69 | 70 | # This option will default to `:apply_to_host_groups` in RSpec 4 (and will 71 | # have no way to turn it off -- the option exists only for backwards 72 | # compatibility in RSpec 3). It causes shared context metadata to be 73 | # inherited by the metadata hash of host groups and examples, rather than 74 | # triggering implicit auto-inclusion in groups with matching metadata. 75 | config.shared_context_metadata_behavior = :apply_to_host_groups 76 | 77 | config.before(:suite) do 78 | CryptocoinPayable.configure do |c| 79 | c.configure_btc do |btc_config| 80 | # Created using BIP39 mnemonic 'dose rug must junk rug spell bracket 81 | # inside tissue artist patrol evil turtle brass ivory' 82 | # See https://iancoleman.io/bip39 83 | # rubocop:disable Metrics/LineLength 84 | btc_config.master_public_key = 'xpub688gtTMXY1ykq6RKrrSyVzGad7HTsTNVfT5UzyWL72fs73skbBFEuBfiYH5BhST5xzfUx7SFw5BF7jbJRDnxSjUtdfTS4Be9veEBdqZW1qg' 85 | # rubocop:enable Metrics/LineLength 86 | end 87 | 88 | c.configure_eth do |eth_config| 89 | # Created using BIP39 mnemonic 'cute season foam off pistol interest 90 | # soup wasp slice oxygen nominee anxiety step raven teach' 91 | # See https://iancoleman.io/bip39 92 | # rubocop:disable Metrics/LineLength 93 | eth_config.master_public_key = 'xpub69AhsZVugHWJ2iwbrYYhJ79W1KsbzUqGuUHRuMguZGa8ZSP6qFNCpy8pvkCUDdc2hNfVFeJL2vxxdgaDxeBGXuWL5hUVfuE9tjDDbX4eRUh' 94 | # rubocop:enable Metrics/LineLength 95 | end 96 | 97 | c.configure_bch do |bch_config| 98 | # Created using BIP39 mnemonic 'over dentist endorse dial muscle 99 | # decline front canvas initial business fashion priority clay tribe 100 | # praise' 101 | # See https://iancoleman.io/bip39 102 | # rubocop:disable Metrics/LineLength 103 | bch_config.master_public_key = 'xpub69m5Zouf7QU8wRjLfQX2F5VtgyTNJ45Xy6xg6SbrynM5D31U7uowkwe55y569b5Aonz9LJySajB1qkdkhFCdLVQE6U51VB6aGMeejKafAET' 104 | # rubocop:enable Metrics/LineLength 105 | end 106 | end 107 | 108 | ActiveRecord::Base.establish_connection(adapter: 'postgresql', database: 'cryptocoin_payable_test') 109 | create_tables 110 | 111 | CryptocoinPayable::CurrencyConversion.coin_types.keys.each do |coin_type| 112 | CryptocoinPayable::CurrencyConversion.create!( 113 | coin_type: coin_type, 114 | currency: 1, 115 | price: 1 116 | ) 117 | end 118 | end 119 | 120 | config.after(:suite) do 121 | drop_tables 122 | end 123 | 124 | config.include RSpec::Benchmark::Matchers 125 | 126 | # The settings below are suggested to provide a good initial experience 127 | # with RSpec, but feel free to customize to your heart's content. 128 | # # This allows you to limit a spec run to individual examples or groups 129 | # # you care about by tagging them with `:focus` metadata. When nothing 130 | # # is tagged with `:focus`, all examples get run. RSpec also provides 131 | # # aliases for `it`, `describe`, and `context` that include `:focus` 132 | # # metadata: `fit`, `fdescribe` and `fcontext`, respectively. 133 | # config.filter_run_when_matching :focus 134 | # 135 | # # Allows RSpec to persist some state between runs in order to support 136 | # # the `--only-failures` and `--next-failure` CLI options. We recommend 137 | # # you configure your source control system to ignore this file. 138 | # config.example_status_persistence_file_path = "spec/examples.txt" 139 | # 140 | # # Limits the available syntax to the non-monkey patched syntax that is 141 | # # recommended. For more details, see: 142 | # # - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/ 143 | # # - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/ 144 | # # - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode 145 | # config.disable_monkey_patching! 146 | # 147 | # # This setting enables warnings. It's recommended, but in some cases may 148 | # # be too noisy due to issues in dependencies. 149 | # config.warnings = true 150 | # 151 | # # Many RSpec users commonly either run the entire suite or an individual 152 | # # file, and it's useful to allow more verbose output when running an 153 | # # individual spec file. 154 | # if config.files_to_run.one? 155 | # # Use the documentation formatter for detailed output, 156 | # # unless a formatter has already been configured 157 | # # (e.g. via a command-line flag). 158 | # config.default_formatter = "doc" 159 | # end 160 | # 161 | # # Print the 10 slowest examples and example groups at the 162 | # # end of the spec run, to help surface which specs are running 163 | # # particularly slow. 164 | # config.profile_examples = 10 165 | # 166 | # # Run specs in random order to surface order dependencies. If you find an 167 | # # order dependency and want to debug it, you can fix the order by providing 168 | # # the seed, which is printed after each run. 169 | # # --seed 1234 170 | # config.order = :random 171 | # 172 | # # Seed global randomization in this process using the `--seed` CLI option. 173 | # # Setting this allows you to use `--seed` to deterministically reproduce 174 | # # test failures related to randomization by passing the same `--seed` value 175 | # # as the one that triggered the failure. 176 | # Kernel.srand config.seed 177 | end 178 | --------------------------------------------------------------------------------