Programme schreiben

So übertragen Sie SOL in einem Programm

Ihr Solana-Programm kann Lamports von einem Konto auf ein anderes übertragen ohne das Systemprogramm "aufzurufen". Die Grundregel ist die Ihr Programm kann Lamports von jedem Konto übertragen, das Ihrem Programm überhaupt auf irgendein Konto gehört .

Das Empfängerkonto muss nicht ein Konto Ihres Programms sein.

/// 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(())
}

Wie bekomme ich die Uhr in ein Programm?

Das Erhalten einer Uhr kann auf zwei Arten erfolgen

  1. Übergabe von SYSVAR_CLOCK_PUBKEY an eine Anweisung
  2. Direkter Zugriff auf die Uhr innerhalb einer Anweisung.

Es ist schön, beide Methoden zu kennen, da einige ältere Programme immer noch den SYSVAR_CLOCK_PUBKEY als Konto erwarten.

Passing Clock als Konto innerhalb einer Anweisung

Lassen Sie uns eine Anweisung erstellen, die ein Konto zum Initialisieren und den Sysvar-Pubkey erhält

Press </> button to view full source
let clock = Clock::from_account_info(&sysvar_clock_pubkey)?;
let current_timestamp = clock.unix_timestamp;

Nun übergeben wir die öffentliche Sysvar-Adresse der Uhr über den Client

