gagliardetto / metaplex-go

Go clients for the Metaplex Solana programs
MIT License
42 stars 30 forks source link

How to mint new edition from master edition via token #2

Closed johnbailon closed 3 years ago

johnbailon commented 3 years ago

Hello, first of all, thanks for working on this. Very helpful in learning Solana/Metaplex as a Go dev. Kudos. Hoping to get some help on this, I'm trying to mint a new edition from an existing master edition to a new address and I'm hitting this error:

Program 11111111111111111111111111111111 invoke [1]
Program 11111111111111111111111111111111 success
Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [1]
Program log: Instruction: InitializeMint
Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 2390 of 200000 compute units
Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success
Program ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL invoke [1]
Program log: Transfer 2039280 lamports to the associated token account
Program 11111111111111111111111111111111 invoke [2]
Program 11111111111111111111111111111111 success
Program log: Allocate space for the associated token account
Program 11111111111111111111111111111111 invoke [2]
Program 11111111111111111111111111111111 success
Program log: Assign the associated token account to the SPL Token program
Program 11111111111111111111111111111111 invoke [2]
Program 11111111111111111111111111111111 success
Program log: Initialize the associated token account
Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [2]
Program log: Instruction: InitializeAccount
Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 3449 of 179470 compute units
Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success
Program ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL consumed 24660 of 200000 compute units
Program ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL success
Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [1]
Program log: Instruction: MintTo
Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 2879 of 200000 compute units
Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success
Program metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s invoke [1]
Program log: Instruction: Mint New Edition from Master Edition Via Token
Program log: Incorrect account owner
Program metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s consumed 5204 of 200000 compute units
Program metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s failed: custom program error: 0x39"

This is how I am doing it: payer is solana.privateKey of account that created and has the master edition withdrawalAddress is account to send the new edition to masterMint is the public key of the NFT

  1. generated new keypair called newMintAccount

  2. created token account: mint auth and freeze auth are payer.PublicKey(), mint account is newMintAccount.PublicKey()

  3. created associated token account: wallet is withdrawalAddress, mint is newMintAccount

  4. associatedTokenAddress is found via solana.FindAssociatedTokenAddress() using withdrawalAddress and newMintAccount.PublicKey()

  5. MintTo instruction: amount 1, mint account is newMintAccount.PublicKey(), destination account is associatedTokenAddress and auth account is payer.PublicKey()

  6. newMetadataKey is found via:

    newMetadataKey, _, err := solana.FindProgramAddress(
        [][]byte{
            []byte(token_metadata.PREFIX),
            solana.TokenMetadataProgramID.Bytes(),
            newMintAccount.PublicKey().Bytes(),
        },
        solana.TokenMetadataProgramID,
    )
  7. newEditionPDA is found via:

    newEditionPDA, _, err := solana.FindProgramAddress(
        [][]byte{
            []byte(token_metadata.PREFIX),
            solana.TokenMetadataProgramID.Bytes(),
            newMintAccount.PublicKey().Bytes(),
            []byte(token_metadata.EDITION),
        },
        solana.TokenMetadataProgramID,
    )
  8. masterRecordMetadata is found via:

    masterRecordMetadata, _, err := solana.FindProgramAddress(
    [][]byte{
        []byte(token_metadata.PREFIX),
        solana.TokenMetadataProgramID.Bytes(),
        masterMint.Bytes(),
    },
    solana.TokenMetadataProgramID,
    )
  9. masterRecordEditionV2PDA is found via:

    masterRecordEditionV2PDA, _, err := solana.FindProgramAddress(
    [][]byte{
        []byte(token_metadata.PREFIX),
        solana.TokenMetadataProgramID.Bytes(),
        masterRecordMetadata.Bytes(),
        []byte(token_metadata.EDITION),
    },
    solana.TokenMetadataProgramID,
    )
  10. editionPDA is found via:

    edition := uint64(1)
    x := math.Floor(float64(edition) / token_metadata.EDITION_MARKER_BIT_SIZE)
    b := make([]byte, 8)
    binary.LittleEndian.PutUint64(b, math.Float64bits(x))
    editionPDA, _, err := solana.FindProgramAddress(
        [][]byte{
            []byte(token_metadata.PREFIX),
            solana.TokenMetadataProgramID.Bytes(),
            masterRecordMetadata.Bytes(),
            []byte(token_metadata.EDITION),
            b,
        },
        solana.TokenMetadataProgramID,
    )
  11. tokenAccountOfMasterMint is found via:

    tokenAccountOfMasterMint, _, err := solana.FindAssociatedTokenAddress(
    payer.PublicKey(),
    masterMint,
    )
  12. Finally, this is the instruction for minting a new edition:

    printEditionInstruction := token_metadata.NewMintNewEditionFromMasterEditionViaTokenInstructionBuilder().
    SetArgs(token_metadata.MintNewEditionFromMasterEditionViaTokenArgs{Edition: edition}).
    SetNewMetadataKeyAccount(newMetadataKey). // pda from token_metadata.PREFIX, solana.TokenMetadataProgramID, newMintAccount.PublicKey()
    SetNewEditionPDAAccount(newEditionPDA).
    SetMasterRecordEditionV2Account(masterRecordEditionV2PDA).
    SetMintOfNewTokenAccount(newMintAccount.PublicKey()).
    SetEditionPDAAccount(editionPDA).
    SetMintAuthorityAccount(payer.PublicKey()).
    SetPayerAccount(payer.PublicKey()).
    SetOwnerOfTokenAccount(payer.PublicKey()).
    SetTokenAccount(tokenAccountOfMasterMint).
    SetUpdateAuthorityInfoAccount(payer.PublicKey()).
    SetMasterRecordMetadataAccount(masterRecordMetadata).
    SetTokenProgramAccount(solana.TokenProgramID).
    SetSystemProgramAccount(solana.SystemProgramID).
    SetRentInfoAccount(solana.SysVarRentPubkey)
