Writing Programs

How to transfer SOL in a program

당신의 Solana Program은 System Program을 호출하지 않고 하나의 Account에서 다른 Account로 전송할 수 있습니다. 핵심적인 규칙은 당신의 Program이 소유한 모든 Account에서 다른 Account들로 lamports를 보낼 수 있다는 것입니다.

전송을 받는 Account는 당신이 Program이 소유한 Account가 아니어야 합니다.

/// Transfers lamports from one account (must be program owned)
/// to another account. The recipient can by any account
fn transfer_service_fee_lamports(
    from_account: &AccountInfo,
    to_account: &AccountInfo,
    amount_of_lamports: u64,
) -> ProgramResult {
    // Does the from account have enough lamports to transfer?
    if **from_account.try_borrow_lamports()? < amount_of_lamports {
        return Err(CustomError::InsufficientFundsForTransaction.into());
    }
    // Debit from_account and credit to_account
    **from_account.try_borrow_mut_lamports()? -= amount_of_lamports;
    **to_account.try_borrow_mut_lamports()? += amount_of_lamports;
    Ok(())
}

/// Primary function handler associated with instruction sent
/// to your program
fn instruction_handler(accounts: &[AccountInfo]) -> ProgramResult {
    // Get the 'from' and 'to' accounts
    let account_info_iter = &mut accounts.iter();
    let from_account = next_account_info(account_info_iter)?;
    let to_service_account = next_account_info(account_info_iter)?;

    // Extract a service 'fee' of 5 lamports for performing this instruction
    transfer_service_fee_lamports(from_account, to_service_account, 5u64)?;

    // Perform the primary instruction
    // ... etc.

    Ok(())
}

How to get clock in a program

2가지 방법으로 clock을 얻을 수 있습니다.

  1. Instruction에 SYSVAR_CLOCK_PUBKEY을 넘기는 것
  2. Instruction 안에서 직접 Clock에 접근하는 것

어떤 레거시 Program들은 Account로 여전히 SYSVAR_CLOCK_PUBKEY를 기대하기 때문에, 두 가지 방법 모두 알아두는 것이 좋습니다.

Passing Clock as an account inside an instruction

초기화를 위한 하나의 Account와 sysvar pubkey를 받는 하나의 Instruction을 만들어 봅시다.

Press </> button to view full source
use borsh::{BorshDeserialize, BorshSerialize};
use solana_program::{
    account_info::{next_account_info, AccountInfo},
    clock::Clock,
    entrypoint,
    entrypoint::ProgramResult,
    msg,
    pubkey::Pubkey,
    sysvar::Sysvar,
};

entrypoint!(process_instruction);

#[derive(BorshSerialize, BorshDeserialize, Debug)]
pub struct HelloState {
    is_initialized: bool,
}

// Accounts required
/// 1. [signer, writable] Payer
/// 2. [writable] Hello state account
/// 3. [] Clock sys var
pub fn process_instruction(
    _program_id: &Pubkey,
    accounts: &[AccountInfo],
    _instruction_data: &[u8],
) -> ProgramResult {
    let accounts_iter = &mut accounts.iter();
    // Payer account
    let _payer_account = next_account_info(accounts_iter)?;
    // Hello state account
    let hello_state_account = next_account_info(accounts_iter)?;
    // Clock sysvar
    let sysvar_clock_pubkey = next_account_info(accounts_iter)?;

    let mut hello_state = HelloState::try_from_slice(&hello_state_account.data.borrow())?;
    hello_state.is_initialized = true;
    hello_state.serialize(&mut &mut hello_state_account.data.borrow_mut()[..])?;
    msg!("Account initialized :)");

    // Type casting [AccountInfo] to [Clock]
    let clock = Clock::from_account_info(&sysvar_clock_pubkey)?;
    // Getting timestamp
    let current_timestamp = clock.unix_timestamp;
    msg!("Current Timestamp: {}", current_timestamp);

    Ok(())
}

이제 Cient를 통해 clock의 sysvar public address를 보냅니다.

Press </> button to view full source
import {
  clusterApiUrl,
  Connection,
  Keypair,
  LAMPORTS_PER_SOL,
  PublicKey,
  SystemProgram,
  SYSVAR_CLOCK_PUBKEY,
  Transaction,
  TransactionInstruction,
} from "@solana/web3.js";

