Розробка на стороні клієнта
Більшість децентралізованих додатків (dApps) використовують TypeScript для взаємодії з розгорнутими програмами Solana. Розуміння того, як інтегрувати вашу програму на стороні клієнта, є важливим для створення функціональних додатків.
Клієнтський SDK Anchor
Anchor спрощує клієнтську взаємодію з програмами Solana через файл мови опису інтерфейсу (IDL), який відображає структуру вашої програми.
У поєднанні з бібліотекою TypeScript від Anchor (@coral-xyz/anchor), IDL забезпечує оптимізований підхід до створення інструкцій та транзакцій.
Налаштування
Пакет @coral-xyz/anchor
встановлюється автоматично при створенні програми Anchor. Після запуску anchor build, Anchor генерує:
- IDL у
target/idl/<program-name>.json
- TypeScript SDK у
target/types/<program-name>.ts
Ці файли абстрагують більшу частину базової складності. Перенесіть їх до вашого TypeScript клієнта, використовуючи таку структуру:
src
├── anchor
│ ├── <program-name>.json
│ └── <program-name>.ts
└── integration.ts
Щоб використовувати адаптер гаманця з TypeScript SDK від Anchor, створіть об'єкт Provider, який поєднує Connection (localhost, devnet або mainnet) та Wallet (адресу, яка оплачує та підписує транзакції).
Налаштуйте Wallet та Connection:
import { useAnchorWallet, useConnection } from "@solana/wallet-adapter-react";
const { connection } = useConnection();
const wallet = useAnchorWallet();
Створіть об'єкт Provider і встановіть його як типовий:
import { AnchorProvider, setProvider } from "@coral-xyz/anchor";
const provider = new AnchorProvider(connection, wallet, {
commitment: "confirmed",
});
setProvider(provider);
Програма
Об'єкт Program від Anchor створює спеціальний API для взаємодії з програмами Solana. Цей API служить центральним інтерфейсом для всієї комунікації з програмами в мережі:
- Надсилання транзакцій,
- Отримання десеріалізованих акаунтів,
- Декодування даних інструкцій,
- Підписка на зміни акаунтів,
- Прослуховування подій
Створіть об'єкт Program, імпортуючи типи та IDL:
import <program-name> from "./<program-name>.json";
import type { <Program-Type> } from "./<program-name>.ts";
import { Program, Idl } from "@coral-xyz/anchor";
const program = new Program(<program-name> as <Program-Type>);
Якщо ви не встановили провайдер за замовчуванням, вкажіть його явно:
const program = new Program(<program-name> as <Program-Type>, provider);
Після налаштування використовуйте Anchor Methods Builder для створення інструкцій та транзакцій. MethodsBuilder
використовує IDL для надання спрощеного формату побудови транзакцій, які викликають інструкції програми.
Базовий шаблон MethodsBuilder
:
await program.methods
.instructionName(instructionDataInputs)
.accounts({})
.signers([])
.rpc();
Передавайте додаткових підписантів, окрім провайдера, використовуючи .signers()
.
Акаунти
Використовуйте синтаксис з крапкою для виклику .accounts
на MethodsBuilder
, передаючи об'єкт з кожним акаунтом, який очікує інструкція на основі IDL.
Транзакції
Стандартним методом для надсилання транзакцій через Anchor є .rpc()
, який надсилає транзакцію безпосередньо в блокчейн.
Для сценаріїв, що вимагають підписання на бекенді (наприклад, створення транзакції на фронтенді з гаманцем користувача, а потім безпечне підписання за допомогою ключової пари бекенду), використовуйте .transaction()
:
const transaction = await program.methods
.instructionName(instructionDataInputs)
.accounts({})
.transaction();
//... Sign the transaction in the backend
// Send the transaction to the chain
await sendTransaction(transaction, connection);
Щоб об'єднати кілька інструкцій Anchor, використовуйте .instruction()
для отримання об'єктів інструкцій:
// Create first instruction
const instructionOne = await program.methods
.instructionOneName(instructionOneDataInputs)
.accounts({})
.instruction();
// Create second instruction
const instructionTwo = await program.methods
.instructionTwoName(instructionTwoDataInputs)
.accounts({})
.instruction();
// Add both instructions to one transaction
const transaction = new Transaction().add(instructionOne, instructionTwo);
// Send transaction
await sendTransaction(transaction, connection);
Fetch and Filter Accounts
Коли ваша програма створює сотні акаунтів, відстежувати їх стає складно. Об'єкт Program надає методи для ефективного отримання та фільтрації акаунтів програми.
Отримайте всі адреси певного типу акаунту:
const accounts = await program.account.counter.all();
Фільтруйте конкретні акаунти за допомогою прапорця memcmp
:
const accounts = await program.account.counter.all([
{
memcmp: {
offset: 8,
bytes: bs58.encode(new BN(0, "le").toArray()),
},
},
]);
Для перевірки змін даних акаунту, отримайте десеріалізовані дані акаунту для конкретного акаунту за допомогою fetch:
const account = await program.account.counter.fetch(ACCOUNT_ADDRESS);
Отримання кількох акаунтів одночасно:
const accounts = await program.account.counter.fetchMultiple([
ACCOUNT_ADDRESS_ONE,
ACCOUNT_ADDRESS_TWO,
]);
Події та вебхуки
Замість того, щоб отримувати дані з блокчейну щоразу, коли користувачі підключають свої гаманці, налаштуйте системи, які прослуховують блокчейн і зберігають відповідні дані в базі даних.
Існує два основні підходи для прослуховування подій у блокчейні:
- Опитування: клієнт періодично перевіряє наявність нових даних через певні інтервали. Сервер відповідає найновішими даними незалежно від змін, потенційно повертаючи дублікати інформації.
- Потокова передача: сервер надсилає дані клієнту лише при виникненні оновлень. Це забезпечує ефективнішу передачу даних у реальному часі, оскільки передаються лише відповідні зміни.
Для потокової передачі інструкцій Anchor використовуйте вебхуки, які прослуховують події та надсилають їх на ваш сервер, коли вони відбуваються. Наприклад, оновлюйте запис у базі даних щоразу, коли відбувається продаж NFT на вашому маркетплейсі.
Anchor надає два макроси для генерації подій:
emit!()
: Генерує події безпосередньо в логи програми за допомогою системного викликуsol_log_data()
, кодуючи дані подій як рядки base64 з префіксом "Program Data"emit_cpi!()
: Генерує події через міжпрограмні виклики (CPI). Дані подій кодуються та включаються в дані інструкції CPI замість логів програми
Макрос emit!()
Реалізація програми:
use anchor_lang::prelude::*;
declare_id!("8T7MsCZyzxboviPJg5Rc7d8iqEcDReYR2pkQKrmbg7dy");
#[program]
pub mod event {
use super::*;
pub fn emit_event(_ctx: Context<EmitEvent>, input: String) -> Result<()> {
emit!(CustomEvent { message: input });
Ok(())
}
}
#[derive(Accounts)]
pub struct EmitEvent {}
#[event]
pub struct CustomEvent {
pub message: String,
}
Прослуховування подій на стороні клієнта за допомогою допоміжних функцій Anchor SDK для декодування base64:
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { Event } from "../target/types/event";
describe("event", () => {
// Configure the client to use the local cluster.
anchor.setProvider(anchor.AnchorProvider.env());
const program = anchor.workspace.Event as Program<Event>;
it("Emits custom event", async () => {
// Set up listener before sending transaction
const listenerId = program.addEventListener("customEvent", event => {
// Process the event data
console.log("Event Data:", event);
});
});
});
Макрос emit_cpi!()
Реалізація програми:
use anchor_lang::prelude::*;
declare_id!("2cDQ2LxKwQ8fnFUz4LLrZ157QzBnhPNeQrTSmWcpVin1");
#[program]
pub mod event_cpi {
use super::*;
pub fn emit_event(ctx: Context<EmitEvent>, input: String) -> Result<()> {
emit_cpi!(CustomEvent { message: input });
Ok(())
}
}
#[event_cpi]
#[derive(Accounts)]
pub struct EmitEvent {}
#[event]
pub struct CustomEvent {
pub message: String,
}
Декодування подій на стороні клієнта:
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { EventCpi } from "../target/types/event_cpi";
describe("event-cpi", () => {
// Configure the client to use the local cluster.
anchor.setProvider(anchor.AnchorProvider.env());
const program = anchor.workspace.EventCpi as Program<EventCpi>;
it("Emits custom event", async () => {
// Fetch the transaction data
const transactionData = await program.provider.connection.getTransaction(
transactionSignature,
{ commitment: "confirmed" },
);
// Decode the event data from the CPI instruction data
const eventIx = transactionData.meta.innerInstructions[0].instructions[0];
const rawData = anchor.utils.bytes.bs58.decode(eventIx.data);
const base64Data = anchor.utils.bytes.base64.encode(rawData.subarray(8));
const event = program.coder.events.decode(base64Data);
console.log(event);
});
});