cryptocoin_payable 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +5 -0
- data/.rspec +1 -0
- data/.rubocop.yml +36 -0
- data/Gemfile +8 -0
- data/LICENSE.txt +22 -0
- data/README.md +237 -0
- data/Rakefile +3 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/cryptocoin_payable.gemspec +39 -0
- data/features/coin_payments.feature +51 -0
- data/features/default.feature +5 -0
- data/features/pricing_processor.feature +4 -0
- data/features/step_definitions/coin_payment_steps.rb +45 -0
- data/features/step_definitions/currency_conversion_steps.rb +12 -0
- data/features/step_definitions/model_step.rb +11 -0
- data/features/step_definitions/processor_steps.rb +7 -0
- data/features/step_definitions/widget_steps.rb +3 -0
- data/features/support/env.rb +26 -0
- data/lib/cryptocoin_payable/adapters/base.rb +99 -0
- data/lib/cryptocoin_payable/adapters/bitcoin.rb +93 -0
- data/lib/cryptocoin_payable/adapters/bitcoin_cash.rb +34 -0
- data/lib/cryptocoin_payable/adapters/ethereum.rb +77 -0
- data/lib/cryptocoin_payable/adapters.rb +27 -0
- data/lib/cryptocoin_payable/coin_payment.rb +141 -0
- data/lib/cryptocoin_payable/coin_payment_transaction.rb +5 -0
- data/lib/cryptocoin_payable/commands/payment_processor.rb +67 -0
- data/lib/cryptocoin_payable/commands/pricing_processor.rb +43 -0
- data/lib/cryptocoin_payable/config.rb +65 -0
- data/lib/cryptocoin_payable/currency_conversion.rb +11 -0
- data/lib/cryptocoin_payable/errors.rb +7 -0
- data/lib/cryptocoin_payable/has_coin_payments.rb +15 -0
- data/lib/cryptocoin_payable/tasks.rb +20 -0
- data/lib/cryptocoin_payable/version.rb +3 -0
- data/lib/cryptocoin_payable.rb +18 -0
- data/lib/generators/cryptocoin_payable/install_generator.rb +27 -0
- data/lib/generators/cryptocoin_payable/templates/create_coin_payment_transactions.rb +16 -0
- data/lib/generators/cryptocoin_payable/templates/create_coin_payments.rb +19 -0
- data/lib/generators/cryptocoin_payable/templates/create_currency_conversions.rb +11 -0
- data/spec/acceptance/adapters/bitcoin_cash_spec.rb +54 -0
- data/spec/acceptance/adapters/bitcoin_spec.rb +79 -0
- data/spec/acceptance/adapters/ethereum_spec.rb +77 -0
- data/spec/dummy/README.rdoc +28 -0
- data/spec/dummy/Rakefile +6 -0
- data/spec/dummy/app/assets/images/.keep +0 -0
- data/spec/dummy/app/assets/javascripts/application.js +13 -0
- data/spec/dummy/app/assets/stylesheets/application.css +13 -0
- data/spec/dummy/app/controllers/application_controller.rb +5 -0
- data/spec/dummy/app/controllers/concerns/.keep +0 -0
- data/spec/dummy/app/helpers/application_helper.rb +2 -0
- data/spec/dummy/app/mailers/.keep +0 -0
- data/spec/dummy/app/models/.keep +0 -0
- data/spec/dummy/app/models/concerns/.keep +0 -0
- data/spec/dummy/app/models/widget.rb +3 -0
- data/spec/dummy/app/views/layouts/application.html.erb +14 -0
- data/spec/dummy/bin/bundle +3 -0
- data/spec/dummy/bin/rails +4 -0
- data/spec/dummy/bin/rake +4 -0
- data/spec/dummy/config/application.rb +22 -0
- data/spec/dummy/config/boot.rb +5 -0
- data/spec/dummy/config/database.yml +25 -0
- data/spec/dummy/config/environment.rb +5 -0
- data/spec/dummy/config/environments/development.rb +29 -0
- data/spec/dummy/config/environments/production.rb +80 -0
- data/spec/dummy/config/environments/test.rb +36 -0
- data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/spec/dummy/config/initializers/cryptocoin_payable.rb +23 -0
- data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
- data/spec/dummy/config/initializers/inflections.rb +16 -0
- data/spec/dummy/config/initializers/mime_types.rb +5 -0
- data/spec/dummy/config/initializers/secret_token.rb +14 -0
- data/spec/dummy/config/initializers/session_store.rb +3 -0
- data/spec/dummy/config/initializers/state_machine.rb +10 -0
- data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/spec/dummy/config/locales/en.yml +23 -0
- data/spec/dummy/config/routes.rb +56 -0
- data/spec/dummy/config.ru +4 -0
- data/spec/dummy/db/development.sqlite3 +0 -0
- data/spec/dummy/db/migrate/20140510023211_create_widgets.rb +5 -0
- data/spec/dummy/db/migrate/20171227225132_create_coin_payments.rb +19 -0
- data/spec/dummy/db/migrate/20171227225133_create_coin_payment_transactions.rb +16 -0
- data/spec/dummy/db/migrate/20171227225134_create_currency_conversions.rb +11 -0
- data/spec/dummy/db/schema.rb +54 -0
- data/spec/dummy/lib/assets/.keep +0 -0
- data/spec/dummy/log/.keep +0 -0
- data/spec/dummy/public/404.html +58 -0
- data/spec/dummy/public/422.html +58 -0
- data/spec/dummy/public/500.html +57 -0
- data/spec/dummy/public/favicon.ico +0 -0
- data/spec/dummy/test/fixtures/widgets.yml +11 -0
- data/spec/dummy/test/models/widget_test.rb +7 -0
- data/spec/fixtures/vcr_cassettes/CryptocoinPayable_Adapters_Bitcoin/gets_an_empty_result_when_no_transactions_found.yml +67 -0
- data/spec/fixtures/vcr_cassettes/CryptocoinPayable_Adapters_Bitcoin/gets_transactions_for_a_given_address.yml +73 -0
- data/spec/fixtures/vcr_cassettes/CryptocoinPayable_Adapters_Bitcoin/raises_an_error_when_an_invalid_address_is_passed.yml +103 -0
- data/spec/fixtures/vcr_cassettes/CryptocoinPayable_Adapters_Bitcoin/when_the_Block_Explorer_API_fails/falls_back_to_using_the_BlockCypher_API.yml +171 -0
- data/spec/fixtures/vcr_cassettes/CryptocoinPayable_Adapters_BitcoinCash/gets_an_empty_result_when_no_transactions_found.yml +62 -0
- data/spec/fixtures/vcr_cassettes/CryptocoinPayable_Adapters_BitcoinCash/gets_transactions_for_a_given_address.yml +65 -0
- data/spec/fixtures/vcr_cassettes/CryptocoinPayable_Adapters_Ethereum/gets_an_empty_result_when_no_transactions_found.yml +44 -0
- data/spec/fixtures/vcr_cassettes/CryptocoinPayable_Adapters_Ethereum/gets_transactions_for_a_given_address.yml +44 -0
- data/spec/fixtures/vcr_cassettes/CryptocoinPayable_Adapters_Ethereum/raises_an_error_when_an_invalid_address_is_passed.yml +44 -0
- data/spec/spec_helper.rb +112 -0
- 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
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
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
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,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,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,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
|