(async () => {
  const programId = new PublicKey(
    "77ezihTV6mTh2Uf3ggwbYF2NyGJJ5HHah1GrdowWJVD3"
  );

  const connection = new Connection(clusterApiUrl("devnet"), "confirmed");

  // Airdropping 1 SOL
  const feePayer = Keypair.generate();
  await connection.confirmTransaction(
    await connection.requestAirdrop(feePayer.publicKey, LAMPORTS_PER_SOL)
  );

  // Hello state account
  const helloAccount = Keypair.generate();

  const accountSpace = 1; // because there exists just one boolean variable
  const rentRequired = await connection.getMinimumBalanceForRentExemption(
    accountSpace
  );

  // Allocating space for hello state account
  const allocateHelloAccountIx = SystemProgram.createAccount({
    fromPubkey: feePayer.publicKey,
    lamports: rentRequired,
    newAccountPubkey: helloAccount.publicKey,
    programId: programId,
    space: accountSpace,
  });

  // Passing Clock Sys Var
  const passClockIx = new TransactionInstruction({
    programId: programId,
    keys: [
      {
        isSigner: true,
        isWritable: true,
        pubkey: feePayer.publicKey,
      },
      {
        isSigner: false,
        isWritable: true,
        pubkey: helloAccount.publicKey,
      },
      {
        isSigner: false,
        isWritable: false,
        pubkey: SYSVAR_CLOCK_PUBKEY,
      },
    ],
  });

  const transaction = new Transaction();
  transaction.add(allocateHelloAccountIx, passClockIx);

  const txHash = await connection.sendTransaction(transaction, [
    feePayer,
    helloAccount,
  ]);

  console.log(`Transaction succeeded. TxHash: ${txHash}`);
})();

Accessing Clock directly inside an instruction

이제 같은 Instruction이지만 Client로부터 SYSVAR_CLOCK_PUBKEY를 받지 않는 Instruction을 만들어 봅시다.

Press </> button to view full source
use borsh::{BorshDeserialize, BorshSerialize};
use solana_program::{
    account_info::{next_account_info, AccountInfo},
    clock::Clock,
    entrypoint,
    entrypoint::ProgramResult,
    msg,
    pubkey::Pubkey,
    sysvar::Sysvar,
};

entrypoint!(process_instruction);

#[derive(BorshSerialize, BorshDeserialize, Debug)]
pub struct HelloState {
    is_initialized: bool,
}

// Accounts required
/// 1. [signer, writable] Payer
/// 2. [writable] Hello state account
pub fn process_instruction(
    _program_id: &Pubkey,
    accounts: &[AccountInfo],
    _instruction_data: &[u8],
) -> ProgramResult {
    let accounts_iter = &mut accounts.iter();
    // Payer account
    let _payer_account = next_account_info(accounts_iter)?;
    // Hello state account
    let hello_state_account = next_account_info(accounts_iter)?;

    // Getting clock directly
    let clock = Clock::get()?;

    let mut hello_state = HelloState::try_from_slice(&hello_state_account.data.borrow())?;
    hello_state.is_initialized = true;
    hello_state.serialize(&mut &mut hello_state_account.data.borrow_mut()[..])?;
    msg!("Account initialized :)");

    // Getting timestamp
    let current_timestamp = clock.unix_timestamp;
    msg!("Current Timestamp: {}", current_timestamp);

    Ok(())
}

Client 사이드 Instruction은 이제 오직 state와 payer Account들만 보낼 필요가 있습니다.

Press </> button to view full source
import {
  clusterApiUrl,
  Connection,
  Keypair,
  LAMPORTS_PER_SOL,
  PublicKey,
  SystemProgram,
  Transaction,
  TransactionInstruction,
} from "@solana/web3.js";

(async () => {
  const programId = new PublicKey(
    "4ZEdbCtb5UyCSiAMHV5eSHfyjq3QwbG3yXb6oHD7RYjk"
  );

  const connection = new Connection(clusterApiUrl("devnet"), "confirmed");

  // Airdropping 1 SOL
  const feePayer = Keypair.generate();
  await connection.confirmTransaction(
    await connection.requestAirdrop(feePayer.publicKey, LAMPORTS_PER_SOL)
  );

  // Hello state account
  const helloAccount = Keypair.generate();

  const accountSpace = 1; // because there exists just one boolean variable
  const rentRequired = await connection.getMinimumBalanceForRentExemption(
    accountSpace
  );

  // Allocating space for hello state account
  const allocateHelloAccountIx = SystemProgram.createAccount({
    fromPubkey: feePayer.publicKey,
    lamports: rentRequired,
    newAccountPubkey: helloAccount.publicKey,
    programId: programId,
    space: accountSpace,
  });

  const initIx = new TransactionInstruction({
    programId: programId,
    keys: [
      {
        isSigner: true,
        isWritable: true,
        pubkey: feePayer.publicKey,
      },
      {
        isSigner: false,
        isWritable: true,
        pubkey: helloAccount.publicKey,
      },
    ],
  });

  const transaction = new Transaction();
  transaction.add(allocateHelloAccountIx, initIx);

  const txHash = await connection.sendTransaction(transaction, [
    feePayer,
    helloAccount,
  ]);

  console.log(`Transaction succeeded. TxHash: ${txHash}`);
})();

How to change account size

당신은 realloc을 사용해 Program이 소유한 Account의 크기를 변경할 수 있습니다. realloc은 Account를 10KB까지 리사이즈 할 수 있습니다. 당신이 Account의 크기를 증가시키기 위해 realloc을 사용할 때, Account의 rent 면제를 유지하기 위해서는 lamports를 보내야 합니다.

Press </> button to view full source
use {
  crate::{
      instruction::WhitelistInstruction,
      state::WhiteListData,
  },
  borsh::{BorshDeserialize, BorshSerialize},
  solana_program::{
      account_info::{next_account_info, AccountInfo},
      entrypoint::ProgramResult,
      msg,
      program::invoke_signed,
      program::invoke,
      program_error::ProgramError,
      pubkey::Pubkey,
      sysvar::Sysvar,
      sysvar::rent::Rent,
      system_instruction,
  },
  std::convert::TryInto,
};

