PagamentiAbbonamenti

Piano di abbonamento

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.

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 endTs finita 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, endTs e i metadati rimangono bloccati.
  • Le modifiche funzionano durante l'ultimo periodo di fatturazione di un piano, purché endTs rimanga invariata.
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.

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

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.

await subscriberClient.subscriptions.instructions
.resumeSubscription({
subscriber: subscriberSigner,
planPda,
tokenMint,
})
.sendTransaction();

Note

  • amount è in unità base. Per un token a 6 decimali, 5_000_000 corrisponde a 5 token.
  • L'SDK TypeScript recupera i termini del piano in tempo reale durante subscribe se vengono omessi.
  • Il SubscribeBuilder Rust necessita dei termini del piano previsti. Recupera e decodifica prima l'account del piano, quindi passa quei campi tramite SubscribeData.
  • Solo il merchant o un wallet elencato in pullers può 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?

Indice dei contenuti

Modifica pagina
© 2026 Solana Foundation. Tutti i diritti riservati.