cryptocoin_payable 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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