gagliardetto commented 3 years ago

Hi

I'm looking into this, and one thing I found is the point 10. might not be matching this: https://github.com/metaplex-foundation/metaplex/blob/f3366f3ab09e9900407f10856659edab6c631fc0/js/packages/common/src/actions/metadata.ts#L1202

(i.e. the editionNumber, which is the last item in the seeds slice, which should be a string converted to a byte slice).

But that's probably no the issue that causes the error. Will update you.

gagliardetto commented 3 years ago

For reference, the MintTo instruction processing source code is here: https://github.com/solana-labs/solana-program-library/blob/e8b7009cc4d8cdd87232ccfc9ce93ab203ada496/token/program/src/processor.rs#L488-L542

And the error happens here: https://github.com/solana-labs/solana-program-library/blob/e8b7009cc4d8cdd87232ccfc9ce93ab203ada496/token/program/src/processor.rs#L519-L524

Which is a function that validates owners: https://github.com/solana-labs/solana-program-library/blob/e8b7009cc4d8cdd87232ccfc9ce93ab203ada496/token/program/src/processor.rs#L832-L867

gagliardetto commented 3 years ago

Nope, scratch that.

gagliardetto commented 3 years ago

MintNewEditionFromMasterEditionViaToken instruction processing: https://github.com/metaplex-foundation/metaplex/blob/397f488935f2075b0798946c30cf5f7b0ddcd3f0/rust/token-metadata/program/src/utils.rs#L912-L1030

Example instruction usage:

  1. https://github.com/metaplex-foundation/metaplex/blob/397f488935f2075b0798946c30cf5f7b0ddcd3f0/rust/token-metadata/program/src/instruction.rs#L380-L434

  2. https://github.com/metaplex-foundation/metaplex/blob/397f488935f2075b0798946c30cf5f7b0ddcd3f0/rust/token-metadata/program/tests/mint_new_edition_from_master_edition_via_token.rs#L133-L152

  3. https://github.com/metaplex-foundation/metaplex/blob/397f488935f2075b0798946c30cf5f7b0ddcd3f0/rust/token-metadata/program/tests/utils/edition_marker.rs#L170-L189

  4. https://github.com/metaplex-foundation/metaplex/blob/0ba4954762c8ad7a7802cbb03268fce5de932b40/js/packages/web/src/actions/setupMintEditionIntoWalletInstructions.ts#L13-L58

gagliardetto commented 3 years ago

When you see a incorrect account owner, it might also mean that the account doesn't exist (yeah, that's very cryptic).

gagliardetto commented 3 years ago

The Incorrect account owner is a MetadataError::IncorrectOwner error.

And it happens because one of these accounts have the wrong owner:

https://github.com/metaplex-foundation/metaplex/blob/397f488935f2075b0798946c30cf5f7b0ddcd3f0/rust/token-metadata/program/src/utils.rs#L936-L939

gagliardetto commented 3 years ago

How to debug:

Check that all of the accounts marked with a // THIS comment 1) exist and 2) are owned by the specified account.

