straight-server 0.1.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 +7 -0
- data/.document +5 -0
- data/.rspec +1 -0
- data/Gemfile +24 -0
- data/Gemfile.lock +138 -0
- data/LICENSE.txt +21 -0
- data/README.md +219 -0
- data/Rakefile +27 -0
- data/VERSION +1 -0
- data/bin/straight-console +12 -0
- data/bin/straight-server +6 -0
- data/db/migrations/001_create_orders.rb +26 -0
- data/db/migrations/002_create_gateways.rb +28 -0
- data/examples/client/client.dart +67 -0
- data/examples/client/client.html +32 -0
- data/lib/straight-server/config.rb +11 -0
- data/lib/straight-server/gateway.rb +260 -0
- data/lib/straight-server/initializer.rb +78 -0
- data/lib/straight-server/logger.rb +18 -0
- data/lib/straight-server/order.rb +62 -0
- data/lib/straight-server/orders_controller.rb +86 -0
- data/lib/straight-server/server.rb +52 -0
- data/lib/straight-server/thread.rb +9 -0
- data/lib/straight-server.rb +25 -0
- data/spec/.straight/config.yml +34 -0
- data/spec/factories.rb +11 -0
- data/spec/lib/gateway_spec.rb +191 -0
- data/spec/lib/order_spec.rb +82 -0
- data/spec/lib/orders_controller_spec.rb +113 -0
- data/spec/spec_helper.rb +77 -0
- data/spec/support/custom_matchers.rb +44 -0
- data/templates/config.yml +46 -0
- metadata +220 -0
@@ -0,0 +1,260 @@
|
|
1
|
+
module StraightServer
|
2
|
+
|
3
|
+
# This module contains common features of Gateway, later to be included
|
4
|
+
# in one of the classes below.
|
5
|
+
module GatewayModule
|
6
|
+
|
7
|
+
class InvalidSignature < Exception; end
|
8
|
+
class InvalidOrderId < Exception; end
|
9
|
+
class CallbackUrlBadResponse < Exception; end
|
10
|
+
class WebsocketExists < Exception; end
|
11
|
+
class WebsocketForCompletedOrder < Exception; end
|
12
|
+
|
13
|
+
CALLBACK_URL_ATTEMPT_TIMEFRAME = 3600 # seconds
|
14
|
+
|
15
|
+
def initialize(*attrs)
|
16
|
+
|
17
|
+
# When the status of an order changes, we send an http request to the callback_url
|
18
|
+
# and also notify a websocket client (if present, of course).
|
19
|
+
@order_callbacks = [
|
20
|
+
lambda do |order|
|
21
|
+
StraightServer::Thread.new do
|
22
|
+
send_callback_http_request order
|
23
|
+
send_order_to_websocket_client order
|
24
|
+
end
|
25
|
+
end
|
26
|
+
]
|
27
|
+
|
28
|
+
@blockchain_adapters = [
|
29
|
+
Straight::Blockchain::BlockchainInfoAdapter.mainnet_adapter,
|
30
|
+
Straight::Blockchain::HelloblockIoAdapter.mainnet_adapter
|
31
|
+
]
|
32
|
+
|
33
|
+
@exchange_rate_adapters = []
|
34
|
+
@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
|
+
end
|
40
|
+
|
41
|
+
# Creates a new order and saves into the DB. Checks if the MD5 hash
|
42
|
+
# is correct first.
|
43
|
+
def create_order(attrs={})
|
44
|
+
StraightServer.logger.info "Creating new order with attrs: #{attrs}"
|
45
|
+
signature = attrs.delete(:signature)
|
46
|
+
raise InvalidOrderId if check_signature && (attrs[:id].nil? || attrs[:id].to_i <= 0)
|
47
|
+
if !check_signature || sign_with_secret(attrs[:id]) == signature
|
48
|
+
order = order_for_keychain_id(
|
49
|
+
amount: attrs[:amount],
|
50
|
+
keychain_id: increment_last_keychain_id!,
|
51
|
+
currency: attrs[:currency],
|
52
|
+
btc_denomination: attrs[:btc_denomination]
|
53
|
+
)
|
54
|
+
order.id = attrs[:id].to_i if attrs[:id]
|
55
|
+
order.data = attrs[:data] if attrs[:data]
|
56
|
+
order.gateway = self
|
57
|
+
order.save
|
58
|
+
self.save
|
59
|
+
StraightServer.logger.info "Order #{order.id} created: #{order.to_h}"
|
60
|
+
order
|
61
|
+
else
|
62
|
+
StraightServer.logger.warn "Invalid signature, cannot create an order for gateway (#{id})"
|
63
|
+
raise InvalidSignature
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# Used to track the current keychain_id number, which is used by
|
68
|
+
# Straight::Gateway to generate addresses from the pubkey. The number is supposed
|
69
|
+
# to be incremented by 1. In the case of a Config file type of Gateway, the value
|
70
|
+
# is stored in a file in the .straight directory.
|
71
|
+
def increment_last_keychain_id!
|
72
|
+
self.last_keychain_id += 1
|
73
|
+
self.save
|
74
|
+
self.last_keychain_id
|
75
|
+
end
|
76
|
+
|
77
|
+
def add_websocket_for_order(ws, order)
|
78
|
+
raise WebsocketExists unless @websockets[order.id].nil?
|
79
|
+
raise WebsocketForCompletedOrder unless order.status < 2
|
80
|
+
StraightServer.logger.info "Opening ws connection for #{order.id}"
|
81
|
+
ws.on(:close) do |event|
|
82
|
+
@websockets.delete(order.id)
|
83
|
+
StraightServer.logger.info "Closing ws connection for #{order.id}"
|
84
|
+
end
|
85
|
+
@websockets[order.id] = ws
|
86
|
+
ws
|
87
|
+
end
|
88
|
+
|
89
|
+
def send_order_to_websocket_client(order)
|
90
|
+
if ws = @websockets[order.id]
|
91
|
+
ws.send(order.to_json)
|
92
|
+
ws.close
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def initialize_exchange_rate_adapters
|
97
|
+
if self.exchange_rate_adapter_names
|
98
|
+
self.exchange_rate_adapter_names.each do |adapter|
|
99
|
+
begin
|
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
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
private
|
109
|
+
|
110
|
+
def sign_with_secret(content, level: 1)
|
111
|
+
result = content.to_s
|
112
|
+
level.times do
|
113
|
+
h = HMAC::SHA1.new(secret)
|
114
|
+
h << result
|
115
|
+
result = h.hexdigest
|
116
|
+
end
|
117
|
+
result
|
118
|
+
end
|
119
|
+
|
120
|
+
# Tries to send a callback HTTP request to the resource specified
|
121
|
+
# in the #callback_url. If it fails for any reason, it keeps trying for an hour (3600 seconds)
|
122
|
+
# making 10 http requests, each delayed by twice the time the previous one was delayed.
|
123
|
+
# This method is supposed to be running in a separate thread.
|
124
|
+
def send_callback_http_request(order, delay: 5)
|
125
|
+
return if callback_url.nil?
|
126
|
+
|
127
|
+
StraightServer.logger.info "Attempting to send request to the callback url for order #{order.id} to #{callback_url}..."
|
128
|
+
|
129
|
+
# Composing the request uri here
|
130
|
+
signature = self.check_signature ? "&signature=#{sign_with_secret(order.id, level: 2)}" : ''
|
131
|
+
data = order.data ? "&data=#{order.data}" : ''
|
132
|
+
uri = URI.parse(callback_url + '?' + order.to_http_params + signature + data)
|
133
|
+
|
134
|
+
begin
|
135
|
+
response = Net::HTTP.get_response(uri)
|
136
|
+
order.callback_response = { code: response.code, body: response.body }
|
137
|
+
order.save
|
138
|
+
raise CallbackUrlBadResponse unless response.code.to_i == 200
|
139
|
+
rescue Exception => e
|
140
|
+
if delay < CALLBACK_URL_ATTEMPT_TIMEFRAME
|
141
|
+
sleep(delay)
|
142
|
+
send_callback_http_request(order, delay: delay*2)
|
143
|
+
else
|
144
|
+
StraightServer.logger.warn "Callback request for order #{order.id} faile, see order's #callback_response field for details"
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
StraightServer.logger.info "Callback request for order #{order.id} performed successfully"
|
149
|
+
end
|
150
|
+
|
151
|
+
end
|
152
|
+
|
153
|
+
# Uses database to load and save attributes
|
154
|
+
class GatewayOnDB < Sequel::Model(:gateways)
|
155
|
+
|
156
|
+
include Straight::GatewayModule
|
157
|
+
include GatewayModule
|
158
|
+
plugin :timestamps, create: :created_at, update: :updated_at
|
159
|
+
plugin :serialization, :marshal, :exchange_rate_adapter_names
|
160
|
+
|
161
|
+
end
|
162
|
+
|
163
|
+
# Uses a config file to load attributes and a special _last_keychain_id file
|
164
|
+
# to store last_keychain_id
|
165
|
+
class GatewayOnConfig
|
166
|
+
|
167
|
+
include Straight::GatewayModule
|
168
|
+
include GatewayModule
|
169
|
+
|
170
|
+
# This is the key that allows users (those, who use the gateway,
|
171
|
+
# online stores, for instance) to connect and create orders.
|
172
|
+
# It is not used directly, but is mixed with all the params being sent
|
173
|
+
# and a MD5 hash is calculted. Then the gateway checks whether the
|
174
|
+
# MD5 hash is correct.
|
175
|
+
attr_accessor :secret
|
176
|
+
|
177
|
+
# This is used to generate the next address to accept payments
|
178
|
+
attr_accessor :last_keychain_id
|
179
|
+
|
180
|
+
# If set to false, doesn't require an unique id of the order along with
|
181
|
+
# the signed md5 hash of that id + secret to be passed into the #create_order method.
|
182
|
+
attr_accessor :check_signature
|
183
|
+
|
184
|
+
# A url to which the gateway will send an HTTP request with the status of the order data
|
185
|
+
# (in JSON) when the status of the order is changed. The response should always be 200,
|
186
|
+
# otherwise the gateway will awesome something went wrong and will keep trying to send requests
|
187
|
+
# to this url according to a specific shedule.
|
188
|
+
attr_accessor :callback_url
|
189
|
+
|
190
|
+
# This will be assigned the number that is the order in which this gateway follows in
|
191
|
+
# the config file.
|
192
|
+
attr_accessor :id
|
193
|
+
|
194
|
+
attr_accessor :exchange_rate_adapter_names
|
195
|
+
|
196
|
+
# Because this is a config based gateway, we only save last_keychain_id
|
197
|
+
# and nothing more.
|
198
|
+
def save
|
199
|
+
File.open(@last_keychain_id_file, 'w') {|f| f.write(last_keychain_id) }
|
200
|
+
end
|
201
|
+
|
202
|
+
# Loads last_keychain_id from a file in the .straight dir.
|
203
|
+
# If the file doesn't exist, we create it. Later, whenever an attribute is updated,
|
204
|
+
# we save it to the file.
|
205
|
+
def load_last_keychain_id!
|
206
|
+
@last_keychain_id_file = StraightServer::Initializer::STRAIGHT_CONFIG_PATH +
|
207
|
+
"/#{name}_last_keychain_id"
|
208
|
+
if File.exists?(@last_keychain_id_file)
|
209
|
+
self.last_keychain_id = File.read(@last_keychain_id_file).to_i
|
210
|
+
else
|
211
|
+
self.last_keychain_id = 0
|
212
|
+
save
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
# This will later be used in the #find_by_id. Because we don't use a DB,
|
217
|
+
# the id will actually be the index of an element in this Array. Thus,
|
218
|
+
# the order in which gateways follow in the config file is important.
|
219
|
+
@@gateways = []
|
220
|
+
|
221
|
+
# Create instances of Gateway by reading attributes from Config
|
222
|
+
i = 0
|
223
|
+
StraightServer::Config.gateways.each do |name, attrs|
|
224
|
+
i += 1
|
225
|
+
gateway = self.new
|
226
|
+
gateway.pubkey = attrs['pubkey']
|
227
|
+
gateway.confirmations_required = attrs['confirmations_required'].to_i
|
228
|
+
gateway.order_class = attrs['order_class']
|
229
|
+
gateway.secret = attrs['secret']
|
230
|
+
gateway.check_signature = attrs['check_signature']
|
231
|
+
gateway.callback_url = attrs['callback_url']
|
232
|
+
gateway.default_currency = attrs['default_currency']
|
233
|
+
gateway.name = name
|
234
|
+
gateway.id = i
|
235
|
+
gateway.exchange_rate_adapter_names = attrs['exchange_rate_adapters']
|
236
|
+
gateway.initialize_exchange_rate_adapters
|
237
|
+
gateway.load_last_keychain_id!
|
238
|
+
@@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
|
247
|
+
|
248
|
+
end
|
249
|
+
|
250
|
+
# It may not be a perfect way to implement such a thing, but it gives enough flexibility to people
|
251
|
+
# so they can simply start using a single gateway on their machines, a gateway which attributes are defined
|
252
|
+
# in a config file instead of a DB. That way they don't need special tools to access the DB and create
|
253
|
+
# a gateway, but can simply edit the config file.
|
254
|
+
Gateway = if StraightServer::Config.gateways_source = 'config'
|
255
|
+
GatewayOnConfig
|
256
|
+
else
|
257
|
+
GatewayOnDB
|
258
|
+
end
|
259
|
+
|
260
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
module StraightServer
|
2
|
+
|
3
|
+
module Initializer
|
4
|
+
|
5
|
+
GEM_ROOT = File.expand_path('../..', File.dirname(__FILE__))
|
6
|
+
STRAIGHT_CONFIG_PATH = ENV['HOME'] + '/.straight'
|
7
|
+
|
8
|
+
def prepare
|
9
|
+
create_config_file unless File.exist?(STRAIGHT_CONFIG_PATH + '/config.yml')
|
10
|
+
read_config_file
|
11
|
+
create_logger
|
12
|
+
connect_to_db
|
13
|
+
run_migrations if migrations_pending?
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def create_config_file
|
19
|
+
puts "\e[1;33mWARNING!\e[0m \e[33mNo file ~/.straight/config was found. Created a sample one for you.\e[0m"
|
20
|
+
puts "You should edit it and try starting the server again.\n"
|
21
|
+
|
22
|
+
FileUtils.mkdir_p(STRAIGHT_CONFIG_PATH) unless File.exist?(STRAIGHT_CONFIG_PATH)
|
23
|
+
FileUtils.cp(GEM_ROOT + '/templates/config.yml', ENV['HOME'] + '/.straight/')
|
24
|
+
puts "Shutting down now.\n\n"
|
25
|
+
exit
|
26
|
+
end
|
27
|
+
|
28
|
+
def read_config_file
|
29
|
+
YAML.load_file(STRAIGHT_CONFIG_PATH + '/config.yml').each do |k,v|
|
30
|
+
StraightServer::Config.send(k + '=', v)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def connect_to_db
|
35
|
+
|
36
|
+
# symbolize keys for convenience
|
37
|
+
db_config = StraightServer::Config.db.inject({}){|memo,(k,v)| memo[k.to_sym] = v; memo}
|
38
|
+
|
39
|
+
db_name = if db_config[:adapter] == 'sqlite'
|
40
|
+
STRAIGHT_CONFIG_PATH + "/" + db_config[:name]
|
41
|
+
else
|
42
|
+
db_config[:name]
|
43
|
+
end
|
44
|
+
|
45
|
+
StraightServer.db_connection = Sequel.connect(
|
46
|
+
"#{db_config[:adapter]}://" +
|
47
|
+
"#{db_config[:user]}#{(":" if db_config[:user])}" +
|
48
|
+
"#{db_config[:password]}#{("@" if db_config[:user] || db_config[:password])}" +
|
49
|
+
"#{db_config[:host]}#{(":" if db_config[:port])}" +
|
50
|
+
"#{db_config[:port]}#{("/" if db_config[:host] || db_config[:port])}" +
|
51
|
+
"#{db_name}"
|
52
|
+
)
|
53
|
+
end
|
54
|
+
|
55
|
+
def run_migrations
|
56
|
+
print "\nPending migrations for the database detected. Migrating..."
|
57
|
+
Sequel::Migrator.run(StraightServer.db_connection, GEM_ROOT + '/db/migrations/')
|
58
|
+
print "done\n\n"
|
59
|
+
end
|
60
|
+
|
61
|
+
def migrations_pending?
|
62
|
+
!Sequel::Migrator.is_current?(StraightServer.db_connection, GEM_ROOT + '/db/migrations/')
|
63
|
+
end
|
64
|
+
|
65
|
+
def create_logger
|
66
|
+
require_relative 'logger'
|
67
|
+
StraightServer.logger = StraightServer::Logger.new(
|
68
|
+
log_level: ::Logger.const_get(Config.logmaster['log_level'].upcase),
|
69
|
+
file: STRAIGHT_CONFIG_PATH + '/' + Config.logmaster['file'],
|
70
|
+
raise_exception: Config.logmaster['raise_exception'],
|
71
|
+
name: Config.logmaster['name'],
|
72
|
+
email_config: Config.logmaster['email_config']
|
73
|
+
)
|
74
|
+
end
|
75
|
+
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module StraightServer
|
2
|
+
class Logger < Logmaster
|
3
|
+
|
4
|
+
# inserts a number of blank lines
|
5
|
+
def blank_lines(n=1)
|
6
|
+
|
7
|
+
n.times { puts "\n" }
|
8
|
+
|
9
|
+
File.open(StraightServer::Initializer::STRAIGHT_CONFIG_PATH + '/' + Config.logmaster['file'], 'a') do |f|
|
10
|
+
n.times do
|
11
|
+
f.puts "\n"
|
12
|
+
end
|
13
|
+
end if Config.logmaster['file']
|
14
|
+
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
module StraightServer
|
2
|
+
|
3
|
+
class Order < Sequel::Model
|
4
|
+
|
5
|
+
include Straight::OrderModule
|
6
|
+
plugin :validation_helpers
|
7
|
+
plugin :timestamps, create: :created_at, update: :updated_at
|
8
|
+
|
9
|
+
plugin :serialization
|
10
|
+
serialize_attributes :marshal, :callback_response
|
11
|
+
|
12
|
+
def gateway
|
13
|
+
@gateway ||= Gateway.find_by_id(gateway_id)
|
14
|
+
end
|
15
|
+
|
16
|
+
def gateway=(g)
|
17
|
+
self.gateway_id = g.id
|
18
|
+
@gateway = g
|
19
|
+
end
|
20
|
+
|
21
|
+
def save
|
22
|
+
super # calling Sequel::Model save
|
23
|
+
@status_changed = false
|
24
|
+
end
|
25
|
+
|
26
|
+
def status_changed?
|
27
|
+
@status_changed
|
28
|
+
end
|
29
|
+
|
30
|
+
def to_h
|
31
|
+
super.merge({ id: id })
|
32
|
+
end
|
33
|
+
|
34
|
+
def to_json
|
35
|
+
to_h.to_json
|
36
|
+
end
|
37
|
+
|
38
|
+
def validate
|
39
|
+
super # calling Sequel::Model validator
|
40
|
+
errors.add(:amount, "is invalid") if !amount.kind_of?(Numeric) || amount <= 0
|
41
|
+
errors.add(:gateway_id, "is invalid") if !gateway_id.kind_of?(Numeric) || gateway_id <= 0
|
42
|
+
validates_unique :id, :address, [:keychain_id, :gateway_id]
|
43
|
+
validates_presence [:address, :keychain_id, :gateway_id, :amount]
|
44
|
+
end
|
45
|
+
|
46
|
+
def to_http_params
|
47
|
+
"order_id=#{id}&amount=#{amount}&status=#{status}&address=#{address}&tid=#{tid}"
|
48
|
+
end
|
49
|
+
|
50
|
+
def start_periodic_status_check
|
51
|
+
StraightServer.logger.info "Starting periodic status checks of the order #{self.id}"
|
52
|
+
super
|
53
|
+
end
|
54
|
+
|
55
|
+
def check_status_on_schedule(period: 10, iteration_index: 0)
|
56
|
+
StraightServer.logger.info "Checking status of order #{self.id}"
|
57
|
+
super
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
module StraightServer
|
2
|
+
|
3
|
+
class OrdersController
|
4
|
+
|
5
|
+
attr_reader :response
|
6
|
+
|
7
|
+
def initialize(env)
|
8
|
+
@env = env
|
9
|
+
@params = env.params
|
10
|
+
@method = env['REQUEST_METHOD']
|
11
|
+
@request_path = env['REQUEST_PATH'].split('/').delete_if { |s| s.nil? || s.empty? }
|
12
|
+
dispatch
|
13
|
+
end
|
14
|
+
|
15
|
+
def create
|
16
|
+
begin
|
17
|
+
order = @gateway.create_order(
|
18
|
+
amount: @params['amount'],
|
19
|
+
currency: @params['currency'],
|
20
|
+
btc_denomination: @params['btc_denomination'],
|
21
|
+
id: @params['order_id'],
|
22
|
+
signature: @params['signature']
|
23
|
+
)
|
24
|
+
StraightServer::Thread.new do
|
25
|
+
order.start_periodic_status_check
|
26
|
+
end
|
27
|
+
[200, {}, order.to_json ]
|
28
|
+
rescue Sequel::ValidationFailed => e
|
29
|
+
StraightServer.logger.warn "validation errors in order, cannot create it."
|
30
|
+
[409, {}, "Invalid order: #{e.message}" ]
|
31
|
+
rescue StraightServer::GatewayModule::InvalidSignature
|
32
|
+
[409, {}, "Invalid signature for id: #{@params['order_id']}" ]
|
33
|
+
rescue StraightServer::GatewayModule::InvalidOrderId
|
34
|
+
StraightServer.logger.warn message = "An invalid id for order supplied: #{@params['order_id']}"
|
35
|
+
[409, {}, message ]
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def show
|
40
|
+
order = Order[@params['id']]
|
41
|
+
if order
|
42
|
+
order.status(reload: true)
|
43
|
+
order.save if order.status_changed?
|
44
|
+
[200, {}, order.to_json]
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def websocket
|
49
|
+
order = Order[@params['id']]
|
50
|
+
if order
|
51
|
+
begin
|
52
|
+
@gateway.add_websocket_for_order ws = Faye::WebSocket.new(@env), order
|
53
|
+
ws.rack_response
|
54
|
+
rescue Gateway::WebsocketExists
|
55
|
+
[403, {}, "Someone is already listening to that order"]
|
56
|
+
rescue Gateway::WebsocketForCompletedOrder
|
57
|
+
[403, {}, "You cannot listen to this order because it is completed (status > 1)"]
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
def dispatch
|
65
|
+
|
66
|
+
StraightServer.logger.blank_lines
|
67
|
+
StraightServer.logger.info "#{@method} #{@env['REQUEST_PATH']}\n#{@params}"
|
68
|
+
|
69
|
+
@gateway = StraightServer::Gateway.find_by_id(@request_path[1])
|
70
|
+
|
71
|
+
@response = if @request_path[3] # if an order id is supplied
|
72
|
+
@params['id'] = @request_path[3].to_i
|
73
|
+
if @request_path[4] == 'websocket'
|
74
|
+
websocket
|
75
|
+
elsif @request_path[4].nil? && @method == 'GET'
|
76
|
+
show
|
77
|
+
end
|
78
|
+
elsif @request_path[3].nil?# && @method == 'POST'
|
79
|
+
create
|
80
|
+
end
|
81
|
+
@response = [404, {}, "#{@method} /#{@request_path.join('/')} Not found"] if @response.nil?
|
82
|
+
end
|
83
|
+
|
84
|
+
end
|
85
|
+
|
86
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module StraightServer
|
2
|
+
class Server < Goliath::API
|
3
|
+
|
4
|
+
use Goliath::Rack::Params
|
5
|
+
include StraightServer::Initializer
|
6
|
+
Faye::WebSocket.load_adapter('goliath')
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
prepare
|
10
|
+
StraightServer.logger.info "Starting Straight server v #{StraightServer::VERSION}"
|
11
|
+
require_relative 'order'
|
12
|
+
require_relative 'gateway'
|
13
|
+
require_relative 'orders_controller'
|
14
|
+
super
|
15
|
+
end
|
16
|
+
|
17
|
+
def response(env)
|
18
|
+
# POST /gateways/1/orders - create order
|
19
|
+
# GET /gateways/1/orders/1 - see order info
|
20
|
+
# /gateways/1/orders/1/websocket - subscribe to order status changes via a websocket
|
21
|
+
|
22
|
+
# This will be more complicated in the future. For now it
|
23
|
+
# just checks that the path starts with /gateways/:id/orders
|
24
|
+
|
25
|
+
StraightServer.logger.watch_exceptions do
|
26
|
+
|
27
|
+
# This is a client implementation example, an html page + a dart script
|
28
|
+
# supposed to only be loaded in development.
|
29
|
+
if Goliath.env == :development
|
30
|
+
if env['REQUEST_PATH'] == '/'
|
31
|
+
return [200, {}, IO.read(Initializer::GEM_ROOT + '/examples/client/client.html')]
|
32
|
+
elsif Goliath.env == :development && env['REQUEST_PATH'] == '/client.dart'
|
33
|
+
return [200, {}, IO.read(Initializer::GEM_ROOT + '/examples/client/client.dart')]
|
34
|
+
end
|
35
|
+
end
|
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"]
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
|
46
|
+
# Assume things went wrong, if they didn't go right
|
47
|
+
[500, {}, "#{env['REQUEST_METHOD']} #{env['REQUEST_PATH']} Server Error"]
|
48
|
+
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require 'json'
|
3
|
+
require 'sequel'
|
4
|
+
require 'straight'
|
5
|
+
require 'logmaster'
|
6
|
+
require 'hmac'
|
7
|
+
require 'hmac-sha1'
|
8
|
+
require 'net/http'
|
9
|
+
require 'faye/websocket'
|
10
|
+
Sequel.extension :migration
|
11
|
+
|
12
|
+
require_relative 'straight-server/config'
|
13
|
+
require_relative 'straight-server/initializer'
|
14
|
+
require_relative 'straight-server/thread'
|
15
|
+
require_relative 'straight-server/orders_controller'
|
16
|
+
|
17
|
+
module StraightServer
|
18
|
+
|
19
|
+
VERSION = '0.1.0'
|
20
|
+
|
21
|
+
class << self
|
22
|
+
attr_accessor :db_connection, :logger
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# If set to db, then use DB table to store gateways,
|
2
|
+
# useful when your run many gateways on the same server.
|
3
|
+
gateways_source: config
|
4
|
+
|
5
|
+
gateways:
|
6
|
+
|
7
|
+
default:
|
8
|
+
pubkey: 'xpub-000'
|
9
|
+
confirmations_required: 0
|
10
|
+
order_class: "StraightServer::Order"
|
11
|
+
secret: 'secret'
|
12
|
+
check_signature: true
|
13
|
+
callback_url: 'http://localhost:3000/payment-callback'
|
14
|
+
default_currency: 'BTC'
|
15
|
+
exchange_rate_adapters:
|
16
|
+
- Bitpay
|
17
|
+
- Coinbase
|
18
|
+
- Bitstamp
|
19
|
+
second_gateway:
|
20
|
+
pubkey: 'xpub-001'
|
21
|
+
confirmations_required: 0
|
22
|
+
order_class: "StraightServer::Order"
|
23
|
+
secret: 'secret'
|
24
|
+
check_signature: false
|
25
|
+
callback_url: 'http://localhost:3001/payment-callback'
|
26
|
+
default_currency: 'BTC'
|
27
|
+
exchange_rate_adapters:
|
28
|
+
- Bitpay
|
29
|
+
- Coinbase
|
30
|
+
- Bitstamp
|
31
|
+
|
32
|
+
db:
|
33
|
+
adapter: sqlite
|
34
|
+
name: straight.db
|