cryptocoin_payable 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +5 -0
  3. data/.rspec +1 -0
  4. data/.rubocop.yml +36 -0
  5. data/Gemfile +8 -0
  6. data/LICENSE.txt +22 -0
  7. data/README.md +237 -0
  8. data/Rakefile +3 -0
  9. data/bin/console +15 -0
  10. data/bin/setup +8 -0
  11. data/cryptocoin_payable.gemspec +39 -0
  12. data/features/coin_payments.feature +51 -0
  13. data/features/default.feature +5 -0
  14. data/features/pricing_processor.feature +4 -0
  15. data/features/step_definitions/coin_payment_steps.rb +45 -0
  16. data/features/step_definitions/currency_conversion_steps.rb +12 -0
  17. data/features/step_definitions/model_step.rb +11 -0
  18. data/features/step_definitions/processor_steps.rb +7 -0
  19. data/features/step_definitions/widget_steps.rb +3 -0
  20. data/features/support/env.rb +26 -0
  21. data/lib/cryptocoin_payable/adapters/base.rb +99 -0
  22. data/lib/cryptocoin_payable/adapters/bitcoin.rb +93 -0
  23. data/lib/cryptocoin_payable/adapters/bitcoin_cash.rb +34 -0
  24. data/lib/cryptocoin_payable/adapters/ethereum.rb +77 -0
  25. data/lib/cryptocoin_payable/adapters.rb +27 -0
  26. data/lib/cryptocoin_payable/coin_payment.rb +141 -0
  27. data/lib/cryptocoin_payable/coin_payment_transaction.rb +5 -0
  28. data/lib/cryptocoin_payable/commands/payment_processor.rb +67 -0
  29. data/lib/cryptocoin_payable/commands/pricing_processor.rb +43 -0
  30. data/lib/cryptocoin_payable/config.rb +65 -0
  31. data/lib/cryptocoin_payable/currency_conversion.rb +11 -0
  32. data/lib/cryptocoin_payable/errors.rb +7 -0
  33. data/lib/cryptocoin_payable/has_coin_payments.rb +15 -0
  34. data/lib/cryptocoin_payable/tasks.rb +20 -0
  35. data/lib/cryptocoin_payable/version.rb +3 -0
  36. data/lib/cryptocoin_payable.rb +18 -0
  37. data/lib/generators/cryptocoin_payable/install_generator.rb +27 -0
  38. data/lib/generators/cryptocoin_payable/templates/create_coin_payment_transactions.rb +16 -0
  39. data/lib/generators/cryptocoin_payable/templates/create_coin_payments.rb +19 -0
  40. data/lib/generators/cryptocoin_payable/templates/create_currency_conversions.rb +11 -0
  41. data/spec/acceptance/adapters/bitcoin_cash_spec.rb +54 -0
  42. data/spec/acceptance/adapters/bitcoin_spec.rb +79 -0
  43. data/spec/acceptance/adapters/ethereum_spec.rb +77 -0
  44. data/spec/dummy/README.rdoc +28 -0
  45. data/spec/dummy/Rakefile +6 -0
  46. data/spec/dummy/app/assets/images/.keep +0 -0
  47. data/spec/dummy/app/assets/javascripts/application.js +13 -0
  48. data/spec/dummy/app/assets/stylesheets/application.css +13 -0
  49. data/spec/dummy/app/controllers/application_controller.rb +5 -0
  50. data/spec/dummy/app/controllers/concerns/.keep +0 -0
  51. data/spec/dummy/app/helpers/application_helper.rb +2 -0
  52. data/spec/dummy/app/mailers/.keep +0 -0
  53. data/spec/dummy/app/models/.keep +0 -0
  54. data/spec/dummy/app/models/concerns/.keep +0 -0
  55. data/spec/dummy/app/models/widget.rb +3 -0
  56. data/spec/dummy/app/views/layouts/application.html.erb +14 -0
  57. data/spec/dummy/bin/bundle +3 -0
  58. data/spec/dummy/bin/rails +4 -0
  59. data/spec/dummy/bin/rake +4 -0
  60. data/spec/dummy/config/application.rb +22 -0
  61. data/spec/dummy/config/boot.rb +5 -0
  62. data/spec/dummy/config/database.yml +25 -0
  63. data/spec/dummy/config/environment.rb +5 -0
  64. data/spec/dummy/config/environments/development.rb +29 -0
  65. data/spec/dummy/config/environments/production.rb +80 -0
  66. data/spec/dummy/config/environments/test.rb +36 -0
  67. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  68. data/spec/dummy/config/initializers/cryptocoin_payable.rb +23 -0
  69. data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  70. data/spec/dummy/config/initializers/inflections.rb +16 -0
  71. data/spec/dummy/config/initializers/mime_types.rb +5 -0
  72. data/spec/dummy/config/initializers/secret_token.rb +14 -0
  73. data/spec/dummy/config/initializers/session_store.rb +3 -0
  74. data/spec/dummy/config/initializers/state_machine.rb +10 -0
  75. data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
  76. data/spec/dummy/config/locales/en.yml +23 -0
  77. data/spec/dummy/config/routes.rb +56 -0
  78. data/spec/dummy/config.ru +4 -0
  79. data/spec/dummy/db/development.sqlite3 +0 -0
  80. data/spec/dummy/db/migrate/20140510023211_create_widgets.rb +5 -0
  81. data/spec/dummy/db/migrate/20171227225132_create_coin_payments.rb +19 -0
  82. data/spec/dummy/db/migrate/20171227225133_create_coin_payment_transactions.rb +16 -0
  83. data/spec/dummy/db/migrate/20171227225134_create_currency_conversions.rb +11 -0
  84. data/spec/dummy/db/schema.rb +54 -0
  85. data/spec/dummy/lib/assets/.keep +0 -0
  86. data/spec/dummy/log/.keep +0 -0
  87. data/spec/dummy/public/404.html +58 -0
  88. data/spec/dummy/public/422.html +58 -0
  89. data/spec/dummy/public/500.html +57 -0
  90. data/spec/dummy/public/favicon.ico +0 -0
  91. data/spec/dummy/test/fixtures/widgets.yml +11 -0
  92. data/spec/dummy/test/models/widget_test.rb +7 -0
  93. data/spec/fixtures/vcr_cassettes/CryptocoinPayable_Adapters_Bitcoin/gets_an_empty_result_when_no_transactions_found.yml +67 -0
  94. data/spec/fixtures/vcr_cassettes/CryptocoinPayable_Adapters_Bitcoin/gets_transactions_for_a_given_address.yml +73 -0
  95. data/spec/fixtures/vcr_cassettes/CryptocoinPayable_Adapters_Bitcoin/raises_an_error_when_an_invalid_address_is_passed.yml +103 -0
  96. data/spec/fixtures/vcr_cassettes/CryptocoinPayable_Adapters_Bitcoin/when_the_Block_Explorer_API_fails/falls_back_to_using_the_BlockCypher_API.yml +171 -0
  97. data/spec/fixtures/vcr_cassettes/CryptocoinPayable_Adapters_BitcoinCash/gets_an_empty_result_when_no_transactions_found.yml +62 -0
  98. data/spec/fixtures/vcr_cassettes/CryptocoinPayable_Adapters_BitcoinCash/gets_transactions_for_a_given_address.yml +65 -0
  99. data/spec/fixtures/vcr_cassettes/CryptocoinPayable_Adapters_Ethereum/gets_an_empty_result_when_no_transactions_found.yml +44 -0
  100. data/spec/fixtures/vcr_cassettes/CryptocoinPayable_Adapters_Ethereum/gets_transactions_for_a_given_address.yml +44 -0
  101. data/spec/fixtures/vcr_cassettes/CryptocoinPayable_Adapters_Ethereum/raises_an_error_when_an_invalid_address_is_passed.yml +44 -0
  102. data/spec/spec_helper.rb +112 -0
  103. metadata +428 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: '082e02924175a9102d410d319122b35c86e15704'