pub fn process_instruction(
  _program_id: &Pubkey,
  accounts: &[AccountInfo],
  input: &[u8],
) -> ProgramResult {
  // Length = BOOL + VEC + Pubkey * n (n = number of keys)
  const INITIAL_ACCOUNT_LEN: usize = 1 + 4 + 0 ;
  msg!("input: {:?}", input);

  let instruction = WhitelistInstruction::try_from_slice(input)?;

  let accounts_iter = &mut accounts.iter();

  let funding_account = next_account_info(accounts_iter)?;
  let pda_account = next_account_info(accounts_iter)?;
  let system_program = next_account_info(accounts_iter)?;

  match instruction {
    WhitelistInstruction::Initialize => {
      msg!("Initialize");

      let (pda, pda_bump) = Pubkey::find_program_address(
          &[
            b"customaddress",
            &funding_account.key.to_bytes(),
          ],
          _program_id,
      );

      let signers_seeds: &[&[u8]; 3] = &[
          b"customaddress",
          &funding_account.key.to_bytes(),
          &[pda_bump],
      ];
      
      if pda.ne(&pda_account.key) {
          return Err(ProgramError::InvalidAccountData);
      }

      let lamports_required = Rent::get()?.minimum_balance(INITIAL_ACCOUNT_LEN);
      let create_pda_account_ix = system_instruction::create_account(
          &funding_account.key,
          &pda_account.key,
          lamports_required,
          INITIAL_ACCOUNT_LEN.try_into().unwrap(),
          &_program_id,
      );

      invoke_signed(
          &create_pda_account_ix,
          &[
              funding_account.clone(),
              pda_account.clone(),
              system_program.clone(),
          ],
          &[signers_seeds],
      )?;
      
      let mut pda_account_state = WhiteListData::try_from_slice(&pda_account.data.borrow())?;

      pda_account_state.is_initialized = true;
      pda_account_state.white_list = Vec::new();
      pda_account_state.serialize(&mut &mut pda_account.data.borrow_mut()[..])?;
      Ok(())
    }
    WhitelistInstruction::AddKey { key } => {
      msg!("AddKey");

      let mut pda_account_state = WhiteListData::try_from_slice(&pda_account.data.borrow())?;
      
      if !pda_account_state.is_initialized {
          return Err(ProgramError::InvalidAccountData);
      }

      let new_size = pda_account.data.borrow().len() + 32;

      let rent = Rent::get()?;
      let new_minimum_balance = rent.minimum_balance(new_size);
      
      let lamports_diff = new_minimum_balance.saturating_sub(pda_account.lamports());
      invoke(
          &system_instruction::transfer(funding_account.key, pda_account.key, lamports_diff),
          &[
              funding_account.clone(),
              pda_account.clone(),
              system_program.clone(),
          ],
      )?;

      pda_account.realloc(new_size, false)?;

      pda_account_state.white_list.push(key);
      pda_account_state.serialize(&mut &mut pda_account.data.borrow_mut()[..])?;

      Ok(())
    }
  }
}

How to do Cross Program Invocation

Program 간의 호출은 당신의 Program안에서 단순히 또 다른 Program의 Instruction을 호출하는 것입니다. 하나의 좋은 예제는 Uniwap의 swap 기능입니다. UniswapV2Router Contract는 스왑을 위해 필요한 로직을 호출하고, 한 사람으로부터 다른 사람에게 스왑 하기 위해 ERC20 Contract의 전송 기능을 호출합니다. 같은 방식으로, 우리는 Program의 Instruction을 다양한 목적으로 호출할 수 있습니다.

첫 번째 예제 SPL Token Program's transfer Instruction을 살펴봅시다. 전송을 위해 필요한 Account들:

  1. The Source Token Account (우리의 Token들을 담고 있는 Account)
  2. The Destination Token Account (우리의 Token들을 전송할 대상 Account)
  3. The Source Token Account's Holder (우리가 서명할 우리의 지갑 Address)
Press </> button to view full source
use solana_program::{
    account_info::{next_account_info, AccountInfo},
    entrypoint,
    entrypoint::ProgramResult,
    msg,
    program::invoke,
    program_error::ProgramError,
    pubkey::Pubkey,
};
use spl_token::instruction::transfer;

entrypoint!(process_instruction);

