-
Notifications
You must be signed in to change notification settings - Fork 0
/
token_util.py
528 lines (474 loc) · 19.5 KB
/
token_util.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
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
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
# token_util.py
import json
import os
import requests
import subprocess
import time
import config
from create_db import Session, Tokens
import logging
logger = logging.getLogger(__name__)
def check_wallet_utxo(wallet):
""" Querying all UTXOs in wallet """
cmd = f"{config.CARDANO_CLI} query utxo " \
f"--address {wallet} " \
f"--testnet-magic {config.TESTNET_ID}"
proc = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE)
out = proc.communicate()
response = str(out[0], 'UTF-8')
logging.info(response.split()[4:])
return response.split()[4:]
def check_testnet():
""" Verifies communication with testnet by querying UTXOs """
cmd = f"{config.CARDANO_CLI} query utxo " \
f"--testnet-magic {config.TESTNET_ID}"
proc = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE)
out = proc.communicate()
response = str(out[0], 'UTF-8')
logging.info(response)
def get_current_slot():
""" Gets blockchain tip returns current slot """
cmd = f"{config.CARDANO_CLI} query tip " \
f"--testnet-magic {config.TESTNET_ID}"
proc = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE)
out = proc.communicate()
response = str(out[0], 'UTF-8')
logging.info(response)
data = json.loads(response)
logging.info(f"Current Slot: {data['slot']}")
return data['slot']
def get_tx_details(tx_hash):
""" Get TX details from BlockFrost.io """
url = f'https://cardano-testnet.blockfrost.io/api/v0/txs/{tx_hash}/utxos'
headers = {"project_id": f"{config.BLOCKFROST_TESTNET}"}
res = requests.get(url, headers=headers)
res.raise_for_status()
if res.status_code == 200:
logging.info("Got TX Details.")
# logging.info(res.json())
return res.json()
else:
logging.info("Something failed here? blockfrost failed")
return False
def before_mint(**kwargs):
""" Sets up the token files and metadata
User should provide all the details in dict """
logging.info(f"before_mint started: \n {kwargs}")
session_uuid = kwargs.get('session_uuid')
# Start DB Session
session = Session()
# Check to see if session already exists
sesh_exists = session.query(Tokens).filter(
Tokens.session_uuid == session_uuid).scalar() is not None
if sesh_exists:
token_data = session.query(Tokens).filter(
Tokens.session_uuid == session_uuid).one()
logging.info(f'Session already exists {token_data}')
else:
# No token session yet, add the data
logging.info(f"New Session {session_uuid}")
token_data = Tokens(session_uuid=session_uuid)
token_data.update(**kwargs)
session.add(token_data)
session.commit()
# ### Start the actual minting process ###
logging.info("Setting up the token data")
# Create Stake keys
# Check to see if we ran this step already
stake_keys_created = session.query(Tokens).filter(
Tokens.session_uuid == session_uuid).filter(
Tokens.stake_keys_created).scalar() is not None
if stake_keys_created:
logging.info("Stake Keys already created for session, skip.")
# return False
else:
logging.info("Create Stake keys")
stake_vkey_file = f'tmp/{session_uuid}-stake.vkey'
stake_skey_file = f'tmp/{session_uuid}-stake.skey'
cmd = f"{config.CARDANO_CLI} stake-address key-gen " \
f"--verification-key-file {stake_vkey_file} " \
f"--signing-key-file {stake_skey_file}"
proc = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE)
out = proc.communicate()
response = str(out[0], 'UTF-8')
if response == '':
logging.info("Stake keys created.")
token_data.stake_keys_created = True
session.add(token_data)
session.commit()
else:
# Stake keys are needed if we fail here we bail out
logging.info("FAIL: Something went wrong creating stake keys.")
return False
# Create Payment keys
# Check to see if we ran this step already
payment_keys_created = session.query(Tokens).filter(
Tokens.session_uuid == session_uuid).filter(
Tokens.payment_keys_created).scalar() is not None
if payment_keys_created:
logging.info("Payment Keys already created for session, skip.")
# return False
else:
logging.info("Create Payment keys")
payment_vkey_file = f'tmp/{session_uuid}-payment.vkey'
payment_skey_file = f'tmp/{session_uuid}-payment.skey'
cmd = f"{config.CARDANO_CLI} address key-gen " \
f"--verification-key-file {payment_vkey_file} " \
f"--signing-key-file {payment_skey_file}"
proc = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE)
out = proc.communicate()
response = str(out[0], 'UTF-8')
if response == '':
logging.info("Payment keys created.")
token_data.payment_keys_created = True
session.add(token_data)
session.commit()
else:
# Payment keys are needed if we fail here we bail out
logging.info("FAIL: Something went wrong creating Payment keys.")
return False
# Create Payment Address
stake_keys_created = session.query(Tokens).filter(
Tokens.session_uuid == session_uuid).filter(
Tokens.stake_keys_created).scalar() is not None
payment_keys_created = session.query(Tokens).filter(
Tokens.session_uuid == session_uuid).filter(
Tokens.payment_keys_created).scalar() is not None
if stake_keys_created and payment_keys_created:
logging.info("Creating Bot Payment Address from stake and Payment keys.")
stake_vkey_file = f'tmp/{session_uuid}-stake.vkey'
payment_vkey_file = f'tmp/{session_uuid}-payment.vkey'
cmd = f"{config.CARDANO_CLI} address build " \
f"--payment-verification-key-file {payment_vkey_file} " \
f"--stake-verification-key-file {stake_vkey_file} " \
f"--testnet-magic {config.TESTNET_ID}"
proc = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE)
out = proc.communicate()
response = str(out[0], 'UTF-8')
bot_payment_addr = response.strip()
logging.info(bot_payment_addr.strip())
token_data.bot_payment_addr = bot_payment_addr
session.add(token_data)
session.commit()
else:
logging.info(f"Either Stake Keys, or Payment Keys have not been created")
logging.info(f"Stake Keys: {stake_keys_created}")
logging.info(f"Payment Keys: {payment_keys_created}")
logging.info("FAIL: Something missing, can't move on.")
return False
# Get the blockchain protocol parameters
# Generally we only need this once and it should stay around
protocol_params = 'tmp/protocol.json'
protocol_params_exist = os.path.isfile(protocol_params)
protocol_params_created = session.query(Tokens).filter(
Tokens.session_uuid == session_uuid).filter(
Tokens.protocol_params_created).scalar is not None
if protocol_params_exist and protocol_params_created:
logging.info("protocol_params_exist, no need to recreate")
else:
cmd = f"{config.CARDANO_CLI} query protocol-parameters " \
f"--testnet-magic {config.TESTNET_ID} " \
f"--out-file {protocol_params}"
proc = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE)
out = proc.communicate()
response = str(out[0], 'UTF-8')
if response == '':
token_data.protocol_params_created = True
session.add(token_data)
session.commit()
logging.info("Saved protocol.json")
else:
logging.info("FAIL: Could not get protocol.json")
return False
# Create Policy Script
policy_vkey = f'tmp/{session_uuid}-policy.vkey'
policy_skey = f'tmp/{session_uuid}-policy.skey'
policy_keys_exist = session.query(Tokens).filter(
Tokens.session_uuid == session_uuid).filter(
Tokens.policy_keys_created).scalar() is not None
if policy_keys_exist:
logging.info("Policy Keys already created for session, skip.")
else:
# Create Keys
cmd = f"{config.CARDANO_CLI} address key-gen " \
f"--verification-key-file {policy_vkey} " \
f"--signing-key-file {policy_skey}"
proc = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE)
out = proc.communicate()
response = str(out[0], 'UTF-8')
if response == '':
logging.info("Policy keys created.")
token_data.policy_keys_created = True
session.add(token_data)
session.commit()
else:
# Policy keys are needed if we fail here we bail out
logging.info("FAIL: Something went wrong creating Policy keys.")
return False
# At this point we need the bot_payment_addr to have UTXO to burn
logging.info(f"Please deposit 5 ADA in the following address:")
logging.info(token_data.bot_payment_addr)
return True
def mint(**kwargs):
""" Minting of the actual token """
# Get session:
session_uuid = kwargs.get('session_uuid')
# Start DB Session
session = Session()
logging.info(f'Minting started for {session_uuid}')
sesh_exists = session.query(Tokens).filter(
Tokens.session_uuid == session_uuid).scalar() is not None
if sesh_exists:
logging.info(f'Session Found: {session_uuid}')
else:
logging.info(f"No Session found: {session_uuid}")
return False
# We have data
token_data = session.query(Tokens).filter(
Tokens.session_uuid == session_uuid).one()
# Temporary arbitrary logic to fail tries to re-mint tokens
if token_data.tx_submitted:
logging.info(f"Session already Minted: {session_uuid}")
return False
# Get current slot
current_slot = get_current_slot()
slot_cushion = config.SLOT_BUFFER
invalid_after_slot = current_slot + slot_cushion
# Add to DB
token_data.current_slot = current_slot
token_data.slot_cushion = slot_cushion
token_data.invalid_after_slot = invalid_after_slot
session.add(token_data)
session.commit()
# Check to see if we have UTXO
utxo = check_wallet_utxo(token_data.bot_payment_addr)
tx_hash = utxo[0]
tx_ix = int(utxo[1])
available_lovelace = int(utxo[2])
if utxo:
# Add UTXO data to DB
token_data.utxo_tx_hash = utxo[0]
token_data.utxo_tx_ix = utxo[1]
token_data.utxo_lovelace = utxo[2]
session.add(token_data)
session.commit()
if available_lovelace >= 5000000:
# Check BlockFrost for tx details to get the return addr
tx_details = get_tx_details(utxo[0])
creator_pay_addr = tx_details['inputs'][0]['address']
token_data.creator_pay_addr = creator_pay_addr
session.add(token_data)
session.commit()
logging.info(f"Added creator_pay_addr to DB, "
f"we will send the token back to this address")
logging.info(creator_pay_addr)
else:
# FAIL
logging.info("Creator failed to send proper funds!")
return False
# Use policy keys to make policy file
# TODO Verify policy keys were made previously
policy_vkey = f'tmp/{session_uuid}-policy.vkey'
policy_script = f'tmp/{session_uuid}-policy.script'
policy_id = ''
policy_script_exists = session.query(Tokens).filter(
Tokens.session_uuid == session_uuid).filter(
Tokens.policy_script_created).scalar() is not None
logging.info(policy_script_exists)
if policy_script_exists:
logging.info("Policy Script already created for session, skip.")
else:
# Building a token locking policy for NFT
policy_dict = {
"type": "all",
"scripts": [
{
"keyHash": "",
"type": "sig"
},
{
"type": "before",
"slot": 0
}
]
}
# Generate policy key-hash
cmd = f"{config.CARDANO_CLI} address key-hash " \
f"--payment-verification-key-file {policy_vkey}"
proc = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE)
out = proc.communicate()
response = str(out[0], 'UTF-8')
policy_keyhash = response.strip()
if policy_keyhash:
logging.info(f"Policy keyHash created: {policy_keyhash}")
token_data.policy_keyhash = policy_keyhash
session.add(token_data)
session.commit()
else:
logging.info("Policy keyHash failed to create")
return False
# Add keyHash and slot to dict
policy_dict["scripts"][0]["keyHash"] = policy_keyhash
policy_dict["scripts"][1]["slot"] = current_slot + slot_cushion
logging.info(f"Policy Dictionary for token: {policy_dict}")
# Write out the policy script to a file for later
policy_script_out = open(policy_script, "w+")
json.dump(policy_dict, policy_script_out)
policy_script_out.close()
token_data.policy_keys_created = True
session.add(token_data)
session.commit()
# Generate policy ID
cmd = f"{config.CARDANO_CLI} transaction policyid --script-file {policy_script}"
proc = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE)
out = proc.communicate()
policy_id = str(out[0], 'UTF-8')
logging.info(f"Policy ID: {policy_id}")
token_data.policy_id = policy_id
session.add(token_data)
session.commit()
# Create Metadata
metadata_file = f'tmp/{session_uuid}-metadata.json'
meta_dict = {
"721": {
token_data.policy_id.strip(): {
f"{token_data.token_number}": {
"image": f"ipfs://{token_data.token_ipfs_hash}",
"ticker": token_data.token_ticker,
"name": token_data.token_name,
"description": token_data.token_desc,
}
}
}
}
# Write out the policy
metadata_out = open(metadata_file, "w+")
json.dump(meta_dict, metadata_out)
metadata_out.close()
logging.info("Created metadata.json")
token_data.metadata_created = True
session.add(token_data)
session.commit()
matx_raw = f'tmp/{session_uuid}-matx.raw'
# Build Raw TX
tx_fee = 0
cmd = f'{config.CARDANO_CLI} transaction build-raw ' \
f'--fee {tx_fee} ' \
f'--tx-in {tx_hash}#{tx_ix} ' \
f'--tx-out {token_data.creator_pay_addr}+{available_lovelace}+"{token_data.token_amount} {token_data.policy_id.strip()}.{token_data.token_ticker}" ' \
f'--mint="{token_data.token_amount} {token_data.policy_id.strip()}.{token_data.token_ticker}" ' \
f'--metadata-json-file {metadata_file} ' \
f'--invalid-hereafter={invalid_after_slot} ' \
f'--out-file {matx_raw}'
proc = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE)
# Command does not return anything
out = proc.communicate()
if out[1] is None:
logging.info(out)
logging.info("Raw transaction created")
token_data.raw_tx_created = True
session.add(token_data)
session.commit()
else:
logging.info(out)
logging.info('Something failed on building the transaction')
return False
# Calculate Fee [or hard set to 3 ADA]
protocol_params = 'tmp/protocol.json'
cmd = f"{config.CARDANO_CLI} transaction calculate-min-fee " \
f"--tx-body-file {matx_raw} " \
f"--tx-in-count 1 " \
f"--tx-out-count 1 " \
f"--witness-count 1 " \
f"--testnet-magic {config.TESTNET_ID} " \
f"--protocol-params-file {protocol_params}"
proc = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE)
out = proc.communicate()
response = str(out[0], 'UTF-8').split()
# calculate-fee always seems low in testnet
# Add 100 Lovelace for reasons ???
tx_fee = int(response[0]) + 100
logging.info(f'The TX fee today: {tx_fee}')
# Build TX with Fees
matx_raw = f'tmp/{session_uuid}-real-matx.raw'
# Build Raw TX
ada_return = available_lovelace - tx_fee
logging.info(f"Return this much plus token back to the "
f"original funder: {ada_return} lovelace")
cmd = f'{config.CARDANO_CLI} transaction build-raw ' \
f'--fee {tx_fee} ' \
f'--tx-in {tx_hash}#{tx_ix} ' \
f'--tx-out {token_data.creator_pay_addr}+{ada_return}+"{token_data.token_amount} {token_data.policy_id.strip()}.{token_data.token_ticker}" ' \
f'--mint="{token_data.token_amount} {token_data.policy_id.strip()}.{token_data.token_ticker}" ' \
f'--metadata-json-file {metadata_file} ' \
f'--invalid-hereafter={invalid_after_slot} ' \
f'--out-file {matx_raw}'
proc = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE)
# Command does not return anything
out = proc.communicate()
if out[1] is None:
logging.info(out)
logging.info("Real Raw transaction created")
else:
logging.info(out)
logging.info('Something failed on building the real transaction')
return False
# Sign TX
payment_skey_file = f'tmp/{session_uuid}-payment.skey'
policy_skey = f'tmp/{session_uuid}-policy.skey'
matx_signed = f'tmp/{session_uuid}-matx.signed'
cmd = f"{config.CARDANO_CLI} transaction sign " \
f"--signing-key-file {payment_skey_file} " \
f"--signing-key-file {policy_skey} " \
f"--script-file {policy_script} " \
f"--testnet-magic {config.TESTNET_ID} " \
f"--tx-body-file {matx_raw} " \
f"--out-file {matx_signed}"
proc = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE)
# Command does not return anything
out = proc.communicate()
if out[1] is None:
logging.info(out)
logging.info("Transaction signed")
token_data.signed_tx_created = True
session.add(token_data)
session.commit()
else:
logging.info(out)
logging.info('Something failed on Transaction signing')
return False
# Send to Blockchain
cmd = f"{config.CARDANO_CLI} transaction submit " \
f"--tx-file {matx_signed} " \
f"--testnet-magic {config.TESTNET_ID}"
proc = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE)
# Command does not return anything
out = proc.communicate()
if out[1] is None:
logging.info(out)
logging.info("Transaction Submitted")
token_data.tx_submitted = True
session.add(token_data)
session.commit()
else:
logging.info(out)
logging.info('Something failed on Transaction Submitted')
return False
# Verify Sent
confirmed = False
while confirmed is False:
creator_utxo = check_wallet_utxo(token_data.creator_pay_addr)
if creator_utxo:
# Sleep while wait for BlockFrost to pick up TX
time.sleep(5)
nft_tx_details = get_tx_details(creator_utxo[0])
logging.info(nft_tx_details)
# Done with minting
token_data.token_tx_hash = creator_utxo[0]
session.add(token_data)
session.commit()
return True
confirmed = False
# Sleep for 5 sec, blocks are 20 seconds
# so we should get a confirmation in 4 tries
time.sleep(5)