Токени, PDA (Program Derived Address) та CPI (Cross-Program Invocation) — три механізми, які перетворюють Solana з простої машини транзакцій на платформу для складних децентралізованих застосунків. Токени зберігають цінність, PDA дає програмам власність над даними, а CPI дозволяє програмам взаємодіяти між собою. Без розуміння цих трьох шарів неможливо написати жоден робочий смарт-контракт на Solana.
Версії для прикладів у «Токени, PDA та CPI» перевірено 2 серпня 2026 року. Стабільна гілка Anchor v1 має релізи 1.0.x і орієнтується на Solana 3.x; Anchor v2 у документації позначений як alpha. Приклади для Anchor 0.29–0.32 залишаються лише відтворюваними прикладами для зафіксованого legacy-середовища: їх не слід переносити в новий проєкт без міграції залежностей і повторного тестування. Клієнт @anchor-lang/core сумісний із legacy @solana/web3.js v1, а не з v2.
Усі приклади нижче використовують кластер Devnet, Solana CLI 1.18.x, Anchor 0.30.x та @solana/spl-token 0.3.x. Перед виконанням переконайтеся, що ви налаштували Devnet:
Середовище: Solana CLI 1.18.x, Anchor 0.30.x, Node.js 20.x, кластер Devnet
Передумова: встановлені solana-cli, anchor, @solana/web3.js, @solana/spl-token
Перевірка кластера: solana config get має показувати rpc url: https://api.devnet.solana.com
Що таке SPL Token та як він працює
Огляд SPL Token Program
SPL Token Program — це окрема програма на Solana, яка реалізує стандарт токенів. Вона не вбудована в runtime, а розгортається як звичайна програма з адресою TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA. Будь-яка ваша програма може взаємодіяти з нею через CPI.
SPL Token Program працює з двома основними об'єктами: mint (емісійний акаунт, що визначає тип токена) та token account (акаунт конкретного власника, що зберігає баланс).
Типи токен-акаунтів
- Mint-акаунт — описує токен: загальна пропозиція, кількість знаків після коми, хто має право mint-нути та freeze-нути. Кожен тип токена має рівно один mint.
- Token-акаунт — пов'язаний з конкретним mint та власником. Зберігає поточний баланс. Один власник може мати багато token-акаунтів для одного й того ж mint (хоча зазвичай це один акаунт).
- Associated Token Account (ATA) — детерміновано згенерований token-акаунт за формулою з owner та mint. Це стандартний спосіб зберігання токенів для користувачів.
Операції з токенами
Основні інструкції SPL Token Program:
- InitializeMint — створює новий mint-акаунт із заданими параметрами.
- MintTo — випускає токени на вказаний token-акаунт.
- Transfer / TransferChecked — переміщує токени між token-акаунтами. TransferChecked додатково перевіряє кількість знаків після коми.
- Burn — знищує токени, зменшуючи загальну пропозицію.
- FreezeAccount / ThawAccount — блокує або розблоковує token-акаунт (якщо mint має увімкнений freeze authority).
- CloseAccount — закриває token-акаунт, повертаючи rent власнику.
Token Program у контексті програм
Ваша програма на Solana не «містить» токени. Вона лише керує token-акаунтами через виклики до SPL Token Program. Програма може вимагати від користувача передати token-акаунт у інструкцію, перевірити його mint і потім викликати TransferChecked через CPI.
Типові помилки
- Передача mint-акаунта замість token-акаунта у функцію, що очікує token-акаунт — призводить до помилки декодування.
- Використання Transfer замість TransferChecked — безпечніше завжди використовувати TransferChecked, оскільки він верифікує decimals.
- Ігнорування rent-exempt вимоги — token-акаунти повинні мати мінімальний баланс для exemption від rent.
Token Extensions: огляд для початківця
Що таке Token Extensions
Token Extensions — це розширена версія SPL Token Program, відома як Token-2022 (адреса: TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb). Вона повністю зворотно сумісна з оригінальним Token Program, але додає можливість приєднувати розширення до mint- та token-акаунтів.
Основні розширення
- Transfer Hook — дозволяє викликати сторонню програму під час кожної передачі токена (корисно для податків, комісій, whitelisting).
- Transfer Fee — автоматично стягує відсоток при кожній передачі.
- Metadata — зберігає name, symbol, uri безпосередньо в mint-акаунті, без потреби в окремій програмі метаданих.
- Permanent Delegate — дозволяє делегату переміщувати токени з будь-якого акаунта без підпису власника.
- Confidential Transfers — приховані баланси та суми передач.
- Interest Bearing — токен із змінною процентною ставкою.
Як використовувати Token Extensions
У CLI використовуйте прапорець --program-id TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb при створенні токена. У TypeScript SDK імпортуйте функції з @solana/spl-token — бібліотека підтримує Token-2022 починаючи з версії 0.3.x через параметр programId.
Концептуальний приклад створення токена з метаданими через TypeScript (Devnet, @solana/spl-token 0.3.x):
Примітка: Це концептуальний приклад для розуміння структури. Для production використовуйте перевірені утиліти з офіційного репозиторію spl-token.
Ключова відмінність: при створенні mint через Token-2022 ви передаєте додаткові інструкції для ініціалізації розширень одразу після InitializeMint2.
Коли обрати Token-2022 замість Token
- Вам потрібні метадані безпосередньо в токені (RWA-токени, стейблкоїни).
- Потрібен Transfer Hook для кастомної логіки при передачах.
- Потрібна комісія за передачу (Transfer Fee).
- Ви створюєте новий проєкт і не маєте зворотної сумісності з існуючими token-акаунтами.
Якщо вам потрібен просто звичайний fungible токен без додаткової логіки — оригінальний Token Program достатній.
Типові помилки
- Створення token-акаунта через оригінальний Token Program для mint, що належить Token-2022 — акаунти не сумісні між програмами.
- Ігнорування додаткового простору, потрібного для розширень — mint-акаунт з розширеннями потребує більшого розміру, отже більшого deposit для rent-exempt.
- Передача programId оригінального Token Program у функції Token-2022 — призводить до невідповідності типів.
Як створити токен на Devnet
Підготовка
Переконайтеся, що ви на Devnet і маєте SOL для оплати транзакцій:
- solana config set --url https://api.devnet.solana.com
- solana airdrop 2 — отримайте 2 SOL на Devnet (якщо airdrop недоступний, використовуйте faucet з офіційного сайту Solana).
Створення токена через CLI
Створіть mint-акаунт з 9 знаками після коми:
spl-token create-token --decimals 9
CLI поверне адресу mint-акаунта. Збережіть її.
Mint-нення токенів
Спочатку створіть associated token account для вашого гаманця:
spl-token create-account <MINT_ADDRESS>
Потім випустіть токени:
spl-token mint <MINT_ADDRESS> 1000
Очікуваний результат: 1000 токенів (з урахуванням 9 decimals — це фактично 1000 * 10^9 найменших одиниць) на вашому token-акаунті.
Перевірка результату
spl-token balance <MINT_ADDRESS>
Має показати 1000.
Типові помилки
- Недостатньо SOL для rent-exempt token-акаунта — переконайтеся, що на гаманці є щонайменше 0.002 SOL понад витрати на створення mint.
- Неправильний decimals — після створення mint змінити decimals неможливо. Якщо вказали 0 замість 9, доведеться створювати новий токен.
- Спроба mint-нути більше, ніж дозволяє supply — за замовчуванням supply необмежений, але якщо ви вказали --supply, перевищення викличе помилку.
PDA простими словами для розробника
Що таке PDA
PDA (Program Derived Address) — це адреса акаунта, яка генерується детерміновано з набору seeds (довільних байтів) та адреси програми. Головна властивість: PDA не має відповідного приватного ключа. Це адреса, яка математично гарантовано лежить поза кривою ed25519, тому жоден ключ не може її підписати.
Як генерується PDA
Алгоритм: береться рядок seeds, до неї додається program_id, обчислюється SHA-256. Якщо результат — точка на кривій ed25519, до seeds додається байт bump (починаючи з 255) і процес повторюється. Перший bump, що дає точку поза кривою, стає частиною PDA.
Формула: PDA = SHA-256(seeds || program_id || bump), де bump — найменше значення від 255 вниз, що дає off-curve результат.
Навіщо потрібен PDA
- Власність програми над даними. Програма може створювати акаунти, які належать їй самій, а не користувачу. Це означає, що лише програма може модифікувати ці дані.
- Детерміновані адреси. Будь-хто, хто знає seeds та program_id, може обчислити PDA без запиту до блокчейну. Це дозволяє знаходити акаунти за логікою (наприклад, «акаунт конфігурації для цього токена»).
- Підпис від імені програми. Програма може «підписати» інструкцію від імені PDA через invoke_signed у CPI.
PDA як signer
Коли програма викликає іншу програму через CPI і передає PDA як signer, вона надає bump разом з інструкцією. Runtime Solana перевіряє, що PDA дійсно належить цій програмі і bump правильний. Це дозволяє PDA виконувати роль підписанта без приватного ключа.
Типові помилки
- Зберігання bump у стані програми замість передачі як аргумент — це припустимо, але не забувайте перевіряти, що збережений bump збігається з обчисленим.
- Використання змінних seeds — якщо seeds залежать від даних, які можуть змінюватися, PDA буде різним у різні моменти. Seeds мають бути стабільними.
- Спроба підписати PDA без invoke_signed — runtime відхилить транзакцію, оскільки PDA не має приватного ключа.
Як створити PDA у програмі
Визначення seeds
Seeds — це масив байтів, який однозначно ідентифікує PDA. Типові патерни:
- b"config" — для глобального акаунта конфігурації програми.
- [b"vault", mint.key().as_ref()] — для акаунта, прив'язаного до конкретного токена.
- [user.key().as_ref(), b"profile"] — для акаунта, прив'язаного до користувача.
Правило: seeds мають бути унікальними в межах однієї програми, щоб різні PDA не колізували.
Створення PDA через Anchor
У Anchor PDA оголошується через атрибут #[account] з полями seeds та bump:
Приклад (Anchor 0.30.x, Rust):
#[derive(Accounts)]
pub struct InitializeVault<'info> {
#[account(
init,
payer = authority,
space = 8 + 32 + 8,
seeds = [b"vault", mint.key().as_ref()],
bump
)]
pub vault: Account<'info, Vault>,
pub mint: Account<'info, Mint>,
#[account(mut)]
pub authority: Signer<'info>,
pub system_program: Program<'info, System>,
}
Очікуваний результат: Anchor автоматично обчислить PDA, створить акаунт, сплатить rent та збереже bump у vault-акаунті (якщо структура має поле bump: u8).
Знаходження існуючого PDA
Коли PDA вже створено і ви звертаєтесь до нього в іншій інструкції:
#[derive(Accounts)]
pub struct UseVault<'info> {
#[account(
seeds = [b"vault", mint.key().as_ref()],
bump = vault.bump
)]
pub vault: Account<'info, Vault>,
pub mint: Account<'info, Mint>,
}
Anchor перевірить, що переданий акаунт дійсно відповідає PDA з цими seeds і що bump збігається.
Використання PDA для зберігання стану
PDA-акаунти зберігають серіалізовані дані вашої структури. Розмір (space) розраховується як: 8 байтів (дискримінатор Anchor) + сума розмірів полів. Наприклад, для структури з Pubkey (32 байти) та u64 (8 байтів) потрібно 8 + 32 + 8 = 48 байтів.
Типові помилки
- Невірний розмір space — якщо вказати замало, ініціалізація завершиться помилкою. Якщо забагато — переплата rent.
- Забуття поля bump у структурі при використанні bump = vault.bump — Anchor не знайде поле для перевірки.
- Передача seeds у неправильному порядку — [b"vault", mint.as_ref()] і [mint.as_ref(), b"vault"] дають різні PDA.
Що таке CPI у Solana
Означення CPI
CPI (Cross-Program Invocation) — це механізм, за якого одна програма на Solana викликає інструкцію іншої програми в межах тієї ж транзакції. Це аналог виклику функції з іншого модуля, але між різними програмами з різною логікою перевірки.
Як працює CPI
Коли програма A викликає програму B через CPI:
- Програма A формує інструкцію для програми B (якби вона була зовнішньою транзакцією).
- Викликає invoke або invoke_signed (якщо потрібно підписати PDA).
- Runtime Solana передає інструкцію програмі B, додаючи програму A до ланцюжка викликів.
- Програма B виконує інструкцію з тими ж акаунтами, що передала програма A.
- Після завершення B, управління повертається до A.
Глибина вкладеності CPI обмежена (зазвичай до 4 рівнів).
Обмеження CPI
- Глибина вкладеності: максимальна глибина ланцюжка CPI — 4. Перевищення призведе до помилки.
- Обчислювальний бюджет: кожен виклик CPI споживає compute units з загального бюджету транзакції (зараз 200 000 CU на Devnet).
- Права підпису: програма може передати лише ті права, які має. Вона не може підписати акаунт користувача — лише свої PDA.
- Зміна акаунтів: програма не може передати в CPI акаунт, який є writable у поточній інструкції, але був readonly у попередній (зміна mutability заборонена).
Типові сценарії CPI
- Ваша програма викликає SPL Token Program для mint, transfer або burn токенів.
- Ваша програма викликає System Program для створення акаунта (через CPI, а не через інструкцію користувача).
- Ваша програма викликає іншу кастомну програму (наприклад, AMM для свапу).
Типові помилки
- Передача неправильного набору акаунтів у CPI — програма-ціль отримає акаунти, які ви передали, і якщо когось не вистачає, інструкція впаде.
- Забуття вказати correct signer — якщо CPI потребує підпису PDA, а ви використали invoke замість invoke_signed, транзакція буде відхилена.
- Перевищення глибини CPI — зазвич виникає при рекурсивних викликах або складних ланцюжках DeFi-протоколів.
Як викликати іншу програму через CPI
Підготовка акаунтів для CPI
Перед викликом CPI переконайтеся, що всі акаунти, які потрібні цільовій програмі, передані у вашу інструкцію та оголошені в структурі accounts. Anchor не додає акаунти автоматично — ви маєте явно їх вказати.
CPI через Anchor
Anchor спрощує CPI через макроси CpiContext. Приклад виклику System Program для створення акаунта (Anchor 0.30.x):
Концептуальний приклад:
pub fn create_account(ctx: Context<CreateAccount>, space: u64) -> Result<()> {
let cpi_accounts = system_program::CreateAccount {
from: ctx.accounts.payer.to_account_info(),
to: ctx.accounts.new_account.to_account_info(),
};
let cpi_program = ctx.accounts.system_program.to_account_info();
anchor_lang::system_program::create_account(
CpiContext::new(cpi_program, cpi_accounts),
space,
)
}
invoke_signed
У чистому Rust (без Anchor обгортки) для CPI з підписом PDA використовується invoke_signed:
Концептуальний приклад (Rust, без Anchor):
invoke_signed(
&instruction,
&account_infos,
&[&seeds[..], &[bump]],
)?;
Останній аргумент — масив signer seeds. Runtime перевірить, що кожен вказаний PDA дійсно належить програмі, що викликає invoke_signed.
Обробка результату CPI
Якщо цільова програма повертає помилку, invoke / invoke_signed поверне ProgramError. У Anchor ви можете обгорнути CPI у стандартний ? оператор, і помилка пошириться далі. Для кастомної обробки використовуйте .map_err().
Типові помилки
- Невідповідність типу акаунта — передача AccountInfo замість конкретного типу Account у CpiContext.
- Забуття system_program у структурі accounts — Anchor не зможе знайти програму для CPI.
- Неправильний порядок signer seeds у invoke_signed — seeds мають точно відповідати тим, що використовувалися для генерації PDA.
Як передати токен через програму
Що потрібно для токен-транзакції
Для передачі токенів через SPL Token Program потрібні:
- Відправник: token-акаунт (writable), його власник (signer).
- Одержувач: token-акаунт (writable).
- Mint: акаунт mint токена (для TransferChecked).
- Token Program: програма, що обробляє інструкцію.
Передача через Anchor
Концептуальний приклад (Anchor 0.30.x):
pub fn transfer_tokens(ctx: Context<TransferTokens>, amount: u64) -> Result<()> {
let cpi_accounts = token::TransferChecked {
from: ctx.accounts.from_ata.to_account_info(),
to: ctx.accounts.to_ata.to_account_info(),
mint: ctx.accounts.mint.to_account_info(),
authority: ctx.accounts.authority.to_account_info(),
};
let cpi_program = ctx.accounts.token_program.to_account_info();
token::transfer_checked(
CpiContext::new(cpi_program, cpi_accounts),
amount,
ctx.accounts.mint.decimals,
)
}
Очікуваний результат: вказана кількість токенів переміщується з from_ata на to_ата. Баланс відправника зменшується, одержувача — збільшується.
Передача від імені PDA
Якщо token-акаунт належить PDA (наприклад, vault), програма має використати CpiContext::new_with_signer:
let seeds = &[b"vault", ctx.accounts.mint.key().as_ref(), &[ctx.accounts.vault.bump]];
let signer_seeds = &[&seeds[..]];
let cpi_context = CpiContext::new_with_signer(
ctx.accounts.token_program.to_account_info(),
cpi_accounts,
signer_seeds,
);
token::transfer_checked(cpi_context, amount, ctx.accounts.mint.decimals)?;
Це дозволяє PDA виступати як authority для token-акаунта, який йому належить.
Перевірка результату
Після виклику перевірте баланси через CLI:
spl-token balance <MINT_ADDRESS> --owner <WALLET_ADDRESS>
Типові помилки
- Передача amount у найменших одиницях замість з урахуванням decimals — якщо decimals = 9, то 1 токен = 1000000000. Передача 1 означає передачу 0.000000001 токена.
- Використання Transfer замість TransferChecked — менш безпечно, оскільки не верифікує decimals.
- Забуття вказати mint.decimals у transfer_checked — Anchor вимагає цей параметр явно.
Як mint-нути токен через Anchor
Що таке mint
Mint (емісія) — це процес створення нових одиниць токена та зарахування їх на token-акаунт. Цю операцію може виконати лише акаунт, який вказаний як mint authority у mint-акаунті.
Mint через Anchor CPI
Концептуальний приклад (Anchor 0.30.x):
pub fn mint_tokens(ctx: Context<MintTokens>, amount: u64) -> Result<()> {
let cpi_accounts = token::MintTo {
mint: ctx.accounts.mint.to_account_info(),
to: ctx.accounts.token_account.to_account_info(),
authority: ctx.accounts.authority.to_account_info(),
};
let cpi_program = ctx.accounts.token_program.to_account_info();
token::mint_to(CpiContext::new(cpi_program, cpi_accounts), amount)
}
Mint authority як PDA
У більшості реальних застосунків mint authority — це PDA програми, а не зовнішній гаманець. Це гарантує, що лише програма може випускати токени:
Під час створення mint-акаунта встановіть authority на PDA:
let mint = token::initialize_mint2(
CpiContext::new(
ctx.accounts.token_program.to_account_info(),
token::InitializeMint2 {
mint: ctx.accounts.mint.to_account_info(),
},
),
9,
&ctx.accounts.vault.key(),
None,
)?;
Потім при mint-ненні використовуйте CpiContext::new_with_signer з seeds vault PDA.
Перевірка результату
spl-token supply <MINT_ADDRESS> — має показати збільшену загальну пропозицію.
spl-token balance <MINT_ADDRESS> — має показати збільшений баланс.
Типові помилки
- Mint authority не збігається з переданим authority у MintTo — переконайтеся, що при ініціалізації mint ви встановили правильний authority.
- Забуття передати signer seeds для PDA-mint-authority — без new_with_signer CPI не зможе підтвердити права.
- Перевищення max supply — якщо при створенні mint було вказано обмеження supply, mint_to впаде при перевищенні.
Як перевірити баланс токена на Devnet
Через CLI
Баланс конкретного гаманця для вказаного токена:
spl-token balance <MINT_ADDRESS>
Якщо у вас кілька token-акаунтів для цього mint, вкажіть власника:
spl-token balance <MINT_ADDRESS> --owner <WALLET_ADDRESS>
Загальна пропозиція токена:
spl-token supply <MINT_ADDRESS>
Через TypeScript SDK
Приклад (@solana/spl-token 0.3.x, @solana/web3.js 1.95.x, Devnet):
import { Connection, PublicKey } from "@solana/web3.js";
import { getAccount, getMint } from "@solana/spl-token";
const connection = new Connection("https://api.devnet.solana.com", "confirmed");
const mintAddress = new PublicKey("<MINT_ADDRESS>");
const walletAddress = new PublicKey("<WALLET_ADDRESS>");
// Отримати інформацію про mint
const mintInfo = await getMint(connection, mintAddress);
console.log("Decimals:", mintInfo.decimals);
console.log("Supply:", mintInfo.supply.toString());
// Отримати баланс через associated token account
const ata = await getAssociatedTokenAddress(mintAddress, walletAddress);
const tokenAccount = await getAccount(connection, ata);
console.log("Balance:", tokenAccount.amount.toString());
Очікуваний результат: виведення decimals, загальної пропозиції та балансу у найменших одиницях. Для відображення в людському форматі поділіть amount на 10^decimals.
Через Explorer
Відкрийте explorer.solana.com, перемкніть на Devnet, введіть адресу token-акаунта. На сторінці акаунта буде вказано баланс та mint. Або введіть адресу гаманця та перейдіть до вкладки «Token Holdings».
Баланс у контексті програми
У програмі на Anchor ви можете прочитати баланс token-акаунта через поле amount типу Account<'info, TokenAccount>. Це корисно для перевірки перед виконанням логіки:
let balance = ctx.accounts.token_account.amount;
require!(balance >= amount, ErrorCode::InsufficientBalance);
Типові помилки
- Читання балансу mint-акаунта замість token-акаунта — mint-акаунт зберігає supply, а не баланс конкретного власника.
- Ігнорування decimals при відображенні — баланс 1000000000 з decimals 9 — це 1 токен, а не мільярд.
- Запит балансу для ATA, який ще не створено — getAccount поверне помилку. Спочатку перевірте існування через try/catch або використовуйте getAssociatedTokenAddressSync з перевіркою.
Типові помилки PDA та CPI у Solana
Помилки PDA
- Seeds mismatch: найчастіша помилка. Програма обчислила PDA з одних seeds, а клієнт передав акаунт, згенерований з інших. Рішення: завжди використовуйте однакову логіку seeds на клієнті та в програмі.
- InvalidSeeds: передані seeds не відповідають жодному PDA цієї програми. Перевірте порядок та вміст seeds.
- Bump не збігається: якщо ви зберігаєте bump у стані, переконайтеся, що він записаний коректно під час ініціалізації.
Помилки CPI
- Missing required signature: CPI потребує підпису, але ви використали invoke замість invoke_signed. Перевірте, чи цільова програма вимагає signer для цього акаунта.
- Account not writable: акаунт, який CPI хоче змінити, переданий як readonly. Додайте mut у Anchor або вкажіть is_writable: true в AccountMeta.
- Illegal owner: акаунт, який ви передаєте в CPI, належить не тій програмі. Наприклад, token-акаунт, створений через Token-2022, не підійде для CPI до оригінального Token Program.
Помилки Token CPI
- InsufficientFunds: спроба перевести більше токенів, ніж є на балансі. Перевіряйте amount до виклику CPI.
- MintMismatch: token-акаунти відправника та одержувача належать різним mint. Переконайтеся, що обидва пов'язані з одним mint.
- AuthorityTypeMismatch: переданий authority не є mint authority або не має потрібних прав. Перевірте, хто встановлений як authority у mint-акаунті.
Як діагностувати
- Читайте error logs: solana logs на локальному validator або деталі транзакції в Explorer показують точний код помилки та програму, що її повернула.
- Використовуйте anchor test -- --features debug: увімкне додаткове логування в Anchor програмах.
- Перевіряйте акаунти через CLI: solana account <ADDRESS> покаже owner, data, lamports. Переконайтеся, що owner правильний.
- Зменшуйте складність: якщо CPI ланцюжок складний, спочатку перевірте кожен виклик окремо.
Профілактика
- Завжди використовуйте TransferChecked замість Transfer — це виловить помилку неправильних decimals раніше.
- Завжди передавайте token_program як окремий акаунт у структурах Anchor — це дозволяє підтримувати як Token Program, так і Token-2022.
- Використовуйте constraint у Anchor для явної перевірки mint та owner перед CPI.
- Тестуйте PDA-генерацію на клієнті та в програмі з однаковими seeds — розбіжність у одному байті дасть різні адреси.
- Не ігноруйте попередження компілятора про unused variables у структурах accounts — кожен акаунт у структурі передається в програму і впливає на перевірки.
Після опанування токенів, PDA та CPI ви маєте повний набір інструментів для написання програм, що керують активами на Solana. Наступний логічний крок — застосувати ці знання в практичних проєктах, де кожна з цих трьох концепцій працює разом.