metaplex-foundation / python-api

136 stars 86 forks source link

Outdated create metadata instruction #47

Open davidkakov111 opened 11 months ago

davidkakov111 commented 11 months ago

Problem: The Metaplex Python API experiences a deployment issue where an outdated instruction, specifically the create_metadata_instruction within the transaction.py file, leads to a contract key with value = None during Solana smart contract deployment.

Details:

File: transaction.py Specific Issue: create_metadata_instruction sends outdated instructions to the Token Metadata Program. Steps to Reproduce:

Deploy a Solana smart contract with the Metaplex Python API. Observe that the outdated instruction results in a contract key without a value. Expected Behavior: The Metaplex Python API should send current instructions to the Token Metadata Program to ensure successful contract deployment.

xll970211 commented 5 months ago

do you have resolve it? @davidkakov111

davidkakov111 commented 5 months ago

No, in Python I don't, because I didn't find enough information about the correct instructions. However, I have solved the Solana NFT minting process in TypeScript. If you want, you can check it out here. It is currently working and much simpler then then it was in python (A few years ago :) ) : https://github.com/davidkakov111/SolanaMysteryBoxShop/tree/TS_NFT_API"

cuongnguyengit commented 5 months ago

Hi David, I have referred to your typescript code. In your function, export async function mintNft( metadataUri: string, name: string, symbol: string, to: string, blockchain_endpoint: string, private_key: string ): Promise<string | unknown>

Inputs contain private_key, It's difficult for me because I need to create an exchange of 1 NFT where sellers and buyers log in via Wallet and sellers need to create NFTs on my exchange and buyers will buy that NFT. So, how can users agree to give private_key to my website to make mint NFT transactions and NFT owner change transactions? I'm new to NFTs, please give me some guidance. Thank you

No, in Python I don't, because I didn't find enough information about the correct instructions. However, I have solved the Solana NFT minting process in TypeScript. If you want, you can check it out here. It is currently working and much simpler then then it was in python (A few years ago :) ) : https://github.com/davidkakov111/SolanaMysteryBoxShop/tree/TS_NFT_API"

xll970211 commented 5 months ago

No, in Python I don't, because I didn't find enough information about the correct instructions. However, I have solved the Solana NFT minting process in TypeScript. If you want, you can check it out here. It is currently working and much simpler then then it was in python (A few years ago :) ) : https://github.com/davidkakov111/SolanaMysteryBoxShop/tree/TS_NFT_API"

OK,Thank you very much

JeffryCA commented 5 months ago

I actually solved this last year for my company - essentially I did this: Use Anchorpy to generate a client:

anchorpy client-gen ./idl.json ./src --program-id metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s

This generated the client for me but did not fill out the correct function identifiers so I had to fix those manually:

# example instruction

def create_metadata_account_v2(
    args: CreateMetadataAccountV2Args, accounts: CreateMetadataAccountV2Accounts
) -> TransactionInstruction:
    keys: list[AccountMeta] = [
        AccountMeta(
            pubkey=accounts["metadata_account"], is_signer=False, is_writable=True
        ),
        AccountMeta(pubkey=accounts["mint"], is_signer=False, is_writable=False),
        AccountMeta(
            pubkey=accounts["mint_authority"], is_signer=True, is_writable=False
        ),
        AccountMeta(pubkey=accounts["payer"], is_signer=True, is_writable=True),
        AccountMeta(
            pubkey=accounts["update_authority"], is_signer=True, is_writable=False
        ),
        AccountMeta(
            pubkey=accounts["system_program"], is_signer=False, is_writable=False
        ),
    ]
    identifier = b"\x18I)\xed,\x8e\xc2\xfe"
    encoded_args = layout.build(
        {
            "data": args["data"].to_encodable(),
            "is_mutable": args["is_mutable"],
        }
    )
    data = identifier + encoded_args
    return TransactionInstruction(keys, PROGRAM_ID, data)

# Corrected Instruction
import borsh_construct as borsh

def get_identifier(discriminant: int) -> bytes:
    layout = borsh.CStruct("number" / borsh.U8)
    return layout.build({"number": discriminant})