// Accounts required
/// 1. [writable] Source Token Account
/// 2. [writable] Destination Token Account
/// 3. [signer] Source Token Account holder's PubKey
/// 4. [] Token Program
pub fn process_instruction(
    _program_id: &Pubkey,
    accounts: &[AccountInfo],
    instruction_data: &[u8],
) -> ProgramResult {
    let accounts_iter = &mut accounts.iter();

    // Accounts required for token transfer

    // 1. Token account we hold
    let source_token_account = next_account_info(accounts_iter)?;
    // 2. Token account to send to
    let destination_token_account = next_account_info(accounts_iter)?;
    // 3. Our wallet address
    let source_token_account_holder = next_account_info(accounts_iter)?;
    // 4. Token Program
    let token_program = next_account_info(accounts_iter)?;

    // Parsing the token transfer amount from instruction data
    // a. Getting the 0th to 8th index of the u8 byte array
    // b. Converting the obtained non zero u8 to a proper u8 (as little endian integers)
    // c. Converting the little endian integers to a u64 number
    let token_transfer_amount = instruction_data
        .get(..8)
        .and_then(|slice| slice.try_into().ok())
        .map(u64::from_le_bytes)
        .ok_or(ProgramError::InvalidAccountData)?;

    msg!(
        "Transferring {} tokens from {} to {}",
        token_transfer_amount,
        source_token_account.key.to_string(),
        destination_token_account.key.to_string()
    );

    // Creating a new TransactionInstruction
    /*
        Internal representation of the instruction's return value (Result<Instruction, ProgramError>)

        Ok(Instruction {
            program_id: *token_program_id, // PASSED FROM USER
            accounts,
            data,
        })
    */

    let transfer_tokens_instruction = transfer(
        &token_program.key,
        &source_token_account.key,
        &destination_token_account.key,
        &source_token_account_holder.key,
        &[&source_token_account_holder.key],
        token_transfer_amount,
    )?;

    let required_accounts_for_transfer = [
        source_token_account.clone(),
        destination_token_account.clone(),
        source_token_account_holder.clone(),
    ];

    // Passing the TransactionInstruction to send
    invoke(
        &transfer_tokens_instruction,
        &required_accounts_for_transfer,
    )?;

    msg!("Transfer successful");

    Ok(())
}

대응되는 Client Instruction은 아래 있습니다. mint와 token 생성 Instruction을 알고 싶다면, 근처에 있는 전체 코드를 참조하세요.

Press </> button to view full source
import {
  clusterApiUrl,
  Connection,
  Keypair,
  LAMPORTS_PER_SOL,
  PublicKey,
  SystemProgram,
  Transaction,
  TransactionInstruction,
} from "@solana/web3.js";
import {
  AccountLayout,
  MintLayout,
  Token,
  TOKEN_PROGRAM_ID,
  u64,
} from "@solana/spl-token";

import * as BN from "bn.js";

// Users
const PAYER_KEYPAIR = Keypair.generate();
const RECEIVER_KEYPAIR = Keypair.generate().publicKey;

// Mint and token accounts
const TOKEN_MINT_ACCOUNT = Keypair.generate();
const SOURCE_TOKEN_ACCOUNT = Keypair.generate();
const DESTINATION_TOKEN_ACCOUNT = Keypair.generate();

// Numbers
const DEFAULT_DECIMALS_COUNT = 9;
const TOKEN_TRANSFER_AMOUNT = 50 * 10 ** DEFAULT_DECIMALS_COUNT;
const TOKEN_TRANSFER_AMOUNT_BUFFER = Buffer.from(
  Uint8Array.of(...new BN(TOKEN_TRANSFER_AMOUNT).toArray("le", 8))
);

