straight-server 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|