Peta Akun

Peta (Maps) adalah sebuah struktur data yang sering kita gunakan dalam pemrograman untuk mengaitkan sebuah kunci dengan suatu nilai. Kunci dan nilai ini dapat bertipe data apa saja. Kunci ini berperan sebagai pengenal dari nilai yang diberikan yang sedang disimpan. Kunci dari peta ini memungkinkan kita untuk memasukkan, mengambil dan memperbarui nilai tersebut secara efisien.

Model dari akun Solana seperti yang kita ketahui memerlukan data program dan data posisi saat itu untuk dapat disimpan di akun yang berbeda. Akun tersebut memiliki alamat yang berkaitan dengan diri mereka. Hal ini sendiri sebenarnya merupakan sebuah peta! Pelajari lebih lanjut mengenai model akun solana disiniopen in new window.

Jadi, tentunya masuk akal untuk menyimpan nilai di akun yang berbeda, dengan alamat berperan sebagai kunci yang dibutuhkan untuk mengambil nilainya. Tetapi terdapat beberapa masalah yang muncul akibat metode ini, antara lain

  • Alamat - alamat yang disebutkan di atas kemungkinan besar bukanlah sebuah kunci yang ideal, yang dapat Anda ingat dan gunakan untuk mengambil nilai yang diperlukan.

  • Alamat - alamat yang disebutkan di atas merujuk ke kunci publik dari Pasangan Kunci yang berbeda, dimana setiap kunci publik (atau alamat) memiliki kunci pribadi yang berkaitan dengannya. Kunci pribadi ini akan diperlukan untuk menandatangani instruksi yang berbeda jika dan bila diperlukan, mengharuskan kita untuk menyimpan kunci pribadi di suatu tempat, yang tentunya tidak direkomendasikan!

Ini menghadirkan masalah yang dihadapi banyak pengembang Solana, yang menerapkan logika seperti Peta ke dalam program mereka. Mari kita lihat beberapa cara bagaimana kita akan mengatasi masalah ini,

Menghasilkan PDA

PDA adalah singkatan dari Program Derived Addressopen in new window, dan secara singkat merupakan alamat - alamat yang diperoleh dari sekumpulan benih, dan id program (atau alamat).

Hal unik tentang PDA adalah, alamat ini tidak terkait dengan kunci pribadi apa pun. Ini karena alamat ini tidak terletak pada kurva ED25519. Oleh karena itu, hanya program dari mana alamat ini diturunkan yang dapat menandatangani instruksi dengan kunci tersebut, dengan menyediakan benih juga. Pelajari lebih lanjut tentang ini di siniopen in new window.

Sekarang setelah mengetahui apa itu PDA, mari kita gunakan mereka untuk memetakan beberapa akun! Kita akan mengambil sebuah contoh dari sebuah program Blog untuk mendemonstrasikan bagaimana hal ini dapat diimplementasikan.

Di dalam program Blog ini, kita ingin agar setiap Pengguna untuk memiliki sebuah Blog. Blog ini dapat memiliki sejumlah Artikel. Itu berarti kita memetakan setiap pengguna ke sebuah blog, dan setiap artikel dipetakan ke blog tertentu.

Singkatnya, ada pemetaan 1:1 antara pengguna dan blognya, sedangkan pemetaan 1:N antara blog dan artikelnya.

Untuk pemetaan 1:1, kita ingin alamat blog diturunkan hanya dari penggunanya, yang memungkinkan kita mengambil blog berdasarkan otoritasnya (atau pengguna). Oleh karena itu, benih untuk blog akan terdiri dari kunci otoritas, dan mungkin awalan "blog", untuk bertindak sebagai pengenal tipe.

Untuk pemetaan 1:N, kita ingin setiap alamat artikel diturunkan tidak hanya dari blog yang terkait dengannya, tetapi juga pengidentifikasi lain, yang memungkinkan kita untuk membedakan antara N jumlah artikel di blog. Dalam contoh di bawah ini, setiap alamat artikel diturunkan dari kunci blog, sebuah slug untuk mengidentifikasi setiap postingan, dan awalan "post", untuk bertindak sebagai pengenal tipe.

Untuk kodenya dapat dilihat pada gambar di bawah ini,

