Solana'da CRUD dApp Nasıl Oluşturulur
Bu kılavuzda, temel bir on-chain CRUD dApp için Solana programını ve UI'ını nasıl oluşturacağınızı ve dağıtacağınızı öğreneceksiniz. Bu dApp, günlük girişleri oluşturmanıza, güncellemenize, okumanıza ve tüm bunları on-chain işlemlerle silmenize olanak tanır.
Ne Öğreneceksiniz
- Ortamınızı kurma
npx create-solana-dapp
kullanma- Anchor program geliştirme
- Anchor PDA'ları ve hesaplar
- Bir Solana programını dağıtma
- On-chain bir programı test etme
- Bir on-chain programı React UI ile bağlama
Ön Koşullar
Bu kılavuz için, birkaç araç ile yerel geliştirme ortamınızı ayarlamanız gerekiyor:
Projeyi Kurma
npx create-solana-dapp
Bu CLI komutu, hızlı Solana dApp oluşturulmasını sağlayacaktır. Kaynak kodunu buradan bulabilirsiniz.
Şimdi, aşağıdaki gibi yanıt verin:
- Proje adı girin:
my-journal-dapp
- Şablon seçin:
Next.js
- UI kütüphanesi seçin:
Tailwind
- Anchor şablonu seçin:
counter
programı
counter
şablonunu seçerek, Rust ile Anchor çerçevesi kullanılarak yazılmış basit bir sayıcı programı
sizin için oluşturulacaktır.
Bu oluşturulan şablon programı düzenlemeye başlamadan önce, her şeyin beklenildiği gibi çalıştığından emin olalım:
cd my-journal-dapp
npm install
npm run dev
Anchor ile Solana Programı Yazma
Eğer Anchor ile yeniyseniz, The Anchor Book ve Anchor Examples öğrenmenize yardımcı olacak harika referanslardır.
my-journal-dapp
içinde anchor/programs/journal/src/lib.rs
dizinine gidin. Bu klasörde zaten oluşturulmuş şablon kodu olacaktır. Bunu silip sıfırdan başlayalım ve her adımı birlikte inceleyelim.
Anchor Programınızı Tanımlayın
use anchor_lang::prelude::*;
// Bu, programınızın genel anahtarıdır ve proje derlendiğinde otomatik olarak güncellenecektir.
declare_id!("7AGmMcgd1SjoMsCcXAAYwRgB9ihCyM8cZqjsUqriNRQt");
#[program]
pub mod journal {
use super::*;
}
Program Durumunuzu Tanımlayın
Durum, hesaba kaydetmek istediğiniz bilgileri tanımlamak için kullanılan veri yapısıdır. Solana on-chain programlarının depolama alanı olmadığından, veriler blok zincirinde yaşayan hesaplarda saklanır.
Anchor kullanırken, program durumunuzu tanımlamak için #[account]
nitelik makrosu kullanılır.
#[account]
#[derive(InitSpace)]
pub struct JournalEntryState {
pub owner: Pubkey,
#[max_len(50)]
pub title: String,
#[max_len(1000)]
pub message: String,
}
Bu günlük dApp için saklayacağımız veriler:
- Günlüğün sahibi
- Her günlük girişinin başlığı
- Her günlük girişinin mesajı
Bir hesabı başlatırken alan tanımlanmalıdır. Yukarıdaki kodda kullanılan InitSpace
makrosu, bir hesabı başlatırken gerekli alanı hesaplamaya yardımcı olacaktır. Alan hakkında daha fazla bilgi için buraya bakın.
Bir Günlük Girişi Oluşturun
Şimdi, bu programa yeni bir günlük girişi oluşturan bir talimat işleyici
ekleyelim. Bunu yapmak için, daha önce tanımladığımız #[program]
kodunu create_journal_entry
talimatını eklemek üzere güncellememiz gerekecek.
Bir günlük girişi oluştururken, kullanıcı günlük girişinin title
ve message
değerlerini sağlamalıdır. Bu nedenle bu iki değişkeni ek argümanlar olarak eklememiz gerekecek.
Bu talimat işleyici fonksiyonunu çağırdığımızda, hesabın owner
değerini, günlük girişinin title
değerini ve günlük girişinin message
değerini JournalEntryState
hesabına kaydetmek istiyoruz.
#[program]
mod journal {
use super::*;
pub fn create_journal_entry(
ctx: Context<CreateEntry>,
title: String,
message: String,
) -> Result<()> {
msg!("Günlük Girişi Oluşturuldu");
msg!("Başlık: {}", title);
msg!("Mesaj: {}", message);
let journal_entry = &mut ctx.accounts.journal_entry;
journal_entry.owner = ctx.accounts.owner.key();
journal_entry.title = title;
journal_entry.message = message;
Ok(())
}
}
Anchor çerçevesiyle, her talimat ilk argüman olarak bir Context
türü alır. Context
makrosu, belirli bir talimat işleyiciye geçirilecek hesapları kapsayan bir yapıyı tanımlamak için kullanılır. Bu nedenle, her Context
, talimat işleyicisine göre belirtilmiş bir tür içermelidir. Bizim durumumuzda, CreateEntry
için bir veri yapısı tanımlamamız gerekiyor:
#[derive(Accounts)]
#[instruction(title: String, message: String)]
pub struct CreateEntry<'info> {
#[account(
init_if_needed,
seeds = [title.as_bytes(), owner.key().as_ref()],
bump,
payer = owner,
space = 8 + JournalEntryState::INIT_SPACE
)]
pub journal_entry: Account<'info, JournalEntryState>,
#[account(mut)]
pub owner: Signer<'info>,
pub system_program: Program<'info, System>,
}
Yukarıdaki kodda, şu makroları kullandık:
#[derive(Accounts)]
makrosu, yapı içinde belirtilen hesapların seri hale getirilmesi ve doğrulaması için kullanılır.#[instruction(...)]
nitelik makrosu, talimat işleyicisine geçirilen talimat verilerine erişmek için kullanılır.#[account(...)]
nitelik makrosu ise hesaplara ek kısıtlamalar belirtir.
Her günlük girişi, on-chain durumu saklayan Bir Program Türetilmiş Adrestir ( PDA
). Burada yeni bir günlük girişi oluşturduğumuz için, init_if_needed
kısıtlamasını kullanarak başlatılmalıdır.
Anchor ile bir PDA, seeds
, bumps
ve init_if_needed
kısıtlamaları ile başlatılır. init_if_needed
kısıtlaması ayrıca, bu hesabın verilerini on-chain tutmak için kimin kira ödeyeceği ve o veriler için ne kadar alan ayrılması gerektiğini tanımlayan payer
ve space
kısıtlamalarını gerektirir.
Not: JournalEntryState
içindeki InitSpace
makrosunu kullanarak, gerekli alanı INIT_SPACE
sabitini kullanarak hesaplayabiliyoruz ve Anchor'ın içsel ayırtıcısı için alan kısıtlamasına 8
ekliyoruz.
Bir Günlük Girişini Güncelleme
Artık yeni bir günlük girişi oluşturabiliyoruz, şimdi bir update_journal_entry
talimat işleyici ekleyelim ve bir UpdateEntry
türünde bir bağlam tanımlayalım.
Bunu yapmak için, talimat, JournalEntryState
hesabına kaydedilen belirli bir PDA'nın verilerini yeniden yazacak/güncelleyerek güncellemelidir.
#[program]
mod journal {
use super::*;
...
pub fn update_journal_entry(
ctx: Context<UpdateEntry>,
title: String,
message: String,
) -> Result<()> {
msg!("Günlük Girişi Güncellendi");
msg!("Başlık: {}", title);
msg!("Mesaj: {}", message);
let journal_entry = &mut ctx.accounts.journal_entry;
journal_entry.message = message;
Ok(())
}
}
#[derive(Accounts)]
#[instruction(title: String, message: String)]
pub struct UpdateEntry<'info> {
#[account(
mut,
seeds = [title.as_bytes(), owner.key().as_ref()],
bump,
realloc = 8 + 32 + 1 + 4 + title.len() + 4 + message.len(),
realloc::payer = owner,
realloc::zero = true,
)]
pub journal_entry: Account<'info, JournalEntryState>,
#[account(mut)]
pub owner: Signer<'info>,
pub system_program: Program<'info, System>,
}
Yukarıdaki kodda, günlük girişi
oluşturmayla çok benzer olduğunu fark etmelisiniz, ancak birkaç önemli fark var. update_journal_entry
, zaten var olan bir PDA'yı düzenlediğinden, onu başlatmamıza gerek yok. Ancak, talimat işleyiciye geçirilen mesaj, saklamak için gereken alan boyutunda farklılık gösterebileceğinden (örneğin message
daha kısa veya daha uzun olabilir), hesabın on-chain alanını yeniden tahsis etmek için birkaç belirli realloc
kısıtlaması kullanmamız gerekecektir:
realloc
- gerekli yeni alanı belirlerrealloc::payer
- yeni alan gereksinimlerine göre hesaplanacak ödemeyi yapacak hesabı tanımlarrealloc::zero
-true
olarak ayarlandığında, hesabın birden fazla kez güncellenebileceğini belirtir
seeds
ve bump
kısıtlamaları da güncellenmek istenen belirli PDA'yı bulabilmek için gereklidir.
mut
kısıtlaması, hesap içindeki verileri değiştirmemize/mutasyon yapmamıza izin verir. Solana blok zincirinin, hesaplardan okuma ve yazma işlemlerini farklı şekilde ele alması nedeniyle, Solana runtime'ının bunları doğru şekilde işlemesi için hangi hesapların değiştirilebilir olduğunu açıkça belirtmemiz gerekir.
Not: Solana'da, boyutunu değiştiren bir yeniden tahsis edildiğinde, işlemin yeni hesap boyutunun kirasını karşılaması gerekir. realloc::payer = owner
niteliği, sahip hesabının kirayı ödeyeceğini gösterir. Bir hesabın kira ödemesini karşılayabilmesi için genellikle bir imzacı olması gerekir (fonların düşürülmesine izin vermek için) ve Anchor içinde, aynı zamanda mutasyon geçirebilmesi için değiştirilebilir olması gerekmektedir, böylece runtime, kirayı karşılamak için gereken lamportları bu hesaptan düşebilir.
Bir Günlük Girişini Silme
Son olarak, DeleteEntry
türünde bir bağlam ile bir delete_journal_entry
talimat işleyici ekleyeceğiz.
Bunu yapmak için, belirli bir günlük girişi için hesabı kapatmamız gerekiyor.
#[program]
mod journal {
use super::*;
...
pub fn delete_journal_entry(_ctx: Context<DeleteEntry>, title: String) -> Result<()> {
msg!("{} başlıklı günlük girişi silindi", title);
Ok(())
}
}
#[derive(Accounts)]
#[instruction(title: String)]
pub struct DeleteEntry<'info> {
#[account(
mut,
seeds = [title.as_bytes(), owner.key().as_ref()],
bump,
close = owner,
)]
pub journal_entry: Account<'info, JournalEntryState>,
#[account(mut)]
pub owner: Signer<'info>,
pub system_program: Program<'info, System>,
}
Yukarıdaki kodda, close
kısıtlamasını kullanarak on-chain'de hesabı kapatır ve kira bedelini günlük girişinin sahibine iade eder.
Hesabı doğrulamak için seeds
ve bump
kısıtlamaları gereklidir.
Anchor Programınızı Derleyin ve Yayınlayın
npm run anchor build
npm run anchor deploy
Bir Solana Programını UI ile Bağlama
create-solana-dapp
zaten sizin için bir cüzdan bağlantılı UI ayarlamaktadır. Tek yapmamız gereken, yeni oluşturduğunuz programla uyumlu hale getirmek için ufak değişiklikler yapmak.
Bu günlük programında üç talimat olduğundan, UI'da bu talimatların her birini çağırmak için bileşenlere ihtiyacımız olacak:
- Giriş oluşturma
- Giriş güncelleme
- Giriş silme
Projenizin reposunda, günlük girişlerini çağırmak için kod eklemek üzere web/components/journal/journal-data-access.tsx
dosyasını açın.
Giriş oluşturmak için useJournalProgram
fonksiyonunu güncelleyin:
const createEntry = useMutation<string, Error, CreateEntryArgs>({
mutationKey: ["journalEntry", "create", { cluster }],
mutationFn: async ({ title, message, owner }) => {
const [journalEntryAddress] = await PublicKey.findProgramAddress(
[Buffer.from(title), owner.toBuffer()],
programId,
);
return program.methods
.createJournalEntry(title, message)
.accounts({
journalEntry: journalEntryAddress,
})
.rpc();
},
onSuccess: signature => {
transactionToast(signature);
accounts.refetch();
},
onError: error => {
toast.error(`Günlük girişi oluşturulamadı: ${error.message}`);
},
});
Ardından, girişleri güncellemek ve silmek için useJournalProgramAccount
fonksiyonunu güncelleyin:
const updateEntry = useMutation<string, Error, CreateEntryArgs>({
mutationKey: ["journalEntry", "update", { cluster }],
mutationFn: async ({ title, message, owner }) => {
const [journalEntryAddress] = await PublicKey.findProgramAddress(
[Buffer.from(title), owner.toBuffer()],
programId,
);
return program.methods
.updateJournalEntry(title, message)
.accounts({
journalEntry: journalEntryAddress,
})
.rpc();
},
onSuccess: signature => {
transactionToast(signature);
accounts.refetch();
},
onError: error => {
toast.error(`Günlük girişi güncellenemedi: ${error.message}`);
},
});
const deleteEntry = useMutation({
mutationKey: ["journal", "deleteEntry", { cluster, account }],
mutationFn: (title: string) =>
program.methods
.deleteJournalEntry(title)
.accounts({ journalEntry: account })
.rpc(),
onSuccess: tx => {
transactionToast(tx);
return accounts.refetch();
},
});
Sonra, bir günlük girişini oluştururken başlık ve mesaj için kullanıcı girdi değerini almak üzere UI'ı web/components/journal/journal-ui.tsx
dosyasında güncelleyin:
export function JournalCreate() {
const { createEntry } = useJournalProgram();
const { publicKey } = useWallet();
const [title, setTitle] = useState("");
const [message, setMessage] = useState("");
const isFormValid = title.trim() !== "" && message.trim() !== "";
const handleSubmit = () => {
if (publicKey && isFormValid) {
createEntry.mutateAsync({ title, message, owner: publicKey });
}
};
if (!publicKey) {
return <p>Cüzdanınızı bağlayın</p>;
}
return (
<div>
<input
type="text"
placeholder="Başlık"
value={title}
onChange={e => setTitle(e.target.value)}
className="input input-bordered w-full max-w-xs"
/>
<textarea
placeholder="Mesaj"
value={message}
onChange={e => setMessage(e.target.value)}
className="textarea textarea-bordered w-full max-w-xs"
/>
<br></br>
<button
type="button"
className="btn btn-xs lg:btn-md btn-primary"
onClick={handleSubmit}
disabled={createEntry.isPending || !isFormValid}
>
Günlük Girişi Oluştur {createEntry.isPending && "..."}
</button>
</div>
);
}
Son olarak, bir günlük girişini güncellerken mesaj için kullanıcı girdi değerini almak üzere journal-ui.tsx
dosyasını güncelleyin:
function JournalCard({ account }: { account: PublicKey }) {
const { accountQuery, updateEntry, deleteEntry } = useJournalProgramAccount({
account,
});
const { publicKey } = useWallet();
const [message, setMessage] = useState("");
const title = accountQuery.data?.title;
const isFormValid = message.trim() !== "";
const handleSubmit = () => {
if (publicKey && isFormValid && title) {
updateEntry.mutateAsync({ title, message, owner: publicKey });
}
};
if (!publicKey) {
return <p>Cüzdanınızı bağlayın</p>;
}
return accountQuery.isLoading ? (
<span className="loading loading-spinner loading-lg"></span>
) : (
<div className="card card-bordered border-base-300 border-4 text-neutral-content">
<div className="card-body items-center text-center">
<div className="space-y-6">
<h2
className="card-title justify-center text-3xl cursor-pointer"
onClick={() => accountQuery.refetch()}
>
{accountQuery.data?.title}
</h2>
<p>{accountQuery.data?.message}</p>
<div className="card-actions justify-around">
<textarea
placeholder="Mesajı güncelleyin"
value={message}
onChange={e => setMessage(e.target.value)}
className="textarea textarea-bordered w-full max-w-xs"
/>
<button
className="btn btn-xs lg:btn-md btn-primary"
onClick={handleSubmit}
disabled={updateEntry.isPending || !isFormValid}
>
Günlük Girişini Güncelle {updateEntry.isPending && "..."}
</button>
</div>
<div className="text-center space-y-4">
<p>
<ExplorerLink
path={`account/${account}`}
label={ellipsify(account.toString())}
/>
</p>
<button
className="btn btn-xs btn-secondary btn-outline"
onClick={() => {
if (
!window.confirm(
"Bu hesabı kapatmak istediğinize emin misiniz?",
)
) {
return;
}
const title = accountQuery.data?.title;
if (title) {
return deleteEntry.mutateAsync(title);
}
}}
disabled={deleteEntry.isPending}
>
Kapat
</button>
</div>
</div>
</div>
</div>
);
}
Kaynaklar
- Günlük dApp:
solana-journal-eight.vercel.app - Örnek kod:
https://github.com/solana-foundation/CRUD-dApp