Rise In Logo

Build on Solana

Custom NFT (Business Logic - Test)

Generating the Smart Contract 

Hi, in this document we will be generating a smart contract together. First, we will generate our boilerplate using codigo and then we will implement our business logic, deploy our contract and integrate the client library. Let’s start by generating the solana program using codigo.

Open the terminal; if not there, go to the path where you created the cidl.yaml file and type the following command:

codigo generate cidl.yaml

This command will generate the native solana program and the client library. The name of the file doesn’t matter. It just needs to match the name the developer created.

Implementing Business Logic

After generating the solana program and client library in the directory where the cidl.yaml exists, will be generated directory program_client_ts and program

Note: In the counter doc, at this point, it mentioned the codigolib; this directory no longer exists. And the generated directory changed name. The client from sdk to programd_client_ts and the contract from generated to program. Also, the stubs files used to be in a directory named stubs, now it is in a directory named src.

Within the program directory, we will find the directory named src. In the src directory, we will see three files, one named mint.rs, another transfer.rs, and finally, the other burn.rs. We will implement the business logic for our NFT program in these files. 

mint.rs

In the mint.rs file replace the comment “// Implement your business logic here...” with the code: 

gem.data.color = color;
gem.data.rarity = rarity;
gem.data.short_description = short_description;
gem.data.mint = *mint.info.key;
gem.data.assoc_account = Some(*assoc_token_account.key);

csl_spl_token::src::cpi::initialize_mint_2(for_initialize_mint_2, 0, *wallet.key, None)?;
csl_spl_assoc_token::src::cpi::create(for_create)?;
csl_spl_token::src::cpi::mint_to(for_mint_to, 1)?;
csl_spl_token::src::cpi::set_authority(for_set_authority, 0, None)?;

What’s happening here is:

  1. We assign our data to the Gem metadata PDA. This is specific for our use case, and doesn’t has nothing to do with the NFT itself.
  2. The generator will automatically inject the CPI calls into the stubs and will pass the correct accounts to each CPI; thus, the developers don’t need to worry about the accounts. 

transfer.rs

In the transfer.rs file, replace the comment “// Implement your business logic here...” with the code: 

gem.data.assoc_account = Some(*destination.key);

// Create the ATA account for new owner if it hasn't been created
if assoc_token_account.lamports() == 0 {
  csl_spl_assoc_token::src::cpi::create(for_create)?;
}

csl_spl_token::src::cpi::transfer_checked(for_transfer_checked, 1, 0)?;
  1. We are changing the Gem metadata owner 
  2. We check if the associated token account hasn’t been created for this use, if they don’t have an ATA, we create it.
  3. We transfer the token

burn.rs

In the transfer.rs file, replace the comment “// Implement your business logic here...” with the code: 

gem.data.assoc_account = None;
csl_spl_token::src::cpi::burn(for_burn, 1)?;
  1. We are changing the Gem metadata owner to None since no one will own it
  2. We burn the token
