Abonnementsplan

Een abonnementsplan stelt een handelaar in staat factureringsvoorwaarden te publiceren die gebruikers kunnen accepteren. Nadat een gebruiker zich heeft geabonneerd, kan de handelaar of een goedgekeurde puller tot het planbedrag per factureringsperiode innen.

Deze handleiding toont de volledige flow als bouwstenen. De handelaar creëert een plan, de abonnee accepteert het, en de handelaar of puller int betalingen van de resulterende abonnements-PDA.

Installeren

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

Een Plan Creëren

De handelaar is eigenaar van het plan. De plan-PDA wordt afgeleid van het handelaarsadres en planId.

Een sponsor kan de rent van het plan financieren door de optionele payer mee te geven, terwijl de handelaar de eigenaar van het plan blijft. Het verwijderen van het plan vergoedt de eigenaar, niet de betaler, dus beheer sponsoring buiten de chain om.

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

Een Plan Bijwerken

De handelaar kan muteerbare planvelden bijwerken na aanmaak. Bestaande abonnees behouden de voorwaarden die zij hebben geaccepteerd, terwijl nieuwe abonnees de huidige planvoorwaarden accepteren.

Een aantal regels zijn van toepassing:

  • Een eindige endTs kan alleen worden ingekort, nooit verlengd of gewist (PlanEndTsCannotExtend).
  • Bij een Sunset plan kun je pullers verwijderen (de nieuwe set moet een subset zijn van de huidige) om een gecompromitteerde puller in te trekken; status, endTs en metadata blijven bevroren.
  • Wijzigingen werken tijdens de laatste factureringsperiode van een plan, zolang endTs ongewijzigd blijft.
  • De instructie bevat de planstatus die werd waargenomen op het moment van ondertekening (expectedCreatedAt, expectedEndTs, expectedPullers, expectedMetadataUri). Als het actieve plan hier niet meer mee overeenkomt, wordt de update geweigerd (StalePlanApproval). Een verouderde ondertekende update kan daardoor geen verwijderde pullers herstellen of latere wijzigingen terugdraaien. De updatePlan-methode van de plugin-client haalt de actuele status automatisch op; bij handmatige implementatie dient u het plan zelf op te halen en de velden door te geven.
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();

Abonneren

De abonnee accepteert de huidige planvoorwaarden. De abonnements-PDA wordt afgeleid van de plan-PDA en het adres van de abonnee.

Om de initialisatie van de autoriteit en het abonnement in één enkele transactie te bundelen, geeft u de schildwacht UNKNOWN_INIT_ID (geëxporteerd door de TypeScript SDK) mee als expectedSubscriptionAuthorityInitId. Het programma accepteert de autoriteit alleen als deze is aangemaakt in de huidige slot, zodat de schildwacht werkt voor nieuwe aanmeldingen. Een terugkerende gebruiker wiens autoriteit in een eerdere slot is aangemaakt, moet de echte initId doorgeven (de plugin-client haalt deze automatisch op) of de aanroep mislukt met 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,
});

Een Betaling Innen

De merchant of een gewhitelistte puller ondertekent de inning. Wanneer het plan gebruikmaakt van een bestemming-allowlist, moet de eigenaar van de ontvanger token account worden vermeld in 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();

Annuleren en Intrekken

Annuleren markeert het abonnement als eindigend. Intrekken sluit de abonnements- PDA nadat de annuleringstermijn is verstreken. De abonnee ondertekent beide transacties.

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

Onmiddellijk opzeggen

Wanneer zowel de abonnee als de huidige planeigenaar ondertekenen, laat cancelSubscriptionNow het abonnement verlopen op het moment van opzegging in plaats van aan het einde van de factureringsperiode. Het kan ook een lopende opzegging met respijtperiode verkorten. De goedkeuring is gekoppeld aan het begin van de periode zoals waargenomen op het moment van ondertekening; als het abonnement sindsdien is gewijzigd, wordt de transactie geweigerd (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();

Een Abonnement Hervatten

Een geannuleerd abonnement kan opnieuw worden geactiveerd voordat het wordt ingetrokken. Zodra het is ingetrokken, wordt het abonnementsaccount gesloten en moet de abonnee opnieuw abonneren. Hervatten vereist het SubscriptionAuthority van de abonnee voor de mint van het plan, dat het programma valideert (eigenaar, mint en init_id) en afwijst als het verouderd of opnieuw geïnitialiseerd is.

De instructie bevat de vervaldatum die de abonnee heeft waargenomen op het moment van ondertekening (expectedExpiresAtTs); een afwijking wordt geweigerd (StaleSubscriptionApproval). Hierdoor kan een verouderde ondertekende hervatting een latere opzegging die de abonnee nooit heeft goedgekeurd, niet ongedaan maken.

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

Opmerkingen

  • amount is in basiseenheden. Voor een token met 6 decimalen betekent 5_000_000 5 tokens.
  • De TypeScript SDK haalt live plantermijnen op tijdens subscribe wanneer u deze weglaat.
  • De Rust SubscribeBuilder heeft de verwachte plantermijnen nodig. Haal het planaccount op en decodeer het eerst, geef die velden vervolgens door via SubscribeData.
  • Alleen de handelaar of een wallet vermeld in pullers kan betalingen innen.
  • De abonnee ondertekent setup-, annulerings- en intrekkingstransacties. De handelaar of goedgekeurde puller ondertekent incassotransacties.

Is this page helpful?

Inhoudsopgave

Pagina Bewerken
© 2026 Solana Foundation. Alle rechten voorbehouden.