(async () => {
  const connection = new Connection(clusterApiUrl("devnet"), "confirmed");
  const programId = new PublicKey(
    "EfYK91eN3AqTwY1C34W6a33qGAtQ8HJYVhNv7cV4uMZj"
  );

  const mintDataSpace = MintLayout.span;
  const mintRentRequired = await connection.getMinimumBalanceForRentExemption(
    mintDataSpace
  );

  const tokenDataSpace = AccountLayout.span;
  const tokenRentRequired = await connection.getMinimumBalanceForRentExemption(
    tokenDataSpace
  );

  // Airdropping some SOL
  await connection.confirmTransaction(
    await connection.requestAirdrop(PAYER_KEYPAIR.publicKey, LAMPORTS_PER_SOL)
  );

  // Allocating space and rent for mint account
  const createMintAccountIx = SystemProgram.createAccount({
    fromPubkey: PAYER_KEYPAIR.publicKey,
    lamports: mintRentRequired,
    newAccountPubkey: TOKEN_MINT_ACCOUNT.publicKey,
    programId: TOKEN_PROGRAM_ID,
    space: mintDataSpace,
  });

  // Initializing mint with decimals and authority
  const initializeMintIx = Token.createInitMintInstruction(
    TOKEN_PROGRAM_ID,
    TOKEN_MINT_ACCOUNT.publicKey,
    DEFAULT_DECIMALS_COUNT,
    PAYER_KEYPAIR.publicKey, // mintAuthority
    PAYER_KEYPAIR.publicKey // freezeAuthority
  );

  // Allocating space and rent for source token account
  const createSourceTokenAccountIx = SystemProgram.createAccount({
    fromPubkey: PAYER_KEYPAIR.publicKey,
    newAccountPubkey: SOURCE_TOKEN_ACCOUNT.publicKey,
    lamports: tokenRentRequired,
    programId: TOKEN_PROGRAM_ID,
    space: tokenDataSpace,
  });

  // Initializing token account with mint and owner
  const initializeSourceTokenAccountIx = Token.createInitAccountInstruction(
    TOKEN_PROGRAM_ID,
    TOKEN_MINT_ACCOUNT.publicKey,
    SOURCE_TOKEN_ACCOUNT.publicKey,
    PAYER_KEYPAIR.publicKey
  );

  // Minting tokens to the source token account for transferring later to destination account
  const mintTokensIx = Token.createMintToInstruction(
    TOKEN_PROGRAM_ID,
    TOKEN_MINT_ACCOUNT.publicKey,
    SOURCE_TOKEN_ACCOUNT.publicKey,
    PAYER_KEYPAIR.publicKey,
    [PAYER_KEYPAIR],
    TOKEN_TRANSFER_AMOUNT
  );

  // Allocating space and rent for destination token account
  const createDestinationTokenAccountIx = SystemProgram.createAccount({
    fromPubkey: PAYER_KEYPAIR.publicKey,
    newAccountPubkey: DESTINATION_TOKEN_ACCOUNT.publicKey,
    lamports: tokenRentRequired,
    programId: TOKEN_PROGRAM_ID,
    space: tokenDataSpace,
  });

  // Initializing token account with mint and owner
  const initializeDestinationTokenAccountIx =
    Token.createInitAccountInstruction(
      TOKEN_PROGRAM_ID,
      TOKEN_MINT_ACCOUNT.publicKey,
      DESTINATION_TOKEN_ACCOUNT.publicKey,
      RECEIVER_KEYPAIR
    );

  // Our program's CPI instruction (transfer)
  const transferTokensIx = new TransactionInstruction({
    programId: programId,
    data: TOKEN_TRANSFER_AMOUNT_BUFFER,
    keys: [
      {
        isSigner: false,
        isWritable: true,
        pubkey: SOURCE_TOKEN_ACCOUNT.publicKey,
      },
      {
        isSigner: false,
        isWritable: true,
        pubkey: DESTINATION_TOKEN_ACCOUNT.publicKey,
      },
      {
        isSigner: true,
        isWritable: true,
        pubkey: PAYER_KEYPAIR.publicKey,
      },
      {
        isSigner: false,
        isWritable: false,
        pubkey: TOKEN_PROGRAM_ID,
      },
    ],
  });

  const transaction = new Transaction();
  // Adding up all the above instructions
  transaction.add(
    createMintAccountIx,
    initializeMintIx,
    createSourceTokenAccountIx,
    initializeSourceTokenAccountIx,
    mintTokensIx,
    createDestinationTokenAccountIx,
    initializeDestinationTokenAccountIx,
    transferTokensIx
  );

  const txHash = await connection.sendTransaction(transaction, [
    PAYER_KEYPAIR,
    TOKEN_MINT_ACCOUNT,
    SOURCE_TOKEN_ACCOUNT,
    DESTINATION_TOKEN_ACCOUNT,
  ]);

  console.log(`Token transfer CPI success: ${txHash}`);
})();

이제 또 다른 System Program's create_account Insctuction 예제를 살펴봅시다. 위에 언급한 Instruction과 이것은 약간의 차이가 있습니다. 우리는 invoke 함수 안에 있는 Account들 중 하나로 token_program을 보내지 않아야 합니다. 그러나, 호출하는 Instruction은 program_id를 보내도록 요구받는 예외 케이스들이 있습니다. 우리의 경우 System Program's program_id("11111111111111111111111111111111") 입니다. 그래서 이제 필요한 Account들은 아래와 같습니다: ``

  1. rent를 지불하는 payer Account
  2. 생성될 Account
  3. System Program Account
Press </> button to view full source
use solana_program::{
    account_info::{next_account_info, AccountInfo},
    entrypoint,
    entrypoint::ProgramResult,
    msg,
    program::invoke,
    program_error::ProgramError,
    pubkey::Pubkey,
    rent::Rent,
    system_instruction::create_account,
    sysvar::Sysvar,
};

entrypoint!(process_instruction);

// Accounts required
/// 1. [signer, writable] Payer Account
/// 2. [signer, writable] General State Account
/// 3. [] System Program
pub fn process_instruction(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
    instruction_data: &[u8],
) -> ProgramResult {
    let accounts_iter = &mut accounts.iter();

    // Accounts required for token transfer

    // 1. Payer account for the state account creation
    let payer_account = next_account_info(accounts_iter)?;
    // 2. Token account we hold
    let general_state_account = next_account_info(accounts_iter)?;
    // 3. System Program
    let system_program = next_account_info(accounts_iter)?;

    msg!(
        "Creating account for {}",
        general_state_account.key.to_string()
    );

    // Parsing the token transfer amount from instruction data
    // a. Getting the 0th to 8th index of the u8 byte array
    // b. Converting the obtained non zero u8 to a proper u8 (as little endian integers)
    // c. Converting the little endian integers to a u64 number
    let account_span = instruction_data
        .get(..8)
        .and_then(|slice| slice.try_into().ok())
        .map(u64::from_le_bytes)
        .ok_or(ProgramError::InvalidAccountData)?;

    let lamports_required = (Rent::get()?).minimum_balance(account_span as usize);

    // Creating a new TransactionInstruction
    /*
        Internal representation of the instruction's return value (Instruction)

        Instruction::new_with_bincode(
            system_program::id(), // NOT PASSED FROM USER
            &SystemInstruction::CreateAccount {
                lamports,
                space,
                owner: *owner,
            },
            account_metas,
        )
    */

    let create_account_instruction = create_account(
        &payer_account.key,
        &general_state_account.key,
        lamports_required,
        account_span,
        program_id,
    );

    let required_accounts_for_create = [
        payer_account.clone(),
        general_state_account.clone(),
        system_program.clone(),
    ];

    // Passing the TransactionInstruction to send (with the issused program_id)
    invoke(&create_account_instruction, &required_accounts_for_create)?;

    msg!("Transfer successful");

    Ok(())
}