Press </> button to view full source
(async () => {
  const programId = new PublicKey(
    "77ezihTV6mTh2Uf3ggwbYF2NyGJJ5HHah1GrdowWJVD3"
  );

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

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

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

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

Direkter Zugriff auf die Uhr innerhalb einer Anweisung

Lassen Sie uns dieselbe Anweisung erstellen, aber ohne den SYSVAR_CLOCK_PUBKEY von der Client-Seite zu erwarten.

Press </> button to view full source
let clock = Clock::get()?;
let current_timestamp = clock.unix_timestamp;

Die clientseitige Anweisung muss jetzt nur noch die Staats- und Zahlerkonten übergeben.

Press </> button to view full source
(async () => {
  const programId = new PublicKey(
    "4ZEdbCtb5UyCSiAMHV5eSHfyjq3QwbG3yXb6oHD7RYjk"
  );

  // No more requirement to pass clock sys var key
  const initAccountIx = new TransactionInstruction({
    programId: programId,
    keys: [
      {
        isSigner: false,
        isWritable: true,
        pubkey: helloAccount.publicKey,
      },
    ],
  });

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

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

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

So ändern Sie die Kontogröße

Sie können die Größe eines programmeigenen Kontos mit der Verwendung ändern von "realloc". realloc kann die Größe eines Kontos auf bis zu 10 KB ändern. Wenn Sie "realloc" verwenden, um die Größe eines Kontos zu erhöhen, müssen Sie Lamports übertragen, um dieses Konto mietfrei zu behalten.

Press </> button to view full source
// adding a publickey to the account
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)?;

Wie man einen programmübergreifenden Aufruf durchführt

Ein programmübergreifender Aufruf wird einfach als Aufruf von Programmanweisungen innerhalb unseres Programms bezeichnet

. Ein bestes Beispiel hervorzuheben ist die "swap"-Funktionalität von Uniswap. Der UniswapV2Router Vertrag, ruft die notwendige Logik auf, und ruft die Übertragungsfunktion des "ERC20"-Vertrags auf, die von einer Person zur anderen wechseln. Genauso können wir die Anweisung eines Programms aufrufen, um eine Vielzahl von Zwecken zu erfüllen.

Werfen wir einen Blick auf unser erstes Beispiel, nämlich die Anweisung zur Übertragung des SPL-Token-Programms. Die erforderlichen Konten, die wir für eine Überweisung benötigen, sind

  1. Das Quell-Token-Konto (Das Konto, auf dem wir unsere Token halten)
  2. Das Ziel-Token-Konto (das Konto, auf das wir unsere Token übertragen würden)
  3. Inhaber des Quell-Token-Kontos (Unsere Wallet-Adresse, für die wir unterschreiben würden)
Press </> button to view full source
let token_transfer_amount = instruction_data
    .get(..8)
    .and_then(|slice| slice.try_into().ok())
    .map(u64::from_le_bytes)
    .ok_or(ProgramError::InvalidAccountData)?;

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(),
];

invoke(
    &transfer_tokens_instruction,
    &required_accounts_for_transfer,
)?;


Die entsprechende Client-Anweisung wäre wie folgt. Um die Mint- und Token-Erstellungsanweisungen zu kennen, lesen Sie bitte den vollständigen Code in der Nähe.

Press </> button to view full source
(async () => {
  const connection = new Connection(clusterApiUrl("devnet"), "confirmed");
  const programId = new PublicKey(
    "EfYK91eN3AqTwY1C34W6a33qGAtQ8HJYVhNv7cV4uMZj"
  );

  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();
  transaction.add(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}`);
})();

Schauen wir uns nun ein weiteres Beispiel an, nämlich die Anweisung create_account des Systemprogramms. Es gibt einen kleinen Unterschied zwischen der oben erwähnten Anweisung und dieser. Dort mussten wir das token_program nie als eines der Konten innerhalb der invoke-Funktion übergeben. Es gibt jedoch Ausnahmen, bei denen Sie die program_id der aufrufenden Anweisung übergeben müssen. In unserem Fall wäre es die program_id des Systemprogramms. ("11111111111111111111111111111111"). So jetzt wären die benötigten Accounts

  1. Das Zahlerkonto, das die Miete finanziert
  2. Das Konto, das erstellt werden soll
  3. Systemprogrammkonto
Press </> button to view full source
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);

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(),
];

invoke(&create_account_instruction, &required_accounts_for_create)?;

Der entsprechende clientseitige Code sieht wie folgt aus

Press </> button to view full source
(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 creataAccountIx = 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(creataAccountIx);

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

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

So erstellen Sie einen PDA

Eine vom Programm abgeleitete Adresse ist einfach ein Konto, das dem Programm gehört, aber keinen privaten Schlüssel hat. Stattdessen wird seine Signatur durch eine Reihe von Samen und eine Beule (eine Nonce, die sicherstellt, dass es außerhalb der Kurve liegt) erhalten. Das „Generieren“ einer Programmadresse unterscheidet sich von dem „Erstellen“. Man kann einen PDA mit Pubkey::find_program_address generieren. Das Erstellen eines PDA bedeutet im Wesentlichen, die Adresse mit Leerzeichen zu initialisieren und den Status darauf zu setzen. Ein normales Keypair-Konto kann außerhalb unseres Programms erstellt und dann zur Initialisierung seines Status gefüttert werden. Leider wurde es für PDAs in einer Kette erstellt, da es nicht möglich ist, im eigenen Namen zu unterzeichnen. Daher verwenden wir "invoke_signed", um die Samen des PDA zusammen mit der Signatur des Finanzierungskontos zu übergeben, was zur Kontoerstellung eines PDA führt.

Press </> button to view full source
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,
);

invoke_signed(
    &create_pda_account_ix,
    &[funding_account.clone(), pda_account.clone()],
    &[signers_seeds],
)?;

Man kann die erforderlichen Konten wie folgt per Client senden

Press </> button to view full source
const PAYER_KEYPAIR = Keypair.generate();

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

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

  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]);
})();

Wie man Konten liest

Fast alle Anweisungen in Solana würden mindestens 2 - 3 Konten erfordern, und sie würden über den Anweisungshandlern erwähnt, in welcher Reihenfolge diese Konten erwartet werden. Es ist ziemlich einfach, wenn wir die Methode iter() in Rust nutzen, anstatt die Konten manuell zu indizieren. Die Methode "next_account_info" schneidet im Grunde den ersten Index des Iterables und gibt das Konto zurück, das im Konten-Array vorhanden ist. Sehen wir uns eine einfache Anweisung an, die eine Reihe von Konten erwartet und jedes von ihnen analysieren muss.

Press </> button to view full source
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(())
}

So verifizieren Sie Konten

Da Programme in Solana zustandslos sind, müssen wir als Programmersteller sicherstellen, dass die übergebenen Konten so weit wie möglich validiert werden, um einen böswilligen Kontoeintrag zu vermeiden. Die grundlegenden Überprüfungen, die man durchführen kann, sind

  1. Überprüfen Sie, ob das erwartete Unterzeichnerkonto tatsächlich unterschrieben hat
  2. Überprüfen Sie, ob die erwarteten Statuskonten als beschreibbar markiert wurden
  3. Überprüfen Sie, ob der Besitzer des erwarteten Statuskontos die aufgerufene Programm-ID ist
  4. Wenn Sie den Status zum ersten Mal initialisieren, überprüfen Sie, ob das Konto bereits initialisiert wurde oder nicht.
  5. Überprüfen Sie, ob alle übergebenen programmübergreifenden IDs (falls erforderlich) wie erwartet sind.

Eine grundlegende Anweisung, die ein Heldenstatuskonto initialisiert, jedoch mit den oben erwähnten Überprüfungen, wird unten definiert.

Press </> button to view full source
pub fn process_instruction(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
    _instruction_data: &[u8],
) -> ProgramResult {
    let accounts_iter = &mut accounts.iter();
    let payer_account = next_account_info(accounts_iter)?;
    let hello_state_account = next_account_info(accounts_iter)?;
    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(())
}

So lesen Sie mehrere Anweisungen aus einer Transaktion

Solana ermöglicht es uns, einen Blick auf alle Anweisungen in der aktuellen Transaktion zu werfen. Wir können sie in einer Variablen und speichern über sie iterieren. Wir können damit viele Dinge tun, z. B. nach verdächtigen Transaktionen suchen.

Press </> button to view full source
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);

}
Last Updated: 12/29/2022, 8:27:01 PM
Contributors: nyk