[
  {
    "path": "README.md",
    "content": "# Metaplex Python API\n\nThis modules allows you to create, mint, transfer and burn NFTs on the Solana blockchain using Python.\n\n## Setup\nFirst, clone down the repository\n\nCreate a virtual environment and install the dependencies in `requirements.txt`. Be sure to use a version of Python >= 3.6\n\n```bash\npython3 -m virtualenv venv\nsource venv/bin/activate\npip install -r requirements.txt\n```\n\nAt this point, you should be good to go.\n\n## Usage\n\nTo create a `MetaplexAPI` object, you need to pass a dicitonary to the constructor with the following keys:\n\n```python\ncfg = {\n    \"PRIVATE_KEY\": YOUR_PRIVATE_KEY,\n    \"PUBLIC_KEY\": YOUR_PUBLIC_KEY,\n    \"DECRYPTION_KEY\": SERVER_DECRYPTION_KEY\n}\napi = MetaplexAPI(cfg)\n```\n\nThe keypair that is passed into the `MetaplexAPI` serves as the fee payer for all network transactions (creating new wallets, minting tokens, transferring tokens, etc). Both keys are base58 encoded.\n\nThe decryption key ensures that messages sent to a server that utilizes this API will not be receiving unencrypted private keys over the wire. These private keys are necessary for signing transactions. The client can encrypt their data by using the same decryption key as the server. This is the syntax:\n\n```python\nfrom cryptography.fernet import Fernet\ncipher = Fernet(DECRYPTION_KEY) # This is the same key that the server has\nencrypted_key = cipher.encrypt(SECRET)\n\n# Send `encrypted_key` to the downstream server to process\n```\n\n## Methods\n\nThis section will go through the following story (if you look at the code snippets) and invoke each of the methods in the API along the way:\n1) Account `A` is created and airdropped 10 SOL\n2) `A` creates an NFT `N`\n3) Account `B` is created\n4) `A` pays `B`'s rent fee\n5) `N` is minted to `B` associated token account\n6) Account `C` is created\n7) `A` pays `C`'s rent fee\n8) `B` transfers `N` to `C`\n9) `C` destroys `N` \n\nLet's get started:\n```bash\n$ python3 -i -m api.metaplex_api\n>>> \n```\n\n### deploy\n`deploy` will create a new NFT token by\n1) Creating a new account from a randomly generated address (invokes `CreateAccount` from the System Program)\n2) Invoking `InitializeMint` on the new account\n3) Initializing the metadata for this account by invoking the `CreateMetatdata` instruction from the Metaplex protocol\n\nArgs:\n\n`api_endpoint`: (str) The RPC endpoint to connect the network. ([devnet](https://api.devnet.solana.com/), [mainnet](https://api.mainnet-beta.solana.com/))\n\n`name`: (str) Name of the NFT Contract (32 bytes max)\n\n`symbol`: (str) Symbol of the NFT Contract (10 bytes max)\n\n`skip_confirmation=True`: A flag that tells you to wait for the transaction to be confirmed if set to `False` (only used for testing, because it requires synchronized/sequential calls)\n\nExample:\n```python\n>>> account = KeyPair()\n>>> cfg = {\"PRIVATE_KEY\": base58.b58encode(account.seed).decode(\"ascii\"), \"PUBLIC_KEY\": str(account.public_key), \"DECRYPTION_KEY\": Fernet.generate_key().decode(\"ascii\")}\n>>> api_endpoint = \"https://api.devnet.solana.com/\"\n>>> Client(api_endpoint).request_airdrop(account.public_key, int(1e10))\n{'jsonrpc': '2.0', 'result': '4ojKmAAesmKtqJkNLRtEjdgg4CkmowuTAjRSpp3K36UvQQvEXwhirV85E8cvWYAD42c3UyFdCtzydMgWokH2mbM', 'id': 1}\n>>> metaplex_api = MetaplexAPI(cfg)\n>>> seller_basis_fees = 0 # value in 10000 \n>>> metaplex_api.deploy(api_endpoint, \"A\"*32, \"A\"*10, seller_basis_fees)\n'{\"status\": 200, \"contract\": \"7bxe7t1aGdum8o97bkuFeeBTcbARaBn9Gbv5sBd9DZPG\", \"msg\": \"Successfully created mint 7bxe7t1aGdum8o97bkuFeeBTcbARaBn9Gbv5sBd9DZPG\", \"tx\": \"2qmiWoVi2PNeAjppe2cNbY32zZCJLXMYgdS1zRVFiKJUHE41T5b1WfaZtR2QdFJUXadrqrjbkpwRN5aG2J3KQrQx\"}'\n>>> \n```\nNote that when sending SOL to the newly generated account, that account will serve as the fee payer. You can check out [this transaction on the Solana Block Exporer]( https://explorer.solana.com/tx/2qmiWoVi2PNeAjppe2cNbY32zZCJLXMYgdS1zRVFiKJUHE41T5b1WfaZtR2QdFJUXadrqrjbkpwRN5aG2J3KQrQx?cluster=devnet).\n\n### wallet\n`wallet` creates a new random public/private keypair\n\n```python\n>>> metaplex_api.wallet()\n'{\"address\": \"VtdBygLSt1EJF5M3nUk5CRxuNNTyZFUsKJ4yUVcC6hh\", \"private_key\": [95, 46, 174, 145, 248, 101, 108, 111, 128, 44, 41, 212, 118, 145, 42, 242, 84, 6, 31, 115, 18, 126, 47, 230, 103, 202, 46, 7, 194, 149, 42, 213]}'\n>>>\n```\nNo network calls are made here\n\n### topup\n`topup` sends a small amount of SOL to the destination account by invoking `Transfer` from the System Program\n\nArgs:\n\n`api_endpoint`: (str) The RPC endpoint to connect the network. ([devnet](https://api.devnet.solana.com/), [mainnet](https://api.mainnet-beta.solana.com/))\n\n`to`: (str) The base58 encoded public key of the destination address\n\n`amount`: (Union[int, None]) This is the number of lamports to send to the destination address. If `None` (default), then the minimum rent exemption balance is transferred.\n\n```python\n>>> metaplex_api.topup(api_endpoint, \"VtdBygLSt1EJF5M3nUk5CRxuNNTyZFUsKJ4yUVcC6hh\")\n'{\"status\": 200, \"msg\": \"Successfully sent 0.00203928 SOL to VtdBygLSt1EJF5M3nUk5CRxuNNTyZFUsKJ4yUVcC6hh\", \"tx\": \"32Dk647Fb6aKJyErVfxgtSfC4xbssoJprcB7BEmEAdYTFK96M5VEQ1z62QxCCC7tAPF1g9TNvMehoGNudLNaKTWE\"}'\n>>> \n```\n[tx link](https://explorer.solana.com/tx/32Dk647Fb6aKJyErVfxgtSfC4xbssoJprcB7BEmEAdYTFK96M5VEQ1z62QxCCC7tAPF1g9TNvMehoGNudLNaKTWE?cluster=devnet)\n\n### mint\n`mint` will mint a token to a designated user account by\n1) Fetching or creating an AssociatedTokenAccount from a Program Derived Address\n2) Invoking `MintTo` with the AssociatedTokenAccount as the destination\n3) Invoking the `UpdateMetadata` instruction from the Metaplex protocol to update the `uri` of the contract (containing the actual content)\n\nArgs:\n\n`api_endpoint`: (str) The RPC endpoint to connect the network. ([devnet](https://api.devnet.solana.com/), [mainnet](https://api.mainnet-beta.solana.com/))\n\n`contract_key`: (str) The base58 encoded public key of the mint address\n\n`dest_key`: (str) The base58 encoded public key of the destinaion address (where the contract will be minted)\n\n`link`: (str) The link to the content of the the NFT\n\n```\n>>> metaplex_api.mint(api_endpoint, \"7bxe7t1aGdum8o97bkuFeeBTcbARaBn9Gbv5sBd9DZPG\", \"VtdBygLSt1EJF5M3nUk5CRxuNNTyZFUsKJ4yUVcC6hh\", \"https://arweave.net/1eH7bZS-6HZH4YOc8T_tGp2Rq25dlhclXJkoa6U55mM/\")\n'{\"status\": 200, \"msg\": \"Successfully minted 1 token to DkrGGuqn183rNyYHQNo9NSDYKZB8FVsaPBGn3F6nG7iH\", \"tx\": \"5r4qY1LudNg49FXyduadoAm83cJDWVeypUX6dsGs91RJqSxzU5qTt9WXfXs3Lzs5ZGQsTDTRpDyiXorv1wCzrzsJ\"}'\n>>> \n```\n[tx link](https://explorer.solana.com/tx/5r4qY1LudNg49FXyduadoAm83cJDWVeypUX6dsGs91RJqSxzU5qTt9WXfXs3Lzs5ZGQsTDTRpDyiXorv1wCzrzsJ?cluster=devnet)\n\n### send\n`send` will send a token from one user account to another user account\n1) Fetching the AssociatedTokenAccount from a Program Derived Address for the sender\n2) Fetching or creatign the AssociatedTokenAccount from a Program Derived Address for the receiver\n3) Invoking `Transfer` (from the Token Program) with the receiver's AssociatedTokenAccount as the destination\n\nArgs:\n\n`api_endpoint`: (str) The RPC endpoint to connect the network. ([devnet](https://api.devnet.solana.com/), [mainnet](https://api.mainnet-beta.solana.com/))\n\n`contract_key`: (str) The base58 encoded public key of the mint address\\\n\n`sender_key`: (str) The base58 encoded public key of the source address (from which the contract will be transferred)\n\n`dest_key`: (str) The base58 encoded public key of the destinaion address (to where the contract will be transferred)\n\n`encrypted_private_key`: (bytes) The encrypted private key of the sender\n\n```python\n>>> metaplex_api.wallet()\n'{\"address\": \"EnMb6ZntX43PFeX2NLcV4dtLhqsxF9hUr3tgF1Cwpwu8\", \"private_key\": [172, 155, 209, 75, 226, 68, 91, 22, 199, 75, 148, 197, 143, 10, 211, 67, 5, 160, 101, 15, 139, 33, 208, 65, 59, 198, 5, 41, 167, 206, 85, 83]}'\n>>> metaplex_api.topup(api_endpoint, \"EnMb6ZntX43PFeX2NLcV4dtLhqsxF9hUr3tgF1Cwpwu8\")\n'{\"status\": 200, \"msg\": \"Successfully sent 0.00203928 SOL to EnMb6ZntX43PFeX2NLcV4dtLhqsxF9hUr3tgF1Cwpwu8\", \"tx\": \"4erc1aPC8fSNV1kb41mgUSgMKMHhd8FdDd4gqFPQ9TmmS48QcaAi9zpNjzMG3UNr1dDw1mBxThZCgJyUPchiV3Jz\"}'\n>>> encrypted_key = metaplex_api.cipher.encrypt(bytes([95, 46, 174, 145, 248, 101, 108, 111, 128, 44, 41, 212, 118, 145, 42, 242, 84, 6, 31, 115, 18, 126, 47, 230, 103, 202, 46, 7, 194, 149, 42, 213]))\n>>> metaplex_api.send(api_endpoint, \"7bxe7t1aGdum8o97bkuFeeBTcbARaBn9Gbv5sBd9DZPG\", \"VtdBygLSt1EJF5M3nUk5CRxuNNTyZFUsKJ4yUVcC6hh\", \"EnMb6ZntX43PFeX2NLcV4dtLhqsxF9hUr3tgF1Cwpwu8\", encrypted_key)\n'{\"status\": 200, \"msg\": \"Successfully transfered token from VtdBygLSt1EJF5M3nUk5CRxuNNTyZFUsKJ4yUVcC6hh to EnMb6ZntX43PFeX2NLcV4dtLhqsxF9hUr3tgF1Cwpwu8\", \"tx\": \"3ZsGcCfjUXviToSB4U6Wg1W1W4rm8bMT7wF8zfauTciK6PdszpLqcvmmYqqrz8mRGK8pQPABVewCk8EdsvNVhzp6\"}'\n```\n[tx link](https://explorer.solana.com/tx/3ZsGcCfjUXviToSB4U6Wg1W1W4rm8bMT7wF8zfauTciK6PdszpLqcvmmYqqrz8mRGK8pQPABVewCk8EdsvNVhzp6?cluster=devnet)\n\n\n### burn\n`burn` will remove a token from the blockchain\n1) Fetching the AssociatedTokenAccount from a Program Derived Address for the owner\n3) Invoking `Burn` (from the Token Program) with the owner's AssociatedTokenAccount as the destination\n\nArgs:\n\n`api_endpoint`: (str) The RPC endpoint to connect the network. (devnet: https://api.devnet.solana.com/, mainnet: https://api.mainnet-beta.solana.com/)\n\n`contract_key`: (str) The base58 encoded public key of the mint address\n\n`owner_key`: (str) The base58 encoded public key of the owner address\n\n`encrypted_private_key`: (bytes) The encrypted private key of the owner\n\n```\n>>> encrypted_key = metaplex_api.cipher.encrypt(bytes([172, 155, 209, 75, 226, 68, 91, 22, 199, 75, 148, 197, 143, 10, 211, 67, 5, 160, 101, 15, 139, 33, 208, 65, 59, 198, 5, 41, 167, 206, 85, 83]))\n>>> metaplex_api.burn(api_endpoint, \"7bxe7t1aGdum8o97bkuFeeBTcbARaBn9Gbv5sBd9DZPG\", \"EnMb6ZntX43PFeX2NLcV4dtLhqsxF9hUr3tgF1Cwpwu8\", encrypted_key)\n'{\"status\": 200, \"msg\": \"Successfully burned token 7bxe7t1aGdum8o97bkuFeeBTcbARaBn9Gbv5sBd9DZPG on EnMb6ZntX43PFeX2NLcV4dtLhqsxF9hUr3tgF1Cwpwu8\", \"tx\": \"5kd5g4mNBSjoTVYwAasWZx6iB8ijaELfBukKrNYBeDvLomK7iTqFH1R29yniEGcfajakDxsqmYCDgDvukihRyZeZ\"}'\n>>> \n```\nhttps://explorer.solana.com/tx/5kd5g4mNBSjoTVYwAasWZx6iB8ijaELfBukKrNYBeDvLomK7iTqFH1R29yniEGcfajakDxsqmYCDgDvukihRyZeZ?cluster=devnet\n\n### Full Example Code:\n\nThis is the sequential code from the previous section. These accounts will need to change if you want to do your own test.\n```\naccount = KeyPair()\ncfg = {\"PRIVATE_KEY\": base58.b58encode(account.seed).decode(\"ascii\"), \"PUBLIC_KEY\": str(account.public_key), \"DECRYPTION_KEY\": Fernet.generate_key().decode(\"ascii\")}\napi_endpoint = \"https://api.devnet.solana.com/\"\nClient(api_endpoint).request_airdrop(account.public_key, int(1e10))\n\n# Create API\nmetaplex_api = MetaplexAPI(cfg)\n\n# Deploy\nmetaplex_api.deploy(api_endpoint, \"A\"*32, \"A\"*10, 0)\n\n# Topup VtdBygLSt1EJF5M3nUk5CRxuNNTyZFUsKJ4yUVcC6hh\nmetaplex_api.topup(api_endpoint, \"VtdBygLSt1EJF5M3nUk5CRxuNNTyZFUsKJ4yUVcC6hh\")\n\n# Mint\nmetaplex_api.mint(api_endpoint, \"7bxe7t1aGdum8o97bkuFeeBTcbARaBn9Gbv5sBd9DZPG\", \"VtdBygLSt1EJF5M3nUk5CRxuNNTyZFUsKJ4yUVcC6hh\", \"https://arweave.net/1eH7bZS-6HZH4YOc8T_tGp2Rq25dlhclXJkoa6U55mM/\")\n\n# Topup EnMb6ZntX43PFeX2NLcV4dtLhqsxF9hUr3tgF1Cwpwu8\nmetaplex_api.topup(api_endpoint, \"EnMb6ZntX43PFeX2NLcV4dtLhqsxF9hUr3tgF1Cwpwu8\")\n\n# Send\nencrypted_key = metaplex_api.cipher.encrypt(bytes([95, 46, 174, 145, 248, 101, 108, 111, 128, 44, 41, 212, 118, 145, 42, 242, 84, 6, 31, 115, 18, 126, 47, 230, 103, 202, 46, 7, 194, 149, 42, 213]))\nmetaplex_api.send(api_endpoint, \"7bxe7t1aGdum8o97bkuFeeBTcbARaBn9Gbv5sBd9DZPG\", \"VtdBygLSt1EJF5M3nUk5CRxuNNTyZFUsKJ4yUVcC6hh\", \"EnMb6ZntX43PFeX2NLcV4dtLhqsxF9hUr3tgF1Cwpwu8\", encrypted_key)\n\n# Burn\nencrypted_key = metaplex_api.cipher.encrypt(bytes([172, 155, 209, 75, 226, 68, 91, 22, 199, 75, 148, 197, 143, 10, 211, 67, 5, 160, 101, 15, 139, 33, 208, 65, 59, 198, 5, 41, 167, 206, 85, 83]))\nmetaplex_api.burn(api_endpoint, \"7bxe7t1aGdum8o97bkuFeeBTcbARaBn9Gbv5sBd9DZPG\", \"EnMb6ZntX43PFeX2NLcV4dtLhqsxF9hUr3tgF1Cwpwu8\", encrypted_key)\n \n```\n"
  },
  {
    "path": "api/__init__.py",
    "content": ""
  },
  {
    "path": "api/metaplex_api.py",
    "content": "import json\nfrom cryptography.fernet import Fernet\nimport base58\nfrom solana.keypair import Keypair \nfrom metaplex.transactions import deploy, topup, mint, send, burn, update_token_metadata\nfrom utils.execution_engine import execute\n\nclass MetaplexAPI():\n\n    def __init__(self, cfg):\n        self.private_key = list(base58.b58decode(cfg[\"PRIVATE_KEY\"]))[:32]\n        self.public_key = cfg[\"PUBLIC_KEY\"]\n        self.keypair = Keypair(self.private_key)\n        self.cipher = Fernet(cfg[\"DECRYPTION_KEY\"])\n\n    def wallet(self):\n        \"\"\" Generate a wallet and return the address and private key. \"\"\"\n        keypair = Keypair()\n        pub_key = keypair.public_key \n        private_key = list(keypair.seed)\n        return json.dumps(\n            {\n                'address': str(pub_key),\n                'private_key': private_key\n            }\n        )\n\n    def deploy(self, api_endpoint, name, symbol, fees, max_retries=3, skip_confirmation=False, max_timeout=60, target=20, finalized=True):\n        \"\"\"\n        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.\n        Returns status code of success or fail, the contract address, and the native transaction data.\n        \"\"\"\n        try:\n            tx, signers, contract = deploy(api_endpoint, self.keypair, name, symbol, fees)\n            print(contract)\n            resp = execute(\n                api_endpoint,\n                tx,\n                signers,\n                max_retries=max_retries,\n                skip_confirmation=skip_confirmation,\n                max_timeout=max_timeout,\n                target=target,\n                finalized=finalized,\n            )\n            resp[\"contract\"] = contract\n            resp[\"status\"] = 200\n            return json.dumps(resp)\n        except:\n            return json.dumps({\"status\": 400})\n\n    def topup(self, api_endpoint, to, amount=None, max_retries=3, skip_confirmation=False, max_timeout=60, target=20, finalized=True):\n        \"\"\"\n        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.\n        \"\"\"\n        try:\n            tx, signers = topup(api_endpoint, self.keypair, to, amount=amount)\n            resp = execute(\n                api_endpoint,\n                tx,\n                signers,\n                max_retries=max_retries,\n                skip_confirmation=skip_confirmation,\n                max_timeout=max_timeout,\n                target=target,\n                finalized=finalized,\n            )\n            resp[\"status\"] = 200\n            return json.dumps(resp)\n        except:\n            return json.dumps({\"status\": 400})\n\n    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 ):\n        \"\"\"\n        Mints an NFT to an account, updates the metadata and creates a master edition\n        \"\"\"\n        tx, signers = mint(api_endpoint, self.keypair, contract_key, dest_key, link, supply=supply)\n        resp = execute(\n            api_endpoint,\n            tx,\n            signers,\n            max_retries=max_retries,\n            skip_confirmation=skip_confirmation,\n            max_timeout=max_timeout,\n            target=target,\n            finalized=finalized,\n        )\n        resp[\"status\"] = 200\n        return json.dumps(resp)\n        # except:\n        #     return json.dumps({\"status\": 400})\n        \n    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 ):\n            \"\"\"\n            Updates the json metadata for a given mint token id.\n            \"\"\"\n            tx, signers = update_token_metadata(api_endpoint, self.keypair, mint_token_id, link, data, fee, creators_addresses, creators_verified, creators_share)\n            resp = execute(\n                api_endpoint,\n                tx,\n                signers,\n                max_retries=max_retries,\n                skip_confirmation=skip_confirmation,\n                max_timeout=max_timeout,\n                target=target,\n                finalized=finalized,\n            )\n            resp[\"status\"] = 200\n            return json.dumps(resp)\n\n\n    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):\n        \"\"\"\n        Transfer a token on a given network and contract from the sender to the recipient.\n        May require a private key, if so this will be provided encrypted using Fernet: https://cryptography.io/en/latest/fernet/\n        Return a status flag of success or fail and the native transaction data. \n        \"\"\"\n        try:\n            private_key = list(self.cipher.decrypt(encrypted_private_key))\n            tx, signers = send(api_endpoint, self.keypair, contract_key, sender_key, dest_key, private_key)\n            resp = execute(\n                api_endpoint,\n                tx,\n                signers,\n                max_retries=max_retries,\n                skip_confirmation=skip_confirmation,\n                max_timeout=max_timeout,\n                target=target,\n                finalized=finalized,\n            )\n            resp[\"status\"] = 200\n            return json.dumps(resp)\n        except:\n            return json.dumps({\"status\": 400})\n\n    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):\n        \"\"\"\n        Burn a token, permanently removing it from the blockchain.\n        May require a private key, if so this will be provided encrypted using Fernet: https://cryptography.io/en/latest/fernet/\n        Return a status flag of success or fail and the native transaction data.\n        \"\"\"\n        try:\n            private_key = list(self.cipher.decrypt(encrypted_private_key))\n            tx, signers = burn(api_endpoint, contract_key, owner_key, private_key)\n            resp = execute(\n                api_endpoint,\n                tx,\n                signers,\n                max_retries=max_retries,\n                skip_confirmation=skip_confirmation,\n                max_timeout=max_timeout,\n                target=target,\n                finalized=finalized,\n            )\n            resp[\"status\"] = 200\n            return json.dumps(resp)\n        except:\n            return json.dumps({\"status\": 400})\n"
  },
  {
    "path": "api.py",
    "content": "from api.metaplex_api import MetaplexAPI\n"
  },
  {
    "path": "metaplex/__init__.py",
    "content": ""
  },
  {
    "path": "metaplex/metadata.py",
    "content": "from typing import Union\nimport struct\nfrom enum import IntEnum\nfrom construct import Bytes, Flag, Int8ul\nfrom construct import Struct as cStruct  # type: ignore\nfrom solana.publickey import PublicKey\nfrom solana.transaction import AccountMeta, TransactionInstruction\nimport base58\nimport base64\n\nMAX_NAME_LENGTH = 32\nMAX_SYMBOL_LENGTH = 10\nMAX_URI_LENGTH = 200\nMAX_CREATOR_LENGTH = 34\nMAX_CREATOR_LIMIT = 5\nclass InstructionType(IntEnum):\n    CREATE_METADATA = 0\n    UPDATE_METADATA = 1\n\nMETADATA_PROGRAM_ID = PublicKey('metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s')\nSYSTEM_PROGRAM_ID = PublicKey('11111111111111111111111111111111')\nSYSVAR_RENT_PUBKEY = PublicKey('SysvarRent111111111111111111111111111111111') \nASSOCIATED_TOKEN_ACCOUNT_PROGRAM_ID = PublicKey('ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL')\nTOKEN_PROGRAM_ID = PublicKey('TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA')\n\ndef get_metadata_account(mint_key):\n    return PublicKey.find_program_address(\n        [b'metadata', bytes(METADATA_PROGRAM_ID), bytes(PublicKey(mint_key))],\n        METADATA_PROGRAM_ID\n    )[0]\n\ndef get_edition(mint_key):\n    return PublicKey.find_program_address(\n        [b'metadata', bytes(METADATA_PROGRAM_ID), bytes(PublicKey(mint_key)), b\"edition\"],\n        METADATA_PROGRAM_ID\n    )[0]\n\ndef create_associated_token_account_instruction(associated_token_account, payer, wallet_address, token_mint_address):\n    keys = [\n        AccountMeta(pubkey=payer, is_signer=True, is_writable=True),\n        AccountMeta(pubkey=associated_token_account, is_signer=False, is_writable=True),\n        AccountMeta(pubkey=wallet_address, is_signer=False, is_writable=False),\n        AccountMeta(pubkey=token_mint_address, is_signer=False, is_writable=False),\n        AccountMeta(pubkey=SYSTEM_PROGRAM_ID, is_signer=False, is_writable=False),\n        AccountMeta(pubkey=TOKEN_PROGRAM_ID, is_signer=False, is_writable=False),\n        AccountMeta(pubkey=SYSVAR_RENT_PUBKEY, is_signer=False, is_writable=False),\n    ]\n    return TransactionInstruction(keys=keys, program_id=ASSOCIATED_TOKEN_ACCOUNT_PROGRAM_ID)\n\ndef _get_data_buffer(name, symbol, uri, fee, creators, verified=None, share=None):\n    if isinstance(share, list):\n        assert(len(share) == len(creators))\n    if isinstance(verified, list):\n        assert(len(verified) == len(creators))\n    args =  [\n        len(name),\n        *list(name.encode()),\n        len(symbol),\n        *list(symbol.encode()),\n        len(uri),\n        *list(uri.encode()),\n        fee,\n    ]\n \n    byte_fmt = \"<\" \n    byte_fmt += \"I\" + \"B\"*len(name)\n    byte_fmt += \"I\" + \"B\"*len(symbol)\n    byte_fmt += \"I\" + \"B\"*len(uri)\n    byte_fmt += \"h\"\n    byte_fmt += \"B\"\n    if creators:\n        args.append(1)\n        byte_fmt += \"I\"\n        args.append(len(creators))\n        for i, creator in enumerate(creators): \n            byte_fmt +=  \"B\"*32 + \"B\" + \"B\"\n            args.extend(list(base58.b58decode(creator)))\n            if isinstance(verified, list):\n                args.append(verified[i])\n            else:\n                args.append(1)\n            if isinstance(share, list):\n                args.append(share[i])\n            else:\n                args.append(100)\n    else:\n        args.append(0) \n    buffer = struct.pack(byte_fmt, *args)\n    return buffer\n    \ndef create_metadata_instruction_data(name, symbol, fee, creators):\n    _data = _get_data_buffer(name, symbol, \" \"*64, fee, creators)\n    metadata_args_layout = cStruct(\n        \"data\" / Bytes(len(_data)),\n        \"is_mutable\" / Flag,\n    )\n    _create_metadata_args = dict(data=_data, is_mutable=True)\n    instruction_layout = cStruct(\n        \"instruction_type\" / Int8ul,\n        \"args\" / metadata_args_layout,\n    )\n    return instruction_layout.build(\n        dict(\n            instruction_type=InstructionType.CREATE_METADATA,\n            args=_create_metadata_args,\n        )\n    )\n\ndef create_metadata_instruction(data, update_authority, mint_key, mint_authority_key, payer):\n    metadata_account = get_metadata_account(mint_key)\n    print(metadata_account)\n    keys = [\n        AccountMeta(pubkey=metadata_account, is_signer=False, is_writable=True),\n        AccountMeta(pubkey=mint_key, is_signer=False, is_writable=False),\n        AccountMeta(pubkey=mint_authority_key, is_signer=True, is_writable=False),\n        AccountMeta(pubkey=payer, is_signer=True, is_writable=False),\n        AccountMeta(pubkey=update_authority, is_signer=False, is_writable=False),\n        AccountMeta(pubkey=SYSTEM_PROGRAM_ID, is_signer=False, is_writable=False),\n        AccountMeta(pubkey=SYSVAR_RENT_PUBKEY, is_signer=False, is_writable=False),\n    ]\n    return TransactionInstruction(keys=keys, program_id=METADATA_PROGRAM_ID, data=data)\n\ndef unpack_metadata_account(data):\n    assert(data[0] == 4)\n    i = 1\n    source_account = base58.b58encode(bytes(struct.unpack('<' + \"B\"*32, data[i:i+32])))\n    i += 32\n    mint_account = base58.b58encode(bytes(struct.unpack('<' + \"B\"*32, data[i:i+32])))\n    i += 32\n    name_len = struct.unpack('<I', data[i:i+4])[0]\n    i += 4\n    name = struct.unpack('<' + \"B\"*name_len, data[i:i+name_len])\n    i += name_len\n    symbol_len = struct.unpack('<I', data[i:i+4])[0]\n    i += 4 \n    symbol = struct.unpack('<' + \"B\"*symbol_len, data[i:i+symbol_len])\n    i += symbol_len\n    uri_len = struct.unpack('<I', data[i:i+4])[0]\n    i += 4 \n    uri = struct.unpack('<' + \"B\"*uri_len, data[i:i+uri_len])\n    i += uri_len\n    fee = struct.unpack('<h', data[i:i+2])[0]\n    i += 2\n    has_creator = data[i] \n    i += 1\n    creators = []\n    verified = []\n    share = []\n    if has_creator:\n        creator_len = struct.unpack('<I', data[i:i+4])[0]\n        i += 4\n        for _ in range(creator_len):\n            creator = base58.b58encode(bytes(struct.unpack('<' + \"B\"*32, data[i:i+32])))\n            creators.append(creator)\n            i += 32\n            verified.append(data[i])\n            i += 1\n            share.append(data[i])\n            i += 1\n    primary_sale_happened = bool(data[i])\n    i += 1\n    is_mutable = bool(data[i])\n    metadata = {\n        \"update_authority\": source_account,\n        \"mint\": mint_account,\n        \"data\": {\n            \"name\": bytes(name).decode(\"utf-8\").strip(\"\\x00\"),\n            \"symbol\": bytes(symbol).decode(\"utf-8\").strip(\"\\x00\"),\n            \"uri\": bytes(uri).decode(\"utf-8\").strip(\"\\x00\"),\n            \"seller_fee_basis_points\": fee,\n            \"creators\": creators,\n            \"verified\": verified,\n            \"share\": share,\n        },\n        \"primary_sale_happened\": primary_sale_happened,\n        \"is_mutable\": is_mutable,\n    }\n    return metadata\n\ndef get_metadata(client, mint_key):\n    metadata_account = get_metadata_account(mint_key)\n    data = base64.b64decode(client.get_account_info(metadata_account)['result']['value']['data'][0])\n    metadata = unpack_metadata_account(data)\n    return metadata\n\ndef update_metadata_instruction_data(name, symbol, uri, fee, creators, verified, share):\n    _data = bytes([1]) + _get_data_buffer(name, symbol, uri, fee, creators,  verified, share) + bytes([0, 0])\n    instruction_layout = cStruct(\n        \"instruction_type\" / Int8ul,\n        \"args\" / Bytes(len(_data)),\n    )\n    return instruction_layout.build(\n        dict(\n            instruction_type=InstructionType.UPDATE_METADATA,\n            args=_data,\n        )\n    )\n\ndef update_metadata_instruction(data, update_authority, mint_key):\n    metadata_account = get_metadata_account(mint_key)\n    keys = [\n        AccountMeta(pubkey=metadata_account, is_signer=False, is_writable=True),\n        AccountMeta(pubkey=update_authority, is_signer=True, is_writable=False),\n    ]\n    return TransactionInstruction(keys=keys, program_id=METADATA_PROGRAM_ID, data=data)\n\ndef create_master_edition_instruction(\n    mint: PublicKey,\n    update_authority: PublicKey,\n    mint_authority: PublicKey,\n    payer: PublicKey,\n    supply: Union[int, None],\n):\n    edition_account = get_edition(mint)\n    metadata_account = get_metadata_account(mint)\n    if supply is None:\n        data = struct.pack(\"<BB\", 10, 0)\n    else:\n        data = struct.pack(\"<BBQ\", 10, 1, supply)\n    keys = [\n        AccountMeta(pubkey=edition_account, is_signer=False, is_writable=True),\n        AccountMeta(pubkey=mint, is_signer=False, is_writable=True),\n        AccountMeta(pubkey=update_authority, is_signer=True, is_writable=False),\n        AccountMeta(pubkey=mint_authority, is_signer=True, is_writable=False),\n        AccountMeta(pubkey=payer, is_signer=True, is_writable=False),\n        AccountMeta(pubkey=metadata_account, is_signer=False, is_writable=False),\n        AccountMeta(pubkey=PublicKey(TOKEN_PROGRAM_ID), is_signer=False, is_writable=False),\n        AccountMeta(pubkey=PublicKey(SYSTEM_PROGRAM_ID), is_signer=False, is_writable=False),\n        AccountMeta(pubkey=PublicKey(SYSVAR_RENT_PUBKEY), is_signer=False, is_writable=False),\n    ]\n    return TransactionInstruction(\n        keys=keys,\n        program_id=METADATA_PROGRAM_ID,\n        data=data,\n    )\n"
  },
  {
    "path": "metaplex/transactions.py",
    "content": "import json\nimport base64\nfrom solana.publickey import PublicKey \nfrom solana.transaction import Transaction\nfrom solana.keypair import Keypair \nfrom solana.rpc.api import Client\nfrom solana.system_program import transfer, TransferParams, create_account, CreateAccountParams \nfrom spl.token._layouts import MINT_LAYOUT, ACCOUNT_LAYOUT\nfrom spl.token.instructions import (\n    get_associated_token_address, mint_to, MintToParams,\n    transfer as spl_transfer, TransferParams as SPLTransferParams,\n    burn as spl_burn, BurnParams,\n    initialize_mint, InitializeMintParams,\n)\nfrom metaplex.metadata import (\n    create_associated_token_account_instruction,\n    create_master_edition_instruction,\n    create_metadata_instruction_data, \n    create_metadata_instruction,\n    get_metadata,\n    update_metadata_instruction_data,\n    update_metadata_instruction,\n    ASSOCIATED_TOKEN_ACCOUNT_PROGRAM_ID,\n    TOKEN_PROGRAM_ID,\n)\n\n\ndef deploy(api_endpoint, source_account, name, symbol, fees):\n    # Initalize Client\n    client = Client(api_endpoint)\n    # List non-derived accounts\n    mint_account = Keypair()\n    token_account = TOKEN_PROGRAM_ID \n    # List signers\n    signers = [source_account, mint_account]\n    # Start transaction\n    tx = Transaction()\n    # Get the minimum rent balance for a mint account\n    min_rent_reseponse = client.get_minimum_balance_for_rent_exemption(MINT_LAYOUT.sizeof()) # type: ignore\n    lamports = min_rent_reseponse[\"result\"]\n    # Generate Mint \n    create_mint_account_ix = create_account(\n        CreateAccountParams(\n            from_pubkey=source_account.public_key,\n            new_account_pubkey=mint_account.public_key,\n            lamports=lamports,\n            space=MINT_LAYOUT.sizeof(),\n            program_id=token_account,\n        )\n    )\n    tx = tx.add(create_mint_account_ix)\n    initialize_mint_ix = initialize_mint(\n        InitializeMintParams(\n            decimals=0,\n            program_id=token_account,\n            mint=mint_account.public_key,\n            mint_authority=source_account.public_key,\n            freeze_authority=source_account.public_key,\n        )\n    )\n    tx = tx.add(initialize_mint_ix)\n    # Create Token Metadata\n    create_metadata_ix = create_metadata_instruction(\n        data=create_metadata_instruction_data(name, symbol, fees, [str(source_account.public_key)]),\n        update_authority=source_account.public_key,\n        mint_key=mint_account.public_key,\n        mint_authority_key=source_account.public_key,\n        payer=source_account.public_key,\n    )\n    tx = tx.add(create_metadata_ix)\n    return tx, signers, str(mint_account.public_key)\n    \n\ndef wallet():\n    \"\"\" Generate a wallet and return the address and private key. \"\"\"\n    account = Keypair()\n    pub_key = account.public_key \n    private_key = list(account.seed)\n    return json.dumps(\n        {\n            'address': str(pub_key),\n            'private_key': private_key\n        }\n    )\n\n\ndef topup(api_endpoint, sender_account, to, amount=None):\n    \"\"\"\n    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.\n    \"\"\"\n    # Connect to the api_endpoint\n    client = Client(api_endpoint)\n    # List accounts \n    dest_account = PublicKey(to)\n    # List signers\n    signers = [sender_account]\n    # Start transaction\n    tx = Transaction()\n    # Determine the amount to send \n    if amount is None:\n        min_rent_reseponse = client.get_minimum_balance_for_rent_exemption(ACCOUNT_LAYOUT.sizeof())\n        lamports = min_rent_reseponse[\"result\"]\n    else:\n        lamports = int(amount)\n    # Generate transaction\n    transfer_ix = transfer(TransferParams(from_pubkey=sender_account.public_key, to_pubkey=dest_account, lamports=lamports))\n    tx = tx.add(transfer_ix)\n    return tx, signers\n\ndef update_token_metadata(api_endpoint, source_account, mint_token_id, link, data, fee, creators_addresses, creators_verified, creators_share):\n    \"\"\"\n    Updates the json metadata for a given mint token id.\n    \"\"\"\n    mint_account = PublicKey(mint_token_id)\n    signers = [source_account]\n\n    tx = Transaction()\n    update_metadata_data = update_metadata_instruction_data(\n        data['name'],\n        data['symbol'],\n        link,\n        fee,\n        creators_addresses,        \n        creators_verified,\n        creators_share,\n    )\n    update_metadata_ix = update_metadata_instruction(\n        update_metadata_data,\n        source_account.public_key,\n        mint_account,\n    )\n    tx = tx.add(update_metadata_ix) \n    return tx, signers\n\n\ndef mint(api_endpoint, source_account, contract_key, dest_key, link, supply=1):\n    \"\"\"\n    Mint a token on the specified network and contract, into the wallet specified by address.\n    Required parameters: batch, sequence, limit\n    These are all 32-bit unsigned ints and are assembled into a 96-bit integer ID on Ethereum and compatible blockchains.\n    Where this is not possible we'll look for an alternate mapping.\n    Additional character fields: name, description, link, created\n    These are text fields intended to be written directly to the blockchain. created is an ISO standard timestamp string (UTC)\n    content is an optional JSON string for customer-specific data.\n    Return a status flag of success or fail and the native transaction data.\n    \"\"\"\n    # Initialize Client\n    client = Client(api_endpoint)\n    # List non-derived accounts\n    mint_account = PublicKey(contract_key)\n    user_account = PublicKey(dest_key)\n    token_account = TOKEN_PROGRAM_ID\n    # List signers\n    signers = [source_account]\n    # Start transaction\n    tx = Transaction()\n    # Create Associated Token Account\n    associated_token_account = get_associated_token_address(user_account, mint_account)\n    associated_token_account_info = client.get_account_info(associated_token_account)\n    # Check if PDA is initialized. If not, create the account\n    account_info = associated_token_account_info['result']['value']\n    if account_info is not None: \n        account_state = ACCOUNT_LAYOUT.parse(base64.b64decode(account_info['data'][0])).state\n    else:\n        account_state = 0\n    if account_state == 0:\n        associated_token_account_ix = create_associated_token_account_instruction(\n            associated_token_account=associated_token_account,\n            payer=source_account.public_key, # signer\n            wallet_address=user_account,\n            token_mint_address=mint_account,\n        )\n        tx = tx.add(associated_token_account_ix)  \n    # Mint NFT to the newly create associated token account\n    mint_to_ix = mint_to(\n        MintToParams(\n            program_id=TOKEN_PROGRAM_ID,\n            mint=mint_account,\n            dest=associated_token_account,\n            mint_authority=source_account.public_key,\n            amount=1,\n            signers=[source_account.public_key],\n        )\n    )\n    tx = tx.add(mint_to_ix) \n    metadata = get_metadata(client, mint_account)\n    update_metadata_data = update_metadata_instruction_data(\n        metadata['data']['name'],\n        metadata['data']['symbol'],\n        link,\n        metadata['data']['seller_fee_basis_points'],\n        metadata['data']['creators'],\n        metadata['data']['verified'],\n        metadata['data']['share'],\n    )\n    update_metadata_ix = update_metadata_instruction(\n        update_metadata_data,\n        source_account.public_key,\n        mint_account,\n    )\n    tx = tx.add(update_metadata_ix) \n    create_master_edition_ix = create_master_edition_instruction(\n        mint=mint_account,\n        update_authority=source_account.public_key,\n        mint_authority=source_account.public_key,\n        payer=source_account.public_key,\n        supply=supply,\n    )\n    tx = tx.add(create_master_edition_ix) \n    return tx, signers\n\n\ndef send(api_endpoint, source_account, contract_key, sender_key, dest_key, private_key):\n    \"\"\"\n    Transfer a token on a given network and contract from the sender to the recipient.\n    May require a private key, if so this will be provided encrypted using Fernet: https://cryptography.io/en/latest/fernet/\n    Return a status flag of success or fail and the native transaction data. \n    \"\"\"\n    # Initialize Client\n    client = Client(api_endpoint)\n    # List non-derived accounts\n    owner_account = Keypair(private_key) # Owner of contract \n    sender_account = PublicKey(sender_key) # Public key of `owner_account`\n    token_account = TOKEN_PROGRAM_ID\n    mint_account = PublicKey(contract_key)\n    dest_account = PublicKey(dest_key)\n    # This is a very rare care, but in the off chance that the source wallet is the recipient of a transfer we don't need a list of 2 keys\n    signers = [source_account, owner_account]\n    # Start transaction\n    tx = Transaction()\n    # Find PDA for sender\n    token_pda_address = get_associated_token_address(sender_account, mint_account)\n    if client.get_account_info(token_pda_address)['result']['value'] is None: \n        raise Exception\n    # Check if PDA is initialized for receiver. If not, create the account\n    associated_token_account = get_associated_token_address(dest_account, mint_account)\n    associated_token_account_info = client.get_account_info(associated_token_account)\n    account_info = associated_token_account_info['result']['value']\n    if account_info is not None: \n        account_state = ACCOUNT_LAYOUT.parse(base64.b64decode(account_info['data'][0])).state\n    else:\n        account_state = 0\n    if account_state == 0:\n        associated_token_account_ix = create_associated_token_account_instruction(\n            associated_token_account=associated_token_account,\n            payer=source_account.public_key, # signer\n            wallet_address=dest_account,\n            token_mint_address=mint_account,\n        )\n        tx = tx.add(associated_token_account_ix)        \n    # Transfer the Token from the sender account to the associated token account\n    spl_transfer_ix = spl_transfer(\n        SPLTransferParams(\n            program_id=token_account,\n            source=token_pda_address,\n            dest=associated_token_account,\n            owner=sender_account,\n            signers=[],\n            amount=1,\n        )\n    )\n    tx = tx.add(spl_transfer_ix)\n    return tx, signers\n\n\ndef burn(api_endpoint, contract_key, owner_key, private_key):\n    \"\"\"\n    Burn a token, permanently removing it from the blockchain.\n    May require a private key, if so this will be provided encrypted using Fernet: https://cryptography.io/en/latest/fernet/\n    Return a status flag of success or fail and the native transaction data.\n    \"\"\"\n    # Initialize Client\n    client = Client(api_endpoint)\n    # List accounts\n    owner_account = PublicKey(owner_key)\n    token_account = TOKEN_PROGRAM_ID\n    mint_account = PublicKey(contract_key)\n    # List signers\n    signers = [Keypair(private_key)]\n    # Start transaction\n    tx = Transaction()\n    # Find PDA for sender\n    token_pda_address = get_associated_token_address(owner_account, mint_account)\n    if client.get_account_info(token_pda_address)['result']['value'] is None: \n        raise Exception\n    # Burn token\n    burn_ix = spl_burn(\n        BurnParams(\n            program_id=token_account,\n            account=token_pda_address,\n            mint=mint_account,\n            owner=owner_account,\n            amount=1,\n            signers=[],\n        )\n    )\n    tx = tx.add(burn_ix)\n    return tx, signers\n"
  },
  {
    "path": "requirements.txt",
    "content": "anyio==3.3.4\nattrs==21.2.0\nbase58==2.1.1\ncachetools==4.2.4\ncertifi==2021.10.8\ncffi==1.15.0\nchardet==4.0.0\ncharset-normalizer==2.0.7\ncheroot==8.5.2\nCherryPy==18.6.1\nconstruct==2.10.67\ncryptography==35.0.0\ned25519==1.5\nh11==0.12.0\nhttpcore==0.13.7\nhttpx==0.20.0\nidna==3.3\niniconfig==1.1.1\njaraco.classes==3.2.1\njaraco.collections==3.4.0\njaraco.functools==3.3.0\njaraco.text==3.5.1\nmore-itertools==8.10.0\npackaging==21.2\npluggy==1.0.0\nportend==3.0.0\npy==1.10.0\npycparser==2.20\nPyNaCl==1.4.0\npyparsing==2.4.7\npytest==6.2.5\npytz==2021.3\nrequests==2.26.0\nrfc3986==1.5.0\nsix==1.16.0\nsniffio==1.2.0\nsolana==0.18.1\ntempora==4.1.2\ntoml==0.10.2\ntyping-extensions==3.10.0.2\nurllib3==1.26.7\nwebsockets==10.0\nzc.lockfile==2.0\n"
  },
  {
    "path": "test/test_api.py",
    "content": "import argparse\nimport string\nimport random\nimport json\nimport time\nimport base58\nfrom solana.keypair import Keypair\nfrom solana.rpc.api import Client\nfrom metaplex.metadata import get_metadata\nfrom cryptography.fernet import Fernet\nfrom api.metaplex_api import MetaplexAPI\n\ndef await_full_confirmation(client, txn, max_timeout=60):\n    if txn is None:\n        return\n    elapsed = 0\n    while elapsed < max_timeout:\n        sleep_time = 1\n        time.sleep(sleep_time)\n        elapsed += sleep_time\n        resp = client.get_confirmed_transaction(txn)\n        while 'result' not in resp:\n            resp = client.get_confirmed_transaction(txn)\n        if resp[\"result\"]:\n            print(f\"Took {elapsed} seconds to confirm transaction {txn}\")\n            break\n\ndef test(api_endpoint=\"https://api.devnet.solana.com/\"):\n    keypair = Keypair()\n    cfg = {\n        \"PRIVATE_KEY\": base58.b58encode(keypair.seed).decode(\"ascii\"),\n        \"PUBLIC_KEY\": str(keypair.public_key),\n        \"DECRYPTION_KEY\": Fernet.generate_key().decode(\"ascii\"),\n    }\n    api = MetaplexAPI(cfg)\n    client = Client(api_endpoint)\n    resp = {}\n    while 'result' not in resp:\n        resp = client.request_airdrop(keypair.public_key, int(1e9))\n    print(\"Request Airdrop:\", resp)\n    txn = resp['result']\n    await_full_confirmation(client, txn)\n    letters = string.ascii_uppercase\n    name = ''.join([random.choice(letters) for i in range(32)])\n    symbol = ''.join([random.choice(letters) for i in range(10)])\n    print(\"Name:\", name)\n    print(\"Symbol:\", symbol)\n    # added seller_basis_fee_points\n    deploy_response = json.loads(api.deploy(api_endpoint, name, symbol, 0))\n    print(\"Deploy:\", deploy_response)\n    assert deploy_response[\"status\"] == 200\n    contract = deploy_response.get(\"contract\")\n    print(get_metadata(client, contract))\n    wallet = json.loads(api.wallet())\n    address1 = wallet.get('address')\n    encrypted_pk1 = api.cipher.encrypt(bytes(wallet.get('private_key')))\n    topup_response = json.loads(api.topup(api_endpoint, address1))\n    print(f\"Topup {address1}:\", topup_response)\n    assert topup_response[\"status\"] == 200\n    mint_to_response = json.loads(api.mint(api_endpoint, contract, address1, \"https://arweave.net/1eH7bZS-6HZH4YOc8T_tGp2Rq25dlhclXJkoa6U55mM/\"))\n    print(\"Mint:\", mint_to_response)\n    # await_confirmation(client, mint_to_response['tx'])\n    assert mint_to_response[\"status\"] == 200\n    print(get_metadata(client, contract))\n    wallet2 = json.loads(api.wallet())\n    address2 = wallet2.get('address')\n    encrypted_pk2 = api.cipher.encrypt(bytes(wallet2.get('private_key')))\n    print(client.request_airdrop(api.public_key, int(1e10)))\n    topup_response2 = json.loads(api.topup(api_endpoint, address2))\n    print(f\"Topup {address2}:\", topup_response2)\n    # await_confirmation(client, topup_response2['tx'])\n    assert topup_response2[\"status\"] == 200\n    send_response = json.loads(api.send(api_endpoint, contract, address1, address2, encrypted_pk1))\n    assert send_response[\"status\"] == 200\n    # await_confirmation(client, send_response['tx'])\n    burn_response = json.loads(api.burn(api_endpoint, contract, address2, encrypted_pk2))\n    print(\"Burn:\", burn_response)\n    # await_confirmation(client, burn_response['tx'])\n    assert burn_response[\"status\"] == 200\n    print(\"Success!\")\n\nif __name__ == \"__main__\":\n    ap = argparse.ArgumentParser()\n    ap.add_argument(\"--network\", default=None)\n    args = ap.parse_args()\n    if args.network == None or args.network == 'devnet':\n        test()\n    elif args.network == 'testnet':\n        test(api_endpoint=\"https://api.testnet.solana.com/\")\n    elif args.network == 'mainnet':\n        test(api_endpoint=\"https://api.mainnet-beta.solana.com/\")\n    else:\n        print(\"Invalid network argument supplied\")\n"
  },
  {
    "path": "utils/execution_engine.py",
    "content": "import time\nfrom solana.keypair import Keypair \nfrom solana.rpc.api import Client\nfrom solana.rpc.types import TxOpts \n\ndef execute(api_endpoint, tx, signers, max_retries=3, skip_confirmation=True, max_timeout=60, target=20, finalized=True):\n    client = Client(api_endpoint)\n    signers = list(map(Keypair, set(map(lambda s: s.seed, signers))))\n    for attempt in range(max_retries):\n        try:\n            result = client.send_transaction(tx, *signers, opts=TxOpts(skip_preflight=True))\n            print(result)\n            signatures = [x.signature for x in tx.signatures]\n            if not skip_confirmation:\n                await_confirmation(client, signatures, max_timeout, target, finalized)\n            return result\n        except Exception as e:\n            print(f\"Failed attempt {attempt}: {e}\")\n            continue\n    raise e\n\ndef await_confirmation(client, signatures, max_timeout=60, target=20, finalized=True):\n    elapsed = 0\n    while elapsed < max_timeout:\n        sleep_time = 1\n        time.sleep(sleep_time)\n        elapsed += sleep_time\n        resp = client.get_signature_statuses(signatures)\n        if resp[\"result\"][\"value\"][0] is not None:\n            confirmations = resp[\"result\"][\"value\"][0][\"confirmations\"]\n            is_finalized = resp[\"result\"][\"value\"][0][\"confirmationStatus\"] == \"finalized\"\n        else:\n            continue\n        if not finalized:\n            if confirmations >= target or is_finalized:\n                print(f\"Took {elapsed} seconds to confirm transaction\")\n                return\n        elif is_finalized:\n            print(f\"Took {elapsed} seconds to confirm transaction\")\n            return"
  }
]