Press </> button to view full source
#[derive(Accounts)]
#[instruction(blog_account_bump: u8)]
pub struct InitializeBlog<'info> {
    #[account(
        init,
        seeds = [
            b"blog".as_ref(),
            authority.key().as_ref()
        ],
        bump = blog_account_bump,
        payer = authority,
        space = Blog::LEN
    )]
    pub blog_account: Account<'info, Blog>,

    #[account(mut)]
    pub authority: Signer<'info>,

    pub system_program: Program<'info, System>
}

#[derive(Accounts)]
#[instruction(post_account_bump: u8, post: Post)]
pub struct CreatePost<'info> {
    #[account(mut, has_one = authority)]
    pub blog_account: Account<'info, Blog>,

    #[account(
        init,
        seeds = [
            b"post".as_ref(),
            blog_account.key().as_ref(),
            post.slug.as_ref(),
        ],
        bump = post_account_bump,
        payer = authority,
        space = Post::LEN
    )]
    pub post_account: Account<'info, Post>,

    #[account(mut)]
    pub authority: Signer<'info>,
    
    pub system_program: Program<'info, System>
}
fn process_create_post(
    accounts: &[AccountInfo],
    slug: String,
    title: String,
    content: String,
    program_id: &Pubkey
) -> ProgramResult {
    if slug.len() > 10 || content.len() > 20 || title.len() > 50 {
        return Err(BlogError::InvalidPostData.into())
    }

    let account_info_iter = &mut accounts.iter();

    let authority_account = next_account_info(account_info_iter)?;
    let blog_account = next_account_info(account_info_iter)?;
    let post_account = next_account_info(account_info_iter)?;
    let system_program = next_account_info(account_info_iter)?;

    if !authority_account.is_signer {
        return Err(ProgramError::MissingRequiredSignature);
    }

    let (blog_pda, _blog_bump) = Pubkey::find_program_address(
        &[b"blog".as_ref(), authority_account.key.as_ref()],
        program_id
    );
    if blog_pda != *blog_account.key || !blog_account.is_writable || blog_account.data_is_empty() {
        return Err(BlogError::InvalidBlogAccount.into())
    }

    let (post_pda, post_bump) = Pubkey::find_program_address(
        &[b"post".as_ref(), slug.as_ref(), authority_account.key.as_ref()],
        program_id
    );
    if post_pda != *post_account.key {
        return Err(BlogError::InvalidPostAccount.into())
    }

    let post_len: usize = 32 + 32 + 1 + (4 + slug.len()) + (4 + title.len()) + (4 + content.len());

    let rent = Rent::get()?;
    let rent_lamports = rent.minimum_balance(post_len);

    let create_post_pda_ix = &system_instruction::create_account(
        authority_account.key,
        post_account.key,
        rent_lamports,
        post_len.try_into().unwrap(),
        program_id
    );
    msg!("Creating post account!");
    invoke_signed(
        create_post_pda_ix, 
        &[
            authority_account.clone(),
            post_account.clone(),
            system_program.clone()
        ],
        &[&[
            b"post".as_ref(),
            slug.as_ref(),
            authority_account.key.as_ref(),
            &[post_bump]
        ]]
    )?;

    let mut post_account_state = try_from_slice_unchecked::<Post>(&post_account.data.borrow()).unwrap();
    post_account_state.author = *authority_account.key;
    post_account_state.blog = *blog_account.key;
    post_account_state.bump = post_bump;
    post_account_state.slug = slug;
    post_account_state.title = title;
    post_account_state.content = content;

    msg!("Serializing Post data");
    post_account_state.serialize(&mut &mut post_account.data.borrow_mut()[..])?;


    let mut blog_account_state = Blog::try_from_slice(&blog_account.data.borrow())?;
    blog_account_state.post_count += 1;

    msg!("Serializing Blog data");
    blog_account_state.serialize(&mut &mut blog_account.data.borrow_mut()[..])?;

    Ok(())
}

