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..33fb30d 100644 --- a/api/metaplex_api.py +++ b/api/metaplex_api.py @@ -1,22 +1,25 @@ 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(): + +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): + @staticmethod + def wallet(): """ Generate a wallet and return the address and private key. """ keypair = Keypair() - pub_key = keypair.public_key + pub_key = keypair.public_key private_key = list(keypair.seed) return json.dumps( { @@ -25,10 +28,12 @@ def wallet(self): } ) - 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('