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.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +2 -0
  3. data/Gemfile.lock +5 -2
  4. data/Gemfile.travis +2 -0
  5. data/README.md +46 -19
  6. data/Rakefile +5 -2
  7. data/VERSION +1 -1
  8. data/db/migrations/003_add_payment_id_to_orders.rb +1 -1
  9. data/db/migrations/004_add_description_to_orders.rb +1 -1
  10. data/db/migrations/005_add_orders_expiration_period_to_gateways.rb +1 -1
  11. data/db/migrations/006_add_check_order_status_in_db_first_to_gateways.rb +1 -1
  12. data/db/migrations/007_add_active_switcher_to_gateways.rb +1 -1
  13. data/db/migrations/008_add_order_counters_to_gateways.rb +1 -1
  14. data/db/migrations/009_add_hashed_id_to_gateways.rb +2 -2
  15. data/db/migrations/010_add_address_reusability_orders.rb +19 -0
  16. data/db/migrations/011_add_callback_data_to_orders.rb +11 -0
  17. data/db/schema.rb +55 -0
  18. data/examples/client/client.html +1 -1
  19. data/examples/client/client.js +1 -1
  20. data/lib/straight-server/config.rb +3 -1
  21. data/lib/straight-server/gateway.rb +133 -34
  22. data/lib/straight-server/initializer.rb +8 -7
  23. data/lib/straight-server/order.rb +12 -5
  24. data/lib/straight-server/orders_controller.rb +45 -13
  25. data/lib/straight-server/throttler.rb +63 -0
  26. data/lib/tasks/db.rake +42 -0
  27. data/spec/.straight/config.yml +3 -2
  28. data/spec/lib/gateway_spec.rb +88 -9
  29. data/spec/lib/order_spec.rb +8 -13
  30. data/spec/lib/orders_controller_spec.rb +36 -4
  31. data/spec/lib/throttle_spec.rb +52 -0
  32. data/straight-server.gemspec +13 -6
  33. data/templates/config.yml +19 -2
  34. metadata +22 -6
  35. data/bin/goliath.log +0 -6
  36. data/bin/goliath.log_stdout.log +0 -51
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 52533bcd5fc953b35fd81b8bf7f4a4b974954b01
4
- data.tar.gz: 6d43d836466f5d29f849536b08286ee7497973f8
3
+ metadata.gz: 3d3e00cdba620b59557e0f28a51cdc3a82a7b953
4
+ data.tar.gz: 53372b11a9ddd0045bba84f4b42e9c2706ec9668
5
5
  SHA512:
6
- metadata.gz: 1350baf54ebb72e7baf2ccfefd48fe9ffd39c309436e9ed9e52f2f174736935e6b0e9d424e6bf08bff5f1fefc6ae5c0a54343bf3082e11b285c82b606d99f461
7
- data.tar.gz: 199b84bf7ed343c417d29d92195180c753ae73da916cd45dd4297d16b62253287ffc977056a9cc2164f8ba4ac4da09c8572368283b0b0de3f0d022b9d91fb903
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.3)
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.8.9)
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=1&amount=1&status=2&address=1NZov2nm6gRCGW6r4q1qHtxXurrWNpPr1q&tid=tid1&data=some+random+data
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 `data`. It is a way for you to pass info back
75
- to your app. It will have the same value as the `data` parameter you passed to the create order request:
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&data=some+random+data
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/1
93
+ GET /gateways/1/orders/:id
93
94
 
94
- may return something like:
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 an order id. Because you need order id, it means you have
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 order ids in your application. Obviously,
201
- if an order with such an id already exists, the request will be rejected. A possible request
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&order_id=1&signature=aa14c26b2ae892a8719b0c2c57f162b967bfbfbdcc38d8883714a0680cf20467
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 the signature may be
214
- known to an attacker once it was used for creating a new order, we can no longer use it directly.
215
- Thus, Straight server will use a double signature calculated like this:
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
- h1 = OpenSSL::HMAC.digest('sha256', secret, "1").unpack("H*").first
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=1&amount=1&status=2&address=1NZov2nm6gRCGW6r4q1qHtxXurrWNpPr1q&tid=tid1&data=some+random+data?signature=aa14c26b2ae892a8719b0c2c57f162b967bfbfbdcc38d8883714a0680cf20467
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
- require 'rake'
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.2
1
+ 0.2.3
@@ -7,7 +7,7 @@ Sequel.migration do
7
7
 
