0

Original Question

I would like to write a python script that can create tokens. Since the Metaplex Python API seems outdated and only targeted to NFTs, I decided to do it myself. With the help of the (unofficial?) Solana Python SDK, I was able to:

  1. create a mint account
  2. create an associated token account
  3. mint some tokens to the ATA

However, now I would like to add some metadata information to my token. The "best" (and probably only) source I found were the Metaplex Docs, where they have an overview over all instructions and arguments. I tried to take a look at the Metaplex Python API, but it is a mess honestly. The information found on the Rust Docs for spl-token-metadata mostly mirrors the Metaplex information. After all I came up with this (somehow I don't have any syntax highlighting [maybe not a feature on this site?], so I have uploaded the code here as well):

from solana.rpc.api import Client, Transaction
from solders.pubkey import Pubkey
from solana.transaction import AccountMeta
from solders.instruction import Instruction
from borsh_construct import CStruct, String, U8, U16, Vec, Option, Bool

# some important public keys
system_program = Pubkey.from_string('11111111111111111111111111111111')
system_rent = Pubkey.from_string('SysvarRent111111111111111111111111111111111')
token_program = Pubkey.from_string("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA")
token_metadata_program = Pubkey.from_string("metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s")

# my own keypair
my_keypair = MY_KEYPAIR
# public key of my token
new_token = TOKEN_PUBLIC_KEY

# structure of the instruction
instruction_structure = CStruct(
    "instructionDiscriminator" / U8,
    "createMetadataAccountArgsV3" / CStruct(
        "data" / CStruct(
            "name" / String,
            "symbol" / String,
            "uri" / String,
            "sellerFeeBasisPoints" / U16,
            "creators" / Option(Vec(CStruct(
                "address" / String,
                "verified" / Bool,
                "share" / U8
            )))
        ),
        "is_mutable" / Bool
    )
)

# data for the instruction
instruction_data = {
    "instructionDiscriminator": 33,
    "createMetadataAccountArgsV3": {
        "data": {
            "name": "My Token Name",
            "symbol": "MTN",
            "uri": "https://arweave.net/somewhere",
            "sellerFeeBasisPoints": 500,
            "creators": [
                {
                    "address": str(my_keypair.pubkey()),
                    "verified": 1,
                    "share": 100
                }
            ],
        },
        "is_mutable": 1
    }
}

# get pda of mint account
metadata_pda = Pubkey.find_program_address([b"metadata", bytes(token_metadata_program), bytes(new_token)], token_metadata_program)[0]

# account list for instruction
accounts = [
    AccountMeta(pubkey=metadata_pda, is_signer=False, is_writable=True), # metadata
    AccountMeta(pubkey=new_token, is_signer=False, is_writable=True), # mint
    AccountMeta(pubkey=my_keypair.pubkey(), is_signer=True, is_writable=False), # mint authority
    AccountMeta(pubkey=my_keypair.pubkey(), is_signer=True, is_writable=True), # payer
    AccountMeta(pubkey=my_keypair.pubkey(), is_signer=False, is_writable=False), # update authority
    AccountMeta(pubkey=system_program, is_signer=False, is_writable=False), # system program
    AccountMeta(pubkey=system_rent, is_signer=False, is_writable=False) # rent
]

# create instruction
instruction = Instruction(token_metadata_program, instruction_structure.build(instruction_data), accounts)

# send a transaction with the instruction to the network
client = Client("https://api.devnet.solana.com")
transaction = Transaction()
transaction.add(instruction)
transaction.recent_blockhash = client.get_latest_blockhash().value.blockhash
transaction.sign(my_keypair)
print(client.send_raw_transaction(transaction.serialize()))

Running this code results in the following error:

solana.rpc.core.RPCException: SendTransactionPreflightFailureMessage { message: "Transaction simulation failed: Error processing Instruction 0: invalid instruction data", data: RpcSimulateTransactionResult(RpcSimulateTransactionResult { err: Some(InstructionError(0, InvalidInstructionData)), logs: Some(["Program metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s invoke [1]", "Program log: Error: InvalidInstructionData", "Program metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s consumed 5950 of 200000 compute units", "Program metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s failed: invalid instruction data"]), accounts: None, units_consumed: Some(0), return_data: None }) }

My main question is obviously: How do I fix this? I personally think it has something to do with the instructionDiscriminator or the way I integrated it. Is this the right place or does it need to be integrated at all? Lastly, do I need the set the "verified" flag in the creator dict myself or is this done by a the program?

Update

Using the idea from @Effe2, I recreated the situation in the mpl-token-metadata JavaScript library:

createMetadataAccountV3(umi, {
    mint: publicKey(TOKEN_PUBLIC_KEY),
    mintAuthority: umi.identity.publicKey,
    payer: umi.identity.publicKey,
    updateAuthority: umi.identity.publicKey,
    data: {
        name: "My Token Name",
        symbol: "MTN",
        uri: "https://arweave.net/somewhere",
        sellerFeeBasisPoints: 500,
        creators: [
            {
                address: solana.identity.publicKey,
                verified: true,
                share: 100
            }
        ],
        collection: null,
        uses: null
    },
    isMutable: true,
    collectionDetails: null,
});

After serialization, these are both (Python & JavaScript) byte arrays:

py: [33, 14, 0, 0, 0, 82, 97, 110, 100, 111, 109, 78, 70, 84, 32, 80, 97, 99, 107, 8, 0, 0, 0, 82, 78, 70, 84, 80, 97, 99, 107, 63, 0, 0, 0, 104, 116, 116, 112, 115, 58, 47, 47, 97, 114, 119, 101, 97, 118, 101, 46, 110, 101, 116, 47, 69, 82, 82, 71, 95, 107, 109, 119, 81, 54, 52, 70, 83, 118, 116, 84, 104, 121, 77, 109, 81, 81, 89, 102, 85, 45, 90, 112, 113, 75, 122, 56, 74, 53, 70, 99, 88, 70, 111, 65, 104, 111, 89, 244, 1, 1, 1, 0, 0, 0, 44, 0, 0, 0, 69, 70, 66, 66, 112, 56, 117, 122, 74, 106, 121, 75, 104, 49, 87, 85, 120, 85, 69, 112, 115, 76, 54, 98, 101, 106, 119, 67, 97, 65, 111, 54, 112, 75, 76, 76, 104, 117, 101, 68, 97, 67, 104, 101, 1, 100, 1]
js: [33, 14, 0, 0, 0, 82, 97, 110, 100, 111, 109, 78, 70, 84, 32, 80, 97, 99, 107, 8, 0, 0, 0, 82, 78, 70, 84, 80, 97, 99, 107, 63, 0, 0, 0, 104, 116, 116, 112, 115, 58, 47, 47, 97, 114, 119, 101, 97, 118, 101, 46, 110, 101, 116, 47, 69, 82, 82, 71, 95, 107, 109, 119, 81, 54, 52, 70, 83, 118, 116, 84, 104, 121, 77, 109, 81, 81, 89, 102, 85, 45, 90, 112, 113, 75, 122, 56, 74, 53, 70, 99, 88, 70, 111, 65, 104, 111, 89, 244, 1, 1, 1, 0, 0, 0, 196, 201, 148, 70, 82, 72, 185, 185, 6, 135, 183, 42, 95, 52, 96, 81, 89, 189, 19, 43, 33, 110, 90, 91, 3, 71, 210, 99, 188, 156, 207, 201, 1, 100, 0, 0, 1, 0]

As you can see, both are equal up until a certain point and the Python serialization is much longer as well. I currently don't know what is causing this and would appreciate any help!

3 Answers 3

1

After some debugging, I finally found the answer. Before we get to that, I would like to point out the mistakes in my previous approach:

  1. I thought I could just ignore optional fields like uses, but they have to be explicitly marked as optional with the type Option and set to None. This way, the serialization adds a zero byte to the final result at their position
  2. The public key needs to be converted directly to Bytes and not String. Converting the public key to String and then serializing results in a very different set of bytes (what a surprise)
  3. The Byte type from borsh-construct is a type with a variable length of bytes and prefixes the actual bytes with its length. However, the metadata program only ever expects 32 bytes, so we have to use the fixed length byte type directly from construct

With this being said, here is my working solution:

from solana.rpc.api import Client, Transaction
from solders.pubkey import Pubkey
from solana.transaction import AccountMeta
from solders.instruction import Instruction
from borsh_construct import CStruct, String, U8, U16, U64, Vec, Option, Bool, Enum
from construct import Bytes

# some important public keys
system_program = Pubkey.from_string('11111111111111111111111111111111')
system_rent = Pubkey.from_string('SysvarRent111111111111111111111111111111111')
token_program = Pubkey.from_string("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA")
token_metadata_program = Pubkey.from_string("metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s")

# my own keypair
my_keypair = MY_KEYPAIR
# public key of my token
new_token = TOKEN_PUBLIC_KEY

# structure of the instruction
instruction_structure = CStruct(
    "instructionDiscriminator" / U8,
    "createMetadataAccountArgsV3" / CStruct(
        "data" / CStruct(
            "name" / String,
            "symbol" / String,
            "uri" / String,
            "sellerFeeBasisPoints" / U16,
            "creators" / Option(Vec(CStruct(
                "address" / Bytes(32),
                "verified" / Bool,
                "share" / U8
            ))),
            "collection" / Option(CStruct(
                "verified" / Bool,
                "key" / String
            )),
            "uses" / Option(CStruct(
                "useMethod" / Enum(
                    "Burn",
                    "Multiple",
                    "Single",
                    enum_name="UseMethod"
                ),
                "remaining" / U64,
                "total" / U64
            ))
        ),
        "isMutable" / Bool,
        "collectionDetails" / Option(String) # fixme: string is not correct, insert correct type
    )
)

# data for the instruction
instruction_data = {
    "instructionDiscriminator": 33,
    "createMetadataAccountArgsV3": {
        "data": {
            "name": "My Token Name",
            "symbol": "MTN",
            "uri": "https://arweave.net/somewhere",
            "sellerFeeBasisPoints": 500,
            "creators": [
                {
                    "address": bytes(my_keypair.pubkey()),
                    "verified": 1,
                    "share": 100
                }
            ],
            "collection": None,
            "uses": None
        },
        "isMutable": 1,
        "collectionDetails": None
    }
}

# get pda of mint account
metadata_pda = Pubkey.find_program_address([b"metadata", bytes(token_metadata_program), bytes(new_token)], token_metadata_program)[0]

# account list for instruction
accounts = [
    AccountMeta(pubkey=metadata_pda, is_signer=False, is_writable=True), # metadata
    AccountMeta(pubkey=new_token, is_signer=False, is_writable=False), # mint
    AccountMeta(pubkey=my_keypair.pubkey(), is_signer=True, is_writable=False), # mint authority
    AccountMeta(pubkey=my_keypair.pubkey(), is_signer=True, is_writable=True), # payer
    AccountMeta(pubkey=my_keypair.pubkey(), is_signer=False, is_writable=False), # update authority
    AccountMeta(pubkey=system_program, is_signer=False, is_writable=False), # system program
    AccountMeta(pubkey=system_rent, is_signer=False, is_writable=False) # rent
]

# create instruction
instruction = Instruction(token_metadata_program, instruction_structure.build(instruction_data), accounts)

# send a transaction with the instruction to the network
client = Client("https://api.devnet.solana.com")
transaction = Transaction()
transaction.add(instruction)
transaction.recent_blockhash = client.get_latest_blockhash().value.blockhash
transaction.sign(my_keypair)
print(client.send_raw_transaction(transaction.serialize()))

Some final remarks: The collectionDetails type is not String and has to be fixed. I also did not test the collection and uses fields, as I have no need for them as well. If you want to use them, please make sure they are defined correctly.

0

For the verified field, you can't set it up by yourself like this. There is a specific call to metaplex program to verify creators.

For instruction data, i don't have the solution, but what i would do : create the same transaction in JS, and compare instruction data. As long as they are different, it means you're missing something.

The instruction discriminator 33 seems correct.

May be you can take a look here : https://github.com/metaplex-foundation/mpl-token-metadata/blob/018ca02e4435fd2cce453ecb56cccd4cceb8a8c9/programs/token-metadata/js/src/generated/instructions/CreateMetadataAccountV3.ts#L78

Could it be linked to the way your data are serialized also ?

1
  • Thanks for the input (can't vote yet)! Through the js docs I found that I wrote is_mutable instead of isMutable, but the problem persists. Maybe it is the serialization after all... Sep 12, 2023 at 19:23
0

Do you know why the create_metadata_instruction function of the Metaplex Python API's deploy function is giving outdated instructions for the metadata program ID (metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s)?

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.