hans
hans

Reputation: 345

Solana Candy Machine v3: BorshIoError "Unexpected variant index: 1" during NFT minting

Problem Description:

I'm trying to mint NFTs using Metaplex's Candy Machine v3, but encountering a BorshIoError during the mint transaction. The error occurs despite following the official documentation.

Error Message:

mint failed: SendTransactionError: Simulation failed. 
Message: Transaction simulation failed: Error processing Instruction 2: Failed to serialize or deserialize account data: Unknown. 
Logs: 
[
  "Program log: Instruction: MintV2",
  "Program 11111111111111111111111111111111 invoke [2]",
  "Program 11111111111111111111111111111111 success",
  "Program CndyV3LdqHUfDLmE5naZjVN8rBZz4tqhdefbAnjHG3JR invoke [2]",
  "Program log: Instruction: MintV2",
  "Program log: ProgramError occurred. Error Code: BorshIoError(\"Unexpected variant index: 1\"). Error Number: 64424509440. Error Message: IO Error: Unexpected variant index: 1.",
  "Program CndyV3LdqHUfDLmE5naZjVN8rBZz4tqhdefbAnjHG3JR consumed 35507 of 748405 compute units",
  "Program CndyV3LdqHUfDLmE5naZjVN8rBZz4tqhdefbAnjHG3JR failed: Failed to serialize or deserialize account data: Unknown",
  "Program Guard1JwRhJkVH6XZhzoYxeBVQe872VH6QggF4BWmS9g consumed 86802 of 799700 compute units",
  "Program Guard1JwRhJkVH6XZhzoYxeBVQe872VH6QggF4BWmS9g failed: Failed to serialize or deserialize account data: Unknown"

Relevant Code:

Candy Machine Deployment:

# basic umi setup
const umi = createUmi(
NFT_CONFIG.RPC_URL || 'https://api.devnet.solana.com' #test on devnet
    )
    .use(mplCandyMachine())
    .use(mplTokenMetadata());
    
    // set deploy wallet
const DEPLOY_WALLET_PATH = path.join(__dirname, 'deploy-wallet.json');
const secretKey = new Uint8Array(
JSON.parse(fs.readFileSync(DEPLOY_WALLET_PATH, 'utf-8'))
    );
const keypair = umi.eddsa.createKeypairFromSecretKey(secretKey);
const signer = createSignerFromKeypair(umi, keypair);
umi.use(keypairIdentity(signer));    
console.log('Using deploy wallet:', signer.publicKey.toString());

console.log('Creating Collection NFT...');
const collectionMint = generateSigner(umi);
const collectionUpdateAuthority = signer;
console.log('Collection Mint address:', collectionMint.publicKey.toString());
console.log('Collection Update Authority:', umi.identity.publicKey.toString());


await createNft(umi, {
      mint: collectionMint,
      authority: umi.identity,
      name: "my-nft-name",
      uri: "my-url-to-collection-json",
      sellerFeeBasisPoints: percentAmount(5,2),
      isCollection: true,
    }).sendAndConfirm(umi);

console.log('Waiting for Collection NFT to be created...');
await new Promise(resolve => setTimeout(resolve, 5000));

console.log('Creating Candy Machine...');
const candyMachine = generateSigner(umi);
console.log('Candy Machine address:', candyMachine.publicKey.toString());

const tx = await create(umi, {
  candyMachine,
  collectionMint: collectionMint.publicKey,
  collectionUpdateAuthority: umi.identity,
  tokenStandard: TokenStandard.NonFungible,
  sellerFeeBasisPoints: percentAmount(5, 2),
  itemsAvailable: BigInt(500),
  creators: [
    {
      address: publicKey(TREASURY_ADDRESS),
      verified: true,
      percentageShare: 100
    }
  ],
  configLineSettings: some({
    prefixName: "",
    nameLength: 32,
    prefixUri: "",
    uriLength: 200,
    isSequential: true
  }),
  guards: {
    startDate: some({ 
      date: dateTime("2025-02-01T00:00:00.000Z")
    }),
    endDate: some({ 
      date: dateTime("2025-04-01T00:00:00.000Z")
    }),
    solPayment: some({
      lamports: sol(1),
      destination: publicKey(TREASURY_ADDRESS),
    })
  }
});

Candy Machine Service (mint function):

async mint(candyMachineAddress: string) {
  try {
    const candyMachine = await fetchCandyMachine(
      this.umi,
      publicKey(candyMachineAddress)
    );

    const nftMint = generateSigner(this.umi);
    
    const tx = await transactionBuilder()
      .add(setComputeUnitLimit(this.umi, { units: 800_000 }))
      .add(
        mintV2(this.umi, {
          candyMachine: candyMachine.publicKey,
          nftMint,
          collectionMint: candyMachine.collectionMint,
          collectionUpdateAuthority: candyMachine.authority,
          mintArgs: {
            solPayment: some({
              destination: publicKey(TREASURY_ADDRESS)
            }),
          }
        })
      );

    return await tx.sendAndConfirm(this.umi);
  } catch (error) {
    console.error('Mint failed:', error);
    throw error;
  }
}

Key Dependencies (package.json):

{
  "dependencies": {
    "@metaplex-foundation/mpl-candy-machine": "^6.1.0",
    "@metaplex-foundation/mpl-token-metadata": "^3.4.0",
    "@metaplex-foundation/umi": "^1.0.0",
    "@metaplex-foundation/umi-bundle-defaults": "^1.0.0",
    "@metaplex-foundation/umi-signer-wallet-adapters": "^1.0.0"
  }
}

What I've Tried:

  1. Followed the official Metaplex documentation for Candy Machine v3
  2. Using the latest versions of all Metaplex dependencies
  3. Verified the Candy Machine deployment and guard configuration
  4. Added compute unit limit to the transaction
  5. Added compute unit limit to the transaction
  6. Verified all addresses (mintAuthority, collectionMint) are correct through logs

Environment:

Question:

What could be causing this BorshIoError during minting, and how can I resolve it? The error suggests a serialization issue, but I'm following the official documentation structure.Don't know what's wrong, I think this is nothing to do with items insert function since I can verify all items were uploaded successfully through Solana Explorer. If you need any more code snippets from these files, please comment below:

Potentially relevant files:

Update

As requested, here are the additional code snippets that might be relevant to the issue:

1. Storage Upload Implementation to Pinata (upload-to-storage.ts):

import { PinataSDK } from "pinata-web3";
import fs from 'fs';
import path from 'path';
import { config } from 'dotenv';

// set my environment
config({ path: path.resolve(process.cwd(), '.env.local') });

const PINATA_JWT = process.env.PINATA_JWT;
const GATEWAY_URL = process.env.NEXT_PUBLIC_GATEWAY_URL;

const IMAGES_DIR = path.join(__dirname, 'assets/images');
const METADATA_DIR = path.join(__dirname, 'assets');

// create Pinata instance
const pinata = new PinataSDK({
  pinataJwt: PINATA_JWT!,
  pinataGateway: GATEWAY_URL
});

async function uploadToStorage() {
  try {
    if (!PINATA_JWT) {
      throw new Error('PINATA_JWT not found in .env.local');
    }

    // 
    const imageFiles = fs.readdirSync(IMAGES_DIR)
      .filter(file => file.startsWith('nft-') && (file.endsWith('.jpg') || file.endsWith('.jpeg')))
      .sort((a, b) => {
        const numA = parseInt(a.replace('nft-', '').replace(/\.(jpg|jpeg)$/, ''));
        const numB = parseInt(b.replace('nft-', '').replace(/\.(jpg|jpeg)$/, ''));
        return numA - numB;
      });

    console.log(`Found ${imageFiles.length} images to upload`);

    
    const uploadResults = [];

    
    const batchSize = 3;
    for (let i = 0; i < imageFiles.length; i += batchSize) {
      const batch = imageFiles.slice(i, Math.min(i + batchSize, imageFiles.length));
      const promises = batch.map(async (imageFile) => {
        console.log(`Processing ${imageFile}...`);
        
        // get index 
        const index = parseInt(imageFile.replace('nft-', '').replace(/\.(jpg|jpeg)$/, '')) - 1;
        
        // get json files respectively
        const jsonPath = path.join(METADATA_DIR, `${index}.json`);
        const metadata = JSON.parse(fs.readFileSync(jsonPath, 'utf-8'));

        // upload file
        const imageData = await fs.promises.readFile(path.join(IMAGES_DIR, imageFile));
        const imageFileObj = new File(
          [imageData],
          imageFile,
          { type: 'image/jpg' }
        );
        const imageUpload = await pinata.upload.file(imageFileObj);
        const imageUrl = await pinata.gateways.convert(imageUpload.IpfsHash);

        // update urls in metadata
        metadata.image = imageUrl;
        metadata.properties.files[0].uri = imageUrl;

        // upload updated metadate
        const metadataFileObj = new File(
          [JSON.stringify(metadata, null, 2)],
          `${index}.json`,
          { type: 'application/json' }
        );
        const metadataUpload = await pinata.upload.file(metadataFileObj);
        const metadataUrl = await pinata.gateways.convert(metadataUpload.IpfsHash);

        // save local updated metadata JSON
        fs.writeFileSync(jsonPath, JSON.stringify(metadata, null, 2));

        return {
          index,
          imageUrl,
          metadataUrl,
          metadata
        };
      });

      // wait for current batch to complete
      const results = await Promise.all(promises);
      uploadResults.push(...results);
      console.log(`Completed batch ${i / batchSize + 1}`);
    }

    // save upload results
    const resultsPath = path.join(__dirname, 'upload-results.json');
    fs.writeFileSync(resultsPath, JSON.stringify(uploadResults, null, 2));

    console.log('\nAll files uploaded successfully!');
    console.log(`Results saved to: ${resultsPath}`);

  } catch (error) {
    console.error('Upload failed:', error);
    throw error;
  }
}

2. Upload Items Implementation (upload-items.ts):

import { 
  createUmi,
  keypairIdentity,
  publicKey,
  createSignerFromKeypair
} from "@metaplex-foundation/umi";
import { 
  mplCandyMachine,
  addConfigLines,
  fetchCandyMachine
} from "@metaplex-foundation/mpl-candy-machine";
import { createUmi as baseCreateUmi } from "@metaplex-foundation/umi-bundle-defaults";
import { config } from 'dotenv';
import * as fs from 'fs';
import * as path from 'path';


config({ path: path.resolve(process.cwd(), '.env.local') });

interface UploadResult {
  index: number;
  imageUrl: string;
  metadataUrl: string;
  metadata: {
    name: string;
    [key: string]: any;
  }
}

async function uploadItems() {
  try {

    const umi = baseCreateUmi(
      process.env.NEXT_PUBLIC_SOLANA_RPC_URL || 'https://api.devnet.solana.com'
    ).use(mplCandyMachine());


    const DEPLOY_WALLET_PATH = path.join(__dirname, 'deploy-wallet.json');
    const secretKey = new Uint8Array(
      JSON.parse(fs.readFileSync(DEPLOY_WALLET_PATH, 'utf-8'))
    );

    const keypair = umi.eddsa.createKeypairFromSecretKey(secretKey);
    const signer = createSignerFromKeypair(umi, keypair);
    umi.use(keypairIdentity(signer));

    // get candy machine address that is saved by "deploy candy machine script
    const candyMachineAddress = process.env.NEXT_PUBLIC_CANDY_MACHINE_ADDRESS;
    if (!candyMachineAddress) {
      throw new Error('Candy Machine address not found in .env.local');
    }

    // get Candy Machine instance
    const candyMachine = await fetchCandyMachine(umi, publicKey(candyMachineAddress));

    // read all JSON files in assets dir
    const ASSETS_DIR = path.join(__dirname, 'assets');
    const files = fs.readdirSync(ASSETS_DIR)
      .filter(file => {
        // exclude collection.json and collection.jpg
        return file.endsWith('.json') && 
               !file.startsWith('collection') && 
               !isNaN(parseInt(file)) // make sure index is number
      })
      .sort((a, b) => parseInt(a) - parseInt(b));

    // get result from upload-storage scripts, which give me real uri of metadata json files
    const configLines = await Promise.all(
      files.map(async (file) => {
        const uploadResults = JSON.parse(
          fs.readFileSync(path.join(__dirname, 'upload-results.json'), 'utf-8')
        ) as UploadResult[];
        
        const index = parseInt(file);
        const result = uploadResults.find((r: UploadResult) => r.index === index);
        
        if (!result) {
          throw new Error(`No upload result found for index ${index}`);
        }

        // nft name
        const name = `xxxx #${index + 1}`;
        
        // nft url
        const uri = result.metadataUrl;

        return { name, uri };
      })
    );

    // batch
    const batchSize = 5;  

    
    for (let i = 0; i < configLines.length; i += batchSize) {
      const batch = configLines.slice(i, Math.min(i + batchSize, configLines.length));
      console.log(`Uploading batch ${Math.floor(i / batchSize) + 1}/${Math.ceil(configLines.length / batchSize)}`);
      
      // add log 
      console.log('AddConfigLines content:', {
        candyMachine: candyMachine.publicKey.toString(),
        index: i,
        configLines: batch.map(line => ({
          name: line.name,
          uri: line.uri,
          fullUri: line.uri
        }))
      });
      
      try {
        await addConfigLines(umi, {
          candyMachine: candyMachine.publicKey,
          index: i,
          configLines: batch
        }).sendAndConfirm(umi);

        console.log(`Uploaded items ${i + 1} to ${i + batch.length}`);
      } catch (error) {
        console.error(`Failed to upload batch ${Math.floor(i / batchSize) + 1}:`, error);
        throw error;
      }
    }

    console.log('All items uploaded successfully!');
  } catch (error) {
    console.error('Failed to upload items:', error);
    throw error;
  }
}


Upvotes: 0

Views: 24

Answers (0)

Related Questions