8
8
  down do
9
9
  drop_index :orders, :payment_id
10
- remove_column :orders, :payment_id
10
+ drop_column :orders, :payment_id
11
11
  end
12
12
 
13
13
  end
@@ -5,7 +5,7 @@ Sequel.migration do
5
5
  end
6
6
 
7
7
  down do
8
- remove_column :orders, :description
8
+ drop_column :orders, :description
9
9
  end
10
10
 
11
11
  end
@@ -5,7 +5,7 @@ Sequel.migration do
5
5
  end
6
6
 
7
7
  down do
8
- remove_column :gateways, :orders_expiration_period
8
+ drop_column :gateways, :orders_expiration_period
9
9
  end
10
10
 
11
11
  end
@@ -5,7 +5,7 @@ Sequel.migration do
5
5
  end
6
6
 
7
7
  down do
8
- remove_column :gateways, :check_order_status_in_db_first
8
+ drop_column :gateways, :check_order_status_in_db_first
9
9
  end
10
10
 
11
11
  end
@@ -5,7 +5,7 @@ Sequel.migration do
5
5
  end
6
6
 
7
7
  down do
8
- remove_column :gateways, :active
8
+ drop_column :gateways, :active
9
9
  end
10
10
 
11
11
  end
@@ -5,7 +5,7 @@ Sequel.migration do
5
5
  end
6
6
 
7
7
  down do
8
- remove_column :gateways, :order_counters
8
+ drop_column :gateways, :order_counters
9
9
  end
10
10
 
11
11
  end
@@ -11,8 +11,8 @@ Sequel.migration do
11
11
  end
12
12
 
13
13
  down do
14
- remove_index :gateways, :hashed_id
15
- remove_column :gateways, :hashed_id
14
+ drop_index :gateways, :hashed_id
15
+ drop_column :gateways, :hashed_id
16
16
  end
17
17
 
18
18
  end
@@ -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
@@ -0,0 +1,11 @@
1
+ Sequel.migration do
2
+
3
+ up do
4
+ add_column :orders, :callback_data, String
5
+ end
6
+
7
+ down do
8
+ remove_column :orders, :callback_data
9
+ end
10
+
11
+ 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
@@ -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="order_id"/><br/>
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>
@@ -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(), order_id :$("input[name=order_id]").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
  }
@@ -14,7 +14,9 @@ module StraightServer
14
14
  :check_order_status_in_db_first,
15
15
  :port,
16
16
  :blockchain_adapters,
17
- :expiration_overtime
17
+ :expiration_overtime,
18
+ :reuse_address_orders_threshold,
19
+ :throttle
18
20
  end
19
21
 
20
22
  end
@@ -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 #{a} was found!"
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] || increment_last_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 = attrs[:id].to_i if attrs[:id]
123
- order.data = attrs[:data] if attrs[:data]
124
- order.gateway = self
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
- # Used to track the current keychain_id number, which is used by
136
- # Straight::Gateway to generate addresses from the pubkey. The number is supposed
137
- # to be incremented by 1. In the case of a Config file type of Gateway, the value
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, level: 2)}" : ''
221
- data = order.data ? "&data=#{order.data}" : ''
222
- uri = URI.parse(callback_url + '?' + order.to_http_params + signature + data)
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
- private
292
-
293
- def encrypt_secret
294
- cipher = OpenSSL::Cipher::AES.new(128, :CBC)
295
- cipher.encrypt
296
- cipher.key = OpenSSL::HMAC.digest('sha256', 'nonce', Config.server_secret).unpack("H*").first[0,16]
297
- cipher.iv = iv = OpenSSL::HMAC.digest('sha256', 'nonce', "#{self.id}#{Config.server_secret}").unpack("H*").first[0,16]
298
- encrypted = cipher.update(self[:secret]) << cipher.final()
299
- base64_encrypted = Base64.strict_encode64(encrypted).encode('utf-8')
300
- result = "#{iv}:#{base64_encrypted}"
301
-
302
- # Check whether we can decrypt. It should not be possible to encrypt the
303
- # gateway secret unless we are sure we can decrypt it.
304
- if decrypt_secret(result) == self[:secret]
305
- self.secret = result
306
- else
307
- raise "Decrypted and original secrets don't match! Cannot proceed with writing the encrypted gateway secret."
308
- end
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)