From c8fc8bf208900a8a8fec697a655e37cf71b803e7 Mon Sep 17 00:00:00 2001 From: FFEvan Date: Mon, 3 Jan 2022 13:19:15 -0500 Subject: [PATCH 1/2] Fixes issue #8 and updates the solana.py library to 0.20.0 --- .gitignore | 135 ++++++++++++++++++++++++++++++++++++++ api/metaplex_api.py | 92 +++++++++++++++----------- metaplex/metadata.py | 90 ++++++++++++++----------- metaplex/transactions.py | 61 +++++++++-------- requirements.txt | 6 +- test/test_api.py | 4 +- utils/execution_engine.py | 22 +++++-- 7 files changed, 292 insertions(+), 118 deletions(-) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..871869d --- /dev/null +++ b/.gitignore @@ -0,0 +1,135 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# ignore idea files +.idea/ + +.DS_Store +.DS_Store/ diff --git a/api/metaplex_api.py b/api/metaplex_api.py index 2bee099..18a6c7a 100644 --- a/api/metaplex_api.py +++ b/api/metaplex_api.py @@ -1,34 +1,39 @@ import json from cryptography.fernet import Fernet import base58 -from solana.keypair import Keypair +from nacl.public import PrivateKey +from solana.keypair import Keypair from metaplex.transactions import deploy, topup, mint, send, burn, update_token_metadata from utils.execution_engine import execute -class MetaplexAPI(): + +def wallet(): + """ Generate a wallet and return the address and private key. """ + keypair = Keypair() + pub_key = keypair.public_key + private_key = list(keypair.seed) + return json.dumps( + { + 'address': str(pub_key), + 'private_key': private_key + } + ) + + +class MetaplexAPI: def __init__(self, cfg): self.private_key = list(base58.b58decode(cfg["PRIVATE_KEY"]))[:32] self.public_key = cfg["PUBLIC_KEY"] - self.keypair = Keypair(self.private_key) + self.keypair = Keypair(PrivateKey(bytes(self.private_key))) self.cipher = Fernet(cfg["DECRYPTION_KEY"]) - def wallet(self): - """ Generate a wallet and return the address and private key. """ - keypair = Keypair() - pub_key = keypair.public_key - private_key = list(keypair.seed) - return json.dumps( - { - 'address': str(pub_key), - 'private_key': private_key - } - ) - - def deploy(self, api_endpoint, name, symbol, fees, max_retries=3, skip_confirmation=False, max_timeout=60, target=20, finalized=True): + def deploy(self, api_endpoint, name, symbol, fees, max_retries=3, skip_confirmation=False, max_timeout=60, + target=20, finalized=True): """ - Deploy a contract to the blockchain (on network that support contracts). Takes the network ID and contract name, plus initialisers of name and symbol. Process may vary significantly between blockchains. - Returns status code of success or fail, the contract address, and the native transaction data. + Deploy a contract to the blockchain (on network that support contracts). Takes the network ID and contract + name, plus initializers of name and symbol. Process may vary significantly between blockchains. Returns + status code of success or fail, the contract address, and the native transaction data. """ try: tx, signers, contract = deploy(api_endpoint, self.keypair, name, symbol, fees) @@ -49,9 +54,11 @@ def deploy(self, api_endpoint, name, symbol, fees, max_retries=3, skip_confirmat except: return json.dumps({"status": 400}) - def topup(self, api_endpoint, to, amount=None, max_retries=3, skip_confirmation=False, max_timeout=60, target=20, finalized=True): + def topup(self, api_endpoint, to, amount=None, max_retries=3, skip_confirmation=False, max_timeout=60, target=20, + finalized=True): """ - Send a small amount of native currency to the specified wallet to handle gas fees. Return a status flag of success or fail and the native transaction data. + Send a small amount of native currency to the specified wallet to handle gas fees. Return a status flag of + success or fail and the native transaction data. """ try: tx, signers = topup(api_endpoint, self.keypair, to, amount=amount) @@ -70,7 +77,8 @@ def topup(self, api_endpoint, to, amount=None, max_retries=3, skip_confirmation= except: return json.dumps({"status": 400}) - def mint(self, api_endpoint, contract_key, dest_key, link, max_retries=3, skip_confirmation=False, max_timeout=60, target=20, finalized=True, supply=1 ): + def mint(self, api_endpoint, contract_key, dest_key, link, max_retries=3, skip_confirmation=False, max_timeout=60, + target=20, finalized=True, supply=1): """ Mints an NFT to an account, updates the metadata and creates a master edition """ @@ -89,27 +97,30 @@ def mint(self, api_endpoint, contract_key, dest_key, link, max_retries=3, skip_c return json.dumps(resp) # except: # return json.dumps({"status": 400}) - - def update_token_metadata(self, api_endpoint, mint_token_id, link, data, creators_addresses, creators_verified, creators_share,fee, max_retries=3, skip_confirmation=False, max_timeout=60, target=20, finalized=True, supply=1 ): - """ + + def update_token_metadata(self, api_endpoint, mint_token_id, link, data, creators_addresses, creators_verified, + creators_share, fee, max_retries=3, skip_confirmation=False, max_timeout=60, target=20, + finalized=True, supply=1): + """ Updates the json metadata for a given mint token id. """ - tx, signers = update_token_metadata(api_endpoint, self.keypair, mint_token_id, link, data, fee, creators_addresses, creators_verified, creators_share) - resp = execute( - api_endpoint, - tx, - signers, - max_retries=max_retries, - skip_confirmation=skip_confirmation, - max_timeout=max_timeout, - target=target, - finalized=finalized, - ) - resp["status"] = 200 - return json.dumps(resp) - + tx, signers = update_token_metadata(api_endpoint, self.keypair, mint_token_id, link, data, fee, + creators_addresses, creators_verified, creators_share) + resp = execute( + api_endpoint, + tx, + signers, + max_retries=max_retries, + skip_confirmation=skip_confirmation, + max_timeout=max_timeout, + target=target, + finalized=finalized, + ) + resp["status"] = 200 + return json.dumps(resp) - def send(self, api_endpoint, contract_key, sender_key, dest_key, encrypted_private_key, max_retries=3, skip_confirmation=False, max_timeout=60, target=20, finalized=True): + def send(self, api_endpoint, contract_key, sender_key, dest_key, encrypted_private_key, max_retries=3, + skip_confirmation=False, max_timeout=60, target=20, finalized=True): """ Transfer a token on a given network and contract from the sender to the recipient. May require a private key, if so this will be provided encrypted using Fernet: https://cryptography.io/en/latest/fernet/ @@ -133,7 +144,8 @@ def send(self, api_endpoint, contract_key, sender_key, dest_key, encrypted_priva except: return json.dumps({"status": 400}) - def burn(self, api_endpoint, contract_key, owner_key, encrypted_private_key, max_retries=3, skip_confirmation=False, max_timeout=60, target=20, finalized=True): + def burn(self, api_endpoint, contract_key, owner_key, encrypted_private_key, max_retries=3, skip_confirmation=False, + max_timeout=60, target=20, finalized=True): """ Burn a token, permanently removing it from the blockchain. May require a private key, if so this will be provided encrypted using Fernet: https://cryptography.io/en/latest/fernet/ diff --git a/metaplex/metadata.py b/metaplex/metadata.py index 5187dc6..a7dfd62 100644 --- a/metaplex/metadata.py +++ b/metaplex/metadata.py @@ -13,28 +13,34 @@ MAX_URI_LENGTH = 200 MAX_CREATOR_LENGTH = 34 MAX_CREATOR_LIMIT = 5 + + class InstructionType(IntEnum): CREATE_METADATA = 0 UPDATE_METADATA = 1 + METADATA_PROGRAM_ID = PublicKey('metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s') SYSTEM_PROGRAM_ID = PublicKey('11111111111111111111111111111111') -SYSVAR_RENT_PUBKEY = PublicKey('SysvarRent111111111111111111111111111111111') +SYSVAR_RENT_PUBKEY = PublicKey('SysvarRent111111111111111111111111111111111') ASSOCIATED_TOKEN_ACCOUNT_PROGRAM_ID = PublicKey('ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL') TOKEN_PROGRAM_ID = PublicKey('TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA') + def get_metadata_account(mint_key): return PublicKey.find_program_address( [b'metadata', bytes(METADATA_PROGRAM_ID), bytes(PublicKey(mint_key))], METADATA_PROGRAM_ID )[0] + def get_edition(mint_key): return PublicKey.find_program_address( [b'metadata', bytes(METADATA_PROGRAM_ID), bytes(PublicKey(mint_key)), b"edition"], METADATA_PROGRAM_ID )[0] + def create_associated_token_account_instruction(associated_token_account, payer, wallet_address, token_mint_address): keys = [ AccountMeta(pubkey=payer, is_signer=True, is_writable=True), @@ -47,12 +53,13 @@ def create_associated_token_account_instruction(associated_token_account, payer, ] return TransactionInstruction(keys=keys, program_id=ASSOCIATED_TOKEN_ACCOUNT_PROGRAM_ID) + def _get_data_buffer(name, symbol, uri, fee, creators, verified=None, share=None): if isinstance(share, list): - assert(len(share) == len(creators)) + assert (len(share) == len(creators)) if isinstance(verified, list): - assert(len(verified) == len(creators)) - args = [ + assert (len(verified) == len(creators)) + args = [ len(name), *list(name.encode()), len(symbol), @@ -61,19 +68,19 @@ def _get_data_buffer(name, symbol, uri, fee, creators, verified=None, share=None *list(uri.encode()), fee, ] - - byte_fmt = "<" - byte_fmt += "I" + "B"*len(name) - byte_fmt += "I" + "B"*len(symbol) - byte_fmt += "I" + "B"*len(uri) + + byte_fmt = "<" + byte_fmt += "I" + "B" * len(name) + byte_fmt += "I" + "B" * len(symbol) + byte_fmt += "I" + "B" * len(uri) byte_fmt += "h" byte_fmt += "B" if creators: args.append(1) byte_fmt += "I" args.append(len(creators)) - for i, creator in enumerate(creators): - byte_fmt += "B"*32 + "B" + "B" + for i, creator in enumerate(creators): + byte_fmt += "B" * 32 + "B" + "B" args.extend(list(base58.b58decode(creator))) if isinstance(verified, list): args.append(verified[i]) @@ -84,12 +91,13 @@ def _get_data_buffer(name, symbol, uri, fee, creators, verified=None, share=None else: args.append(100) else: - args.append(0) + args.append(0) buffer = struct.pack(byte_fmt, *args) return buffer - + + def create_metadata_instruction_data(name, symbol, fee, creators): - _data = _get_data_buffer(name, symbol, " "*64, fee, creators) + _data = _get_data_buffer(name, symbol, " " * 64, fee, creators) metadata_args_layout = cStruct( "data" / Bytes(len(_data)), "is_mutable" / Flag, @@ -106,6 +114,7 @@ def create_metadata_instruction_data(name, symbol, fee, creators): ) ) + def create_metadata_instruction(data, update_authority, mint_key, mint_authority_key, payer): metadata_account = get_metadata_account(mint_key) print(metadata_account) @@ -120,37 +129,38 @@ def create_metadata_instruction(data, update_authority, mint_key, mint_authority ] return TransactionInstruction(keys=keys, program_id=METADATA_PROGRAM_ID, data=data) + def unpack_metadata_account(data): - assert(data[0] == 4) + assert (data[0] == 4) i = 1 - source_account = base58.b58encode(bytes(struct.unpack('<' + "B"*32, data[i:i+32]))) + source_account = base58.b58encode(bytes(struct.unpack('<' + "B" * 32, data[i:i + 32]))) i += 32 - mint_account = base58.b58encode(bytes(struct.unpack('<' + "B"*32, data[i:i+32]))) + mint_account = base58.b58encode(bytes(struct.unpack('<' + "B" * 32, data[i:i + 32]))) i += 32 - name_len = struct.unpack(' Date: Mon, 3 Jan 2022 14:59:35 -0500 Subject: [PATCH 2/2] Making the wallet function a staticmethod --- api/metaplex_api.py | 26 +++++++++++++------------- test/test_api.py | 10 +++++++--- 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/api/metaplex_api.py b/api/metaplex_api.py index 18a6c7a..33fb30d 100644 --- a/api/metaplex_api.py +++ b/api/metaplex_api.py @@ -7,19 +7,6 @@ from utils.execution_engine import execute -def wallet(): - """ Generate a wallet and return the address and private key. """ - keypair = Keypair() - pub_key = keypair.public_key - private_key = list(keypair.seed) - return json.dumps( - { - 'address': str(pub_key), - 'private_key': private_key - } - ) - - class MetaplexAPI: def __init__(self, cfg): @@ -28,6 +15,19 @@ def __init__(self, cfg): self.keypair = Keypair(PrivateKey(bytes(self.private_key))) self.cipher = Fernet(cfg["DECRYPTION_KEY"]) + @staticmethod + def wallet(): + """ Generate a wallet and return the address and private key. """ + keypair = Keypair() + pub_key = keypair.public_key + private_key = list(keypair.seed) + return json.dumps( + { + 'address': str(pub_key), + 'private_key': private_key + } + ) + def deploy(self, api_endpoint, name, symbol, fees, max_retries=3, skip_confirmation=False, max_timeout=60, target=20, finalized=True): """ diff --git a/test/test_api.py b/test/test_api.py index d58e050..4f7f4be 100644 --- a/test/test_api.py +++ b/test/test_api.py @@ -10,6 +10,7 @@ from cryptography.fernet import Fernet from api.metaplex_api import MetaplexAPI + def await_full_confirmation(client, txn, max_timeout=60): if txn is None: return @@ -25,6 +26,7 @@ def await_full_confirmation(client, txn, max_timeout=60): print(f"Took {elapsed} seconds to confirm transaction {txn}") break + def test(api_endpoint="https://api.devnet.solana.com/"): keypair = Keypair() cfg = { @@ -51,13 +53,14 @@ def test(api_endpoint="https://api.devnet.solana.com/"): assert deploy_response["status"] == 200 contract = deploy_response.get("contract") print(get_metadata(client, contract)) - wallet = json.loads(wallet()) + wallet = json.loads(MetaplexAPI.wallet()) address1 = wallet.get('address') encrypted_pk1 = api.cipher.encrypt(bytes(wallet.get('private_key'))) topup_response = json.loads(api.topup(api_endpoint, address1)) print(f"Topup {address1}:", topup_response) assert topup_response["status"] == 200 - mint_to_response = json.loads(api.mint(api_endpoint, contract, address1, "https://arweave.net/1eH7bZS-6HZH4YOc8T_tGp2Rq25dlhclXJkoa6U55mM/")) + mint_to_response = json.loads( + api.mint(api_endpoint, contract, address1, "https://arweave.net/1eH7bZS-6HZH4YOc8T_tGp2Rq25dlhclXJkoa6U55mM/")) print("Mint:", mint_to_response) # await_confirmation(client, mint_to_response['tx']) assert mint_to_response["status"] == 200 @@ -79,11 +82,12 @@ def test(api_endpoint="https://api.devnet.solana.com/"): assert burn_response["status"] == 200 print("Success!") + if __name__ == "__main__": ap = argparse.ArgumentParser() ap.add_argument("--network", default=None) args = ap.parse_args() - if args.network == None or args.network == 'devnet': + if args.network is None or args.network == 'devnet': test() elif args.network == 'testnet': test(api_endpoint="https://api.testnet.solana.com/")