Client 사이드 코드는 아래처럼 보일 것입니다.

Press </> button to view full source
import { clusterApiUrl, Connection, Keypair } from "@solana/web3.js";
import { LAMPORTS_PER_SOL, PublicKey, SystemProgram } from "@solana/web3.js";
import { Transaction, TransactionInstruction } from "@solana/web3.js";

import * as BN from "bn.js";

// Users
const PAYER_KEYPAIR = Keypair.generate();
const GENERAL_STATE_KEYPAIR = Keypair.generate();

const ACCOUNT_SPACE_BUFFER = Buffer.from(
  Uint8Array.of(...new BN(100).toArray("le", 8))
);

(async () => {
  const connection = new Connection(clusterApiUrl("devnet"), "confirmed");
  const programId = new PublicKey(
    "DkuQ5wsndkzXfgqDB6Lgf4sDjBi4gkLSak1dM5Mn2RuQ"
  );

  // Airdropping some SOL
  await connection.confirmTransaction(
    await connection.requestAirdrop(PAYER_KEYPAIR.publicKey, LAMPORTS_PER_SOL)
  );

  // Our program's CPI instruction (create_account)
  const createAccountIx = new TransactionInstruction({
    programId: programId,
    data: ACCOUNT_SPACE_BUFFER,
    keys: [
      {
        isSigner: true,
        isWritable: true,
        pubkey: PAYER_KEYPAIR.publicKey,
      },
      {
        isSigner: true,
        isWritable: true,
        pubkey: GENERAL_STATE_KEYPAIR.publicKey,
      },
      {
        isSigner: false,
        isWritable: false,
        pubkey: SystemProgram.programId,
      },
    ],
  });

  const transaction = new Transaction();
  // Adding up all the above instructions
  transaction.add(createAccountIx);

  const txHash = await connection.sendTransaction(transaction, [
    PAYER_KEYPAIR,
    GENERAL_STATE_KEYPAIR,
  ]);

  console.log(`Create Account CPI Success: ${txHash}`);
})();

How to create a PDA

Program Derived Address는 쉽게 말해 private key를 갖고 있지 않은 프로그램이 소유한 Account입니다. 대신, seeds와 bump(곡선 밖에 있도록 만드는 넌스 값) 값에 의해 서명됩니다. Program Address를 "Generating" 하는 것은 "creating" 하는 것과는 다릅니다. 누군가 Pubkey::find_program_address를 사용해 PDA를 generate 할 수 있습니다. PDA를 creating 하는 것은 주소 공간을 초기화하고 상태를 설정하는 것을 의미합니다. 보통의 키쌍을 가진 Account는 Program 밖에서 생성될 수 있고 상태를 초기화할 수 있습니다. 불행히도, PDA는 서명할 수 없는 특성 때문에 On-Chain에서 생성될 수 있습니다. 따라서 PDA의 생성을 위해 funding Account의 서명과 함께 PDA의 seeds를 넘기기 위해 invoke_signed를 사용합니다.

Press </> button to view full source
use borsh::{BorshDeserialize, BorshSerialize};
use solana_program::{
    account_info::{next_account_info, AccountInfo},
    entrypoint,
    entrypoint::ProgramResult,
    program::invoke_signed,
    program_error::ProgramError,
    pubkey::Pubkey,
    rent::Rent,
    system_instruction,
    sysvar::Sysvar,
};

entrypoint!(process_instruction);

#[derive(BorshSerialize, BorshDeserialize, Debug)]
pub struct HelloState {
    is_initialized: bool,
}