def create_metadata_account_v2(
    args: CreateMetadataAccountV2Args, accounts: CreateMetadataAccountV2Accounts
) -> TransactionInstruction:
    keys: list[AccountMeta] = [
        AccountMeta(
            pubkey=accounts["metadata_account"], is_signer=False, is_writable=True
        ),
        AccountMeta(pubkey=accounts["mint"], is_signer=False, is_writable=False),
        AccountMeta(
            pubkey=accounts["mint_authority"], is_signer=True, is_writable=False
        ),
        AccountMeta(pubkey=accounts["payer"], is_signer=True, is_writable=True),
        AccountMeta(
            pubkey=accounts["update_authority"], is_signer=True, is_writable=False
        ),
        AccountMeta(
            pubkey=accounts["system_program"], is_signer=False, is_writable=False
        ),
    ]
    identifier = get_identifier(16) # This number you get from the metaplex docs!
    encoded_args = layout.build(
        {
            "data": args["data"].to_encodable(),
            "is_mutable": args["is_mutable"],
        }
    )
    data = identifier + encoded_args
    return TransactionInstruction(keys, PROGRAM_ID, data)

But to be honest I had to write many helpers and to make the whole flow work nicely. Would not recommend it. Will ask If I can open source the code.

At then end I had a mint function like this one:

import base64
from typing import List, Optional, Union

from solana.keypair import Keypair
from solana.publickey import PublicKey
from solana.rpc.async_api import AsyncClient
from solana.system_program import CreateAccountParams, create_account
from solana.transaction import Transaction
from spl.token._layouts import ACCOUNT_LAYOUT, MINT_LAYOUT
from spl.token.constants import TOKEN_PROGRAM_ID
from spl.token.instructions import (
    InitializeMintParams,
    MintToParams,
    create_associated_token_account,
    get_associated_token_address,
    initialize_mint,
    mint_to,
)

from app.internal.pymetaplex.TokenMetadata.instructions import (
    CreateMasterEditionV3Accounts,
    CreateMasterEditionV3Args,
    CreateMetadataAccountV3Args,
    UpdateMetadataAccountV2Accounts,
    UpdateMetadataAccountV2Args,
    VerifySizedCollectionItemAccounts,
    create_master_edition_v3,
    create_metadata_account_v3,
    update_metadata_account_v2,
    verify_sized_collection_item,
)

from app.internal.pymetaplex.TokenMetadata.types import (
    Collection,
    CollectionDetailsKind,
    Creator,
    DataV2,
    Uses,
)
# from app.internal.pymetaplex.TokenMetadata.utils.edition import get_edition_account
from app.internal.pymetaplex.TokenMetadata.utils.execute import send_and_confirm
# from app.internal.pymetaplex.TokenMetadata.utils.metadata import get_metadata_account

#### These were in other files
from app.internal.pymetaplex.TokenMetadata.program_id import PROGRAM_ID
def get_metadata_account(mint_key: str) -> PublicKey:
    mint_key = mint_key if isinstance(mint_key, PublicKey) else PublicKey(mint_key)
    return PublicKey.find_program_address(
        [b"metadata", bytes(PROGRAM_ID), bytes(mint_key)],
        PROGRAM_ID,
    )[0]

def get_edition_account(mint_key: str) -> PublicKey:
    mint_key = mint_key if isinstance(mint_key, PublicKey) else PublicKey(mint_key)
    return PublicKey.find_program_address(
        [
            b"metadata",
            bytes(PROGRAM_ID),
            bytes(PublicKey(mint_key)),
            b"edition",
        ],
        PROGRAM_ID,
    )[0]
#### 

SYSTEM_PROGRAM_ID = PublicKey("11111111111111111111111111111111")
SYSVAR_RENT_PUBKEY = PublicKey("SysvarRent111111111111111111111111111111111")