fn process_init_blog(
    accounts: &[AccountInfo],
    program_id: &Pubkey
) -> ProgramResult {
    let account_info_iter = &mut accounts.iter();
    
    let authority_account = next_account_info(account_info_iter)?;
    let blog_account = next_account_info(account_info_iter)?;
    let system_program = next_account_info(account_info_iter)?;

    if !authority_account.is_signer {
        return Err(ProgramError::MissingRequiredSignature);
    }

    let (blog_pda, blog_bump) = Pubkey::find_program_address(
        &[b"blog".as_ref(), authority_account.key.as_ref()],
        program_id 
    );
    if blog_pda != *blog_account.key {
        return Err(BlogError::InvalidBlogAccount.into())
    }

    let rent = Rent::get()?;
    let rent_lamports = rent.minimum_balance(Blog::LEN);
    
    let create_blog_pda_ix = &system_instruction::create_account(
        authority_account.key,
        blog_account.key,
        rent_lamports,
        Blog::LEN.try_into().unwrap(),
        program_id
    );
    msg!("Creating blog account!");
    invoke_signed(
        create_blog_pda_ix, 
        &[
            authority_account.clone(),
            blog_account.clone(),
            system_program.clone()
        ],
        &[&[
            b"blog".as_ref(),
            authority_account.key.as_ref(),
            &[blog_bump]
        ]]
    )?;

    let mut blog_account_state = Blog::try_from_slice(&blog_account.data.borrow())?;
    blog_account_state.authority = *authority_account.key;
    blog_account_state.bump = blog_bump;
    blog_account_state.post_count = 0;
    blog_account_state.serialize(&mut &mut blog_account.data.borrow_mut()[..])?;
    

    Ok(())
}

Di sisi klien, Anda dapat menggunakan PublicKey.findProgramAddress() untuk memperoleh alamat akun Blog dan Artikel yang diperlukan, yang dapat Anda teruskan ke connection.getAccountInfo() untuk mengambil data akun. Contoh ditunjukkan di bawah ini,

Press </> button to view full source
async () => {
  const connection = new Connection("http://localhost:8899", "confirmed");

  const [blogAccount] = await PublicKey.findProgramAddress(
    [Buffer.from("blog"), user.publicKey.toBuffer()],
    MY_PROGRAM_ID
  );

  const [postAccount] = await PublicKey.findProgramAddress(
    [Buffer.from("post"), Buffer.from("slug-1"), user.publicKey.toBuffer()],
    MY_PROGRAM_ID
  );

  const blogAccountInfo = await connection.getAccountInfo(blogAccount);
  const blogAccountState = BLOG_ACCOUNT_DATA_LAYOUT.decode(
    blogAccountInfo.data
  );
  console.log("Blog account state: ", blogAccountState);

  const postAccountInfo = await connection.getAccountInfo(postAccount);
  const postAccountState = POST_ACCOUNT_DATA_LAYOUT.decode(
    postAccountInfo.data
  );
  console.log("Post account state: ", postAccountState);
};

Akun Peta Tunggal

Cara lain untuk mengimplementasikan pemetaan adalah dengan memiliki struktur data BTreeMap yang disimpan secara eksplisit dalam satu akun. Alamat akun ini sendiri bisa berupa PDA, atau kunci publik dari pasangan kunci yang dihasilkan.

Metode pemetaan akun ini tidak ideal karena alasan berikut,

  • Anda harus menginisialisasi akun yang menyimpan BTreeMap terlebih dahulu, sebelum Anda dapat memasukkan pasangan nilai kunci yang diperlukan ke dalamnya. Kemudian, Anda juga harus menyimpan alamat akun ini di suatu tempat, untuk memperbaruinya setiap saat.

  • Ada batasan memori untuk sebuah akun, di mana sebuah akun dapat memiliki ukuran maksimum 10 megabita, yang membatasi BTreeMap untuk menyimpan sejumlah besar pasangan nilai kunci.

Oleh karena itu, setelah mempertimbangkan kasus penggunaan Anda, Anda dapat menerapkan metode ini seperti yang ditunjukkan di bawah ini,