Build and Deploy the Contract
  1. Navigate to the generated directory and type the command `cargo build-sbf`; this command will build the contract.
  2. Open a new terminal, and type the command `solana-test-validator`; this command will start a new solana test validator to where we will deploy the contract.
  3. From the program directory and after building the contract, execute the following command to deploy to the validator `solana program deploy target/deploy/nft.so

After completing the deployment, you will get a program id. Copy and save this program id so we can configure the client library.

Integrate the Client Library

From the terminal, navigate to the program_client_ts directory, and type the command `yarn install` to install the node_modules dependencies. 

In your application, copy and paste the following code:

import {Connection, Keypair, PublicKey, sendAndConfirmTransaction, SystemProgram, Transaction,} from "@solana/web3.js";
import * as fs from "fs/promises";
import * as path from "path";
import * as os from "os";
import {
  burnSendAndConfirm,
  CslSplToken,
  deriveGemMetadataPDA,
  getGemMetadata,
  initializeClient,
  mintSendAndConfirm,
  transferSendAndConfirm,
} from "./index";
import {getMinimumBalanceForRentExemptAccount, getMint, TOKEN_PROGRAM_ID,} from "@solana/spl-token";
import * as console from "console";

// DevNet Program: HJLeDWKzPbBZbZJGFffcrHP1ceWD5RVr89RzZvWDroXC
//https://explorer.solana.com/address/HJLeDWKzPbBZbZJGFffcrHP1ceWD5RVr89RzZvWDroXC?cluster=devnet

async function main(feePayer: Keypair) {
  const args = process.argv.slice(2);
  const connection = new Connection("http://127.0.0.1:8899", {
    commitment: "confirmed"
  });
  const progId = new PublicKey(args[0]!);

  initializeClient(progId, connection);

  /**
   * Create a keypair for the mint
   */
  const mint = Keypair.generate();
  console.info("+==== Mint Address ====+");
  console.info(mint.publicKey.toBase58());

  /**
   * Create two wallets
   */
  const johnDoeWallet = Keypair.generate();
  console.info("+==== John Doe Wallet ====+");
  console.info(johnDoeWallet.publicKey.toBase58());

  const janeDoeWallet = Keypair.generate();
  console.info("+==== Jane Doe Wallet ====+");
  console.info(janeDoeWallet.publicKey.toBase58());

  const rent = await getMinimumBalanceForRentExemptAccount(connection);
  await sendAndConfirmTransaction(
    connection,
    new Transaction()
      .add(
        SystemProgram.createAccount({
          fromPubkey: feePayer.publicKey,
          newAccountPubkey: johnDoeWallet.publicKey,
          space: 0,
          lamports: rent,
          programId: SystemProgram.programId,
        }),
      )
      .add(
        SystemProgram.createAccount({
          fromPubkey: feePayer.publicKey,
          newAccountPubkey: janeDoeWallet.publicKey,
          space: 0,
          lamports: rent,
          programId: SystemProgram.programId,
        }),
      ),
    [feePayer, johnDoeWallet, janeDoeWallet],
  );

  /**
   * Derive the Gem Metadata so we can retrieve it later
   */
  const [gemPub] = deriveGemMetadataPDA(
    {
      mint: mint.publicKey,
    },
    progId,
  );
  console.info("+==== Gem Metadata Address ====+");
  console.info(gemPub.toBase58());

  /**
   * Derive the John Doe's Associated Token Account, this account will be
   * holding the minted NFT.
   */
  const [johnDoeATA] = CslSplToken.deriveAccountPDA({
    wallet: johnDoeWallet.publicKey,
    mint: mint.publicKey,
    tokenProgram: TOKEN_PROGRAM_ID,
  });
  console.info("+==== John Doe ATA ====+");
  console.info(johnDoeATA.toBase58());

  /**
   * Derive the Jane Doe's Associated Token Account, this account will be
   * holding the minted NFT when John Doe transfer it
   */
  const [janeDoeATA] = CslSplToken.deriveAccountPDA({
    wallet: janeDoeWallet.publicKey,
    mint: mint.publicKey,
    tokenProgram: TOKEN_PROGRAM_ID,
  });
  console.info("+==== Jane Doe ATA ====+");
  console.info(janeDoeATA.toBase58());

  /**
   * Mint a new NFT into John's wallet (technically, the Associated Token Account)
   */
  console.info("+==== Minting... ====+");
  await mintSendAndConfirm({
    wallet: johnDoeWallet.publicKey,
    color: "Purple",
    rarity: "Rare",
    shortDescription: "Only possible to collect from the lost temple event",
    signers: {
      feePayer: feePayer,
      funding: feePayer,
      mint: mint,
      owner: johnDoeWallet,
    },
  });
  console.info("+==== Minted ====+");

  /**
   * Get the minted token
   */
  let mintAccount = await getMint(connection, mint.publicKey);
  console.info("+==== Mint ====+");
  console.info(mintAccount);

  /**
   * Get the Gem Metadata
   */
  let gem = await getGemMetadata(gemPub);
  console.info("+==== Gem Metadata ====+");
  console.info(gem);
  console.assert(gem!.assocAccount!.toBase58(), johnDoeATA.toBase58());

  /**
   * Transfer John Doe's NFT to Jane Doe Wallet (technically, the Associated Token Account)
   */
  console.info("+==== Transferring... ====+");
  await transferSendAndConfirm({
    wallet: janeDoeWallet.publicKey,
    mint: mint.publicKey,
    source: johnDoeATA,
    destination: janeDoeATA,
    signers: {
      feePayer: feePayer,
      funding: feePayer,
      authority: johnDoeWallet,
    },
  });
  console.info("+==== Transferred ====+");

  /**
   * Get the minted token
   */
  mintAccount = await getMint(connection, mint.publicKey);
  console.info("+==== Mint ====+");
  console.info(mintAccount);

  /**
   * Get the Gem Metadata
   */
  gem = await getGemMetadata(gemPub);
  console.info("+==== Gem Metadata ====+");
  console.info(gem);
  console.assert(gem!.assocAccount!.toBase58(), janeDoeATA.toBase58());

  /**
   * Burn the NFT
   */
  console.info("+==== Burning... ====+");
  await burnSendAndConfirm({
    mint: mint.publicKey,
    wallet: janeDoeWallet.publicKey,
    signers: {
      feePayer: feePayer,
      owner: janeDoeWallet,
    },
  });
  console.info("+==== Burned ====+");

  /**
   * Get the minted token
   */
  mintAccount = await getMint(connection, mint.publicKey);
  console.info("+==== Mint ====+");
  console.info(mintAccount);

  /**
   * Get the Gem Metadata
   */
  gem = await getGemMetadata(gemPub);
  console.info("+==== Gem Metadata ====+");
  console.info(gem);
  console.assert(typeof gem!.assocAccount, "undefined");
}

fs.readFile(path.join(os.homedir(), ".config/solana/id.json")).then((file) =>
  main(Keypair.fromSecretKey(new Uint8Array(JSON.parse(file.toString())))),
);

Note: The code itself have comments explaining the different steps. Thus, I won’t describe it here, but if you have any questions or doubts, please ping. The example also relies on @solana/spl-token; this package needs to be installed manually by executing yarn add @solana/spl-token also the ts-node package is not installed

Execute the client by typing the following command:

npx ts-node app.ts your_program_id

Output

Note: The address will differ…

+==== Mint Address ====+
CTtDbZ54do5axYQWDavaygTsktegTjHJUT4M5Z3Zd4wb
+==== John Doe Wallet ====+
E5yDru94UES4tYvjc3E2nzZdcotMwFoSNNttxhqaVarz
+==== Jane Doe Wallet ====+
2nxjgpBJARWBb5RyyrvszccWm9QaJhV75CnY9ui1FW92

+==== Gem Metadata Address ====+
FjCq8QurQwAq7ayDVgK1TAfzXtabzdUbk1J5Y2Q9WLq6
+==== John Doe ATA ====+
HYn9NQ9VKvVCTdbsSNRpBBGSNhyWMVVAXGHyH4jtmW7J
+==== Jane Doe ATA ====+
5fzQoZs8qtjxS4HtLdviatxRKzmsL8kDC1vC5nrquLF7
+==== Minting... ====+
+==== Minted ====+
+==== Mint ====+
{
 address: PublicKey [PublicKey(CTtDbZ54do5axYQWDavaygTsktegTjHJUT4M5Z3Zd4wb)] {
  _bn: <BN: aa539193d927d7b11d4461d74e2a81286e43ce150d7ba44a9f8b818516a752aa>
 },
 mintAuthority: null,
 supply: 1n,
 decimals: 0,
 isInitialized: true,
 freezeAuthority: null,
 tlvData: <Buffer >
}
+==== Gem Metadata ====+
{
 color: 'Purple',
 rarity: 'Rare',
 shortDescription: 'Only possible to collect from the lost temple event',
 mint: PublicKey [PublicKey(CTtDbZ54do5axYQWDavaygTsktegTjHJUT4M5Z3Zd4wb)] {
  _bn: <BN: aa539193d927d7b11d4461d74e2a81286e43ce150d7ba44a9f8b818516a752aa>
 },
 assocAccount: PublicKey [PublicKey(HYn9NQ9VKvVCTdbsSNRpBBGSNhyWMVVAXGHyH4jtmW7J)] {
  _bn: <BN: f5df0a6c6cacc6bcb05f50504230b4e909b8670c64bec57657fe99cdd1261291>
 }
}
+==== Transferring... ====+
+==== Transferred ====+
+==== Mint ====+
{
 address: PublicKey [PublicKey(CTtDbZ54do5axYQWDavaygTsktegTjHJUT4M5Z3Zd4wb)] {
  _bn: <BN: aa539193d927d7b11d4461d74e2a81286e43ce150d7ba44a9f8b818516a752aa>
 },
 mintAuthority: null,
 supply: 1n,
 decimals: 0,
 isInitialized: true,
 freezeAuthority: null,
 tlvData: <Buffer >
}
+==== Gem Metadata ====+
{
 color: 'Purple',
 rarity: 'Rare',
 shortDescription: 'Only possible to collect from the lost temple event',
 mint: PublicKey [PublicKey(CTtDbZ54do5axYQWDavaygTsktegTjHJUT4M5Z3Zd4wb)] {
  _bn: <BN: aa539193d927d7b11d4461d74e2a81286e43ce150d7ba44a9f8b818516a752aa>
 },
 assocAccount: PublicKey [PublicKey(5fzQoZs8qtjxS4HtLdviatxRKzmsL8kDC1vC5nrquLF7)] {
  _bn: <BN: 456bbb8b0693fc340ad3b4806a074d0d017e1772611a573e74c58869fb91abde>
 }
}
+==== Burning... ====+
+==== Burned ====+
+==== Mint ====+
{
 address: PublicKey [PublicKey(CTtDbZ54do5axYQWDavaygTsktegTjHJUT4M5Z3Zd4wb)] {
  _bn: <BN: aa539193d927d7b11d4461d74e2a81286e43ce150d7ba44a9f8b818516a752aa>
 },
 mintAuthority: null,
 supply: 0n,
 decimals: 0,
 isInitialized: true,
 freezeAuthority: null,
 tlvData: <Buffer >
}
+==== Gem Metadata ====+
{
 color: 'Purple',
 rarity: 'Rare',
 shortDescription: 'Only possible to collect from the lost temple event',
 mint: PublicKey [PublicKey(CTtDbZ54do5axYQWDavaygTsktegTjHJUT4M5Z3Zd4wb)] {
  _bn: <BN: aa539193d927d7b11d4461d74e2a81286e43ce150d7ba44a9f8b818516a752aa>
 },
 assocAccount: undefined
}

I highlighted in greed what change after each call of the instruction. Here we can see the token in devnet https://explorer.solana.com/address/CTtDbZ54do5axYQWDavaygTsktegTjHJUT4M5Z3Zd4wb/transfers?cluster=devnet and we can observe the transfer.

Useful Links

List of all built in instructions:Link is here.

Token standard: Link is here.

Rise In Logo

Rise together in web3

Website

BootcampsMembersPartner with UsBlogEvents

Company

About UsFAQTerms ConditionsPrivacy PolicyGDPR NoticeCookies