// Accounts required
/// 1. [signer, writable] Funding account
/// 2. [writable] PDA account
/// 3. [] System Program
pub fn process_instruction(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
    instruction_data: &[u8],
) -> ProgramResult {
    const ACCOUNT_DATA_LEN: usize = 1;

    let accounts_iter = &mut accounts.iter();
    // Getting required accounts
    let funding_account = next_account_info(accounts_iter)?;
    let pda_account = next_account_info(accounts_iter)?;
    let system_program = next_account_info(accounts_iter)?;

    // Getting PDA Bump from instruction data
    let (pda_bump, _) = instruction_data
        .split_first()
        .ok_or(ProgramError::InvalidInstructionData)?;

    // Checking if passed PDA and expected PDA are equal
    let signers_seeds: &[&[u8]; 3] = &[
        b"customaddress",
        &funding_account.key.to_bytes(),
        &[*pda_bump],
    ];
    let pda = Pubkey::create_program_address(signers_seeds, program_id)?;

    if pda.ne(&pda_account.key) {
        return Err(ProgramError::InvalidAccountData);
    }

    // Assessing required lamports and creating transaction instruction
    let lamports_required = Rent::get()?.minimum_balance(ACCOUNT_DATA_LEN);
    let create_pda_account_ix = system_instruction::create_account(
        &funding_account.key,
        &pda_account.key,
        lamports_required,
        ACCOUNT_DATA_LEN.try_into().unwrap(),
        &program_id,
    );
    // Invoking the instruction but with PDAs as additional signer
    invoke_signed(
        &create_pda_account_ix,
        &[
            funding_account.clone(),
            pda_account.clone(),
            system_program.clone(),
        ],
        &[signers_seeds],
    )?;

    // Setting state for PDA
    let mut pda_account_state = HelloState::try_from_slice(&pda_account.data.borrow())?;
    pda_account_state.is_initialized = true;
    pda_account_state.serialize(&mut &mut pda_account.data.borrow_mut()[..])?;

    Ok(())
}

Client에서는 아래처럼 요구되는 Account들을 보낼 수 있습니다.

Press </> button to view full source
import {
  clusterApiUrl,
  Connection,
  Keypair,
  LAMPORTS_PER_SOL,
  PublicKey,
  SystemProgram,
  Transaction,
  TransactionInstruction,
} from "@solana/web3.js";

const PAYER_KEYPAIR = Keypair.generate();

(async () => {
  const connection = new Connection(clusterApiUrl("devnet"), "confirmed");
  const programId = new PublicKey(
    "6eW5nnSosr2LpkUGCdznsjRGDhVb26tLmiM1P8RV1QQp"
  );

  // Airdop to Payer
  await connection.confirmTransaction(
    await connection.requestAirdrop(PAYER_KEYPAIR.publicKey, LAMPORTS_PER_SOL)
  );

  const [pda, bump] = await PublicKey.findProgramAddress(
    [Buffer.from("customaddress"), PAYER_KEYPAIR.publicKey.toBuffer()],
    programId
  );

  console.log(`PDA Pubkey: ${pda.toString()}`);

  const createPDAIx = new TransactionInstruction({
    programId: programId,
    data: Buffer.from(Uint8Array.of(bump)),
    keys: [
      {
        isSigner: true,
        isWritable: true,
        pubkey: PAYER_KEYPAIR.publicKey,
      },
      {
        isSigner: false,
        isWritable: true,
        pubkey: pda,
      },
      {
        isSigner: false,
        isWritable: false,
        pubkey: SystemProgram.programId,
      },
    ],
  });

  const transaction = new Transaction();
  transaction.add(createPDAIx);

  const txHash = await connection.sendTransaction(transaction, [PAYER_KEYPAIR]);
  console.log(`Created PDA successfully. Tx Hash: ${txHash}`);
})();

How to read accounts

솔라나에서 거의 모든 Instruction들은 최소 2~3개의 Account들을 요구합니다. 그리고 그것들은 어떤 순서로 요구하는지에 대해 Instruction handler에 언급될 것입니다. 만약 Account들을 수동으로 명시하는 것 대신에 Rust에 있는 iter()의 이점을 활용한다면 간단해집니다. next_account_info 메소드는 기본적으로 iterable 한 객체를 자르고 Accout 배열에 있는 현재 Account를 반환합니다. 다수의 Account들과 그들 각각에 대한 파싱을 요구하는 간단한 Instruction 예제를 봅시다.

Press </> button to view full source
use borsh::{BorshDeserialize, BorshSerialize};
use solana_program::{
    account_info::{next_account_info, AccountInfo},
    entrypoint,
    entrypoint::ProgramResult,
    pubkey::Pubkey,
};

entrypoint!(process_instruction);

#[derive(BorshSerialize, BorshDeserialize, Debug)]
pub struct HelloState {
    is_initialized: bool,
}

// Accounts required
/// 1. [signer] Payer
/// 2. [writable] Hello state account
/// 3. [] Rent account
/// 4. [] System Program
pub fn process_instruction(
    _program_id: &Pubkey,
    accounts: &[AccountInfo],
    _instruction_data: &[u8],
) -> ProgramResult {
    // Fetching all the accounts as a iterator (facilitating for loops and iterations)
    let accounts_iter = &mut accounts.iter();
    // Payer account
    let payer_account = next_account_info(accounts_iter)?;
    // Hello state account
    let hello_state_account = next_account_info(accounts_iter)?;
    // Rent account
    let rent_account = next_account_info(accounts_iter)?;
    // System Program
    let system_program = next_account_info(accounts_iter)?;

    Ok(())
}

How to verify accounts

Solana Program에는 상태가 존재하지 않기 때문에, Program creator는 악의적인 Account의 진입을 피하기 위해 보내려는 Account들을 가능한 많이 검증해야 합니다. 아래와 같은 기본적인 사항들을 체크할 수 있습니다.

  1. 예상한 대로 서명 Account가 실제로 서명한 것인지 확인
  2. 예상한 대로 상태 Account가 writable로 표시되었는지 확인
  3. 예상한 대로 상태 Account의 소유자가 호출된 program id와 같은지 확인
  4. 처음으로 상태를 초기화하는 거라면, Account가 이미 초기화되었는지 확인
  5. 필요할 때 Cross Program id들이 넘어왔는지 확인

