forked from inuitwallet/ALP-Server
-
Notifications
You must be signed in to change notification settings - Fork 0
/
pool_server.py
370 lines (325 loc) · 14.5 KB
/
pool_server.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
import json
import logging
import os
from threading import Timer
from requestlogger import WSGILogger, ApacheFormatter
from logging.handlers import TimedRotatingFileHandler
import bottle
from bottle_sqlite import SQLitePlugin
from bottle import run, request
from bitcoinrpc.authproxy import AuthServiceProxy
import time
import credit
import payout
import database
import load_config
from utils import AddressCheck
from exchanges import Bittrex, BTER, CCEDK, Poloniex, TestExchange
__author__ = 'sammoth'
app = bottle.Bottle()
bottle.debug(True)
# Create the log directory
if not os.path.isdir('logs'):
os.mkdir('logs')
# Set the WSGI Logger facility
log_time = int(time.time())
handlers = [TimedRotatingFileHandler('logs/server-{}.log'.format(log_time),
when='midnight'), ]
server = WSGILogger(app, handlers, ApacheFormatter())
# Build the application Logger
log = logging.Logger('ALP')
rotating_file = TimedRotatingFileHandler('logs/alp-{}.log'.format(log_time),
when='midnight')
stream = logging.StreamHandler()
formatter = logging.Formatter(fmt='%(asctime)s - %(levelname)s - %(message)s',
datefmt='%y-%m-%d %H:%M:%S')
stream.setFormatter(formatter)
rotating_file.setFormatter(formatter)
log.addHandler(rotating_file)
log.addHandler(stream)
# Create the database if one doesn't exist
database.build(log)
# Install the SQLite plugin
app.install(SQLitePlugin(dbfile='pool.db', keyword='db'))
# Create the Exchange wrapper objects
wrappers = {'bittrex': Bittrex(),
'bter': BTER(),
'ccedk': CCEDK(),
'poloniex': Poloniex(),
'test_exchange': TestExchange()}
log.info('load pool config')
app.config.load_config('pool_config')
log.info('load exchange config')
load_config.load(app, 'exchange_config')
log.info('set up a json-rpc connection with nud')
rpc = AuthServiceProxy("http://{}:{}@{}:{}".format(app.config['rpc.user'],
app.config['rpc.pass'],
app.config['rpc.host'],
app.config['rpc.port']))
if os.environ.get('RUN_TIMERS', 0) == 1:
# Set the timer for credits
Timer(60.0, credit.credit, kwargs={'app': app, 'rpc': rpc, 'log': log}).start()
# Set the timer for payouts
Timer(86400.0, payout.pay, kwargs={'rpc': rpc, 'log': log}).start()
def check_headers(headers):
"""
Ensure the correct headers get passed in the request
:param headers:
:return:
"""
if headers.get('Content-Type') != 'application/json':
log.warn('invalid header - need app/json')
return False
return True
@app.get('/')
def root():
"""
Show a default page with info about the server health
:return:
"""
return {'success': True, 'message': 'ALP Server is operational'}
@app.post('/register')
def register(db):
"""
Register a new user on the server
Requires:
user - API public Key
address - Valid NBT payout address
exchange - supported exchange
unit - supported currency
:return:
"""
log.info('/register')
# Check the content type
if not check_headers(request.headers):
return {'success': False, 'message': 'Content-Type header must be set to '
'\'application/json\''}
# Get the post parameters
try:
user = request.json.get('user')
address = request.json.get('address')
exchange = request.json.get('exchange')
unit = request.json.get('unit')
except AttributeError:
log.warn('no json found in request')
return {'success': False, 'message': 'no json found in request'}
# check for missing parameters
if not user:
log.warn('no user provided')
return {'success': False, 'message': 'no user provided'}
if not address:
log.warn('no address provided')
return {'success': False, 'message': 'no address provided'}
if not exchange:
log.warn('no exchange provided')
return {'success': False, 'message': 'no exchange provided'}
if not unit:
log.warn('no unit provided')
return {'success': False, 'message': 'no unit provided'}
# Check for a valid address
if address[0] != 'B':
log.warn('%s is not a valid NBT address. No \'B\'', address)
return {'success': False, 'message': '{} is not a valid NBT address. It '
'should start with a \'B\''.format(address)}
address_check = AddressCheck()
if not address_check.check_checksum(address):
log.warn('%s is not a valid NBT address. Bad checksum', address)
return {'success': False, 'message': '{} is not a valid NBT address. The '
'checksum doesn\'t match'.format(address)}
# Check that the requests exchange is supported by the server
if exchange not in app.config['exchanges']:
log.warn('%s is not supported', exchange)
return {'success': False, 'message': '{} is not supported'.format(exchange)}
# Check that the unit is supported on the server
if unit not in app.config['{}.units'.format(exchange)]:
log.warn('%s is not supported on %s', unit, exchange)
return {'success': False, 'message': '{} is not supported on {}'.format(unit,
exchange)}
# Check if the user already exists in the database
check = db.execute("SELECT id FROM users WHERE user=? AND address=? AND "
"exchange=? AND unit=?;", (user, address, exchange,
unit)).fetchone()
if check:
log.warn('user is already registered')
return {'success': False, 'message': 'user is already registered'}
db.execute("INSERT INTO users ('user','address','exchange','unit') VALUES (?,?,?,?)",
(user, address, exchange, unit))
log.info('user %s successfully registered', user)
return {'success': True, 'message': 'user successfully registered'}
@app.post('/liquidity')
def liquidity(db):
"""
Allow the user to submit liquidity validations to the server
Will be multiple times per minute
With the submitted data get the users orders and update the database accordingly
Requires:
user - API public Key
req - dictionary to be used to get open orders
sign - result of signing req with API private key
exchange - valid, supported exchange
unit - valid, supported unit
:return:
"""
log.info('/liquidity')
# Check the content type
if not check_headers(request.headers):
return {'success': False, 'message': 'Content-Type header must be set to '
'\'application/json\''}
# Get the post parameters
try:
user = request.json.get('user')
sign = request.json.get('sign')
exchange = request.json.get('exchange')
unit = request.json.get('unit')
req = request.json.get('req')
except AttributeError:
log.warn('no json found in request')
return {'success': False, 'message': 'no json found in request'}
if not user:
log.warn('no user provided')
return {'success': False, 'message': 'no user provided'}
if not sign:
log.warn('no sign provided')
return {'success': False, 'message': 'no sign provided'}
if not exchange:
log.warn('no exchange provided')
return {'success': False, 'message': 'no exchange provided'}
if not unit:
log.warn('no unit provided')
return {'success': False, 'message': 'no unit provided'}
if not req:
log.warn('no req provided')
return {'success': False, 'message': 'no req provided'}
# use the submitted data to request the users orders
orders = wrappers[exchange].validate_request(user, unit, req, sign)
price = get_price()
# clear existing orders for the user
log.info('clear existing orders for user %s', user)
db.execute("DELETE FROM orders WHERE user=? AND exchange=? AND unit=?", (user,
exchange,
unit))
# Loop through the orders
for order in orders:
# Calculate how the order price is from the known good price
order_deviation = 1.0 - (min(order['price'], price) / max(order['price'], price))
# Using the server tolerance determine if the order is tier 1 or tier 2
# Only tier 1 is compensated by the server
tier = 'tier_2'
if order_deviation <= app.config['{}.{}.{}.tolerance'.format(exchange, unit,
order['type'])]:
tier = 'tier_1'
# save the order details
db.execute("INSERT INTO orders ('user','tier','order_id','order_amount',"
"'order_type','exchange','unit') VALUES (?,?,?,?,?,?,?)",
(user, tier, str(order['id']), float(order['amount']),
str(order['type']), exchange, unit))
log.info('user %s orders saved for validation', user)
return {'success': True, 'message': 'orders saved for validation'}
@app.get('/exchanges')
def exchanges():
"""
Show the app.config as it pertains to the supported exchanges
:return:
"""
log.info('/exchanges')
data = {}
for exchange in app.config['exchanges']:
if exchange not in data:
data[exchange] = {}
for unit in app.config['{}.units'.format(exchange)]:
data[exchange][unit] = {'ask': {'tolerance': app.config['{}.{}.ask.'
'tolerance'
''.format(exchange,
unit)],
'tier_1': {
'reward': app.config['{}.{}.ask.{}'
'.reward'
''.format(exchange,
unit,
'tier_1')]
},
'tier_2': {
'reward': app.config['{}.{}.ask.{}'
'.reward'
''.format(exchange,
unit,
'tier_2')]
}
},
'bid': {'tolerance': app.config['{}.{}.bid.'
'tolerance'
''.format(exchange,
unit)],
'tier_1': {
'reward': app.config['{}.{}.bid.{}'
'.reward'
''.format(exchange,
unit,
'tier_1')]
},
'tier_2': {
'reward': app.config['{}.{}.bid.{}'
'.reward'
''.format(exchange,
unit,
'tier_2')]
}
}
}
return data
@app.get('/status')
def status(db):
"""
Display the overall pool status.
This will be the number of users and the amount of liquidity for each tier, side,
unit, exchange for the last full round and the current round
:param db:
:return:
We want to display
* Total liquidity provided by pool
* Total tier_1 by pool
* Total tier_2 by pool
* total liquidity by exchange
* total tier_1 by exchange
* total tier_2 by exchange
* total liquidity by exchange/pair
* total tier_1 by exchange/pair
* total tier_2 by exchange/pair
* total liquidity by exchange/pair/side
* total tier_1 by exchange/pair/side
* total tier_2 by exchange/pair/side
* number of users
* number of active users
* amount 1 NBT will be rewarded currently
"""
log.info('/status')
credit_data = db.execute("SELECT * FROM credits WHERE time=(SELECT time FROM credits "
"ORDER BY time DESC LIMIT 1)").fetchall()
return credit_data
def get_price():
"""
Set the server price primarily from the NuBot streaming server but falling back to
feeds if that fails
:return:
"""
return 1234
@app.error(code=500)
def error500(error):
return json.dumps({'success': False, 'message': '500 error'})
@app.error(code=502)
def error502(error):
return json.dumps({'success': False, 'message': '502 error'})
@app.error(code=503)
def error503(error):
return json.dumps({'success': False, 'message': '503 error'})
@app.error(code=404)
def error404(error):
return json.dumps({'success': False, 'message': '404 {} not found'
''.format(request.url)})
@app.error(code=405)
def error405(error):
return json.dumps({'success': False, 'message': '405 error. '
'Incorrect HTTP method used'})
if __name__ == '__main__':
# Run the server
run(server, host='localhost', port=int(os.environ.get("PORT", 3333)), debug=True)