printEditionInstruction := token_metadata.NewMintNewEditionFromMasterEditionViaTokenInstructionBuilder().
            SetArgs(token_metadata.MintNewEditionFromMasterEditionViaTokenArgs{Edition: edition}).
            SetNewMetadataKeyAccount(newMetadataKey). // pda from token_metadata.PREFIX, solana.TokenMetadataProgramID, newMintAccount.PublicKey()
            SetNewEditionPDAAccount(newEditionPDA).
            SetMasterRecordEditionV2Account(masterRecordEditionV2PDA). // THIS: assert_owned_by(master_edition_account_info, program_id)?;
            SetMintOfNewTokenAccount(newMintAccount.PublicKey()). // THIS: assert_owned_by(mint_info, &spl_token::id())?;
            SetEditionPDAAccount(editionPDA).
            SetMintAuthorityAccount(payer.PublicKey()).
            SetPayerAccount(payer.PublicKey()).
            SetOwnerOfTokenAccount(payer.PublicKey()).
            SetTokenAccount(tokenAccountOfMasterMint). // THIS: assert_owned_by(token_account_info, &spl_token::id())?;
            SetUpdateAuthorityInfoAccount(payer.PublicKey()).
            SetMasterRecordMetadataAccount(masterRecordMetadata). // THIS: assert_owned_by(master_metadata_account_info, program_id)?;
            SetTokenProgramAccount(solana.TokenProgramID).
            SetSystemProgramAccount(solana.SystemProgramID).
            SetRentInfoAccount(solana.SysVarRentPubkey)

NOTES:

gagliardetto commented 3 years ago

You can print the IDs (use https://github.com/gagliardetto/solana-go#pretty-print-transactionsinstructions) and then use the solana CLI to check for their existence and ownership:

$ solana -u mainnet-beta account BpbtsWaS7Uo2XAxaRXKtXkUbEiAJUBZ8HeTSJmaZGgys

Public Key: BpbtsWaS7Uo2XAxaRXKtXkUbEiAJUBZ8HeTSJmaZGgys
Balance: 0.00157296 SOL
Owner: vau1zxA2LbssAUEF7Gpw91zMM1LvXrvpzJtmZ58rPsn
Executable: false
Rent Epoch: 234
Length: 98 (0x62) bytes
0000:   01 83 b8 05  ea a6 55 7b  0f 08 76 7c  bd 77 70 f7   ......U{..v|.wp.
0010:   96 13 fc cc  63 a4 0b cf  87 ac 8d d2  82 d6 2c 2f   ....c.........,/
0020:   cc e6 dd 3d  4b 7c f8 b8  6f fd 11 7f  0b 01 18 c2   ...=K|..o.......
0030:   20 15 3c 2e  44 84 70 c5  a3 ba 97 a2  51 84 83 9b    .<.D.p.....Q...
0040:   8d f8 51 10  f2 8d dc 96  2c 20 f0 54  7b 42 86 cd   ..Q....., .T{B..
0050:   90 33 6c db  44 d4 65 e1  67 05 e8 3b  aa 0c 69 bf   .3l.D.e.g..;..i.
0060:   7d 00    
johnbailon commented 3 years ago

Brilliant! Thank you for your effort, @gagliardetto. Much appreciated. The links to the lines of code on how metaplex's metadata program checks ownership did it, and also using the CLI to check ownership of said addresses.

(Although I have a new issue now I am trying to resolve first on my own. The phantom wallets receiving the new print editions are also receiving an "unknown" token in it. It doesn't seem to be that way when using the Metaplex Store to mint master and print editions.)

So just to close it all up, there were 3 issues related to this that are now resolved:

  1. masterRecordEditionV2PDA (9. on first post) was wrong, it should be:

    masterRecordEditionV2PDA, _, err := solana.FindProgramAddress(
        [][]byte{
            []byte(token_metadata.PREFIX),
            solana.TokenMetadataProgramID.Bytes(),
            masterMint.Bytes(), // this was masterRecordMetadata.Bytes()
            []byte(token_metadata.EDITION),
        },
        solana.TokenMetadataProgramID,
    )
  2. editionPDA (10. on first post) was also wrong, it should be:

    editionPDA, _, err := solana.FindProgramAddress(
        [][]byte{
            []byte(token_metadata.PREFIX),
            solana.TokenMetadataProgramID.Bytes(),
            masterMint.Bytes(), // this was masterRecordMetadata.Bytes()
            []byte(token_metadata.EDITION),
            []byte(strEdition),
        },
        solana.TokenMetadataProgramID,
    )
  3. And the last error, which was a huge oversight on my part, the master edition I was using as an example was actually minted to another address, different from the payer wallet, hence the incorrect owner error. 🤦 🤦

gagliardetto commented 3 years ago

Awesome! Thanks for the summary of how you solved the issue!