straight-server 0.1.2 → 0.2.0
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/.travis.yml +8 -0
- data/Gemfile +3 -1
- data/Gemfile.lock +57 -47
- data/Gemfile.travis +26 -0
- data/README.md +175 -22
- data/Rakefile +7 -0
- data/VERSION +1 -1
- data/benchmark/addons.yml +15 -0
- data/benchmark/config.yml +78 -0
- data/benchmark/default_last_keychain_id +1 -0
- data/benchmark/server_secret +1 -0
- data/bin/goliath.log +6 -0
- data/bin/goliath.log_stdout.log +51 -0
- data/bin/straight-server-benchmark +68 -0
- data/db/migrations/003_add_payment_id_to_orders.rb +13 -0
- data/db/migrations/004_add_description_to_orders.rb +11 -0
- data/db/migrations/005_add_orders_expiration_period_to_gateways.rb +11 -0
- data/db/migrations/006_add_check_order_status_in_db_first_to_gateways.rb +11 -0
- data/db/migrations/007_add_active_switcher_to_gateways.rb +11 -0
- data/db/migrations/008_add_order_counters_to_gateways.rb +11 -0
- data/db/migrations/009_add_hashed_id_to_gateways.rb +18 -0
- data/examples/client/client.dart +5 -0
- data/examples/client/client.html +7 -15
- data/examples/client/client.js +15 -0
- data/lib/straight-server/config.rb +1 -1
- data/lib/straight-server/gateway.rb +241 -59
- data/lib/straight-server/initializer.rb +170 -44
- data/lib/straight-server/logger.rb +1 -1
- data/lib/straight-server/order.rb +74 -9
- data/lib/straight-server/orders_controller.rb +23 -6
- data/lib/straight-server/random_string.rb +18 -0
- data/lib/straight-server/server.rb +44 -17
- data/lib/straight-server/utils/hash_string_to_sym_keys.rb +24 -0
- data/lib/straight-server.rb +6 -3
- data/spec/.straight/config.yml +16 -0
- data/spec/.straight/server_secret +1 -0
- data/spec/fixtures/addons.yml +19 -0
- data/spec/fixtures/test_addon.rb +8 -0
- data/spec/lib/gateway_spec.rb +93 -13
- data/spec/lib/initializer_spec.rb +104 -0
- data/spec/lib/order_spec.rb +59 -0
- data/spec/lib/orders_controller_spec.rb +34 -1
- data/spec/lib/utils/hash_string_to_sym_keys.rb +18 -0
- data/spec/spec_helper.rb +10 -2
- data/straight-server.gemspec +36 -8
- data/templates/addons.yml +15 -0
- data/templates/config.yml +41 -0
- metadata +47 -5
@@ -4,19 +4,89 @@ module StraightServer
|
|
4
4
|
# in one of the classes below.
|
5
5
|
module GatewayModule
|
6
6
|
|
7
|
+
# Temporary fix for straight server benchmarking
|
8
|
+
@@redis = StraightServer::Config.redis[:connection] if StraightServer::Config.redis
|
9
|
+
@@websockets = {}
|
10
|
+
|
11
|
+
def fetch_transactions_for(address)
|
12
|
+
try_adapters(@blockchain_adapters) { |b| b.fetch_transactions_for(address) }
|
13
|
+
end
|
14
|
+
|
7
15
|
class InvalidSignature < Exception; end
|
8
16
|
class InvalidOrderId < Exception; end
|
9
17
|
class CallbackUrlBadResponse < Exception; end
|
10
18
|
class WebsocketExists < Exception; end
|
11
19
|
class WebsocketForCompletedOrder < Exception; end
|
20
|
+
class GatewayInactive < Exception; end
|
21
|
+
class NoBlockchainAdapters < Exception
|
22
|
+
def message
|
23
|
+
"No blockchain adapters were found! StraightServer cannot query the blockchain.\n" +
|
24
|
+
"Check your ~/.straight/config.yml file and make sure valid blockchain adapters\n" +
|
25
|
+
"are present."
|
26
|
+
end
|
27
|
+
end
|
28
|
+
class NoWebsocketsForNewGateway < Exception
|
29
|
+
def message
|
30
|
+
"You're trying to get access to websockets on a Gateway that hasn't been saved yet"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
class OrderCountersDisabled < Exception
|
34
|
+
def message
|
35
|
+
"Please enable order counting in config file! You can do is using the following option:\n\n" +
|
36
|
+
" count_orders: true\n\n" +
|
37
|
+
"and don't forget to provide Redis connection info by adding this to the config file as well:\n\n" +
|
38
|
+
" redis:\n" +
|
39
|
+
" host: localhost\n" +
|
40
|
+
" port: 6379\n" +
|
41
|
+
" db: null\n"
|
42
|
+
end
|
43
|
+
end
|
12
44
|
|
13
45
|
CALLBACK_URL_ATTEMPT_TIMEFRAME = 3600 # seconds
|
14
46
|
|
15
|
-
|
16
|
-
|
47
|
+
|
48
|
+
############# Initializers methods ########################################################
|
49
|
+
# We have separate methods, because with GatewayOnDB they are called from #after_initialize
|
50
|
+
# but in GatewayOnConfig they are called from #initialize intself.
|
51
|
+
# #########################################################################################
|
52
|
+
#
|
53
|
+
def initialize_exchange_rate_adapters
|
54
|
+
@exchange_rate_adapters ||= []
|
55
|
+
if self.exchange_rate_adapter_names
|
56
|
+
self.exchange_rate_adapter_names.each do |adapter|
|
57
|
+
begin
|
58
|
+
@exchange_rate_adapters << Straight::ExchangeRate.const_get("#{adapter}Adapter").instance
|
59
|
+
rescue NameError => e
|
60
|
+
puts "WARNING: No exchange rate adapter with the name #{a} was found!"
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def initialize_blockchain_adapters
|
67
|
+
@blockchain_adapters = []
|
68
|
+
StraightServer::Config.blockchain_adapters.each do |a|
|
69
|
+
|
70
|
+
adapter = begin
|
71
|
+
Straight::Blockchain.const_get("#{a}Adapter")
|
72
|
+
rescue NameError
|
73
|
+
begin
|
74
|
+
Kernel.const_get(a)
|
75
|
+
rescue NameError
|
76
|
+
puts "WARNING: No blockchain adapter with the name #{a} was found!"
|
77
|
+
nil
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
@blockchain_adapters << adapter.mainnet_adapter if adapter
|
82
|
+
end
|
83
|
+
raise NoBlockchainAdapters if @blockchain_adapters.empty?
|
84
|
+
end
|
85
|
+
|
86
|
+
def initialize_callbacks
|
17
87
|
# When the status of an order changes, we send an http request to the callback_url
|
18
88
|
# and also notify a websocket client (if present, of course).
|
19
|
-
@order_callbacks
|
89
|
+
@order_callbacks = [
|
20
90
|
lambda do |order|
|
21
91
|
StraightServer::Thread.new do
|
22
92
|
send_callback_http_request order
|
@@ -24,27 +94,25 @@ module StraightServer
|
|
24
94
|
end
|
25
95
|
end
|
26
96
|
]
|
97
|
+
end
|
27
98
|
|
28
|
-
|
29
|
-
Straight::Blockchain::BlockchainInfoAdapter.mainnet_adapter,
|
30
|
-
Straight::Blockchain::HelloblockIoAdapter.mainnet_adapter
|
31
|
-
]
|
32
|
-
|
33
|
-
@exchange_rate_adapters = []
|
99
|
+
def initialize_status_check_schedule
|
34
100
|
@status_check_schedule = Straight::GatewayModule::DEFAULT_STATUS_CHECK_SCHEDULE
|
35
|
-
@websockets = {}
|
36
|
-
|
37
|
-
super
|
38
|
-
initialize_exchange_rate_adapters # should always go after super
|
39
101
|
end
|
102
|
+
#
|
103
|
+
############# END OF Initializers methods ##################################################
|
104
|
+
|
40
105
|
|
41
106
|
# Creates a new order and saves into the DB. Checks if the MD5 hash
|
42
107
|
# is correct first.
|
43
108
|
def create_order(attrs={})
|
109
|
+
|
110
|
+
raise GatewayInactive unless self.active
|
111
|
+
|
44
112
|
StraightServer.logger.info "Creating new order with attrs: #{attrs}"
|
45
113
|
signature = attrs.delete(:signature)
|
46
|
-
raise InvalidOrderId if check_signature && (attrs[:id].nil? || attrs[:id].to_i <= 0)
|
47
114
|
if !check_signature || sign_with_secret(attrs[:id]) == signature
|
115
|
+
raise InvalidOrderId if check_signature && (attrs[:id].nil? || attrs[:id].to_i <= 0)
|
48
116
|
order = order_for_keychain_id(
|
49
117
|
amount: attrs[:amount],
|
50
118
|
keychain_id: increment_last_keychain_id!,
|
@@ -75,47 +143,69 @@ module StraightServer
|
|
75
143
|
end
|
76
144
|
|
77
145
|
def add_websocket_for_order(ws, order)
|
78
|
-
raise WebsocketExists unless
|
146
|
+
raise WebsocketExists unless websockets[order.id].nil?
|
79
147
|
raise WebsocketForCompletedOrder unless order.status < 2
|
80
148
|
StraightServer.logger.info "Opening ws connection for #{order.id}"
|
81
149
|
ws.on(:close) do |event|
|
82
|
-
|
150
|
+
websockets.delete(order.id)
|
83
151
|
StraightServer.logger.info "Closing ws connection for #{order.id}"
|
84
152
|
end
|
85
|
-
|
153
|
+
websockets[order.id] = ws
|
86
154
|
ws
|
87
155
|
end
|
88
156
|
|
157
|
+
def websockets
|
158
|
+
raise NoWebsocketsForNewGateway unless self.id
|
159
|
+
@@websockets[self.id]
|
160
|
+
end
|
161
|
+
|
89
162
|
def send_order_to_websocket_client(order)
|
90
|
-
if ws =
|
163
|
+
if ws = websockets[order.id]
|
91
164
|
ws.send(order.to_json)
|
92
165
|
ws.close
|
93
166
|
end
|
94
167
|
end
|
95
168
|
|
96
|
-
def
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
@exchange_rate_adapters << Kernel.const_get("Straight::ExchangeRate::#{adapter}Adapter").new
|
101
|
-
rescue NameError
|
102
|
-
raise NameError, "No such adapter exists: Straight::ExchangeRate::#{adapter}Adapter"
|
103
|
-
end
|
104
|
-
end
|
169
|
+
def sign_with_secret(content, level: 1)
|
170
|
+
result = content.to_s
|
171
|
+
level.times do
|
172
|
+
result = OpenSSL::HMAC.digest('sha256', secret, result).unpack("H*").first
|
105
173
|
end
|
174
|
+
result
|
106
175
|
end
|
107
176
|
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
h = HMAC::SHA1.new(secret)
|
114
|
-
h << result
|
115
|
-
result = h.hexdigest
|
116
|
-
end
|
117
|
-
result
|
177
|
+
def order_status_changed(order)
|
178
|
+
statuses = Order::STATUSES.invert
|
179
|
+
if StraightServer::Config.count_orders
|
180
|
+
increment_order_counter!(statuses[order.old_status], -1) if order.old_status
|
181
|
+
increment_order_counter!(statuses[order.status])
|
118
182
|
end
|
183
|
+
super
|
184
|
+
end
|
185
|
+
|
186
|
+
def order_counters(reload: false)
|
187
|
+
return @order_counters if @order_counters && !reload
|
188
|
+
@order_counters = {
|
189
|
+
new: get_order_counter(:new),
|
190
|
+
unconfirmed: get_order_counter(:unconfirmed),
|
191
|
+
paid: get_order_counter(:paid),
|
192
|
+
underpaid: get_order_counter(:underpaid),
|
193
|
+
overpaid: get_order_counter(:overpaid),
|
194
|
+
expired: get_order_counter(:expired)
|
195
|
+
}
|
196
|
+
end
|
197
|
+
|
198
|
+
def get_order_counter(counter_name)
|
199
|
+
raise OrderCountersDisabled unless StraightServer::Config.count_orders
|
200
|
+
@@redis.get("#{StraightServer::Config.redis[:prefix]}:gateway_#{id}:#{counter_name}_orders_counter").to_i || 0
|
201
|
+
end
|
202
|
+
|
203
|
+
def increment_order_counter!(counter_name, by=1)
|
204
|
+
raise OrderCountersDisabled unless StraightServer::Config.count_orders
|
205
|
+
@@redis.incrby("#{StraightServer::Config.redis[:prefix]}:gateway_#{id}:#{counter_name}_orders_counter", by)
|
206
|
+
end
|
207
|
+
|
208
|
+
private
|
119
209
|
|
120
210
|
# Tries to send a callback HTTP request to the resource specified
|
121
211
|
# in the #callback_url. If it fails for any reason, it keeps trying for an hour (3600 seconds)
|
@@ -141,7 +231,7 @@ module StraightServer
|
|
141
231
|
sleep(delay)
|
142
232
|
send_callback_http_request(order, delay: delay*2)
|
143
233
|
else
|
144
|
-
StraightServer.logger.warn "Callback request for order #{order.id}
|
234
|
+
StraightServer.logger.warn "Callback request for order #{order.id} failed, see order's #callback_response field for details"
|
145
235
|
end
|
146
236
|
end
|
147
237
|
|
@@ -157,6 +247,75 @@ module StraightServer
|
|
157
247
|
include GatewayModule
|
158
248
|
plugin :timestamps, create: :created_at, update: :updated_at
|
159
249
|
plugin :serialization, :marshal, :exchange_rate_adapter_names
|
250
|
+
plugin :serialization, :marshal
|
251
|
+
plugin :after_initialize
|
252
|
+
|
253
|
+
def self.find_by_hashed_id(s)
|
254
|
+
self.where(hashed_id: s).first
|
255
|
+
end
|
256
|
+
|
257
|
+
def before_create
|
258
|
+
super
|
259
|
+
encrypt_secret
|
260
|
+
end
|
261
|
+
|
262
|
+
def after_create
|
263
|
+
@@websockets[self.id] = {}
|
264
|
+
update(hashed_id: OpenSSL::HMAC.digest('sha256', Config.server_secret, self.id.to_s).unpack("H*").first)
|
265
|
+
end
|
266
|
+
|
267
|
+
def after_initialize
|
268
|
+
@status_check_schedule = Straight::GatewayModule::DEFAULT_STATUS_CHECK_SCHEDULE
|
269
|
+
@@websockets[self.id] ||= {} if self.id
|
270
|
+
initialize_callbacks
|
271
|
+
initialize_exchange_rate_adapters
|
272
|
+
initialize_blockchain_adapters
|
273
|
+
initialize_status_check_schedule
|
274
|
+
end
|
275
|
+
|
276
|
+
# We cannot allow to store gateway secret in a DB plaintext, this would be completetly unsecure.
|
277
|
+
# Althougth we use symmetrical encryption here and store the encryption key in the
|
278
|
+
# server's in a special file (~/.straight/server_secret), which in turn can also be stolen,
|
279
|
+
# this is still marginally better than doing nothing.
|
280
|
+
#
|
281
|
+
# Also, server admnistrators now have the freedom of developing their own strategy
|
282
|
+
# of storing that secret - it doesn't have to be stored on the same machine.
|
283
|
+
def secret
|
284
|
+
decrypt_secret
|
285
|
+
end
|
286
|
+
|
287
|
+
def self.find_by_id(id)
|
288
|
+
self[id]
|
289
|
+
end
|
290
|
+
|
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
|
309
|
+
end
|
310
|
+
|
311
|
+
def decrypt_secret(encrypted_field=self[:secret])
|
312
|
+
decipher = OpenSSL::Cipher::AES.new(128, :CBC)
|
313
|
+
iv, encrypted = encrypted_field.split(':')
|
314
|
+
decipher.decrypt
|
315
|
+
decipher.key = OpenSSL::HMAC.digest('sha256', 'nonce', Config.server_secret).unpack("H*").first[0,16]
|
316
|
+
decipher.iv = iv
|
317
|
+
decipher.update(Base64.decode64(encrypted)) + decipher.final
|
318
|
+
end
|
160
319
|
|
161
320
|
end
|
162
321
|
|
@@ -192,19 +351,35 @@ module StraightServer
|
|
192
351
|
attr_accessor :id
|
193
352
|
|
194
353
|
attr_accessor :exchange_rate_adapter_names
|
354
|
+
attr_accessor :orders_expiration_period
|
355
|
+
|
356
|
+
# This affects whether it is possible to create a new order with the gateway.
|
357
|
+
# If it's set to false, then it won't be possible to create a new order, but
|
358
|
+
# it will keep checking on the existing ones.
|
359
|
+
attr_accessor :active
|
360
|
+
|
361
|
+
def self.find_by_hashed_id(s)
|
362
|
+
self.find_by_id(s)
|
363
|
+
end
|
364
|
+
|
365
|
+
def initialize
|
366
|
+
initialize_callbacks
|
367
|
+
initialize_exchange_rate_adapters
|
368
|
+
initialize_blockchain_adapters
|
369
|
+
initialize_status_check_schedule
|
370
|
+
end
|
195
371
|
|
196
372
|
# Because this is a config based gateway, we only save last_keychain_id
|
197
373
|
# and nothing more.
|
198
374
|
def save
|
199
|
-
|
375
|
+
save_last_keychain_id!
|
200
376
|
end
|
201
377
|
|
202
378
|
# Loads last_keychain_id from a file in the .straight dir.
|
203
379
|
# If the file doesn't exist, we create it. Later, whenever an attribute is updated,
|
204
380
|
# we save it to the file.
|
205
381
|
def load_last_keychain_id!
|
206
|
-
@last_keychain_id_file
|
207
|
-
"/#{name}_last_keychain_id"
|
382
|
+
@last_keychain_id_file ||= StraightServer::Initializer::ConfigDir.path + "/#{name}_last_keychain_id"
|
208
383
|
if File.exists?(@last_keychain_id_file)
|
209
384
|
self.last_keychain_id = File.read(@last_keychain_id_file).to_i
|
210
385
|
else
|
@@ -213,6 +388,17 @@ module StraightServer
|
|
213
388
|
end
|
214
389
|
end
|
215
390
|
|
391
|
+
def save_last_keychain_id!
|
392
|
+
@last_keychain_id_file ||= StraightServer::Initializer::ConfigDir.path + "/#{name}_last_keychain_id"
|
393
|
+
File.open(@last_keychain_id_file, 'w') {|f| f.write(last_keychain_id) }
|
394
|
+
end
|
395
|
+
|
396
|
+
# This method is a replacement for the Sequel's model one used in DB version of the gateway
|
397
|
+
# and it finds gateways using the index of @@gateways Array.
|
398
|
+
def self.find_by_id(id)
|
399
|
+
@@gateways[id.to_i-1]
|
400
|
+
end
|
401
|
+
|
216
402
|
# This will later be used in the #find_by_id. Because we don't use a DB,
|
217
403
|
# the id will actually be the index of an element in this Array. Thus,
|
218
404
|
# the order in which gateways follow in the config file is important.
|
@@ -223,27 +409,23 @@ module StraightServer
|
|
223
409
|
StraightServer::Config.gateways.each do |name, attrs|
|
224
410
|
i += 1
|
225
411
|
gateway = self.new
|
226
|
-
gateway.pubkey
|
227
|
-
gateway.confirmations_required
|
228
|
-
gateway.order_class
|
229
|
-
gateway.secret
|
230
|
-
gateway.check_signature
|
231
|
-
gateway.callback_url
|
232
|
-
gateway.default_currency
|
233
|
-
gateway.
|
234
|
-
gateway.
|
412
|
+
gateway.pubkey = attrs['pubkey']
|
413
|
+
gateway.confirmations_required = attrs['confirmations_required'].to_i
|
414
|
+
gateway.order_class = attrs['order_class']
|
415
|
+
gateway.secret = attrs['secret']
|
416
|
+
gateway.check_signature = attrs['check_signature']
|
417
|
+
gateway.callback_url = attrs['callback_url']
|
418
|
+
gateway.default_currency = attrs['default_currency']
|
419
|
+
gateway.orders_expiration_period = attrs['orders_expiration_period']
|
420
|
+
gateway.active = attrs['active']
|
421
|
+
gateway.name = name
|
422
|
+
gateway.id = i
|
235
423
|
gateway.exchange_rate_adapter_names = attrs['exchange_rate_adapters']
|
236
424
|
gateway.initialize_exchange_rate_adapters
|
237
425
|
gateway.load_last_keychain_id!
|
426
|
+
@@websockets[i] = {}
|
238
427
|
@@gateways << gateway
|
239
|
-
end
|
240
|
-
|
241
|
-
|
242
|
-
# This method is a replacement for the Sequel's model one used in DB version of the gateway
|
243
|
-
# and it finds gateways using the index of @@gateways Array.
|
244
|
-
def self.find_by_id(id)
|
245
|
-
@@gateways[id.to_i-1]
|
246
|
-
end
|
428
|
+
end if StraightServer::Config.gateways
|
247
429
|
|
248
430
|
end
|
249
431
|
|
@@ -251,7 +433,7 @@ module StraightServer
|
|
251
433
|
# so they can simply start using a single gateway on their machines, a gateway which attributes are defined
|
252
434
|
# in a config file instead of a DB. That way they don't need special tools to access the DB and create
|
253
435
|
# a gateway, but can simply edit the config file.
|
254
|
-
Gateway = if StraightServer::Config.gateways_source
|
436
|
+
Gateway = if StraightServer::Config.gateways_source == 'config'
|
255
437
|
GatewayOnConfig
|
256
438
|
else
|
257
439
|
GatewayOnDB
|
@@ -2,76 +2,202 @@ module StraightServer
|
|
2
2
|
|
3
3
|
module Initializer
|
4
4
|
|
5
|
-
GEM_ROOT
|
6
|
-
|
5
|
+
GEM_ROOT = File.expand_path('../..', File.dirname(__FILE__))
|
6
|
+
|
7
|
+
module ConfigDir
|
8
|
+
|
9
|
+
class << self
|
10
|
+
|
11
|
+
# Determine config dir or set default. Useful when we want to
|
12
|
+
# have different settings for production or staging or development environments.
|
13
|
+
def set!(path=nil)
|
14
|
+
@@config_dir = path and return if path
|
15
|
+
@@config_dir = ENV['HOME'] + '/.straight'
|
16
|
+
ARGV.each do |a|
|
17
|
+
if a =~ /\A--config-dir=.+/
|
18
|
+
@@config_dir = File.expand_path(a.sub('--config-dir=', ''))
|
19
|
+
break
|
20
|
+
elsif a =~ /\A-c .+/
|
21
|
+
@@config_dir = File.expand_path(a.sub('-c ', ''))
|
22
|
+
break
|
23
|
+
end
|
24
|
+
end
|
25
|
+
puts "Setting config dir to #{@@config_dir}"
|
26
|
+
end
|
27
|
+
|
28
|
+
def path
|
29
|
+
@@config_dir
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
7
35
|
|
8
36
|
def prepare
|
9
|
-
|
37
|
+
ConfigDir.set!
|
38
|
+
create_config_files
|
10
39
|
read_config_file
|
11
40
|
create_logger
|
12
41
|
connect_to_db
|
13
|
-
run_migrations
|
42
|
+
run_migrations if migrations_pending?
|
43
|
+
setup_redis_connection if StraightServer::Config.count_orders
|
44
|
+
initialize_routes
|
14
45
|
end
|
15
46
|
|
16
|
-
|
47
|
+
def add_route(path, &block)
|
48
|
+
@routes[path] = block
|
49
|
+
end
|
17
50
|
|
18
|
-
|
51
|
+
def create_config_files
|
52
|
+
FileUtils.mkdir_p(ConfigDir.path) unless File.exist?(ConfigDir.path)
|
53
|
+
|
54
|
+
unless File.exist?(ConfigDir.path + '/addons.yml')
|
55
|
+
puts "\e[1;33mNOTICE!\e[0m \e[33mNo file ~/.straight/addons.yml was found. Created an empty sample for you.\e[0m"
|
56
|
+
puts "No need to restart until you actually list your addons there. Now will continue loading StraightServer."
|
57
|
+
FileUtils.cp(GEM_ROOT + '/templates/addons.yml', ENV['HOME'] + '/.straight/')
|
58
|
+
end
|
59
|
+
|
60
|
+
unless File.exist?(ConfigDir.path + '/server_secret')
|
61
|
+
puts "\e[1;33mNOTICE!\e[0m \e[33mNo file ~/.straight/server_secret was found. Created one for you.\e[0m"
|
62
|
+
puts "No need to restart so far. Now will continue loading StraightServer."
|
63
|
+
File.open(ConfigDir.path + '/server_secret', "w") do |f|
|
64
|
+
f.puts String.random(16)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
unless File.exist?(ConfigDir.path + '/config.yml')
|
19
69
|
puts "\e[1;33mWARNING!\e[0m \e[33mNo file ~/.straight/config was found. Created a sample one for you.\e[0m"
|
20
70
|
puts "You should edit it and try starting the server again.\n"
|
21
71
|
|
22
|
-
FileUtils.mkdir_p(STRAIGHT_CONFIG_PATH) unless File.exist?(STRAIGHT_CONFIG_PATH)
|
23
72
|
FileUtils.cp(GEM_ROOT + '/templates/config.yml', ENV['HOME'] + '/.straight/')
|
24
73
|
puts "Shutting down now.\n\n"
|
25
74
|
exit
|
26
75
|
end
|
27
76
|
|
28
|
-
|
29
|
-
YAML.load_file(STRAIGHT_CONFIG_PATH + '/config.yml').each do |k,v|
|
30
|
-
StraightServer::Config.send(k + '=', v)
|
31
|
-
end
|
32
|
-
end
|
77
|
+
end
|
33
78
|
|
34
|
-
|
79
|
+
def read_config_file
|
80
|
+
YAML.load_file(ConfigDir.path + '/config.yml').each do |k,v|
|
81
|
+
StraightServer::Config.send(k + '=', v)
|
82
|
+
end
|
83
|
+
StraightServer::Config.server_secret = File.read(ConfigDir.path + '/server_secret').chomp
|
84
|
+
end
|
35
85
|
|
36
|
-
|
37
|
-
db_config = StraightServer::Config.db.inject({}){|memo,(k,v)| memo[k.to_sym] = v; memo}
|
86
|
+
def connect_to_db
|
38
87
|
|
39
|
-
|
40
|
-
|
41
|
-
else
|
42
|
-
db_config[:name]
|
43
|
-
end
|
88
|
+
# symbolize keys for convenience
|
89
|
+
db_config = StraightServer::Config.db.keys_to_sym
|
44
90
|
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
"#{db_config[:host]}#{(":" if db_config[:port])}" +
|
50
|
-
"#{db_config[:port]}#{("/" if db_config[:host] || db_config[:port])}" +
|
51
|
-
"#{db_name}"
|
52
|
-
)
|
91
|
+
db_name = if db_config[:adapter] == 'sqlite'
|
92
|
+
ConfigDir.path + "/" + db_config[:name]
|
93
|
+
else
|
94
|
+
db_config[:name]
|
53
95
|
end
|
54
96
|
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
97
|
+
StraightServer.db_connection = Sequel.connect(
|
98
|
+
"#{db_config[:adapter]}://" +
|
99
|
+
"#{db_config[:user]}#{(":" if db_config[:user])}" +
|
100
|
+
"#{db_config[:password]}#{("@" if db_config[:user] || db_config[:password])}" +
|
101
|
+
"#{db_config[:host]}#{(":" if db_config[:port])}" +
|
102
|
+
"#{db_config[:port]}#{("/" if db_config[:host] || db_config[:port])}" +
|
103
|
+
"#{db_name}"
|
104
|
+
)
|
105
|
+
end
|
106
|
+
|
107
|
+
def run_migrations
|
108
|
+
print "\nPending migrations for the database detected. Migrating..."
|
109
|
+
Sequel::Migrator.run(StraightServer.db_connection, GEM_ROOT + '/db/migrations/')
|
110
|
+
print "done\n\n"
|
111
|
+
end
|
112
|
+
|
113
|
+
def migrations_pending?
|
114
|
+
!Sequel::Migrator.is_current?(StraightServer.db_connection, GEM_ROOT + '/db/migrations/')
|
115
|
+
end
|
116
|
+
|
117
|
+
def create_logger
|
118
|
+
require_relative 'logger'
|
119
|
+
StraightServer.logger = StraightServer::Logger.new(
|
120
|
+
log_level: ::Logger.const_get(Config.logmaster['log_level'].upcase),
|
121
|
+
file: ConfigDir.path + '/' + Config.logmaster['file'],
|
122
|
+
raise_exception: Config.logmaster['raise_exception'],
|
123
|
+
name: Config.logmaster['name'],
|
124
|
+
email_config: Config.logmaster['email_config']
|
125
|
+
)
|
126
|
+
end
|
60
127
|
|
61
|
-
|
62
|
-
|
128
|
+
def initialize_routes
|
129
|
+
@routes = {}
|
130
|
+
add_route /\A\/gateways\/.+?\/orders(\/.+)?\Z/ do |env|
|
131
|
+
controller = OrdersController.new(env)
|
132
|
+
controller.response
|
63
133
|
end
|
134
|
+
end
|
64
135
|
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
136
|
+
# Loads addon modules into StraightServer::Server. To be useful,
|
137
|
+
# an addon most probably has to implement self.extended(server) callback.
|
138
|
+
# That way, it can access the server object and, for example, add routes
|
139
|
+
# with StraightServer::Server#add_route.
|
140
|
+
#
|
141
|
+
# Addon modules can be both rubygems or files under ~/.straight/addons/.
|
142
|
+
# If ~/.straight/addons.yml contains a 'path' key for a particular addon, then it means
|
143
|
+
# the addon is placed under the ~/.straight/addons/. If not, it is assumed it
|
144
|
+
# is already in the LOAD_PATH somehow, with rubygems for example.
|
145
|
+
def load_addons
|
146
|
+
# load ~/.straight/addons.yml
|
147
|
+
addons = YAML.load_file(ConfigDir.path + '/addons.yml')
|
148
|
+
addons.each do |name, addon|
|
149
|
+
StraightServer.logger.info "Loading #{name} addon"
|
150
|
+
if addon['path'] # First, check the ~/.straight/addons dir
|
151
|
+
require ConfigDir.path + '/' + addon['path']
|
152
|
+
else # then assume it's already loaded using rubygems
|
153
|
+
require name
|
154
|
+
end
|
155
|
+
# extending the current server object with the addon
|
156
|
+
extend Kernel.const_get("StraightServer::Addon::#{addon['module']}")
|
157
|
+
end if addons
|
158
|
+
end
|
159
|
+
|
160
|
+
# Finds orders that have statuses < 2 and starts querying the blockchain
|
161
|
+
# for them (unless they are also expired). This is for cases when the server was shut down,
|
162
|
+
# but some orders statuses are not resolved.
|
163
|
+
def resume_tracking_active_orders!
|
164
|
+
StraightServer::Order.where('status < 2').each do |order|
|
165
|
+
|
166
|
+
# Order is expired, but status is < 2! Suspcicious, probably
|
167
|
+
# an unclean shutdown of the server. Let's check and update the status manually once.
|
168
|
+
if order.time_left_before_expiration < 1
|
169
|
+
StraightServer.logger.info "Order #{order.id} seems to be expired, but status remains #{order.status}. Will check for status update manually."
|
170
|
+
order.status(reload: true)
|
171
|
+
|
172
|
+
# if we still see no transactions to that address,
|
173
|
+
# consider the order truly expired and update the status accordingly
|
174
|
+
order.status = StraightServer::Order::STATUSES[:expired] if order.status < 2
|
175
|
+
order.save
|
176
|
+
StraightServer.logger.info "Order #{order.id} status updated, new status is #{order.status}"
|
177
|
+
|
178
|
+
# Order is NOT expired and status is < 2. Let's keep tracking it.
|
179
|
+
else
|
180
|
+
StraightServer.logger.info "Resuming tracking of order #{order.id}, current status is #{order.status}, time before expiration: #{order.time_left_before_expiration} seconds."
|
181
|
+
StraightServer::Thread.new do
|
182
|
+
order.start_periodic_status_check
|
183
|
+
end
|
184
|
+
end
|
74
185
|
end
|
186
|
+
end
|
187
|
+
|
188
|
+
# Loads redis gem and sets up key prefixes for order counters
|
189
|
+
# for the current straight environment.
|
190
|
+
def setup_redis_connection
|
191
|
+
require 'redis'
|
192
|
+
Config.redis = Config.redis.keys_to_sym
|
193
|
+
Config.redis[:connection] = Redis.new(
|
194
|
+
host: Config.redis[:host],
|
195
|
+
port: Config.redis[:port],
|
196
|
+
db: Config.redis[:db],
|
197
|
+
password: Config.redis[:password]
|
198
|
+
)
|
199
|
+
Config.redis[:prefix] ||= "StraightServer:#{Config.environment}"
|
200
|
+
end
|
75
201
|
|
76
202
|
end
|
77
203
|
|
@@ -6,7 +6,7 @@ module StraightServer
|
|
6
6
|
|
7
7
|
n.times { puts "\n" }
|
8
8
|
|
9
|
-
File.open(StraightServer::Initializer::
|
9
|
+
File.open(StraightServer::Initializer::ConfigDir.path + '/' + Config.logmaster['file'], 'a') do |f|
|
10
10
|
n.times do
|
11
11
|
f.puts "\n"
|
12
12
|
end
|