Abonnementplan

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 endTs kann 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, endTs und Metadaten bleiben eingefroren.
  • Änderungen sind während der letzten Abrechnungsperiode eines Plans möglich, solange endTs unverä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. Der updatePlan-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

  • amount ist in Basiseinheiten. Bei einem Token mit 6 Dezimalstellen bedeutet 5_000_000 5 Token.
  • Das TypeScript-SDK ruft aktuelle Planbedingungen während subscribe ab, wenn diese weggelassen werden.
  • Das Rust-SubscribeBuilder benötigt die erwarteten Planbedingungen. Rufen Sie zunächst das Plan-Konten ab und dekodieren Sie es, und übergeben Sie dann diese Felder über SubscribeData.
  • Nur der Händler oder eine in pullers aufgefü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?

Inhaltsverzeichnis

Seite bearbeiten
© 2026 Solana Foundation. Alle Rechte vorbehalten.