async def mint(
    client: AsyncClient,
    source_account: Keypair,
    destination_account: PublicKey,
    name: str,
    symbol: str,
    uri: str,
    seller_fee_basis_points: int,
    creators: Optional[List[Creator]] = None,
    uses: Optional[Uses] = None,
    collection_details: Optional[CollectionDetailsKind] = None,
    update_primary_sale_happened: bool = True,
    collection_mint: Optional[PublicKey] = None,
    verify_collection: Optional[bool] = True,
    max_supply: Optional[int] = 0,
) -> Union[PublicKey, dict]:
    """This function is a helper function to mint nfts.

    Args:
        client (AsyncClient): AsyncClient from solana.rpc.async_api
        source_account (Keypair): Payer, Update authority.
        destination_account (PublicKey): Final NFT owner address.
        name (str): name of nft on chain
        symbol (str): symbol of nft on chain
        uri (str): uri of nft on chain
        seller_fee_basis_points (int): seller_fee_basis_points of nft on chain
        creators (List[Creator]): List of on chain creators
        uses (Uses, optional): Nft uses. For more info look at the metaplex docs. Defaults to None.
        collection_details (CollectionDetailsKind, optional): Like CollectionDetailsKind(value=V1Value(size=0)). If provided it means this nft is the parent of a collection. Defaults to None.
        collection_mint (PublicKey, optional): If provided the nft will point to collection_mint nft as his parent. Defaults to None.
        update_primary_sale_happened (bool, optional): If false, 100% of the proceeds of a sale on the 2ndary market place will go to the creators. Defaults to True.
        verify_collection (bool, optional): Only makes sense in combinatino with collection_mint. Defaults to True.
        max_supply (int, optional): Number of editions that can be printed from Master Edition. Defaults to 0.

    returns:
        Tuple[str, str]
        1. Pubic key of the mint account.
        2. transaction
    """

    # Preprocess Inputs
    if collection_mint:
        # collection can not be initialized as verified
        collection = Collection(verified=False, key=collection_mint)
    else:
        collection = None

    # init transaction
    tx = Transaction()
    # List non-derived accounts
    mint_account = Keypair()
    # List signers
    signers = [source_account, mint_account]

    ### 1. Create Mint Account Instruction
    # Get the minimum rent balance for a mint account
    min_rent_reseponse = await client.get_minimum_balance_for_rent_exemption(
        MINT_LAYOUT.sizeof()
    )
    lamports = min_rent_reseponse["result"]
    # every token on solana needs an associated account
    create_mint_account_ix = create_account(
        CreateAccountParams(
            from_pubkey=source_account.public_key,
            new_account_pubkey=mint_account.public_key,
            lamports=lamports,
            space=MINT_LAYOUT.sizeof(),
            program_id=TOKEN_PROGRAM_ID,
        )
    )
    tx = tx.add(create_mint_account_ix)

    ### 2. here we create one spl token
    initialize_mint_ix = initialize_mint(
        InitializeMintParams(
            decimals=0,
            program_id=TOKEN_PROGRAM_ID,
            mint=mint_account.public_key,
            mint_authority=source_account.public_key,
            freeze_authority=source_account.public_key,
        )
    )
    tx = tx.add(initialize_mint_ix)

    ### 3. Get or Create Associated Token Account Instruction
    # List signers
    # signers = [source_account]
    # Create Associated Token Account
    associated_token_account = get_associated_token_address(
        destination_account, mint_account.public_key
    )
    # Check if PDA is initialized. If not, create the account
    associated_token_account_info = await client.get_account_info(
        associated_token_account
    )
    account_info = associated_token_account_info["result"]["value"]
    if account_info is not None:
        account_state = ACCOUNT_LAYOUT.parse(
            base64.b64decode(account_info["data"][0])
        ).state
    else:
        account_state = 0
    if account_state == 0:
        associated_token_account_ix = create_associated_token_account(
            owner=destination_account,
            payer=source_account.public_key,  # signer
            mint=mint_account.public_key,
        )
        tx = tx.add(associated_token_account_ix)

    ### 4. Create Metadata Account Instruction
    data = DataV2(
        name=name,
        symbol=symbol,
        uri=uri,
        seller_fee_basis_points=seller_fee_basis_points,
        creators=creators,
        collection=collection,
        uses=uses,
    )
    metadata_args = CreateMetadataAccountV3Args(
        data=data, is_mutable=True, collection_details=collection_details
    )

    metadata_account = PublicKey(get_metadata_account(mint_account.public_key))
    metadata_accounts = CreateMasterEditionV3Accounts(
        metadata_account=metadata_account,
        mint=mint_account.public_key,
        mint_authority=source_account.public_key,
        payer=source_account.public_key,
        update_authority=source_account.public_key,
        system_program=SYSTEM_PROGRAM_ID,
        rent=SYSVAR_RENT_PUBKEY,
    )
    create_metadata_ix = create_metadata_account_v3(metadata_args, metadata_accounts)
    tx = tx.add(create_metadata_ix)

    ### 5. Mint one token
    # Mint NFT to the associated token account
    mint_to_ix = mint_to(
        MintToParams(
            program_id=TOKEN_PROGRAM_ID,
            mint=mint_account.public_key,
            dest=associated_token_account,
            mint_authority=source_account.public_key,
            amount=1,
            signers=[source_account.public_key],
        )
    )
    tx = tx.add(mint_to_ix)

    ### 6. Set primary sale happened to True
    if update_primary_sale_happened:
        update_metadata_args = UpdateMetadataAccountV2Args(
            data=None,
            update_authority=None,
            primary_sale_happened=True,
            is_mutable=None,
        )
        update_metadata_accounts = UpdateMetadataAccountV2Accounts(
            metadata_account=metadata_account,
            update_authority=source_account.public_key,
        )
        update_metadata_tx = update_metadata_account_v2(
            args=update_metadata_args, accounts=update_metadata_accounts
        )
        tx = tx.add(update_metadata_tx)

    ### 7. Create Master Edition Instruction
    master_edition_args = CreateMasterEditionV3Args(max_supply=max_supply)
    edition_account = get_edition_account(mint_account.public_key)
    master_edition_accounts = CreateMasterEditionV3Accounts(
        edition=edition_account,
        mint=mint_account.public_key,
        update_authority=source_account.public_key,
        mint_authority=source_account.public_key,
        payer=source_account.public_key,
        metadata=metadata_account,
        token_program=TOKEN_PROGRAM_ID,
        system_program=SYSTEM_PROGRAM_ID,
        rent=SYSVAR_RENT_PUBKEY,
    )

    create_master_edition_ix = create_master_edition_v3(
        master_edition_args, master_edition_accounts
    )
    tx = tx.add(create_master_edition_ix)

    ### 8. Verify Collection
    if collection is not None and verify_collection:
        collection_metadata = get_metadata_account(collection_mint)
        collection_master_edition_account = get_edition_account(collection_mint)
        verify_collection_accounts = VerifySizedCollectionItemAccounts(
            metadata=metadata_account,
            collection_authority=source_account.public_key,
            payer=source_account.public_key,
            collection_mint=collection_mint,
            collection=collection_metadata,
            collection_master_edition_account=collection_master_edition_account,
            collection_authority_record=source_account.public_key,
        )
        verify_collection_ix = verify_sized_collection_item(
            accounts=verify_collection_accounts
        )
        tx = tx.add(verify_collection_ix)

    ### 9. Execute Transaction
    result = await send_and_confirm(client, tx, signers)
    return mint_account.public_key, result["result"]