아래는 위에서 언급한 체크 사항들이 정의된 에제로, 상태 Account를 초기화하는 기본 Instruction입니다.

Press </> button to view full source
use borsh::{BorshDeserialize, BorshSerialize};
use solana_program::{
    account_info::{next_account_info, AccountInfo},
    clock::Clock,
    entrypoint,
    entrypoint::ProgramResult,
    msg,
    program_error::ProgramError,
    pubkey::Pubkey,
    rent::Rent,
    system_program::ID as SYSTEM_PROGRAM_ID,
    sysvar::Sysvar,
};

entrypoint!(process_instruction);

#[derive(BorshSerialize, BorshDeserialize, Debug)]
pub struct HelloState {
    is_initialized: bool,
}

// Accounts required
/// 1. [signer] Payer
/// 2. [writable] Hello state account
/// 3. [] System Program
pub fn process_instruction(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
    _instruction_data: &[u8],
) -> ProgramResult {
    let accounts_iter = &mut accounts.iter();
    // Payer account
    let payer_account = next_account_info(accounts_iter)?;
    // Hello state account
    let hello_state_account = next_account_info(accounts_iter)?;
    // System Program
    let system_program = next_account_info(accounts_iter)?;

    let rent = Rent::get()?;

    // Checking if payer account is the signer
    if !payer_account.is_signer {
        return Err(ProgramError::MissingRequiredSignature);
    }

    // Checking if hello state account is rent exempt
    if !rent.is_exempt(hello_state_account.lamports(), 1) {
        return Err(ProgramError::AccountNotRentExempt);
    }

    // Checking if hello state account is writable
    if !hello_state_account.is_writable {
        return Err(ProgramError::InvalidAccountData);
    }

    // Checking if hello state account's owner is the current program
    if hello_state_account.owner.ne(&program_id) {
        return Err(ProgramError::IllegalOwner);
    }

    // Checking if the system program is valid
    if system_program.key.ne(&SYSTEM_PROGRAM_ID) {
        return Err(ProgramError::IncorrectProgramId);
    }

    let mut hello_state = HelloState::try_from_slice(&hello_state_account.data.borrow())?;

    // Checking if the state has already been initialized
    if hello_state.is_initialized {
        return Err(ProgramError::AccountAlreadyInitialized);
    }

    hello_state.is_initialized = true;
    hello_state.serialize(&mut &mut hello_state_account.data.borrow_mut()[..])?;
    msg!("Account initialized :)");

    Ok(())
}

How to read multiple instructions from a transaction

Solana는 현재 Transaction의 모든 Instruction들을 엿볼 수 있게 해 줍니다. 우리는 Instruction들을 변수에 저장하고 반복시킵니다. 우린 이걸 가지고 의심되는 Transaction들을 검사한다던지 등 많은 것들을 할 수 있습니다.

Press </> button to view full source
use anchor_lang::{
    prelude::*,
    solana_program::{
        sysvar,
        serialize_utils::{read_pubkey,read_u16}
    }
};


declare_id!("8DJXJRV8DBFjJDYyU9cTHBVK1F1CTCi6JUBDVfyBxqsT");

#[program]
pub mod cookbook {
    use super::*;

    pub fn read_multiple_instruction<'info>(ctx: Context<ReadMultipleInstruction>, creator_bump: u8) -> Result<()> {
        let instruction_sysvar_account = &ctx.accounts.instruction_sysvar_account;

        let instruction_sysvar_account_info = instruction_sysvar_account.to_account_info();

        let id = "8DJXJRV8DBFjJDYyU9cTHBVK1F1CTCi6JUBDVfyBxqsT";

        let instruction_sysvar = instruction_sysvar_account_info.data.borrow();

        let mut idx = 0;

        let num_instructions = read_u16(&mut idx, &instruction_sysvar)
        .map_err(|_| MyError::NoInstructionFound)?;

        for index in 0..num_instructions {
            let mut current = 2 + (index * 2) as usize;
            let start = read_u16(&mut current, &instruction_sysvar).unwrap();

            current = start as usize;
            let num_accounts = read_u16(&mut current, &instruction_sysvar).unwrap();
            current += (num_accounts as usize) * (1 + 32);
            let program_id = read_pubkey(&mut current, &instruction_sysvar).unwrap();

            if program_id != id
            {
                msg!("Transaction had ix with program id {}", program_id);
                return Err(MyError::SuspiciousTransaction.into());
            }
        }

        Ok(())
    }

}

#[derive(Accounts)]
#[instruction(creator_bump:u8)]
pub struct ReadMultipleInstruction<'info> {
    #[account(address = sysvar::instructions::id())]
    instruction_sysvar_account: UncheckedAccount<'info>
}

#[error_code]
pub enum MyError {
    #[msg("No instructions found")]
    NoInstructionFound,
    #[msg("Suspicious transaction detected")]
    SuspiciousTransaction
}
Last Updated:
Contributors: TaeGit