-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathmain.py
More file actions
507 lines (410 loc) · 18.6 KB
/
main.py
File metadata and controls
507 lines (410 loc) · 18.6 KB
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
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
import logging
from flask import Flask,render_template,request,send_from_directory
from dotenv import load_dotenv
import time, datetime
import os
from eth_account import Account
from eth_account.signers.local import LocalAccount
from web3 import HTTPProvider, Web3
from web3.middleware import construct_sign_and_send_raw_middleware
from eth_defi.abi import get_deployed_contract
from eth_defi.token import fetch_erc20_details
from eth_defi.confirmation import wait_transactions_to_complete
from urllib.parse import urlencode
import requests, json, re
import sqlite3
import hashlib
logging.basicConfig(level=logging.INFO)
load_dotenv()
TOKEN_CHAIN_ID=42161
ERC_20_TOKEN_ADDRESS = "0xa78d8321B20c4Ef90eCd72f2588AA985A4BDb684"
DEBUG_DRIP = 0.00000001
ETH_DRIP = 0.00005
ANT_DRIP = 0.000005
# What date to start checking for faucet drips
# Allows to report total ant/eth awarded without penalizing early tests
TIME_HORIZON = 1747942869
# Rate informaion in seconds
RATE_WINDOW = 60 * 60
RATE_LIMIT = 6
# Read and setup a local private key
private_key = os.environ.get("PRIVATE_KEY")
assert private_key is not None, "You must set PRIVATE_KEY environment variable"
assert private_key.startswith("0x"), "Private key must start with 0x hex prefix"
HCAPTCHA_KEY = os.environ.get('HCAPTCHA_KEY')
assert HCAPTCHA_KEY is not None, "You must set HCAPTCHA_KEY environment variable"
HCAPTCHA_SITEKEY = os.environ.get('HCAPTCHA_SITEKEY')
assert HCAPTCHA_SITEKEY is not None, "You must set HCAPTCHA_SITEKEY environment variable"
ALCHEMY_KEY = os.environ.get('API_KEY')
assert ALCHEMY_KEY is not None, "You must set API_KEY environment variable"
HASH_KEY = os.environ.get('HASH_KEY')
assert HASH_KEY is not None, "You must set HASH_KEY environment variable"
FORUM_THREAD = 'https://forum.autonomi.community/t/community-faucet-live/41299/'
FORUM_AUTHOR_DATA = 'https://forum.autonomi.community/u/{author}/summary.json'
FORUM_POST_AUTHOR = "<span itemprop='name'>{author}</span></a>"
v2_url = "https://arb-mainnet.g.alchemy.com/v2/" + ALCHEMY_KEY
web3 = Web3(HTTPProvider(v2_url))
#print(f"Connected to blockchain, chain id is {web3.eth.chain_id}. the latest block is {web3.eth.block_number:,}")
account: LocalAccount = Account.from_key(private_key)
web3.middleware_onion.add(construct_sign_and_send_raw_middleware(account))
# Connect to or create a SQLite3 database
faucetdb = sqlite3.connect('faucet.db', check_same_thread=False)
# Make sure Faucet Database is ready to store data
def prepare_faucet_database():
# Get a cursor
cur = faucetdb.cursor()
# Check if we have a proper database
try:
cur.execute("SELECT * FROM faucet LIMIT 1")
# storing the data in a list
data_list = cur.fetchall()
# we survived, and ready roll
return True
# Nope, lets try to set one up
except sqlite3.OperationalError:
try:
# Try to create our table
cur.execute("""CREATE TABLE IF NOT EXISTS faucet(
timestamp INTEGER,
wallet TEXT,
eth REAL,
ant REAL,
eth_tx TEXT,
ant_tx TEXT,
author TEXT,
badges TEXT,
PRIMARY KEY (timestamp, wallet));""")
cur.execute("CREATE INDEX IF NOT EXISTS author ON faucet(author);")
# Insert a record
cur.execute('''INSERT INTO faucet VALUES(
0,'0xdead',0,0,'0xinit','0xinit','-1',0)''')
# Save the changes
faucetdb.commit()
return True
except sqlite3.Error as e:
app.logger.info("And error occured with the db: " + str(e.args[0]))
return False
# Drip coins to wallet
def drip_coins(wallet):
#return {'status': True, 'eth_tx': '0x622e2f0d1604d46e5cb553dd799f4034527f68bbd3fc3d561cc44039240d0d34', 'ant_tx': '0x6ee8553310fc684ee648ffcd569c96485e6c5a5b1276cbf3b83677eb7f5e1dfb'}
if not web3.is_checksum_address(wallet):
return { "status": False, "reason": "value provided is not a valid wallet" }
# We successfully connected to the Token chain
if TOKEN_CHAIN_ID == web3.eth.chain_id:
# Get users current status of token and their address
erc_20 = get_deployed_contract(web3, "ERC20MockDecimals.json", ERC_20_TOKEN_ADDRESS)
token_details = fetch_erc20_details(web3, ERC_20_TOKEN_ADDRESS)
ant_balance = erc_20.functions.balanceOf(account.address).call()
eth_balance = web3.eth.get_balance(account.address)
decimal_eth_balance = eth_balance/(10**18)
decimal_ant_balance = token_details.convert_to_decimals(ant_balance)
# Make sure we have a balance to send
if decimal_eth_balance < ETH_DRIP:
return { "status": False, "reason": "Out of ETH"}
if decimal_ant_balance < ANT_DRIP:
return { "status": False, "reason": "Out of ANT"}
#return { "status": True, "ant": ant_balance, "eth": eth_balance }
# Convert a human-readable number to fixed decimal with 18 decimal places
ant_amount = token_details.convert_to_raw(ANT_DRIP)
try:
ant_tx_hash = erc_20.functions.transfer(wallet, ant_amount).transact({"from": account.address})
# Wait for the transactions to complete
complete = wait_transactions_to_complete(web3, [ant_tx_hash], max_timeout=datetime.timedelta(minutes=5))
# Check our results
for receipt in complete.values():
if receipt.status != 1:
return { "status": False, "reason": "ANT trasnaction did not confirm" }
except Exception as error:
template = "An exception of type {0} occurred. Arguments:\n{1!r}"
message = template.format(type(error).__name__, error.args)
return { "status": False, "reason": message }
#accountNonce = '0x' + str(web3.eth.get_transaction_count(account.address) + 1)
#return { "status": False, "reason": accountNonce }
try:
eth_amount = token_details.convert_to_raw(ETH_DRIP)
eth_tx_hash = web3.eth.send_transaction({
# "nonce": accountNonce,
"to": wallet,
"from": account.address,
"value": eth_amount,
})
except Exception as error:
template = "An exception of type {0} occurred. Arguments:\n{1!r}"
message = template.format(type(error).__name__, error.args)
return { "status": False, "reason": message }
# Wait for the transactions to complete
complete = wait_transactions_to_complete(web3, [eth_tx_hash], max_timeout=datetime.timedelta(minutes=5))
# Check our results
for receipt in complete.values():
if receipt.status != 1:
return { "status": False, "reason": "ETH transaction did not confirm" }
return { "status": True, "eth_tx": eth_tx_hash.hex(), "ant_tx": ant_tx_hash.hex() }
else:
return { "status": False, "reason": "token is: "+web3.eth.chain_id}
# See if wallet has already received payments
def check_db_for_wallet(wallet):
#return False
# Get a cursor
cur = faucetdb.cursor()
# See if we get a cached record
cur.execute("SELECT timestamp FROM faucet WHERE wallet = ? AND timestamp > ? LIMIT 1", [ wallet, str(TIME_HORIZON) ])
cached_result = cur.fetchall()
if cached_result:
return True
# See if author has already received payments
def check_db_for_author(author):
#return True
# Get a cursor
cur = faucetdb.cursor()
# See if we get a cached record
cur.execute("SELECT timestamp FROM faucet WHERE author = ? AND timestamp > ? LIMIT 1", [ author, str(TIME_HORIZON) ])
cached_result = cur.fetchall()
if cached_result:
return True
# Check for volume of drips
def check_db_for_drips():
#return False
# Get a cursor
cur = faucetdb.cursor()
# Count successful drips
cur.execute("SELECT count(timestamp) AS drips FROM faucet;")
drips_result = cur.fetchall()
if drips_result:
#return { "status": False, "reason": drips_result }
if int(drips_result[0][0]):
# Return the number of records minus the inital test
return { "status": True, "reason": drips_result[0][0]-1 }
return { "status": False, "reason": "no results" }
# Check for velocity of drips
def check_db_for_rate():
#return False
# Get a cursor
cur = faucetdb.cursor()
# See if we get a cached record
#cur.execute("SELECT count(timestamp) FROM faucet WHERE timestamp > ( unixepoch() - " + str(RATE_WINDOW) + ")")
cur.execute("SELECT count(timestamp) AS drips FROM faucet WHERE timestamp > ( unixepoch() - ? )", [str(RATE_WINDOW)])
cached_result = cur.fetchall()
if cached_result:
if int(cached_result[0][0]) >= int(RATE_LIMIT):
return cached_result[0][0]
return False
# Save wallet in db
def add_db(wallet,author,ant_tx,eth_tx):
# Get a cursor
cur = faucetdb.cursor()
at = int(time.time())
ant_drip = ANT_DRIP if ant_tx != '0xtest_harness' else 0.0
eth_drip = ETH_DRIP if eth_tx != '0xtest_harness' else 0.0
app.logger.info("Inserting: "+str(at)+" "+author+" "+wallet)
cur.execute("INSERT INTO faucet VALUES ( ?, ?, ?, ?, ?, ?, ?, ?);",
[ at, wallet, eth_drip, ant_drip, eth_tx, ant_tx, author, "1" ])
faucetdb.commit()
# Validate a h-captcha-response
def check_hcaptcha(captcha):
#return True
payload = { "secret": HCAPTCHA_KEY,
"site-key": HCAPTCHA_SITEKEY,
"response": captcha }
#return urlencode(payload)
response = requests.post("https://api.hcaptcha.com/siteverify",
data = payload )
# Parse the json response
#return(response.text)
results = json.loads(response.text)
if isinstance(results,dict) and \
"success" in results and \
results["success"]:
return True
else:
return False
# Check that we have a forum user and valid post
def check_forum_auth(form_data):
# sanitize author
author = re.sub(r"[^A-Za-z0-9._-]+", '', form_data["author"])[0:64]
if form_data["author"] != author:
return { "status": "fail", "reason": "Invalid member name" }
# check db for author
if check_db_for_author(author):
return { "status": "fail", "reason": "member already received payout maximum" }
try:
# Check forum for active user
#return { "status": False, "reason": FORUM_AUTHOR_DATA.format(author=author) }
response = requests.get(FORUM_AUTHOR_DATA.format(author=author))
# Parse the json response
#return { "status": False, "reason": response.text }
results = json.loads(response.text)
except:
return { "status": "fail", "reason": "Failed forum author request" }
#return { "status": False, "reason": results['user_summary']['days_visited'] }
if isinstance(results,dict) and \
"user_summary" in results and \
isinstance(results["user_summary"],dict) and \
"days_visited" in results["user_summary"]:
if int(results["user_summary"]["days_visited"]) < 2:
return { "status": "fail", "reason": "Member too new" }
else:
return { "status": "fail", "reason": "No valid member found" }
#return { "status": "fail", "reason": "".join([FORUM_THREAD,'[0-9]{1,16}'])}
#return { "status": "fail", "reason": "".join([FORUM_THREAD,'[0-9]+',r'\?u=',author])}
# Sanitize Forum Post URL
pattern = re.compile("".join([FORUM_THREAD,r'[0-9]{1,16}']))
if not pattern.match(form_data["post"]):
return { "status": "fail", "reason": "Invalid forum link" }
try:
# Check forum for post for auth
response = requests.get(form_data["post"])
# Parse the response
#return { "status": False, "reason": response.text }
results = response.text
except:
return { "status": "fail", "reason": "Failed forum post request" }
#return { "status": "fail", "reason": FORUM_POST_AUTHOR.format(author=author) in results }
# Check for author of the post
if not FORUM_POST_AUTHOR.format(author=author) in results:
return { "status": "fail", "reason": "Invalid author" }
# Check post for authors key
if not generate_author_hash(author,get_wallet_from_formdata(form_data)) in results:
return { "status": "fail", "reason": "Invalid confirmation code"}
#return { "status": "fail", "reason": results }
#return { "status": "fail", "reason": "save for "+author}
return { "status": True, "author": author }
# Generate Hash
def generate_author_hash(author,wallet):
challenge = str(HASH_KEY)+author+str(wallet)
return hashlib.md5(challenge.encode()).hexdigest()
# Dig wallet out of form
def get_wallet_from_formdata(form_data):
if "mm_wallet" in form_data and \
len(form_data["mm_wallet"]) > 0 and \
len(form_data["mm_wallet"]) <= 42:
# Sanitize input data
wallet = web3.to_checksum_address(re.sub(r"[^A-Fa-fXx0-9]+", '', form_data["mm_wallet"]))
elif "wallet" in form_data and \
len(form_data["wallet"]) > 0 and \
len(form_data["wallet"]) <= 42:
# Sanitize input data
wallet = re.sub(r"[^A-Fa-fXx0-9]+", '', form_data["wallet"])
else:
return False
return wallet
# Validate our form
def validate_request(form_data):
# Did we get a hcaptcha to test?
if "h-captcha-response" in form_data:
captcha = check_hcaptcha(form_data["h-captcha-response"])
if not captcha:
return { "status": "fail", "reason": "Failed captcha" }
#else:
# return [{ "captcha": captcha }]
else:
return { "status": "fail", "reason": "No captcha provided"}
# Did we get a forum post
if "post" not in form_data or \
len(form_data["post"]) == 0 or \
len(form_data["post"]) > 128 or \
FORUM_THREAD not in form_data["post"]:
return { "status": "fail", "reason": "Invalid forum post link" }
if "author" not in form_data or \
len(form_data["author"]) == 0 or \
len(form_data["author"]) > 64:
return { "status": "fail", "reason": "Member name out of bounds" }
# Let's do some checking
results = check_forum_auth(form_data)
if results and \
isinstance(results, dict) and \
"status" in results:
# If we got a failure, just return the results
if results["status"] != True:
return results
if "author" in results:
author = results["author"]
else:
return { "status": "fail", "reason": "Invalid authorization" }
else:
return { "status": "fail", "reason": "No results from forum confirmation"}
# Get wallet from form input
wallet = get_wallet_from_formdata(form_data)
if wallet:
# See if this wallet has already suceeded
if check_db_for_wallet(wallet):
return { "status": "fail", "reason": "wallet already received payout maximum" }
# Check for rate limit
rate_limit = check_db_for_rate()
if rate_limit:
#return { "status": "fail", "reason": rate_limit }
return { "status": "fail", "reason": "rate limit exceeded" }
# OK let's drip
results = drip_coins(wallet)
app.logger.info("Returned from drip" + str(results))
# What did we get back?
if results and \
isinstance(results, dict) and \
"status" in results:
# If we got a failure, just return the results
if results["status"] != True:
return results
# Let's store our success
add_db(wallet,author,results['ant_tx'],results['eth_tx'])
results["Wallet"]=wallet
#return { "status": "fail", "reason": "Always fail" }
return results
else:
return { "status": "fail", "reason": "Drip crashed" }
else:
return { "status": "fail", "reason": "Invalid Wallet" }
app = Flask(__name__)
# Set a content length to protect from overflows
app.config['MAX_CONTENT_LENGTH'] = 1 * 1000 * 1000
@app.route('/')
def home():
return render_template('form.html',sitekey=HCAPTCHA_SITEKEY,forum_link=FORUM_THREAD)
@app.route('/form')
def form():
return home()
@app.route('/drips')
def drips():
# Verify the request looks ok
my_data = check_db_for_drips()
if my_data["status"]!=True:
return render_template('fail.html', results = my_data["reason"])
else:
return render_template('drips.html', results = my_data["reason"])
@app.route('/data/', methods = ['POST', 'GET'])
def data():
if request.method == 'GET':
return f"The URL /data is accessed directly. Try going to '/form' to submit form"
if request.method == 'POST':
# Get the form data
form_data = request.form
if "author" not in form_data or \
len(form_data["author"]) == 0:
return render_template("data.html", form_data={"reason":"No forum member name provided"})
wallet = get_wallet_from_formdata(form_data)
if not wallet:
return render_template("data.html",form_data={"reason":"No wallet provided"})
my_data = {}
my_data["challenge"] = generate_author_hash(form_data["author"],wallet)
return render_template("challenge.html",form_data=my_data,forum_topic=FORUM_THREAD)
@app.route('/confirm/', methods = ['POST', 'GET'])
def confirm():
if request.method == 'GET':
return f"The URL /data is accessed directly. Try going to '/form' to submit form"
if request.method == 'POST':
# Get the form data
form_data = request.form
#return render_template("data.html",form_data=form_data)
# Verify the request looks ok
my_data = validate_request(form_data)
if my_data["status"]!=True:
return render_template('fail.html', results=my_data["reason"])
return render_template('success.html', my_data = my_data)
# Load Browser Favorite Icon if accessed directly
@app.route('/favicon.ico')
def favicon():
return app.send_static_file('favicon.ico')
if __name__ == '__main__':
# Initialize database
if prepare_faucet_database():
app.run(debug=True, host='0.0.0.0', port=5010)
else:
print("Program terminated due to issues with faucet database")