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
@@ -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
|