План підписки

План підписки дозволяє продавцю опублікувати умови виставлення рахунків, які користувачі можуть прийняти. Після того, як користувач підписується, продавець або затверджений збирач може стягувати до суми плану кожен розрахунковий період.

Цей посібник показує повний процес як окремі блоки. Продавець створює план, підписник приймає його, а продавець або збирач отримує платежі з отриманого PDA підписки.

Встановлення

pnpm add @solana/subscriptions @solana/kit @solana/kit-plugin-rpc @solana/kit-plugin-signer @solana-program/token

Створення плану

Продавець є власником плану. PDA плану виводиться з адреси продавця та planId.

Спонсор може фінансувати rent плану, передавши необов'язковий параметр payer, тоді як мерчант залишається власником плану. Видалення плану повертає кошти власнику, а не платнику, тому керуйте спонсорством поза мережею.

import { address, createClient } from '@solana/kit';
import { solanaLocalRpc } from '@solana/kit-plugin-rpc';
import { signer } from '@solana/kit-plugin-signer';
import { findPlanPda, subscriptionsProgram } from '@solana/subscriptions';
const merchantClient = createClient()
.use(signer(merchantSigner))
.use(solanaLocalRpc({ rpcUrl: 'http://127.0.0.1:8899' }))
.use(subscriptionsProgram());
const planId = 1n;
const tokenMint = address('TOKEN_MINT_ADDRESS_HERE');
const amount = 5_000_000n;
const periodHours = 720n;
const metadataUri = 'https://example.com/plan.json';
const destinations = [merchantSigner.address];
const pullers = [address('PULLER_WALLET_ADDRESS_HERE')];
await merchantClient.subscriptions.instructions
.createPlan({
planId,
mint: tokenMint,
amount,
periodHours,
endTs: 0n,
destinations,
pullers,
metadataUri,
})
.sendTransaction();
const [planPda] = await findPlanPda({
owner: merchantSigner.address,
planId,
});

Оновлення плану

Продавець може оновлювати змінювані поля плану після створення. Існуючі підписники зберігають умови, які вони прийняли, тоді як нові підписники приймають поточні умови плану.

Діють кілька правил:

  • Кінцевий endTs можна лише скоротити, але не продовжити або очистити (PlanEndTsCannotExtend).
  • У плані Sunset можна видалити пулери (новий набір має бути підмножиною поточного), щоб відкликати скомпрометований пулер; статус, endTs і метадані залишаються незмінними.
  • Редагування дозволяється протягом останнього розрахункового періоду плану, якщо endTs не змінюється.
  • Інструкція містить стан плану, зафіксований під час підписання (expectedCreatedAt, expectedEndTs, expectedPullers, expectedMetadataUri). Якщо поточний план більше не відповідає цьому стану, оновлення відхиляється (StalePlanApproval), тому застаріле підписане оновлення не може відновити видалених пулерів або скасувати пізніші зміни. Метод updatePlan клієнта плагіна автоматично отримує актуальний стан; під час ручного формування запиту отримайте план і передайте поля самостійно.
import { PlanStatus } from '@solana/subscriptions';
const updatedMetadataUri = 'https://example.com/updated-plan.json';
const updatedPullers = [address('NEW_PULLER_WALLET_ADDRESS_HERE')];
await merchantClient.subscriptions.instructions
.updatePlan({
owner: merchantSigner,
planPda,
status: PlanStatus.Active,
endTs: 0n,
pullers: updatedPullers,
metadataUri: updatedMetadataUri,
})
.sendTransaction();

Підписатися

Підписник приймає умови поточного плану. PDA підписки виводиться з PDA плану та адреси підписника.

Щоб об'єднати ініціалізацію authority та підписку в одну транзакцію, передайте сентинел UNKNOWN_INIT_ID (експортується TypeScript SDK) як expectedSubscriptionAuthorityInitId. Програма приймає authority лише якщо вона була створена в поточному slot, тому сентинел підходить для нових реєстрацій; користувач, що повертається, authority якого була створена в більш ранньому slot, повинен передати справжній initId (клієнт плагіна отримає його автоматично), інакше виклик завершиться помилкою StaleSubscriptionAuthority.

