Ein Abonnementplan ermöglicht es einem Händler, Abrechnungsbedingungen zu veröffentlichen, die Nutzer akzeptieren können. Nachdem ein Nutzer abonniert hat, kann der Händler oder ein autorisierter Einzieher bis zum Planbetrag in jeder Abrechnungsperiode einziehen.
Diese Anleitung zeigt den vollständigen Ablauf als Bausteine. Der Händler erstellt einen Plan, der Abonnent akzeptiert ihn, und der Händler oder Einzieher zieht Zahlungen aus dem resultierenden Abonnement-PDA ein.
Installation
pnpm add @solana/subscriptions @solana/kit @solana/kit-plugin-rpc @solana/kit-plugin-signer @solana-program/token
Plan erstellen
Der Händler ist Eigentümer des Plans. Der Plan-PDA wird von der Händleradresse
und planId abgeleitet.
Ein Sponsor kann die rent des Plans finanzieren, indem er den optionalen payer übergibt, während der Händler der Eigentümer des Plans bleibt. Das Löschen des Plans erstattet den Betrag dem Eigentümer, nicht dem Zahler – daher sollte die Sponsorschaft off-chain gesteuert werden.
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,});
Plan aktualisieren
Der Händler kann veränderbare Planfelder nach der Erstellung aktualisieren. Bestehende Abonnenten behalten die von ihnen akzeptierten Bedingungen bei, während neue Abonnenten die aktuellen Planbedingungen akzeptieren.
Es gelten einige Regeln:
- Eine endliche
endTskann nur verkürzt, niemals verlängert oder gelöscht werden (PlanEndTsCannotExtend). - Bei einem
Sunset-Plan können Sie Puller entfernen (die neue Gruppe muss eine Teilmenge der aktuellen sein), um einen kompromittierten Puller zu sperren; Status,endTsund Metadaten bleiben eingefroren. - Änderungen sind während der letzten Abrechnungsperiode eines Plans möglich,
solange
endTsunverändert bleibt. - Die Anweisung enthält den Planzustand, der beim Signieren beobachtet wurde
(
expectedCreatedAt,expectedEndTs,expectedPullers,expectedMetadataUri). Stimmt der aktuelle Plan nicht mehr überein, wird die Aktualisierung abgelehnt (StalePlanApproval). Eine veraltete signierte Aktualisierung kann daher weder entfernte Puller wiederherstellen noch spätere Änderungen rückgängig machen. DerupdatePlan-Aufruf des Plugin-Clients ruft den aktuellen Zustand automatisch ab; beim manuellen Erstellen müssen Sie den Plan selbst abrufen und die Felder übergeben.
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();
Abonnieren
Der Abonnent akzeptiert die aktuellen Planbedingungen. Die Abonnement-PDA wird aus der Plan-PDA und der Abonnentenadresse abgeleitet.
Um die Initialisierung der Autorität und das Abonnieren in einer einzigen Transaktion zu bündeln, übergeben Sie den Sentinel UNKNOWN_INIT_ID (vom TypeScript-SDK exportiert) als expectedSubscriptionAuthorityInitId. Das Programm akzeptiert die Autorität nur, wenn sie im aktuellen slot erstellt wurde – daher funktioniert der Sentinel für neue Anmeldungen. Ein wiederkehrender Nutzer, dessen Autorität in einem früheren slot erstellt wurde, muss die echte initId übergeben (der Plugin-Client ruft sie für Sie ab), andernfalls schlägt der Aufruf mit StaleSubscriptionAuthority fehl.
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,});
Eine Zahlung einziehen
Der Händler oder ein autorisierter Puller unterzeichnet den Einzug. Wenn der
Plan eine Ziel-Zulassungsliste verwendet, muss der Inhaber des Empfänger-token
account in destinations aufgeführt sein.
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();
Abbrechen und Widerrufen
Das Abbrechen markiert das Abonnement als auslaufend. Das Widerrufen schließt das Abonnement-PDA, nachdem die Ablaufzeit der Kündigung verstrichen ist. Der Abonnent signiert beide Transaktionen.
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();
Sofort kündigen
Wenn sowohl der Abonnent als auch der aktuelle Plan-Eigentümer signieren, lässt cancelSubscriptionNow das Abonnement zum Zeitpunkt der Kündigung ablaufen – anstatt am Ende des Abrechnungszeitraums. Es kann auch eine ausstehende Kündigung mit Nachfrist verkürzen. Die Genehmigung ist an den beim Signieren beobachteten Periodenbeginn gebunden; hat sich das Abonnement seitdem geändert, wird die Transaktion abgelehnt (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();
Ein Abonnement Fortsetzen
Ein gekündigtes Abonnement kann reaktiviert werden, bevor es widerrufen wird.
Nach dem Widerruf wird das Abonnement-Konten geschlossen und der Abonnent muss
erneut abonnieren. Für die Fortsetzung benötigt der Abonnent
SubscriptionAuthority für die Mint des Plans, den das Programm validiert
(Inhaber, Mint und init_id) und ablehnt, wenn er veraltet oder neu
initialisiert ist.
Die Anweisung enthält das Ablaufdatum, das der Abonnent beim Signieren beobachtet hat (expectedExpiresAtTs); eine Abweichung wird abgelehnt (StaleSubscriptionApproval). Daher kann eine veraltete signierte Wiederaufnahme eine spätere Kündigung, der der Abonnent nie zugestimmt hat, nicht aufheben.
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();
Hinweise
amountist in Basiseinheiten. Bei einem Token mit 6 Dezimalstellen bedeutet5_000_0005Token.- Das TypeScript-SDK ruft aktuelle Planbedingungen während
subscribeab, wenn diese weggelassen werden. - Das Rust-
SubscribeBuilderbenötigt die erwarteten Planbedingungen. Rufen Sie zunächst das Plan-Konten ab und dekodieren Sie es, und übergeben Sie dann diese Felder überSubscribeData. - Nur der Händler oder eine in
pullersaufgeführte Wallet kann Zahlungen einziehen. - Der Abonnent signiert Einrichtungs-, Abbruch- und Widerrufstransaktionen. Der Händler oder ein autorisierter Einzieher signiert Einzugstransaktionen.
Is this page helpful?