straight-server 0.2.2 → 0.2.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile +2 -0
- data/Gemfile.lock +5 -2
- data/Gemfile.travis +2 -0
- data/README.md +46 -19
- data/Rakefile +5 -2
- data/VERSION +1 -1
- data/db/migrations/003_add_payment_id_to_orders.rb +1 -1
- data/db/migrations/004_add_description_to_orders.rb +1 -1
- data/db/migrations/005_add_orders_expiration_period_to_gateways.rb +1 -1
- data/db/migrations/006_add_check_order_status_in_db_first_to_gateways.rb +1 -1
- data/db/migrations/007_add_active_switcher_to_gateways.rb +1 -1
- data/db/migrations/008_add_order_counters_to_gateways.rb +1 -1
- data/db/migrations/009_add_hashed_id_to_gateways.rb +2 -2
- data/db/migrations/010_add_address_reusability_orders.rb +19 -0
- data/db/migrations/011_add_callback_data_to_orders.rb +11 -0
- data/db/schema.rb +55 -0
- data/examples/client/client.html +1 -1
- data/examples/client/client.js +1 -1
- data/lib/straight-server/config.rb +3 -1
- data/lib/straight-server/gateway.rb +133 -34
- data/lib/straight-server/initializer.rb +8 -7
- data/lib/straight-server/order.rb +12 -5
- data/lib/straight-server/orders_controller.rb +45 -13
- data/lib/straight-server/throttler.rb +63 -0
- data/lib/tasks/db.rake +42 -0
- data/spec/.straight/config.yml +3 -2
- data/spec/lib/gateway_spec.rb +88 -9
- data/spec/lib/order_spec.rb +8 -13
- data/spec/lib/orders_controller_spec.rb +36 -4
- data/spec/lib/throttle_spec.rb +52 -0
- data/straight-server.gemspec +13 -6
- data/templates/config.yml +19 -2
- metadata +22 -6
- data/bin/goliath.log +0 -6
- data/bin/goliath.log_stdout.log +0 -51
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3d3e00cdba620b59557e0f28a51cdc3a82a7b953
|
4
|
+
data.tar.gz: 53372b11a9ddd0045bba84f4b42e9c2706ec9668
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0ca180d944906bd732574b91794012e0661e09de8d47900906ccd06e9269746aaf2a39be3ceffd02ccb2e9e4f9a89e4b9d824ceee2293a9b4c69f9ffc8473f44
|
7
|
+
data.tar.gz: 656718bdf4cc21510fa657efd33265545e7d4cc9a0b47f27e263dfd888e5d55c6216056f897b2af2555e414622731ffaa30313824b67d31b21b9556a54ee2bd6
|
data/Gemfile
CHANGED
@@ -8,6 +8,7 @@ gem "sequel"
|
|
8
8
|
gem "logmaster", '0.1.5'
|
9
9
|
gem "ruby-hmac"
|
10
10
|
gem "httparty"
|
11
|
+
gem "money-tree", "0.9.0"
|
11
12
|
|
12
13
|
# Add dependencies to develop your gem here.
|
13
14
|
# Include everything needed to run rake, tests, features, etc.
|
@@ -18,6 +19,7 @@ group :development do
|
|
18
19
|
end
|
19
20
|
|
20
21
|
group :test do
|
22
|
+
gem 'timecop'
|
21
23
|
gem 'rspec'
|
22
24
|
gem 'factory_girl'
|
23
25
|
gem 'sqlite3'
|
data/Gemfile.lock
CHANGED
@@ -51,7 +51,7 @@ GEM
|
|
51
51
|
hashie (3.4.1)
|
52
52
|
highline (1.7.2)
|
53
53
|
http_parser.rb (0.6.0)
|
54
|
-
httparty (0.13.
|
54
|
+
httparty (0.13.5)
|
55
55
|
json (~> 1.8)
|
56
56
|
multi_xml (>= 0.5.2)
|
57
57
|
i18n (0.7.0)
|
@@ -74,7 +74,7 @@ GEM
|
|
74
74
|
mime-types (2.5)
|
75
75
|
mini_portile (0.6.2)
|
76
76
|
minitest (5.6.1)
|
77
|
-
money-tree (0.
|
77
|
+
money-tree (0.9.0)
|
78
78
|
ffi
|
79
79
|
multi_json (1.11.0)
|
80
80
|
multi_xml (0.5.5)
|
@@ -120,6 +120,7 @@ GEM
|
|
120
120
|
money-tree
|
121
121
|
satoshi-unit
|
122
122
|
thread_safe (0.3.5)
|
123
|
+
timecop (0.7.3)
|
123
124
|
tzinfo (1.2.2)
|
124
125
|
thread_safe (~> 0.1)
|
125
126
|
websocket-driver (0.5.4)
|
@@ -139,6 +140,7 @@ DEPENDENCIES
|
|
139
140
|
httparty
|
140
141
|
jeweler (~> 2.0.1)
|
141
142
|
logmaster (= 0.1.5)
|
143
|
+
money-tree (= 0.9.0)
|
142
144
|
redis
|
143
145
|
rspec
|
144
146
|
ruby-hmac
|
@@ -146,3 +148,4 @@ DEPENDENCIES
|
|
146
148
|
sequel
|
147
149
|
sqlite3
|
148
150
|
straight (= 0.2.2)
|
151
|
+
timecop
|
data/Gemfile.travis
CHANGED
@@ -8,6 +8,7 @@ gem "sequel"
|
|
8
8
|
gem "logmaster", '0.1.5'
|
9
9
|
gem "ruby-hmac"
|
10
10
|
gem "httparty"
|
11
|
+
gem "money-tree", "0.9.0"
|
11
12
|
|
12
13
|
# Add dependencies to develop your gem here.
|
13
14
|
# Include everything needed to run rake, tests, features, etc.
|
@@ -18,6 +19,7 @@ group :development do
|
|
18
19
|
end
|
19
20
|
|
20
21
|
group :test do
|
22
|
+
gem 'timecop'
|
21
23
|
gem 'rspec'
|
22
24
|
gem 'factory_girl'
|
23
25
|
gem 'sqlite3'
|
data/README.md
CHANGED
@@ -61,20 +61,21 @@ Below I assume it runs on localhost on port 9696.
|
|
61
61
|
|
62
62
|
the result of this request will be the following json:
|
63
63
|
|
64
|
-
{"status":0,"amount":1,"address":"1NZov2nm6gRCGW6r4q1qHtxXurrWNpPr1q","tid":null,"id":1 }
|
64
|
+
{"status":0,"amount":1,"address":"1NZov2nm6gRCGW6r4q1qHtxXurrWNpPr1q","tid":null,"id":1, keychain_id: 1, last_keychain_id: 1 }
|
65
65
|
|
66
66
|
Now you can obviously use that output to provide your user with the address and the expected
|
67
67
|
amount to be sent there. At this point, the server starts automatically tracking the order address
|
68
68
|
in a separate thread, so that when the money arrive, a callback will be issued to the url provided
|
69
69
|
in the `~/.straight/config.yml` file for the current gateway. This callback request will contain order info too.
|
70
|
+
|
70
71
|
Here's an example of a callback url request that could be made by Straight server when order status changes:
|
71
72
|
|
72
|
-
GET http://mystore.com/payment-callback?order_id=
|
73
|
+
GET http://mystore.com/payment-callback?order_id=234&amount=1&status=2&address=1NZov2nm6gRCGW6r4q1qHtxXurrWNpPr1q&tid=tid1&callback_data=some+random+data&keychain_id=1&last_keychain_id=1
|
73
74
|
|
74
|
-
As you may have noticed, there's a parameter called `
|
75
|
-
to your app. It will have the same value as the `
|
75
|
+
As you may have noticed, there's a parameter called `callback_data`. It is a way for you to pass info back
|
76
|
+
to your app. It will have the same value as the `callback_data` parameter you passed to the create order request:
|
76
77
|
|
77
|
-
POST /gateways/1/orders?amount=1&
|
78
|
+
POST /gateways/1/orders?amount=1&callback_data=some+random+data
|
78
79
|
|
79
80
|
You can specify amount in other currencies, as well as various BTC denominations.
|
80
81
|
It will be converted using the current exchange rate (see [Straight::ExchangeAdapter](https://github.com/snitko/straight/blob/master/lib/straight/exchange_rate_adapter.rb)) into satoshis:
|
@@ -89,11 +90,13 @@ It will be converted using the current exchange rate (see [Straight::ExchangeAda
|
|
89
90
|
**Checking the order manually**
|
90
91
|
You can check the status of the order manually with the following request:
|
91
92
|
|
92
|
-
GET /gateways/1/orders
|
93
|
+
GET /gateways/1/orders/:id
|
93
94
|
|
94
|
-
|
95
|
+
where `:id` can either be order `id` (CAUTION: order `id` is NOT the same as `keychain_id`) or
|
96
|
+
`payment_id` - both are returned in the json data when the order
|
97
|
+
is created (see above). The request above may return something like:
|
95
98
|
|
96
|
-
{"status":2,"amount":1,"address":"1NZov2nm6gRCGW6r4q1qHtxXurrWNpPr1q","tid":"f0f9205e41bf1b79cb7634912e86bb840cedf8b1d108bd2faae1651ca79a5838","id":1 }
|
99
|
+
{"status":2,"amount":1,"address":"1NZov2nm6gRCGW6r4q1qHtxXurrWNpPr1q","tid":"f0f9205e41bf1b79cb7634912e86bb840cedf8b1d108bd2faae1651ca79a5838","id":1, "keychain_id": 1, "last_keychain_id": 1 }
|
97
100
|
|
98
101
|
**Subscribing to the order using websockets**:
|
99
102
|
You can also subscribe to the order status changes using websockets at:
|
@@ -195,13 +198,12 @@ Go to your `~/.straight/config.yml` directory and set two options for each of yo
|
|
195
198
|
check_signature: true
|
196
199
|
|
197
200
|
This will force gateways to check signatures when you try to create a new order. A signature is
|
198
|
-
a HMAC SHA256 hash of the secret and
|
201
|
+
a HMAC SHA256 hash of the secret and a keychain_id. Because you need keychain_id, it means you have
|
199
202
|
to actually provide it manually in the params. It can be any integer > 0, but it's better
|
200
|
-
that it is a consecutive integer, so keep track of
|
201
|
-
|
202
|
-
(assuming secret is the line mentioned above in the sample config) would look like this:
|
203
|
+
that it is a consecutive integer, so keep track of the last keychain_id that was used in your
|
204
|
+
application. A possible request (assuming secret is the line mentioned above in the sample config) would look like this:
|
203
205
|
|
204
|
-
POST /gateways/1/orders?amount=1&
|
206
|
+
POST /gateways/1/orders?amount=1&keychain_id=1&signature=aa14c26b2ae892a8719b0c2c57f162b967bfbfbdcc38d8883714a0680cf20467
|
205
207
|
|
206
208
|
An example of obtaining such signature in Ruby:
|
207
209
|
|
@@ -210,21 +212,46 @@ An example of obtaining such signature in Ruby:
|
|
210
212
|
secret = 'a long string of random chars'
|
211
213
|
OpenSSL::HMAC.digest('sha256', secret, "1").unpack("H*").first # "1" may be order_id here
|
212
214
|
|
213
|
-
Straight server will also sign the callback url request. However, since
|
214
|
-
|
215
|
-
|
215
|
+
Straight server will also sign the callback url request. However, since keychain_id may potentially
|
216
|
+
be shared between 2 or more orders, the callback signature is based on internal `order_id` returned
|
217
|
+
with the json after you create the said order. Here's an example of such a signature:
|
216
218
|
|
219
|
+
order.id #=> 234
|
217
220
|
secret = 'a long string of random chars'
|
218
|
-
|
219
|
-
h2 = OpenSSL::HMAC.digest('sha256', secret, h1).unpack("H*").first
|
221
|
+
h = OpenSSL::HMAC.digest('sha256', secret, 234).unpack("H*").first
|
220
222
|
|
221
223
|
and then send the request to the callback url with that signature:
|
222
224
|
|
223
|
-
GET http://mystore.com/payment-callback?order_id=
|
225
|
+
GET http://mystore.com/payment-callback?order_id=234&amount=1&status=2&address=1NZov2nm6gRCGW6r4q1qHtxXurrWNpPr1q&tid=tid1&callback_data=some+random+data?signature=aa14c26b2ae892a8719b0c2c57f162b967bfbfbdcc38d8883714a0680cf20467&keychain_id=1&last_keychain_id=1
|
224
226
|
|
225
227
|
It is now up to your application to calculate that signature, compare it and
|
226
228
|
make sure that only one such request is allowed (that is, if signature was used, it cannot be used again).
|
227
229
|
|
230
|
+
What is keychain_id and why do we need it?
|
231
|
+
------------------------------------------
|
232
|
+
|
233
|
+
`keychain_id` is used to derive the next address from your BIP32 pubkey.
|
234
|
+
If you try to create orders with the same `keychain_id` they will also have the same
|
235
|
+
address, which is, as you can imagine, not a very good idea. However it is allowed and there's
|
236
|
+
a good reason for that.
|
237
|
+
|
238
|
+
Wallets that support BIP32 pubkeys will only do a forward address lookup for a limited number of
|
239
|
+
addreses. For example, if you have 20 expired, unpaid orders and someone sends you money to the address
|
240
|
+
of the 21-st order, your wallet may not see that. Thus, it is important to ensure that there are
|
241
|
+
no more than N expired orders in a row. The respective setting in the config file is called
|
242
|
+
`reuse_address_orders_threshold` and the default value is 20.
|
243
|
+
|
244
|
+
If you have 20 orders in a row and try to create another one, straight-server will see that and will
|
245
|
+
automatically reuse the `keychain_id` (and consequently, the address too) of the 20-th order. It will
|
246
|
+
also set the 21-st order's `reused` field to the value of `1`.
|
247
|
+
|
248
|
+
CAUTION: while you don't need to provide `keychain_id` when creating orders with gateways that
|
249
|
+
do not require signatures, you still must do it with gateways that do require signatures.
|
250
|
+
In this case, it is very important to make sure that you don't accidentally provide `keychain_id`
|
251
|
+
that is too far away from the last used one. For example, if the gateway's `last_keychain_id` is `10`,
|
252
|
+
do not use `35` for the next order, use `11`. `last_gateway_id` is always returned with other info
|
253
|
+
when you create or check order status.
|
254
|
+
|
228
255
|
Querying the blockchain
|
229
256
|
-----------------------
|
230
257
|
Straight currently uses third-party services, such as Blokchain.info and Helloblock.io to track
|
data/Rakefile
CHANGED
@@ -2,14 +2,17 @@
|
|
2
2
|
|
3
3
|
require 'rubygems'
|
4
4
|
require 'bundler'
|
5
|
+
require 'rake'
|
6
|
+
|
5
7
|
begin
|
6
|
-
Bundler.setup(:default, :development)
|
8
|
+
Bundler.setup(:default, :development, :test)
|
7
9
|
rescue Bundler::BundlerError => e
|
8
10
|
$stderr.puts e.message
|
9
11
|
$stderr.puts "Run `bundle install` to install missing gems"
|
10
12
|
exit e.status_code
|
11
13
|
end
|
12
|
-
|
14
|
+
|
15
|
+
Dir.glob('lib/tasks/*.rake').each { |r| load r }
|
13
16
|
|
14
17
|
require 'jeweler'
|
15
18
|
Jeweler::Tasks.new do |gem|
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.2.
|
1
|
+
0.2.3
|
@@ -0,0 +1,19 @@
|
|
1
|
+
Sequel.migration do
|
2
|
+
|
3
|
+
up do
|
4
|
+
drop_index :orders, [:keychain_id, :gateway_id]
|
5
|
+
drop_index :orders, :address
|
6
|
+
add_index :orders, [:keychain_id, :gateway_id]
|
7
|
+
add_index :orders, :address
|
8
|
+
add_column :orders, :reused, Integer, default: 0
|
9
|
+
end
|
10
|
+
|
11
|
+
down do
|
12
|
+
drop_index :orders, [:keychain_id, :gateway_id]
|
13
|
+
drop_index :orders, :address
|
14
|
+
drop_column :orders, :reused
|
15
|
+
add_index :orders, [:keychain_id, :gateway_id], unique: true
|
16
|
+
add_index :orders, :address, unique: true
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
data/db/schema.rb
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
Sequel.migration do
|
2
|
+
change do
|
3
|
+
create_table(:gateways, :ignore_index_errors=>true) do
|
4
|
+
primary_key :id
|
5
|
+
Integer :confirmations_required, :default=>0, :null=>false
|
6
|
+
Integer :last_keychain_id, :default=>0, :null=>false
|
7
|
+
String :pubkey, :size=>255, :null=>false
|
8
|
+
String :order_class, :size=>255, :null=>false
|
9
|
+
String :secret, :size=>255, :null=>false
|
10
|
+
String :name, :size=>255, :null=>false
|
11
|
+
String :default_currency, :default=>"BTC", :size=>255
|
12
|
+
String :callback_url, :size=>255
|
13
|
+
TrueClass :check_signature, :default=>false, :null=>false
|
14
|
+
String :exchange_rate_adapter_names, :size=>255
|
15
|
+
DateTime :created_at, :null=>false
|
16
|
+
DateTime :updated_at
|
17
|
+
Integer :orders_expiration_period
|
18
|
+
TrueClass :check_order_status_in_db_first
|
19
|
+
TrueClass :active, :default=>true
|
20
|
+
String :order_counters, :size=>255
|
21
|
+
String :hashed_id, :size=>255
|
22
|
+
|
23
|
+
index [:hashed_id]
|
24
|
+
index [:id], :unique=>true
|
25
|
+
index [:name], :unique=>true
|
26
|
+
index [:pubkey], :unique=>true
|
27
|
+
end
|
28
|
+
|
29
|
+
create_table(:orders, :ignore_index_errors=>true) do
|
30
|
+
primary_key :id
|
31
|
+
String :address, :size=>255, :null=>false
|
32
|
+
String :tid, :size=>255
|
33
|
+
Integer :status, :default=>0, :null=>false
|
34
|
+
Integer :keychain_id, :null=>false
|
35
|
+
Bignum :amount, :null=>false
|
36
|
+
Integer :gateway_id, :null=>false
|
37
|
+
String :data, :size=>255
|
38
|
+
String :callback_response, :text=>true
|
39
|
+
DateTime :created_at, :null=>false
|
40
|
+
DateTime :updated_at
|
41
|
+
String :payment_id, :size=>255
|
42
|
+
String :description, :size=>255
|
43
|
+
Integer :reused, :default=>0
|
44
|
+
|
45
|
+
index [:address]
|
46
|
+
index [:id], :unique=>true
|
47
|
+
index [:keychain_id, :gateway_id]
|
48
|
+
index [:payment_id], :unique=>true
|
49
|
+
end
|
50
|
+
|
51
|
+
create_table(:schema_info) do
|
52
|
+
Integer :version, :default=>0, :null=>false
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
data/examples/client/client.html
CHANGED
@@ -16,7 +16,7 @@
|
|
16
16
|
<p>Use this form to generate a new order:</p>
|
17
17
|
Gateway id: <input name="gateway_id"/><br/>
|
18
18
|
Signature: <input name="signature"/><br/>
|
19
|
-
Keychain id: <input name="
|
19
|
+
Keychain id: <input name="keychain_id"/><br/>
|
20
20
|
Amount (in default currency for the gateway, usually in satoshi, but could be USD or EUR): <input name="amount"/>
|
21
21
|
<p><button id="create_order">Create Order</button></p>
|
22
22
|
</div>
|
data/examples/client/client.js
CHANGED
@@ -5,7 +5,7 @@ jQuery(function($) {
|
|
5
5
|
url: '/gateways/' + $("input[name=gateway_id]").val() + '/orders',
|
6
6
|
type: 'POST',
|
7
7
|
dataType: 'json',
|
8
|
-
data: { amount: $("input[name=amount]").val(), signature: $("input[name=signature]").val(),
|
8
|
+
data: { amount: $("input[name=amount]").val(), signature: $("input[name=signature]").val(), keychain_id :$("input[name=keychain_id]").val() },
|
9
9
|
success: function(response) {
|
10
10
|
window.location = '/pay/' + response.payment_id
|
11
11
|
}
|
@@ -57,7 +57,7 @@ module StraightServer
|
|
57
57
|
begin
|
58
58
|
@exchange_rate_adapters << Straight::ExchangeRate.const_get("#{adapter}Adapter").instance
|
59
59
|
rescue NameError => e
|
60
|
-
puts "WARNING: No exchange rate adapter with the name #{
|
60
|
+
puts "WARNING: No exchange rate adapter with the name #{adapter} was found!"
|
61
61
|
end
|
62
62
|
end
|
63
63
|
end
|
@@ -113,16 +113,28 @@ module StraightServer
|
|
113
113
|
signature = attrs.delete(:signature)
|
114
114
|
if !check_signature || sign_with_secret(attrs[:keychain_id]) == signature
|
115
115
|
raise InvalidOrderId if check_signature && (attrs[:keychain_id].nil? || attrs[:keychain_id].to_i <= 0)
|
116
|
+
|
117
|
+
# If we decide to reuse the order, we simply need to supply the
|
118
|
+
# keychain_id that was used in the order we're reusing.
|
119
|
+
# The address will be generated correctly.
|
120
|
+
if reused_order = find_reusable_order
|
121
|
+
attrs[:keychain_id] = reused_order.keychain_id
|
122
|
+
end
|
123
|
+
|
116
124
|
order = order_for_keychain_id(
|
117
125
|
amount: attrs[:amount],
|
118
|
-
keychain_id: attrs[:keychain_id] ||
|
126
|
+
keychain_id: attrs[:keychain_id] || self.last_keychain_id+1,
|
119
127
|
currency: attrs[:currency],
|
120
128
|
btc_denomination: attrs[:btc_denomination]
|
121
129
|
)
|
122
|
-
order.id
|
123
|
-
order.data
|
124
|
-
order.
|
130
|
+
order.id = attrs[:id].to_i if attrs[:id]
|
131
|
+
order.data = attrs[:data] if attrs[:data]
|
132
|
+
order.callback_data = attrs[:callback_data] if attrs[:callback_data]
|
133
|
+
order.gateway = self
|
134
|
+
order.reused = reused_order.reused + 1 if reused_order
|
125
135
|
order.save
|
136
|
+
|
137
|
+
self.update_last_keychain_id(attrs[:keychain_id]) unless order.reused > 0
|
126
138
|
self.save
|
127
139
|
StraightServer.logger.info "Order #{order.id} created: #{order.to_h}"
|
128
140
|
order
|
@@ -132,14 +144,9 @@ module StraightServer
|
|
132
144
|
end
|
133
145
|
end
|
134
146
|
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
# is stored in a file in the .straight directory.
|
139
|
-
def increment_last_keychain_id!
|
140
|
-
self.last_keychain_id += 1
|
141
|
-
self.save
|
142
|
-
self.last_keychain_id
|
147
|
+
def update_last_keychain_id(new_value=nil)
|
148
|
+
#new_value = nil if new_value && new_value.empty?
|
149
|
+
new_value ? self.last_keychain_id = new_value : self.last_keychain_id += 1
|
143
150
|
end
|
144
151
|
|
145
152
|
def add_websocket_for_order(ws, order)
|
@@ -205,6 +212,25 @@ module StraightServer
|
|
205
212
|
@@redis.incrby("#{StraightServer::Config.redis[:prefix]}:gateway_#{id}:#{counter_name}_orders_counter", by)
|
206
213
|
end
|
207
214
|
|
215
|
+
# If we have more than Config.reuse_address_orders_threshold i a row for this gateway,
|
216
|
+
# this method returns the one which keychain_id (and, consequently, address) is to be reused.
|
217
|
+
# It also checks (just in case) if any transactions has been made to the addres-to-be-reused,
|
218
|
+
# because even though the order itself might be expired, the address might have been used for
|
219
|
+
# something else.
|
220
|
+
#
|
221
|
+
# If there were transactions to it, there's actually no need to reuse the address and we can
|
222
|
+
# safely return nil.
|
223
|
+
#
|
224
|
+
# Also, see comments for #find_expired_orders_row method.
|
225
|
+
def find_reusable_order
|
226
|
+
expired_orders = find_expired_orders_row
|
227
|
+
if expired_orders.size >= Config.reuse_address_orders_threshold &&
|
228
|
+
fetch_transactions_for(expired_orders.last.address).empty?
|
229
|
+
return expired_orders.last
|
230
|
+
end
|
231
|
+
nil
|
232
|
+
end
|
233
|
+
|
208
234
|
private
|
209
235
|
|
210
236
|
# Tries to send a callback HTTP request to the resource specified
|
@@ -217,9 +243,9 @@ module StraightServer
|
|
217
243
|
StraightServer.logger.info "Attempting to send request to the callback url for order #{order.id} to #{callback_url}..."
|
218
244
|
|
219
245
|
# Composing the request uri here
|
220
|
-
signature = self.check_signature ? "&signature=#{sign_with_secret(order.id
|
221
|
-
|
222
|
-
uri
|
246
|
+
signature = self.check_signature ? "&signature=#{sign_with_secret(order.id)}" : ''
|
247
|
+
callback_data = order.callback_data ? "&callback_data=#{order.callback_data}" : ''
|
248
|
+
uri = URI.parse(callback_url + '?' + order.to_http_params + signature + callback_data)
|
223
249
|
|
224
250
|
begin
|
225
251
|
response = Net::HTTP.get_response(uri)
|
@@ -238,6 +264,63 @@ module StraightServer
|
|
238
264
|
StraightServer.logger.info "Callback request for order #{order.id} performed successfully"
|
239
265
|
end
|
240
266
|
|
267
|
+
|
268
|
+
# Wallets that support BIP32 do a limited address lookup. If you have 20 empty addresses in a row
|
269
|
+
# (actually not 20, but Config.reuse_address_orders_threshold, 20 is the default value) it won't
|
270
|
+
# look past it and if an order is generated with the 21st address and Bitcoins are paid there,
|
271
|
+
# the wallet may not detect it. Thus we need to always check for the number of expired orders
|
272
|
+
# in a row and reuse an address.
|
273
|
+
#
|
274
|
+
# This method takes care of the first part of that equation: finds the row of expired orders.
|
275
|
+
# It works like this:
|
276
|
+
#
|
277
|
+
# 1. Finds 20 last orders
|
278
|
+
# 2. Checks if they form a row of expired orders, that is if there is no non-expired non-new orders
|
279
|
+
# in the array:
|
280
|
+
#
|
281
|
+
# if YES (all orders in the row are indeed expired)
|
282
|
+
# a) Try the next 20 until we find that one non-expired, non-new order
|
283
|
+
# b) Put all orders in an array, then slice it so only the oldest 20 are there
|
284
|
+
# c) return 20 oldest expired orders
|
285
|
+
#
|
286
|
+
# if NO (some orders are paid)
|
287
|
+
# Return the row of expired orders - which is not enough to trigger a reuse
|
288
|
+
# (the triger is in the #find_reusable_order method, which calls this one).
|
289
|
+
def find_expired_orders_row
|
290
|
+
|
291
|
+
orders = []
|
292
|
+
row = nil
|
293
|
+
offset = 0
|
294
|
+
|
295
|
+
while row.nil? || row.size > 0
|
296
|
+
row = Order.where(gateway_id: self.id).order(Sequel.desc(:keychain_id), Sequel.desc(:reused)).limit(Config.reuse_address_orders_threshold).offset(offset).to_a
|
297
|
+
|
298
|
+
row.reject! do |o|
|
299
|
+
reject = false
|
300
|
+
row.each do |o2|
|
301
|
+
reject = true if o.keychain_id == o2.keychain_id && o.reused < o2.reused
|
302
|
+
end
|
303
|
+
reject
|
304
|
+
end
|
305
|
+
|
306
|
+
row.sort! { |o1, o2| o2.id <=> o1.id }
|
307
|
+
|
308
|
+
row.each do |o|
|
309
|
+
if o.status == Order::STATUSES[:expired]
|
310
|
+
orders.unshift(o)
|
311
|
+
elsif o.status == Order::STATUSES[:new]
|
312
|
+
next
|
313
|
+
else
|
314
|
+
return orders[0...Config.reuse_address_orders_threshold]
|
315
|
+
end
|
316
|
+
end
|
317
|
+
offset += Config.reuse_address_orders_threshold
|
318
|
+
end
|
319
|
+
|
320
|
+
orders
|
321
|
+
|
322
|
+
end
|
323
|
+
|
241
324
|
end
|
242
325
|
|
243
326
|
# Uses database to load and save attributes
|
@@ -250,15 +333,28 @@ module StraightServer
|
|
250
333
|
plugin :serialization, :marshal
|
251
334
|
plugin :after_initialize
|
252
335
|
|
336
|
+
|
253
337
|
def self.find_by_hashed_id(s)
|
254
338
|
self.where(hashed_id: s).first
|
255
339
|
end
|
256
340
|
|
341
|
+
# This virtual attribute is important because it's difficult to detect whether secret was actually
|
342
|
+
# updated or not. Sequel's #changed_columns may mistakenly say :secret attr was changed, while it
|
343
|
+
# hasn't. Thus we provide a manual way of ensuring this. It's also better and works as safety switch:
|
344
|
+
# we don't want somebody accidentally updating a secret.
|
345
|
+
attr_accessor :update_secret
|
346
|
+
|
257
347
|
def before_create
|
258
348
|
super
|
259
349
|
encrypt_secret
|
260
350
|
end
|
261
351
|
|
352
|
+
def before_update
|
353
|
+
encrypt_secret if @update_secret
|
354
|
+
@update_secret = false
|
355
|
+
super
|
356
|
+
end
|
357
|
+
|
262
358
|
def after_create
|
263
359
|
@@websockets[self.id] = {}
|
264
360
|
update(hashed_id: OpenSSL::HMAC.digest('sha256', Config.server_secret, self.id.to_s).unpack("H*").first)
|
@@ -288,25 +384,28 @@ module StraightServer
|
|
288
384
|
self[id]
|
289
385
|
end
|
290
386
|
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
387
|
+
def encrypt_secret
|
388
|
+
cipher = OpenSSL::Cipher::AES.new(128, :CBC)
|
389
|
+
cipher.encrypt
|
390
|
+
cipher.key = OpenSSL::HMAC.digest('sha256', 'nonce', Config.server_secret).unpack("H*").first[0,16]
|
391
|
+
|
392
|
+
cipher.iv = iv = OpenSSL::HMAC.digest('sha256', 'nonce', "#{self.class.max(:id)}#{Config.server_secret}").unpack("H*").first[0,16]
|
393
|
+
raise "cipher.iv cannot be nil" unless iv
|
394
|
+
|
395
|
+
encrypted = cipher.update(self[:secret]) << cipher.final()
|
396
|
+
base64_encrypted = Base64.strict_encode64(encrypted).encode('utf-8')
|
397
|
+
result = "#{iv}:#{base64_encrypted}"
|
398
|
+
|
399
|
+
# Check whether we can decrypt. It should not be possible to encrypt the
|
400
|
+
# gateway secret unless we are sure we can decrypt it.
|
401
|
+
if decrypt_secret(result) == self[:secret]
|
402
|
+
self.secret = result
|
403
|
+
else
|
404
|
+
raise "Decrypted and original secrets don't match! Cannot proceed with writing the encrypted gateway secret."
|
309
405
|
end
|
406
|
+
end
|
407
|
+
|
408
|
+
private
|
310
409
|
|
311
410
|
def decrypt_secret(encrypted_field=self[:secret])
|
312
411
|
decipher = OpenSSL::Cipher::AES.new(128, :CBC)
|