import { createClient } from '@solana/kit';
import { solanaLocalRpc } from '@solana/kit-plugin-rpc';
import { signer } from '@solana/kit-plugin-signer';
import { findAssociatedTokenPda, TOKEN_PROGRAM_ADDRESS } from '@solana-program/token';
import {
fetchMaybeSubscriptionAuthority,
findSubscriptionAuthorityPda,
findSubscriptionDelegationPda,
subscriptionsProgram,
} from '@solana/subscriptions';
const subscriberClient = createClient()
.use(signer(subscriberSigner))
.use(solanaLocalRpc({ rpcUrl: 'http://127.0.0.1:8899' }))
.use(subscriptionsProgram());
const [subscriberAta] = await findAssociatedTokenPda({
mint: tokenMint,
owner: subscriberSigner.address,
tokenProgram: TOKEN_PROGRAM_ADDRESS,
});
const [subscriptionAuthorityPda] = await findSubscriptionAuthorityPda({
user: subscriberSigner.address,
tokenMint,
});
const subscriptionAuthority = await fetchMaybeSubscriptionAuthority(
subscriberClient.rpc,
subscriptionAuthorityPda,
);
if (!subscriptionAuthority.exists) {
await subscriberClient.subscriptions.instructions
.initSubscriptionAuthority({
tokenMint,
tokenProgram: TOKEN_PROGRAM_ADDRESS,
userAta: subscriberAta,
})
.sendTransaction();
}
await subscriberClient.subscriptions.instructions
.subscribe({
merchant: merchantSigner.address,
planId,
tokenMint,
})
.sendTransaction();
const [subscriptionPda] = await findSubscriptionDelegationPda({
planPda,
subscriber: subscriberSigner.address,
});

Прийняти Платіж

Мерчант або авторизований пулер підписує операцію збору коштів. Якщо план використовує дозволений список одержувачів, власник token account одержувача має бути вказаний у destinations.

const receiverAta = address('MERCHANT_TOKEN_ACCOUNT_ADDRESS_HERE');
await merchantClient.subscriptions.instructions
.transferSubscription({
caller: merchantOrPullerSigner,
delegator: subscriberSigner.address,
tokenMint,
subscriptionPda,
planPda,
amount: 200_000n,
receiverAta,
tokenProgram: TOKEN_PROGRAM_ADDRESS,
})
.sendTransaction();

Скасування та відкликання

Скасування позначає підписку як таку, що завершується. Відкликання закриває PDA підписки після того, як минув термін дії скасування. Підписник підписує обидві транзакції.

await subscriberClient.subscriptions.instructions
.cancelSubscription({
subscriber: subscriberSigner,
planPda,
subscriptionPda,
})
.sendTransaction();
// Run this after the cancelled subscription's expiresAtTs has elapsed.
await subscriberClient.subscriptions.instructions
.revokeSubscription({
authority: subscriberSigner,
planPda,
subscriptionPda,
})
.sendTransaction();

Негайне скасування

Коли і підписник, і поточний власник плану підписують запит, cancelSubscriptionNow завершує підписку в момент скасування, а не наприкінці розрахункового періоду. Також можна скоротити очікуване скасування в пільговому періоді. Підтвердження прив'язане до початку розрахункового періоду, зафіксованого під час підписання; якщо підписка змінилася відтоді, транзакція відхиляється (StaleSubscriptionApproval).

import { fetchSubscriptionDelegation } from '@solana/subscriptions';
const subscription = await fetchSubscriptionDelegation(
subscriberClient.rpc,
subscriptionPda,
);
await subscriberClient.subscriptions.instructions
.cancelSubscriptionNow({
subscriber: subscriberSigner,
merchant: merchantSigner,
planPda,
expectedCurrentPeriodStartTs: subscription.data.currentPeriodStartTs,
})
.sendTransaction();

Відновлення підписки

Скасовану підписку можна повторно активувати до її відкликання. Після відкликання обліковий запис підписки закривається, і підписник повинен підписатися знову. Для відновлення потрібен SubscriptionAuthority підписника для монети плану, який програма перевіряє (власник, монета та init_id) і відхиляє, якщо він застарілий або повторно ініціалізований.

Інструкція містить термін дії, зафіксований підписником під час підписання (expectedExpiresAtTs); розбіжність призводить до відхилення (StaleSubscriptionApproval), тому застаріле підписане відновлення не може скасувати пізніше скасування, яке підписник ніколи не затверджував.

import { fetchSubscriptionDelegation } from '@solana/subscriptions';
const subscription = await fetchSubscriptionDelegation(
subscriberClient.rpc,
subscriptionPda,
);
await subscriberClient.subscriptions.instructions
.resumeSubscription({
subscriber: subscriberSigner,
planPda,
tokenMint,
expectedExpiresAtTs: subscription.data.expiresAtTs,
})
.sendTransaction();

Примітки

  • amount вказується в базових одиницях. Для токена з 6 десятковими знаками 5_000_000 означає 5 токенів.
  • TypeScript SDK отримує актуальні умови плану під час subscribe, якщо ви їх не вказуєте.
  • Rust SubscribeBuilder потребує очікуваних умов плану. Спочатку отримайте та декодуйте обліковий запис плану, а потім передайте ці поля через SubscribeData.
  • Лише мерчант або гаманець, зазначений у pullers, може збирати платежі.
  • Підписник підписує транзакції налаштування, скасування та відкликання. Мерчант або авторизований збирач підписує транзакції збору платежів.

Is this page helpful?