Press </> button to view full source
fn process_init_map(accounts: &[AccountInfo], program_id: &Pubkey) -> ProgramResult {
    let account_info_iter = &mut accounts.iter();

    let authority_account = next_account_info(account_info_iter)?;
    let map_account = next_account_info(account_info_iter)?;
    let system_program = next_account_info(account_info_iter)?;

    if !authority_account.is_signer {
        return Err(ProgramError::MissingRequiredSignature)
    }

    let (map_pda, map_bump) = Pubkey::find_program_address(
        &[b"map".as_ref()],
        program_id
    );

    if map_pda != *map_account.key || !map_account.is_writable || !map_account.data_is_empty() {
        return Err(BlogError::InvalidMapAccount.into())
    }

    let rent = Rent::get()?;
    let rent_lamports = rent.minimum_balance(MapAccount::LEN);

    let create_map_ix = &system_instruction::create_account(
        authority_account.key, 
        map_account.key, 
        rent_lamports, 
        MapAccount::LEN.try_into().unwrap(), 
        program_id
    );

    msg!("Creating MapAccount account");
    invoke_signed(
        create_map_ix, 
        &[
            authority_account.clone(),
            map_account.clone(),
            system_program.clone()
        ],
        &[&[
            b"map".as_ref(),
            &[map_bump]
        ]]
    )?;

    msg!("Deserializing MapAccount account");
    let mut map_state = try_from_slice_unchecked::<MapAccount>(&map_account.data.borrow()).unwrap();
    let empty_map: BTreeMap<Pubkey, Pubkey> = BTreeMap::new();

    map_state.is_initialized = 1;
    map_state.map = empty_map;

    msg!("Serializing MapAccount account");
    map_state.serialize(&mut &mut map_account.data.borrow_mut()[..])?;

    Ok(())
}

fn process_insert_entry(accounts: &[AccountInfo], program_id: &Pubkey) -> ProgramResult {
    
    let account_info_iter = &mut accounts.iter();

    let a_account = next_account_info(account_info_iter)?;
    let b_account = next_account_info(account_info_iter)?;
    let map_account = next_account_info(account_info_iter)?;

    if !a_account.is_signer {
        return Err(ProgramError::MissingRequiredSignature)
    }

    if map_account.data.borrow()[0] == 0 || *map_account.owner != *program_id {
        return Err(BlogError::InvalidMapAccount.into())
    }

    msg!("Deserializing MapAccount account");
    let mut map_state = try_from_slice_unchecked::<MapAccount>(&map_account.data.borrow())?;

    if map_state.map.contains_key(a_account.key) {
        return Err(BlogError::AccountAlreadyHasEntry.into())
    }

    map_state.map.insert(*a_account.key, *b_account.key);
    
    msg!("Serializing MapAccount account");
    map_state.serialize(&mut &mut map_account.data.borrow_mut()[..])?;

    Ok(())
}

Kode pada sisi klien untuk menguji program di atas akan terlihat seperti yang ditunjukkan di bawah ini,

Press </> button to view full source
const insertABIx = new TransactionInstruction({
  programId: MY_PROGRAM_ID,
  keys: [
    {
      pubkey: userA.publicKey,
      isSigner: true,
      isWritable: true,
    },
    {
      pubkey: userB.publicKey,
      isSigner: false,
      isWritable: false,
    },
    {
      pubkey: mapKey,
      isSigner: false,
      isWritable: true,
    },
  ],
  data: Buffer.from(Uint8Array.of(1)),
});

const insertBCIx = new TransactionInstruction({
  programId: MY_PROGRAM_ID,
  keys: [
    {
      pubkey: userB.publicKey,
      isSigner: true,
      isWritable: true,
    },
    {
      pubkey: userC.publicKey,
      isSigner: false,
      isWritable: false,
    },
    {
      pubkey: mapKey,
      isSigner: false,
      isWritable: true,
    },
  ],
  data: Buffer.from(Uint8Array.of(1)),
});

const insertCAIx = new TransactionInstruction({
  programId: MY_PROGRAM_ID,
  keys: [
    {
      pubkey: userC.publicKey,
      isSigner: true,
      isWritable: true,
    },
    {
      pubkey: userA.publicKey,
      isSigner: false,
      isWritable: false,
    },
    {
      pubkey: mapKey,
      isSigner: false,
      isWritable: true,
    },
  ],
  data: Buffer.from(Uint8Array.of(1)),
});

const tx = new Transaction();
tx.add(initMapIx);
tx.add(insertABIx);
tx.add(insertBCIx);
tx.add(insertCAIx);
Last Updated: 9/20/2022, 1:22:28 AM
Contributors: akangaziz