4
+ data.tar.gz: '0936ed5dac497c1edd445782753dbfb47c03701c'
5
+ SHA512:
6
+ metadata.gz: c5f7175563f7b3a5a9b9a5d9daf7f76c9515bec1abcc000b7bd0bf26a80cde297ff5a50c89dd5b56017319d407820c2405ff26d782dab40f15921bd678770a18
7
+ data.tar.gz: de2df907054a5e3c1f1f35077c83cf52a432041a32c5e4cf5bbd57ae4e3924689051df78f6eb8a63f9895d5a2ddb6d47ed28c3e3eac83f06406e5654c8e1e3d9
data/.gitignore ADDED
@@ -0,0 +1,5 @@
1
+ spec/dummy/log/*
2
+ *.gem
3
+ /Gemfile.lock
4
+ /spec/dummy/log/development.log
5
+ /spec/dummy/db/test.sqlite3
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --require spec_helper
data/.rubocop.yml ADDED
@@ -0,0 +1,36 @@
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'
data/Gemfile ADDED
@@ -0,0 +1,8 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in cryptocoin_payable.gemspec
4
+ gemspec
5
+
6
+ group :test do
7
+ gem 'cucumber-rails', require: false
8
+ end
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
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.
data/README.md ADDED
@@ -0,0 +1,237 @@
1
+ # Cryptocoin Payable
2
+
3
+ Forked from [Bitcoin Payable](https://github.com/Sailias/bitcoin_payable)
4
+
5
+ A rails gem that enables any model to have crypto coin payments.
6
+ 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:
7
+
8
+ - https://etherscan.io
9
+ - https://blockexplorer.com
10
+
11
+ Supported coins are:
12
+
13
+ - Bitcoin
14
+ - Bitcoin Cash
15
+ - Ethereum
16
+
17
+ Payments have the following states:
18
+
19
+ - `pending`
20
+ - `partial_payment`
21
+ - `paid_in_full`
22
+ - `comped` (useful for refunding payments)
23
+ - `confirmed` (enters state after n blockchain confirmations, see `confirmations` config option)
24
+ - `expired` (useful for auto-expiring incomplete payments, see `expire_payments_after` config option)
25
+
26
+ No private keys needed, No bitcoind blockchain indexing on new servers, just address and payments.
27
+
28
+ **Donations appreciated**
29
+
30
+ `142WJW4Zzc9iV7uFdbei8Unpe8WcLhUgmE`
31
+
32
+ ## Installation
33
+
34
+ Add this line to your application's Gemfile:
35
+
36
+ gem 'cryptocoin_payable', git: 'https://github.com/Sailias/cryptocoin_payable', branch: 'master'
37
+
38
+ And then execute:
39
+
40
+ $ bundle
41
+
42
+ $ rails g cryptocoin_payable:install
43
+
44
+ $ bundle exec rake db:migrate
45
+
46
+ $ populate coin_payable.rb (see below)
47
+
48
+ $ bundle exec rake cryptocoin_payable:process_prices (see below)
49
+
50
+ ## Uninstall
51
+
52
+ $ rails d cryptocoin_payable:install
53
+
54
+ ## Run Tests
55
+
56
+ cucumber features
57
+ rspec
58
+
59
+ ## Usage
60
+
61
+ ### Configuration
62
+
63
+ config/initializers/coin_payable.rb
64
+
65
+ CryptocoinPayable.configure do |config|
66
+ # config.currency = :usd
67
+ # config.testnet = true
68
+
69
+ config.request_delay = 0.5
70
+ config.expire_payments_after = 15.minutes
71
+
72
+ config.configure_btc do |btc_config|
73
+ # btc_config.confirmations = 3
74
+ # btc_config.node_path = ''
75
+
76
+ btc_config.master_public_key = 'tpub...'
77
+ end
78
+
79
+ config.configure_bch do |bch_config|
80
+ # bch_config.confirmations = 3
81
+ # btc_config.node_path = ''
82
+
83
+ bch_config.master_public_key = 'tpub...'
84
+ end
85
+
86
+ config.configure_eth do |eth_config|
87
+ # eth_config.confirmations = 12
88
+ # eth_config.node_path = ''
89
+
90
+ eth_config.master_public_key = 'tpub...'
91
+ end
92
+ end
93
+
94
+ In order to use the bitcoin network and issue real addresses, CryptocoinPayable.config.testnet must be set to false:
95
+
96
+ CryptocoinPayable.config.testnet = false
97
+
98
+ Consider adding a request delay (in seconds) to prevent API rate limit errors:
99
+
100
+ CryptocoinPayable.config.request_delay = 0.5
101
+
102
+ #### Node Path
103
+
104
+ The derivation path for the node that will be creating your addresses.
105
+
106
+ #### Master Public Key
107
+
108
+ A BIP32 MPK in "Extended Key" format used when configuring bitcoin payments (see `btc_config.master_public_key` above).
109
+
110
+ Public net starts with: xpub
111
+ Testnet starts with: tpub
112
+
113
+ * Obtain your BIP32 MPK from http://bip32.org/
114
+
115
+ ### Adding it to your model
116
+
117
+ class Product < ActiveRecord::Base
118
+ has_coin_payments
119
+ end
120
+
121
+ ### Creating a payment from your application
122
+
123
+ def create_payment(amount_in_cents)
124
+ self.coin_payments.create!(reason: 'sale', price: amount_in_cents, coin_type: :btc)
125
+ end
126
+
127
+ ### Update payments with the current price of BTC based on your currency
128
+
129
+ CryptocoinPayable also supports local currency conversions and BTC exchange rates.
130
+
131
+ The `process_prices` rake task connects to api.bitcoinaverage.com to get the 24 hour weighted average of BTC for your specified currency.
132
+ It then updates all payments that havent received an update in the last 30 minutes with the new value owing in BTC.
133
+ This *honors* the price of a payment for 30 minutes at a time.
134
+
135
+ `rake cryptocoin_payable:process_prices`
136
+
137
+ ### Processing payments
138
+
139
+ 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.
140
+ 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.
141
+ This is very valuable for accounting later. (capital gains of all payments received)
142
+
143
+ If a partial payment is made, the BTC value is recalculated for the remaining *dollar* amount with the latest exchange rate.
144
+ 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
145
+ 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)
146
+
147
+ This prevents people from gaming the payments by paying very little BTC in hopes the price will rise.
148
+ Payments are not recalculated based on the current value of BTC, but in dollars.
149
+
150
+ To run the payment processor:
151
+
152
+ `rake cryptocoin_payable:process_payments`
153
+
154
+ ### Notify your application when a payment is made
155
+
156
+ Use the `coin_payment_paid` and `coin_payment_confirmed` methods
157
+
158
+ def Product < ActiveRecord::Base
159
+ has_coin_payments
160
+
161
+ def create_payment(amount_in_cents)
162
+ self.coin_payments.create!(reason: 'sale', price: amount_in_cents, type: :btc)
163
+ end
164
+
165
+ # Runs when the payment is first detected on the network.
166
+ def coin_payment_paid(payment)
167
+ self.notify!
168
+ end
169
+
170
+ # Runs when enough confirmations have occurred.
171
+ def coin_payment_confirmed(payment)
172
+ self.ship!
173
+ end
174
+ end
175
+
176
+ ### Delete old CurrencyConversion data
177
+
178
+ Every time the payment processor is run, several rows are inserted into the
179
+ database to record the value of the coin at a given instance in time. Over time,
180
+ your application will accumulate historical currency conversion data and you may
181
+ want to clear it out:
182
+
183
+ ```
184
+ rake cryptocoin_payable:delete_currency_conversions
185
+ ```
186
+
187
+ By default, it will delete any data older than 1 month. You can configure this
188
+ using an env variable:
189
+
190
+ ```
191
+ DELETE_BEFORE=2017-12-15 rake cryptocoin_payable:delete_currency_conversions
192
+ ```
193
+
194
+ ### Comp a payment
195
+
196
+ This will bypass the payment, set the state to comped and call back to your app that the payment has been processed.
197
+
198
+ `@coin_payment.comp`
199
+
200
+ ### Expire a payment
201
+
202
+ `@coin_payment.expire`
203
+
204
+ Payments will auto-expire if you set the `expire_payments_after` option. The
205
+ exact timing is not precise because payment expiry is evaluated whenever
206
+ payment_processor runs.
207
+
208
+ ### View all the transactions in the payment
209
+
210
+ coin_payment = @product.coin_payments.first
211
+ coin_payment.transactions.find_each do |transaction|
212
+ puts transaction.block_hash
213
+ puts transaction.block_time
214
+
215
+ puts transaction.transaction_hash
216
+
217
+ puts transaction.estimated_value
218
+ puts transaction.estimated_time
219
+
220
+ puts transaction.coin_conversion
221
+
222
+ puts transaction.confirmations
223
+ end
224
+
225
+ ## Contributing
226
+
227
+ 1. Fork it
228
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
229
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
230
+ 4. Push to the branch (`git push origin my-new-feature`)
231
+ 5. Create new Pull Request
232
+
233
+ ## Contributors
234
+
235
+ * andersonlewin
236
+ * krtschmr
237
+ * mhluska
data/Rakefile ADDED
@@ -0,0 +1,3 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rake'
3
+ require 'cryptocoin_payable/tasks'
data/bin/console ADDED
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'bundler/setup'
4
+ require 'active_record'
5
+ require 'cryptocoin_payable'
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__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
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
@@ -0,0 +1,39 @@
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']
10
+ spec.email = ['jsalis@bitcoinsultants.ca']
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
+
22
+ spec.add_development_dependency 'activerecord', '~> 5.1'
23
+ spec.add_development_dependency 'bundler', '~> 1.15'
24
+ spec.add_development_dependency 'cucumber', '~> 3.1'
25
+ spec.add_development_dependency 'database_cleaner', '~> 1.7'
26
+ spec.add_development_dependency 'rake', '~> 12.3'
27
+ spec.add_development_dependency 'rspec-rails', '~> 3.7'
28
+ spec.add_development_dependency 'rubocop', '~> 0.59'
29
+ spec.add_development_dependency 'sqlite3', '~> 1.3'
30
+ spec.add_development_dependency 'vcr', '~> 4.0'
31
+ spec.add_development_dependency 'webmock', '~> 3.4'
32
+
33
+ spec.add_dependency 'cash-addr', '~> 0.2'
34
+ spec.add_dependency 'eth', '0.4.8'
35
+ spec.add_dependency 'money-tree', '0.10.0'
36
+ spec.add_dependency 'rails'
37
+ spec.add_dependency 'state_machine', '~> 1.2'
38
+ end
39
+ # rubocop:enable Metrics/BlockLength
@@ -0,0 +1,51 @@
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%
@@ -0,0 +1,5 @@
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
@@ -0,0 +1,4 @@
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
@@ -0,0 +1,45 @@
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
+ tx_hash: SecureRandom.uuid,
26
+ block_hash: '00000000000000606aa74093ed91d657192a3772732ee4d99a7b7be8075eafa2',
27
+ block_time: DateTime.iso8601('2017-12-26T21:38:44.000+00:00'),
28
+ estimated_tx_time: DateTime.iso8601('2017-12-26T21:30:19.858+00:00'),
29
+ estimated_tx_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
@@ -0,0 +1,12 @@
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
@@ -0,0 +1,11 @@
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
@@ -0,0 +1,7 @@
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
@@ -0,0 +1,3 @@
1
+ Given(/^the widget should have (\d+) coin_payments$/) do |n|
2
+ expect(@widget.coin_payments.count).to eq(n.to_i)
3
+ end
@@ -0,0 +1,26 @@
1
+ ENV['RAILS_ENV'] ||= 'test'
2
+ require File.expand_path('../../spec/dummy/config/environment.rb', __dir__)
3
+
4
+ ENV['RAILS_ROOT'] ||= File.dirname(__FILE__) + '../../../spec/dummy'
5
+
6
+ require 'cucumber/rails'
7
+ require 'cucumber/rspec/doubles'
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
@@ -0,0 +1,99 @@
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
+ # tx_hash: string,
20
+ # block_hash: string,
21
+ # block_time: nil | string,
22
+ # estimated_tx_time: nil | string,
23
+ # estimated_tx_value: integer,
24
+ # confirmations: integer,
25
+ # }
26
+ # `block_time` and `estimated_tx_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 parse_timestamp(timestamp)
76
+ timestamp.nil? ? nil : DateTime.strptime(timestamp.to_s, '%s')
77
+ end
78
+
79
+ def parse_time(time)
80
+ time.nil? ? nil : DateTime.iso8601(time)
81
+ end
82
+
83
+ private
84
+
85
+ def get_request(url)
86
+ uri = URI.parse(url)
87
+ http = Net::HTTP.new(uri.host, uri.port)
88
+ http.use_ssl = uri.scheme == 'https'
89
+ request = Net::HTTP::Get.new(uri.request_uri)
90
+ result = http.request(request)
91
+
92
+ request_delay = CryptocoinPayable.configuration.request_delay
93
+ sleep request_delay if request_delay
94
+
95
+ result
96
+ end
97
+ end
98
+ end
99
+ end