davidkakov111 commented 5 months ago

Hi @cuongnguyengit,

In your task, it seems you need to follow the Solana documentation to use WalletConnect for user management and so on. (This is a very good and very fast solution for NFT minting with Solana Pay and WalletConnect, which may be what you need: https://github.com/solana-developers/solana-pay-nft-minter)

I didn't use WalletConnect in my webshop because my goal was to simplify the payment process with a unique method and avoid any data transfer between the webshop and the user to prevent common phishing risks. For this function, I pass my private key from my main Django webshop; this Next.js project is just a "branch" of the web3 webshop.

(Your users should never give their private key to you; it is illegal.)

cuongnguyengit commented 5 months ago

Hi @davidkakov111,

I have gained more knowledge about Solana and NFTs. I have the following task, I look forward to hearing your opinion. My task is to create a Marketplace using Solana NFT to exchange (buy and sell) game accounts, meaning here account and password information is sensitive, only the owner can see it. According to what I learned, information about NFT metadata can be easily viewed through https://explorer.solana.com/. The easiest part I think is creating NFTs, but to display the created NFTs of all the sellers is relatively vague to me. We look forward to receiving your comments.

davidkakov111 commented 5 months ago

I also don't have deep knowledge in this field, but I'll try to provide an answer: To display the NFTs of all the sellers, you first need to acquire information about these NFTs. For example, you need to obtain the NFT addresses of these NFTs and then retrieve the metadata from the blockchain or another source using these NFT addresses through a blockchain endpoint like "https://rpc.ankr.com/solana_devnet" or "https://api.devnet.solana.com/" (these are for the devnet). However, in recent times, the free endpoints sometimes experience "outages" due to high demand in this bull market, so you may need to use a non-free endpoint like QuickNode. The exact query is documented on the endpoint website. Then, you will receive the metadata URI, and with this, you can easily retrieve the photo of the NFT, the name of the NFT, and so on, to display them on the marketplace. If the game account is to be represented as NFTs, you may not need to use passwords, etc., because you can't include them in the details of the NFT :), but you can identify users by their wallet using WalletConnect, for example (I don't have experience in this), and then check if the NFT account is owned by this user. If yes, then they gain the rights to this account and you can log in this user. (Note: I haven't done a project like this before, so there may be irrelevant parts in my answer.)