straight-server 0.1.2 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
@@ -1,6 +1,6 @@
|
|
1
1
|
module StraightServer
|
2
2
|
|
3
|
-
class Order < Sequel::Model
|
3
|
+
class Order < Sequel::Model
|
4
4
|
|
5
5
|
include Straight::OrderModule
|
6
6
|
plugin :validation_helpers
|
@@ -8,6 +8,12 @@ module StraightServer
|
|
8
8
|
|
9
9
|
plugin :serialization
|
10
10
|
serialize_attributes :marshal, :callback_response
|
11
|
+
serialize_attributes :marshal, :data
|
12
|
+
|
13
|
+
plugin :after_initialize
|
14
|
+
def after_initialize
|
15
|
+
@status = self[:status] || 0
|
16
|
+
end
|
11
17
|
|
12
18
|
def gateway
|
13
19
|
@gateway ||= Gateway.find_by_id(gateway_id)
|
@@ -18,17 +24,42 @@ module StraightServer
|
|
18
24
|
@gateway = g
|
19
25
|
end
|
20
26
|
|
27
|
+
# This method is called from the Straight::OrderModule::Prependable
|
28
|
+
# using super(). The reason it is reloaded here is because sometimes
|
29
|
+
# we want to query the DB first and see if status has changed there.
|
30
|
+
#
|
31
|
+
# If it indeed changed in the DB and is > 1, then the original
|
32
|
+
# Straight::OrderModule::Prependable#status method will not try to
|
33
|
+
# query the blockchain (using adapters) because the status has already
|
34
|
+
# been changed to be > 1.
|
35
|
+
#
|
36
|
+
# This is mainly useful for debugging. For example,
|
37
|
+
# when testing payments, you don't actually want to pay, you can just
|
38
|
+
# run the server console, change order status in the DB and see how your
|
39
|
+
# client picks it up, showing you that your order has been paid for.
|
40
|
+
#
|
41
|
+
# If you want the feature described above on,
|
42
|
+
# set StraightServer::Config.check_order_status_in_db_first to true
|
43
|
+
def status(as_sym: false, reload: false)
|
44
|
+
if reload && StraightServer::Config.check_order_status_in_db_first
|
45
|
+
@old_status = self.status
|
46
|
+
self.refresh
|
47
|
+
unless self[:status] == @old_status
|
48
|
+
@status = self[:status]
|
49
|
+
@status_changed = true
|
50
|
+
self.gateway.order_status_changed(self)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
self[:status] = @status
|
54
|
+
end
|
55
|
+
|
21
56
|
def save
|
22
57
|
super # calling Sequel::Model save
|
23
58
|
@status_changed = false
|
24
59
|
end
|
25
60
|
|
26
|
-
def status_changed?
|
27
|
-
@status_changed
|
28
|
-
end
|
29
|
-
|
30
61
|
def to_h
|
31
|
-
super.merge({ id: id })
|
62
|
+
super.merge({ id: id, payment_id: payment_id, amount_in_btc: amount_in_btc(as: :string) })
|
32
63
|
end
|
33
64
|
|
34
65
|
def to_json
|
@@ -39,6 +70,8 @@ module StraightServer
|
|
39
70
|
super # calling Sequel::Model validator
|
40
71
|
errors.add(:amount, "is invalid") if !amount.kind_of?(Numeric) || amount <= 0
|
41
72
|
errors.add(:gateway_id, "is invalid") if !gateway_id.kind_of?(Numeric) || gateway_id <= 0
|
73
|
+
errors.add(:description, "should be shorter than 255 charachters") if description.kind_of?(String) && description.length > 255
|
74
|
+
errors.add(:gateway, "is inactive, cannot create order for inactive gateway") unless gateway.active
|
42
75
|
validates_unique :id, :address, [:keychain_id, :gateway_id]
|
43
76
|
validates_presence [:address, :keychain_id, :gateway_id, :amount]
|
44
77
|
end
|
@@ -47,16 +80,48 @@ module StraightServer
|
|
47
80
|
"order_id=#{id}&amount=#{amount}&status=#{status}&address=#{address}&tid=#{tid}"
|
48
81
|
end
|
49
82
|
|
50
|
-
def
|
51
|
-
|
83
|
+
def before_create
|
84
|
+
self.payment_id = gateway.sign_with_secret("#{keychain_id}#{amount}#{created_at}")
|
85
|
+
|
86
|
+
# Save info about current exchange rate at the time of purchase
|
87
|
+
unless gateway.default_currency == 'BTC'
|
88
|
+
self.data = {} unless self.data
|
89
|
+
self.data[:exchange_rate] = { price: gateway.current_exchange_rate, currency: gateway.default_currency }
|
90
|
+
end
|
91
|
+
|
52
92
|
super
|
53
93
|
end
|
54
94
|
|
55
|
-
|
95
|
+
# Update Gateway's order_counters, incrementing the :new counter.
|
96
|
+
# All other increments/decrements happen in the the Gateway#order_status_changed callback,
|
97
|
+
# but the initial :new increment needs this code because the Gateway#order_status_changed
|
98
|
+
# isn't called in this case.
|
99
|
+
def after_create
|
100
|
+
self.gateway.increment_order_counter!(:new) if StraightServer::Config.count_orders
|
101
|
+
end
|
102
|
+
|
103
|
+
# Reloads the method in Straight engine. We need to take
|
104
|
+
# Order#created_at into account now, so that we don't start checking on
|
105
|
+
# an order that is already expired. Or, if it's not expired yet,
|
106
|
+
# we make sure to stop all checks as soon as it expires, but not later.
|
107
|
+
def start_periodic_status_check(duration: gateway.orders_expiration_period)
|
108
|
+
StraightServer.logger.info "Starting periodic status checks of order #{self.id}"
|
109
|
+
if (t = time_left_before_expiration) > 0
|
110
|
+
check_status_on_schedule(duration: t)
|
111
|
+
end
|
112
|
+
self.save if self.status_changed?
|
113
|
+
end
|
114
|
+
|
115
|
+
def check_status_on_schedule(period: 10, iteration_index: 0, duration: 600, time_passed: 0)
|
56
116
|
StraightServer.logger.info "Checking status of order #{self.id}"
|
57
117
|
super
|
58
118
|
end
|
59
119
|
|
120
|
+
def time_left_before_expiration(duration: gateway.orders_expiration_period)
|
121
|
+
time_passed_after_creation = (Time.now - created_at).to_i
|
122
|
+
gateway.orders_expiration_period-time_passed_after_creation
|
123
|
+
end
|
124
|
+
|
60
125
|
end
|
61
126
|
|
62
127
|
end
|
@@ -13,13 +13,20 @@ module StraightServer
|
|
13
13
|
end
|
14
14
|
|
15
15
|
def create
|
16
|
+
|
17
|
+
unless @gateway
|
18
|
+
StraightServer.logger.warn "Gateway not found"
|
19
|
+
return [404, {}, "Gateway not found" ]
|
20
|
+
end
|
21
|
+
|
16
22
|
begin
|
17
23
|
order = @gateway.create_order(
|
18
|
-
amount: @params['amount'],
|
24
|
+
amount: @params['amount'], # this is satoshi
|
19
25
|
currency: @params['currency'],
|
20
26
|
btc_denomination: @params['btc_denomination'],
|
21
27
|
id: @params['order_id'],
|
22
|
-
signature: @params['signature']
|
28
|
+
signature: @params['signature'],
|
29
|
+
data: @params['data']
|
23
30
|
)
|
24
31
|
StraightServer::Thread.new do
|
25
32
|
order.start_periodic_status_check
|
@@ -33,11 +40,14 @@ module StraightServer
|
|
33
40
|
rescue StraightServer::GatewayModule::InvalidOrderId
|
34
41
|
StraightServer.logger.warn message = "An invalid id for order supplied: #{@params['order_id']}"
|
35
42
|
[409, {}, message ]
|
43
|
+
rescue StraightServer::GatewayModule::GatewayInactive
|
44
|
+
StraightServer.logger.warn message = "The gateway is inactive, you cannot create order with it"
|
45
|
+
[503, {}, message ]
|
36
46
|
end
|
37
47
|
end
|
38
48
|
|
39
49
|
def show
|
40
|
-
order = Order[@params['id']]
|
50
|
+
order = Order[@params['id']] || (@params['id'] =~ /[^\d]+/ && Order[:payment_id => @params['id']])
|
41
51
|
if order
|
42
52
|
order.status(reload: true)
|
43
53
|
order.save if order.status_changed?
|
@@ -46,7 +56,13 @@ module StraightServer
|
|
46
56
|
end
|
47
57
|
|
48
58
|
def websocket
|
49
|
-
|
59
|
+
|
60
|
+
order = if @params['id'] =~ /[^\d]+/
|
61
|
+
Order[:payment_id => @params['id']]
|
62
|
+
else
|
63
|
+
Order[@params['id']]
|
64
|
+
end
|
65
|
+
|
50
66
|
if order
|
51
67
|
begin
|
52
68
|
@gateway.add_websocket_for_order ws = Faye::WebSocket.new(@env), order
|
@@ -66,10 +82,11 @@ module StraightServer
|
|
66
82
|
StraightServer.logger.blank_lines
|
67
83
|
StraightServer.logger.info "#{@method} #{@env['REQUEST_PATH']}\n#{@params}"
|
68
84
|
|
69
|
-
@gateway = StraightServer::Gateway.
|
85
|
+
@gateway = StraightServer::Gateway.find_by_hashed_id(@request_path[1])
|
70
86
|
|
71
87
|
@response = if @request_path[3] # if an order id is supplied
|
72
|
-
@params['id'] = @request_path[3]
|
88
|
+
@params['id'] = @request_path[3]
|
89
|
+
@params['id'] = @params['id'].to_i if @params['id'] =~ /\A\d+\Z/
|
73
90
|
if @request_path[4] == 'websocket'
|
74
91
|
websocket
|
75
92
|
elsif @request_path[4].nil? && @method == 'GET'
|
@@ -6,12 +6,24 @@ module StraightServer
|
|
6
6
|
Faye::WebSocket.load_adapter('goliath')
|
7
7
|
|
8
8
|
def initialize
|
9
|
+
super
|
9
10
|
prepare
|
10
|
-
StraightServer.logger.info "
|
11
|
+
StraightServer.logger.info "starting Straight Server v #{StraightServer::VERSION}"
|
11
12
|
require_relative 'order'
|
12
13
|
require_relative 'gateway'
|
13
14
|
require_relative 'orders_controller'
|
14
|
-
|
15
|
+
load_addons
|
16
|
+
resume_tracking_active_orders!
|
17
|
+
end
|
18
|
+
|
19
|
+
def options_parser(opts, options)
|
20
|
+
# Even though we define that option here, it is purely for the purposes of compliance with
|
21
|
+
# Goliath server. If don't do that, there will be an exception saying "unrecognized argument".
|
22
|
+
# In reality, we make use of --config-dir value in the in StraightServer::Initializer and stored
|
23
|
+
# it in StraightServer::Initializer.config_dir property.
|
24
|
+
opts.on('-c', '--config-dir STRING', "Directory where config files and addons are placed") do |val|
|
25
|
+
options[:config_dir] = File.expand_path(val || ENV['HOME'] + '/.straight' )
|
26
|
+
end
|
15
27
|
end
|
16
28
|
|
17
29
|
def response(env)
|
@@ -24,21 +36,15 @@ module StraightServer
|
|
24
36
|
|
25
37
|
StraightServer.logger.watch_exceptions do
|
26
38
|
|
27
|
-
#
|
28
|
-
#
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
if env['REQUEST_PATH'] =~ /\A\/gateways\/.+?\/orders(\/.+)?\Z/
|
38
|
-
controller = OrdersController.new(env)
|
39
|
-
return controller.response
|
40
|
-
else
|
41
|
-
return [404, {}, "#{env['REQUEST_METHOD']} #{env['REQUEST_PATH']} Not found"]
|
39
|
+
# If the process is daemonized, we get Sequel::DatabaseDisconnectError with Postgres.
|
40
|
+
# The explanation is here: https://github.com/thuehlinger/daemons/issues/31
|
41
|
+
# Until I figure out where to call connect_to_db so that it connects to the DB
|
42
|
+
# AFTER the process is daemonized, this shall remain as it is now.
|
43
|
+
begin
|
44
|
+
return process_request(env)
|
45
|
+
rescue Sequel::DatabaseDisconnectError
|
46
|
+
connect_to_db
|
47
|
+
return process_request(env)
|
42
48
|
end
|
43
49
|
|
44
50
|
end
|
@@ -47,6 +53,27 @@ module StraightServer
|
|
47
53
|
[500, {}, "#{env['REQUEST_METHOD']} #{env['REQUEST_PATH']} Server Error"]
|
48
54
|
|
49
55
|
end
|
56
|
+
|
57
|
+
# This is a separate method now because of the need to rescue Sequel::DatabaseDisconnectError
|
58
|
+
# As soon as we figure out where should #connect_to_db be placed so that it is executed AFTER the process
|
59
|
+
# is daemonized, I'll refactor the code.
|
60
|
+
def process_request(env)
|
61
|
+
# This is a client implementation example, an html page + a dart script
|
62
|
+
# supposed to only be loaded in development.
|
63
|
+
if Goliath.env == :development
|
64
|
+
if env['REQUEST_PATH'] == '/'
|
65
|
+
return [200, {}, IO.read(Initializer::GEM_ROOT + '/examples/client/client.html')]
|
66
|
+
elsif Goliath.env == :development && env['REQUEST_PATH'] == '/client.js'
|
67
|
+
return [200, {}, IO.read(Initializer::GEM_ROOT + '/examples/client/client.js')]
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
@routes.each do |path, action| # path is a regexp
|
72
|
+
return action.call(env) if env['REQUEST_PATH'] =~ path
|
73
|
+
end
|
74
|
+
# no block was called, means no route matched. Let's render 404
|
75
|
+
return [404, {}, "#{env['REQUEST_METHOD']} #{env['REQUEST_PATH']} Not found"]
|
76
|
+
end
|
50
77
|
|
51
78
|
end
|
52
79
|
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
class Hash
|
2
|
+
|
3
|
+
# Replace String keys in the current hash with symbol keys
|
4
|
+
def keys_to_sym!
|
5
|
+
new_hash = keys_to_sym
|
6
|
+
self.clear
|
7
|
+
new_hash.each do |k,v|
|
8
|
+
self[k] = v
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def keys_to_sym
|
13
|
+
symbolized_hash = {}
|
14
|
+
self.each do |k,v|
|
15
|
+
if k =~ /\A[a-zA-Z0-9!?_]+\Z/
|
16
|
+
symbolized_hash[k.to_sym] = v
|
17
|
+
else
|
18
|
+
symbolized_hash[k] = v
|
19
|
+
end
|
20
|
+
end
|
21
|
+
symbolized_hash
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
data/lib/straight-server.rb
CHANGED
@@ -3,12 +3,15 @@ require 'json'
|
|
3
3
|
require 'sequel'
|
4
4
|
require 'straight'
|
5
5
|
require 'logmaster'
|
6
|
-
require '
|
7
|
-
require '
|
6
|
+
require 'openssl'
|
7
|
+
require 'base64'
|
8
8
|
require 'net/http'
|
9
9
|
require 'faye/websocket'
|
10
10
|
Sequel.extension :migration
|
11
11
|
|
12
|
+
|
13
|
+
require_relative 'straight-server/utils/hash_string_to_sym_keys'
|
14
|
+
require_relative 'straight-server/random_string'
|
12
15
|
require_relative 'straight-server/config'
|
13
16
|
require_relative 'straight-server/initializer'
|
14
17
|
require_relative 'straight-server/thread'
|
@@ -16,7 +19,7 @@ require_relative 'straight-server/orders_controller'
|
|
16
19
|
|
17
20
|
module StraightServer
|
18
21
|
|
19
|
-
VERSION =
|
22
|
+
VERSION = File.read(File.expand_path('../', File.dirname(__FILE__)) + '/VERSION')
|
20
23
|
|
21
24
|
class << self
|
22
25
|
attr_accessor :db_connection, :logger
|
data/spec/.straight/config.yml
CHANGED
@@ -1,6 +1,8 @@
|
|
1
1
|
# If set to db, then use DB table to store gateways,
|
2
2
|
# useful when your run many gateways on the same server.
|
3
3
|
gateways_source: config
|
4
|
+
environment: test
|
5
|
+
count_orders: true
|
4
6
|
|
5
7
|
gateways:
|
6
8
|
|
@@ -12,10 +14,12 @@ gateways:
|
|
12
14
|
check_signature: true
|
13
15
|
callback_url: 'http://localhost:3000/payment-callback'
|
14
16
|
default_currency: 'BTC'
|
17
|
+
orders_expiration_period: 900 # seconds
|
15
18
|
exchange_rate_adapters:
|
16
19
|
- Bitpay
|
17
20
|
- Coinbase
|
18
21
|
- Bitstamp
|
22
|
+
active: true
|
19
23
|
second_gateway:
|
20
24
|
pubkey: 'xpub-001'
|
21
25
|
confirmations_required: 0
|
@@ -24,11 +28,23 @@ gateways:
|
|
24
28
|
check_signature: false
|
25
29
|
callback_url: 'http://localhost:3001/payment-callback'
|
26
30
|
default_currency: 'BTC'
|
31
|
+
orders_expiration_period: 300 # seconds
|
27
32
|
exchange_rate_adapters:
|
28
33
|
- Bitpay
|
29
34
|
- Coinbase
|
30
35
|
- Bitstamp
|
36
|
+
active: true
|
37
|
+
|
38
|
+
blockchain_adapters:
|
39
|
+
- BlockchainInfo
|
40
|
+
- Mycelium
|
31
41
|
|
32
42
|
db:
|
33
43
|
adapter: sqlite
|
34
44
|
name: straight.db
|
45
|
+
|
46
|
+
redis:
|
47
|
+
host: localhost
|
48
|
+
port: 6379
|
49
|
+
db: null
|
50
|
+
password: null
|
@@ -0,0 +1 @@
|
|
1
|
+
global server secret
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# This is where you put info about your addons.
|
2
|
+
#
|
3
|
+
# Addons are just modules that extend the StraightServer::Server module.
|
4
|
+
#
|
5
|
+
# Addon modules can be both rubygems or files under ~/.straight/addons/.
|
6
|
+
# If ~/.straight/addons.yml contains a 'path' key for a particular addon, then it means
|
7
|
+
# the addon is placed under the ~/.straight/addons/. If not, it is assumed it is
|
8
|
+
# already in the LOAD_PATH somehow, with rubygems for example.
|
9
|
+
#
|
10
|
+
#
|
11
|
+
# Example:
|
12
|
+
#
|
13
|
+
# payment_ui: # <- name doesn't affect anything, just shows up in the log file
|
14
|
+
# path: addons/payment_ui # <- This is unnecessary if addon is already in the LOAD_PATH
|
15
|
+
# module: PaymentUI # <- actual module should be a submodule of StraightServer::Addon
|
16
|
+
|
17
|
+
test_addon:
|
18
|
+
path: 'addons/test_addon'
|
19
|
+
module: 'TestAddon'
|
data/spec/lib/gateway_spec.rb
CHANGED
@@ -5,6 +5,7 @@ RSpec.describe StraightServer::Gateway do
|
|
5
5
|
before(:each) do
|
6
6
|
@gateway = StraightServer::GatewayOnConfig.find_by_id(1)
|
7
7
|
@order_mock = double("order mock")
|
8
|
+
allow(@order_mock).to receive(:old_status)
|
8
9
|
[:id, :gateway=, :save, :to_h, :id=].each { |m| allow(@order_mock).to receive(m) }
|
9
10
|
@order_for_keychain_id_args = { amount: 1, keychain_id: 1, currency: nil, btc_denomination: nil }
|
10
11
|
end
|
@@ -13,7 +14,7 @@ RSpec.describe StraightServer::Gateway do
|
|
13
14
|
@gateway.last_keychain_id = 0
|
14
15
|
expect( -> { @gateway.create_order(amount: 1, signature: 'invalid', id: 1) }).to raise_exception(StraightServer::GatewayModule::InvalidSignature)
|
15
16
|
expect(@gateway).to receive(:order_for_keychain_id).with(@order_for_keychain_id_args).once.and_return(@order_mock)
|
16
|
-
@gateway.create_order(amount: 1, signature:
|
17
|
+
@gateway.create_order(amount: 1, signature: hmac_sha256(1, 'secret'), id: 1)
|
17
18
|
end
|
18
19
|
|
19
20
|
it "checks md5 signature only if that setting is set ON for a particular gateway" do
|
@@ -25,8 +26,8 @@ RSpec.describe StraightServer::Gateway do
|
|
25
26
|
end
|
26
27
|
|
27
28
|
it "doesn't allow nil or empty order id if signature checks are enabled" do
|
28
|
-
expect( -> { @gateway.create_order(amount: 1, signature: '
|
29
|
-
expect( -> { @gateway.create_order(amount: 1, signature: '
|
29
|
+
expect( -> { @gateway.create_order(amount: 1, signature: hmac_sha256(nil, 'secret'), id: nil) }).to raise_exception(StraightServer::GatewayModule::InvalidOrderId)
|
30
|
+
expect( -> { @gateway.create_order(amount: 1, signature: hmac_sha256('', 'secret'), id: '') }).to raise_exception(StraightServer::GatewayModule::InvalidOrderId)
|
30
31
|
end
|
31
32
|
|
32
33
|
it "sets order amount in satoshis calculated from another currency" do
|
@@ -36,6 +37,16 @@ RSpec.describe StraightServer::Gateway do
|
|
36
37
|
expect(@gateway.create_order(amount: 2252.706, currency: 'USD').amount).to eq(500000000)
|
37
38
|
end
|
38
39
|
|
40
|
+
it "doesn't allow to create a new order if the gateway is inactive" do
|
41
|
+
@gateway.active = false
|
42
|
+
expect( -> { @gateway.create_order }).to raise_exception(StraightServer::GatewayModule::GatewayInactive)
|
43
|
+
@gateway.active = true
|
44
|
+
end
|
45
|
+
|
46
|
+
it "loads blockchain adapters according to the config file" do
|
47
|
+
expect(@gateway.blockchain_adapters.map(&:class)).to eq([Straight::Blockchain::BlockchainInfoAdapter, Straight::Blockchain::MyceliumAdapter])
|
48
|
+
end
|
49
|
+
|
39
50
|
context "callback url" do
|
40
51
|
|
41
52
|
before(:each) do
|
@@ -64,7 +75,7 @@ RSpec.describe StraightServer::Gateway do
|
|
64
75
|
it "signs the callback if gateway has a secret" do
|
65
76
|
@gateway = StraightServer::GatewayOnConfig.find_by_id(1) # Gateway 1 requires signatures
|
66
77
|
expect(@response_mock).to receive(:code).twice.and_return("200")
|
67
|
-
expect(URI).to receive(:parse).with('http://localhost:3000/payment-callback?' + @order.to_http_params + "&signature=#{
|
78
|
+
expect(URI).to receive(:parse).with('http://localhost:3000/payment-callback?' + @order.to_http_params + "&signature=#{hmac_sha256(hmac_sha256(@order.id, 'secret'), 'secret')}")
|
68
79
|
expect(Net::HTTP).to receive(:get_response).and_return(@response_mock)
|
69
80
|
@gateway.order_status_changed(@order)
|
70
81
|
end
|
@@ -88,6 +99,51 @@ RSpec.describe StraightServer::Gateway do
|
|
88
99
|
|
89
100
|
end
|
90
101
|
|
102
|
+
describe "order counters" do
|
103
|
+
|
104
|
+
it "uses 0 for non-existent order counters and increments them" do
|
105
|
+
expect(@gateway.order_counters(reload: true)).to include({ new: 0, unconfirmed: 0, paid: 0, underpaid: 0, overpaid: 0, expired: 0 })
|
106
|
+
@gateway.increment_order_counter!(:new)
|
107
|
+
expect(@gateway.order_counters(reload: true)[:new]).to eq(1)
|
108
|
+
end
|
109
|
+
|
110
|
+
it "raises exception when trying to access counters but the feature is disabled" do
|
111
|
+
allow(StraightServer::Config).to receive(:count_orders).and_return(false)
|
112
|
+
expect( -> { @gateway.order_counters(reload: true) }).to raise_exception(StraightServer::Gateway::OrderCountersDisabled)
|
113
|
+
expect( -> { @gateway.increment_order_counter!(:new) }).to raise_exception(StraightServer::Gateway::OrderCountersDisabled)
|
114
|
+
end
|
115
|
+
|
116
|
+
it "updates gateway's order counters when an associated order status changes" do
|
117
|
+
allow_any_instance_of(StraightServer::Order).to receive(:transaction).and_return({ tid: 'xxx' })
|
118
|
+
allow(@gateway).to receive(:send_callback_http_request)
|
119
|
+
allow(@gateway).to receive(:send_order_to_websocket_client)
|
120
|
+
|
121
|
+
expect(@gateway.order_counters(reload: true)).to eq({ new: 0, unconfirmed: 0, paid: 0, underpaid: 0, overpaid: 0, expired: 0 })
|
122
|
+
order = create(:order, gateway_id: @gateway.id)
|
123
|
+
expect(@gateway.order_counters(reload: true)).to eq({ new: 1, unconfirmed: 0, paid: 0, underpaid: 0, overpaid: 0, expired: 0 })
|
124
|
+
order.status = 2
|
125
|
+
expect(@gateway.order_counters(reload: true)).to eq({ new: 0, unconfirmed: 0, paid: 1, underpaid: 0, overpaid: 0, expired: 0 })
|
126
|
+
|
127
|
+
expect(@gateway.order_counters(reload: true)).to eq({ new: 0, unconfirmed: 0, paid: 1, underpaid: 0, overpaid: 0, expired: 0 })
|
128
|
+
order = create(:order, gateway_id: @gateway.id)
|
129
|
+
expect(@gateway.order_counters(reload: true)).to eq({ new: 1, unconfirmed: 0, paid: 1, underpaid: 0, overpaid: 0, expired: 0 })
|
130
|
+
order.status = 1
|
131
|
+
expect(@gateway.order_counters(reload: true)).to eq({ new: 0, unconfirmed: 1, paid: 1, underpaid: 0, overpaid: 0, expired: 0 })
|
132
|
+
order.status = 5
|
133
|
+
expect(@gateway.order_counters(reload: true)).to eq({ new: 0, unconfirmed: 0, paid: 1, underpaid: 0, overpaid: 0, expired: 1 })
|
134
|
+
end
|
135
|
+
|
136
|
+
it "doesn't increment orders on status update unless the option is turned on (but no exception raised)" do
|
137
|
+
allow(StraightServer::Config).to receive(:count_orders).and_return(false)
|
138
|
+
allow_any_instance_of(StraightServer::Order).to receive(:transaction).and_return({ tid: 'xxx' })
|
139
|
+
allow(@gateway).to receive(:send_callback_http_request)
|
140
|
+
allow(@gateway).to receive(:send_order_to_websocket_client)
|
141
|
+
order = create(:order, gateway_id: @gateway.id)
|
142
|
+
expect(StraightServer::Config.redis[:connection].get("#{StraightServer::Config.redis[:prefix]}:gateway_#{@gateway.id}:new_orders_counter")).to be_nil
|
143
|
+
end
|
144
|
+
|
145
|
+
end
|
146
|
+
|
91
147
|
describe "config based gateway" do
|
92
148
|
|
93
149
|
it "loads all the gateways from the config file and assigns correct attributes" do
|
@@ -113,16 +169,23 @@ RSpec.describe StraightServer::Gateway do
|
|
113
169
|
expect(File.read("#{ENV['HOME']}/.straight/default_last_keychain_id").to_i).to eq(1)
|
114
170
|
|
115
171
|
expect(@gateway).to receive(:order_for_keychain_id).with(@order_for_keychain_id_args.merge({ keychain_id: 2})).once.and_return(@order_mock)
|
116
|
-
@gateway.create_order(amount: 1, signature:
|
172
|
+
@gateway.create_order(amount: 1, signature: hmac_sha256(1, 'secret'), id: 1)
|
117
173
|
expect(File.read("#{ENV['HOME']}/.straight/default_last_keychain_id").to_i).to eq(2)
|
118
174
|
end
|
175
|
+
|
176
|
+
it "searches for Gateway using regular ids when find_by_hashed_id method is called" do
|
177
|
+
expect(StraightServer::GatewayOnConfig.find_by_hashed_id(1)).not_to be_nil
|
178
|
+
end
|
119
179
|
|
120
180
|
end
|
121
181
|
|
122
182
|
describe "db based gateway" do
|
123
183
|
|
124
184
|
before(:each) do
|
125
|
-
|
185
|
+
# clean the database
|
186
|
+
DB.run("DELETE FROM gateways")
|
187
|
+
|
188
|
+
@gateway = StraightServer::GatewayOnDB.new(
|
126
189
|
confirmations_required: 0,
|
127
190
|
pubkey: 'xpub-000',
|
128
191
|
order_class: 'StraightServer::Order',
|
@@ -134,21 +197,40 @@ RSpec.describe StraightServer::Gateway do
|
|
134
197
|
end
|
135
198
|
|
136
199
|
it "saves and retrieves last_keychain_id from the db" do
|
200
|
+
@gateway.save
|
137
201
|
expect(DB[:gateways][:name => 'default'][:last_keychain_id]).to eq(0)
|
138
202
|
@gateway.increment_last_keychain_id!
|
139
203
|
expect(DB[:gateways][:name => 'default'][:last_keychain_id]).to eq(1)
|
140
204
|
|
141
205
|
expect(@gateway).to receive(:order_for_keychain_id).with(@order_for_keychain_id_args.merge({ keychain_id: 2})).once.and_return(@order_mock)
|
142
|
-
@gateway.create_order(amount: 1, signature:
|
206
|
+
@gateway.create_order(amount: 1, signature: hmac_sha256(1, 'secret'), id: 1)
|
143
207
|
expect(DB[:gateways][:name => 'default'][:last_keychain_id]).to eq(2)
|
144
208
|
end
|
145
209
|
|
210
|
+
it "encryptes and decrypts the gateway secret" do
|
211
|
+
expect(@gateway.send(:encrypt_secret)).to eq("96c1c24edff5c1c2:6THJEZqg+2qlDhtWE2Tytg==")
|
212
|
+
expect(@gateway.send(:decrypt_secret)).to eq("secret")
|
213
|
+
expect(@gateway.secret).to eq("secret")
|
214
|
+
end
|
215
|
+
|
216
|
+
it "finds orders using #find_by_id method which is essentially an alias for Gateway[]" do
|
217
|
+
@gateway.save
|
218
|
+
expect(StraightServer::GatewayOnDB.find_by_id(@gateway.id)).to eq(@gateway)
|
219
|
+
end
|
220
|
+
|
221
|
+
it "assigns hashed_id to gateway and then finds gateway using that value" do
|
222
|
+
@gateway.save
|
223
|
+
hashed_id = hmac_sha256(@gateway.id, 'global server secret')
|
224
|
+
expect(@gateway.hashed_id).to eq(hashed_id)
|
225
|
+
expect(StraightServer::GatewayOnDB.find_by_hashed_id(hashed_id)).to eq(@gateway)
|
226
|
+
end
|
227
|
+
|
146
228
|
end
|
147
229
|
|
148
230
|
describe "handling websockets" do
|
149
231
|
|
150
232
|
before(:each) do
|
151
|
-
|
233
|
+
StraightServer::GatewayModule.class_variable_set(:@@websockets, { @gateway.id => {} })
|
152
234
|
@ws = double("websocket mock")
|
153
235
|
allow(@ws).to receive(:on).with(:close)
|
154
236
|
allow(@order_mock).to receive(:id).and_return(1)
|
@@ -157,7 +239,7 @@ RSpec.describe StraightServer::Gateway do
|
|
157
239
|
|
158
240
|
it "adds a new websocket for the order" do
|
159
241
|
@gateway.add_websocket_for_order(@ws, @order_mock)
|
160
|
-
expect(@gateway.
|
242
|
+
expect(@gateway.websockets).to eq({1 => @ws})
|
161
243
|
end
|
162
244
|
|
163
245
|
it "sends a message to the websocket when status of the order is changed and closes the connection" do
|
@@ -182,10 +264,8 @@ RSpec.describe StraightServer::Gateway do
|
|
182
264
|
|
183
265
|
end
|
184
266
|
|
185
|
-
def
|
186
|
-
|
187
|
-
h << key.to_s
|
188
|
-
h.hexdigest
|
267
|
+
def hmac_sha256(key, secret)
|
268
|
+
OpenSSL::HMAC.digest('sha256', secret, key.to_s).unpack("H*").first
|
189
269
|
end
|
190
270
|
|
191
271
|
end
|