straight-server 0.2.2 → 0.2.3
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.
- 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)
|