Un piano di abbonamento consente a un commerciante di pubblicare i termini di fatturazione che gli utenti possono accettare. Dopo che un utente si abbona, il commerciante o un puller autorizzato può riscuotere fino all'importo del piano per ogni periodo di fatturazione.
Questa guida mostra il flusso completo come blocchi costitutivi. Il commerciante crea un piano, l'abbonato lo accetta e il commerciante o il puller riscuote i pagamenti dal PDA di abbonamento risultante.
Installazione
pnpm add @solana/subscriptions @solana/kit @solana/kit-plugin-rpc @solana/kit-plugin-signer @solana-program/token
Crea un piano
Il commerciante possiede il piano. Il PDA del piano è derivato dall'indirizzo
del commerciante e planId.
Uno sponsor può finanziare il rent del piano passando il parametro opzionale payer, mentre il merchant rimane il proprietario del piano. L'eliminazione del piano rimborsa il proprietario, non il payer, quindi gestisci la sponsorizzazione off-chain.
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,});
Aggiorna un piano
Il commerciante può aggiornare i campi modificabili del piano dopo la creazione. Gli abbonati esistenti mantengono i termini che hanno accettato, mentre i nuovi abbonati accettano i termini attuali del piano.
Si applicano alcune regole:
- Una
endTsfinita può solo essere accorciata, mai estesa o azzerata (PlanEndTsCannotExtend). - Con un piano
Sunsetè possibile rimuovere i puller (il nuovo insieme deve essere un sottoinsieme di quello attuale) per revocare un puller compromesso; lo stato,endTse i metadati rimangono bloccati. - Le modifiche funzionano durante l'ultimo periodo di fatturazione di un piano,
purché
endTsrimanga invariata. - L'istruzione porta con sé lo stato del piano osservato al momento della firma
(
expectedCreatedAt,expectedEndTs,expectedPullers,expectedMetadataUri). Se il piano attivo non corrisponde più, l'aggiornamento viene rifiutato (StalePlanApproval); pertanto un aggiornamento firmato non aggiornato non può ripristinare i puller rimossi né annullare modifiche successive. Il metodoupdatePlandel client plugin recupera lo stato attivo per te; se costruisci manualmente, recupera il piano e passa i campi tu stesso.
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();
Iscrizione
Il sottoscrittore accetta i termini del piano corrente. Il PDA della sottoscrizione è derivato dal PDA del piano e dall'indirizzo del sottoscrittore.
Per raggruppare l'inizializzazione dell'autorità e la sottoscrizione in un'unica transazione, passa
il valore sentinella UNKNOWN_INIT_ID (esportato dall'SDK TypeScript) come
expectedSubscriptionAuthorityInitId. Il programma accetta l'autorità solo se
è stata creata nel slot corrente, quindi il sentinella funziona per le nuove registrazioni; un
utente di ritorno la cui autorità è stata creata in un slot precedente deve passare il
vero initId (il client plugin lo recupera per te) altrimenti la chiamata fallisce con
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,});
Riscuoti un Pagamento
Il commerciante o un puller autorizzato firma la riscossione. Quando il piano
utilizza una lista consentita di destinazione, il proprietario del token account
ricevente deve essere elencato 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();
Annulla e Revoca
L'annullamento contrassegna l'abbonamento come in scadenza. La revoca chiude il PDA dell'abbonamento dopo che è trascorso il periodo di scadenza dell'annullamento. Il sottoscrittore firma entrambe le transazioni.
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();
Cancella Immediatamente
Quando sia il sottoscrittore che il proprietario attuale del piano firmano,
cancelSubscriptionNow fa scadere la sottoscrizione al momento della cancellazione anziché
alla fine del periodo di fatturazione. Può anche abbreviare una cancellazione con periodo di grazia in sospeso. L'approvazione è vincolata all'inizio del periodo osservato al momento della firma; se
la sottoscrizione è cambiata nel frattempo, la transazione viene rifiutata
(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();
Riprendi un Abbonamento
Un abbonamento annullato può essere riattivato prima che venga revocato. Una
volta revocato, l'account dell'abbonamento viene chiuso e il sottoscrittore deve
abbonarsi nuovamente. La ripresa richiede
l'SubscriptionAuthority del sottoscrittore per il mint del piano, che il
programma convalida (proprietario, mint e init_id) e rifiuta se obsoleto o
reinizializzato.
L'istruzione porta con sé la scadenza osservata dal sottoscrittore al momento della firma
(expectedExpiresAtTs); una mancata corrispondenza viene rifiutata (StaleSubscriptionApproval),
pertanto un ripristino firmato non aggiornato non può annullare una cancellazione successiva che il sottoscrittore non ha mai
approvato.
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();
Note
amountè in unità base. Per un token a 6 decimali,5_000_000corrisponde a5token.- L'SDK TypeScript recupera i termini del piano in tempo reale durante
subscribese vengono omessi. - Il
SubscribeBuilderRust necessita dei termini del piano previsti. Recupera e decodifica prima l'account del piano, quindi passa quei campi tramiteSubscribeData. - Solo il merchant o un wallet elencato in
pullerspuò riscuotere i pagamenti. - Il sottoscrittore firma le transazioni di configurazione, annullamento e revoca. Il merchant o il puller autorizzato firma le transazioni di riscossione.
Is this page helpful?