Plan subskrypcji

Plan subskrypcji umożliwia sprzedawcy publikowanie warunków rozliczeniowych, które użytkownicy mogą zaakceptować. Po zasubskrybowaniu przez użytkownika, sprzedawca lub upoważniony pobierający może pobierać do kwoty określonej w planie w każdym okresie rozliczeniowym.

Ten przewodnik przedstawia pełny przepływ jako elementy składowe. Sprzedawca tworzy plan, subskrybent go akceptuje, a sprzedawca lub pobierający pobiera płatności z wynikowego PDA subskrypcji.

Instalacja

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

Utworzenie planu

Sprzedawca jest właścicielem planu. PDA planu jest wyprowadzane z adresu sprzedawcy i planId.

Sponsor może sfinansować rent planu, przekazując opcjonalny parametr payer, podczas gdy merchant pozostaje właścicielem planu. Usunięcie planu zwraca środki właścicielowi, a nie płatnikowi, dlatego sponsorowanie należy kontrolować poza łańcuchem.

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,
});

Aktualizacja planu

Sprzedawca może aktualizować modyfikowalne pola planu po jego utworzeniu. Dotychczasowi subskrybenci zachowują warunki, które zaakceptowali, podczas gdy nowi subskrybenci akceptują aktualne warunki planu.

Obowiązuje kilka zasad:

  • Skończony endTs można jedynie skrócić, nigdy wydłużyć ani wyczyścić (PlanEndTsCannotExtend).
  • W planie Sunset można usunąć pullery (nowy zestaw musi być podzbiorem bieżącego), aby unieważnić przejęty puller; status, endTs i metadane pozostają niezmienione.
  • Edycje są możliwe w trakcie ostatniego okresu rozliczeniowego planu, o ile endTs pozostaje bez zmian.
  • Instrukcja zawiera stan planu zaobserwowany w momencie podpisywania (expectedCreatedAt, expectedEndTs, expectedPullers, expectedMetadataUri). Jeśli aktualny plan nie pasuje już do zapisanego stanu, aktualizacja zostaje odrzucona (StalePlanApproval), więc nieaktualna podpisana aktualizacja nie może przywrócić usuniętych pullerów ani cofnąć późniejszych edycji. Metoda updatePlan klienta wtyczki pobiera dla Ciebie aktualny stan; podczas ręcznego budowania należy samodzielnie pobrać plan i przekazać odpowiednie pola.
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();

Subskrybuj

Subskrybent akceptuje warunki bieżącego planu. PDA subskrypcji jest wyprowadzany z PDA planu i adresu subskrybenta.

Aby połączyć inicjalizację authority i subskrypcję w jedną transakcję, przekaż wartość sentinel UNKNOWN_INIT_ID (eksportowaną przez TypeScript SDK) jako expectedSubscriptionAuthorityInitId. Program akceptuje authority tylko wtedy, gdy zostało ono utworzone w bieżącym slot, więc sentinel działa przy nowych rejestracjach; powracający użytkownik, którego authority zostało utworzone we wcześniejszym slot, musi przekazać prawdziwy initId (klient wtyczki pobiera go automatycznie), w przeciwnym razie wywołanie zakończy się błędem 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,
});

Pobierz płatność

Sprzedawca lub autoryzowany puller podpisuje operację pobrania. Gdy plan korzysta z listy dozwolonych miejsc docelowych, właściciel token account odbiorcy musi być uwzględniony w 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();

Anulowanie i Unieważnienie

Anulowanie oznacza subskrypcję jako kończącą się. Unieważnienie zamyka PDA subskrypcji po upływie terminu wygaśnięcia anulowania. Subskrybent podpisuje obie transakcje.

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();

Anuluj Natychmiast

Gdy zarówno subskrybent, jak i aktualny właściciel planu podpiszą żądanie, cancelSubscriptionNow wygasa subskrypcję w momencie anulowania, zamiast na koniec okresu rozliczeniowego. Może również skrócić oczekujące anulowanie w okresie karencji. Zatwierdzenie jest powiązane z początkiem okresu zaobserwowanym w momencie podpisywania; jeśli subskrypcja zmieniła się od tego czasu, transakcja zostaje odrzucona (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();

Wznowienie Subskrypcji

Anulowaną subskrypcję można reaktywować przed jej unieważnieniem. Po unieważnieniu konto subskrypcji zostaje zamknięte i subskrybent musi ponownie subskrybować. Wznowienie wymaga od subskrybenta SubscriptionAuthority dla mintowania planu, który program weryfikuje (właściciel, mint oraz init_id) i odrzuca, jeśli jest nieaktualny lub ponownie zainicjalizowany.

Instrukcja zawiera datę wygaśnięcia zaobserwowaną przez subskrybenta w momencie podpisywania (expectedExpiresAtTs); niezgodność skutkuje odrzuceniem (StaleSubscriptionApproval), więc nieaktualne podpisane wznowienie nie może usunąć późniejszego anulowania, którego subskrybent nigdy nie zatwierdził.

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();

Uwagi

  • amount jest w jednostkach bazowych. Dla tokena z 6 miejscami dziesiętnymi, 5_000_000 oznacza 5 tokenów.
  • TypeScript SDK pobiera aktualne warunki planu podczas subscribe, gdy się je pominie.
  • Rust SubscribeBuilder wymaga oczekiwanych warunków planu. Najpierw pobierz i zdekoduj konto planu, a następnie przekaż te pola przez SubscribeData.
  • Tylko merchant lub portfel wymieniony w pullers może pobierać płatności.
  • Subskrybent podpisuje transakcje konfiguracji, anulowania i unieważnienia. Merchant lub zatwierdzony podmiot pobierający podpisuje transakcje pobierania płatności.

Is this page helpful?

Spis treści

Edytuj stronę