PaiementsAbonnements

Plan d'abonnement

Un plan d'abonnement permet à un commerçant de publier des conditions de facturation que les utilisateurs peuvent accepter. Après qu'un utilisateur s'abonne, le commerçant ou un préleveur autorisé peut collecter jusqu'au montant du plan à chaque période de facturation.

Ce guide présente le flux complet sous forme de blocs de construction. Le commerçant crée un plan, l'abonné l'accepte, et le commerçant ou le préleveur collecte les paiements depuis le PDA d'abonnement résultant.

Installation

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

Créer un plan

Le commerçant est propriétaire du plan. Le PDA du plan est dérivé de l'adresse du commerçant et de 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,
});

Mettre à jour un plan

Le commerçant peut mettre à jour les champs modifiables du plan après sa création. Les abonnés existants conservent les conditions qu'ils ont acceptées, tandis que les nouveaux abonnés acceptent les conditions actuelles du plan.

Quelques règles s'appliquent :

  • Un endTs fini ne peut être que raccourci, jamais prolongé ou effacé (PlanEndTsCannotExtend).
  • Dans un plan Sunset, vous pouvez supprimer des pullers (le nouvel ensemble doit être un sous-ensemble de l'ensemble actuel) pour révoquer un puller compromis ; le statut, le endTs et les métadonnées restent figés.
  • Les modifications sont possibles pendant la dernière période de facturation d'un plan, à condition que le endTs reste inchangé.
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();

S'abonner

L'abonné accepte les conditions du plan en vigueur. Le PDA d'abonnement est dérivé du PDA du plan et de l'adresse de l'abonné.

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

Collecter un paiement

Le marchand ou un puller whitelisté signe la collecte. Lorsque le plan utilise une liste d'autorisation de destinations, le propriétaire du token account récepteur doit être répertorié dans 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();

Annuler et Révoquer

L'annulation marque l'abonnement comme se terminant. La révocation ferme le PDA de l'abonnement une fois le délai d'expiration de l'annulation écoulé. L'abonné signe les deux transactions.

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

Reprendre un Abonnement

Un abonnement annulé peut être réactivé avant d'être révoqué. Une fois révoqué, le compte d'abonnement est clôturé et l'abonné doit s'abonner à nouveau. La reprise nécessite le SubscriptionAuthority de l'abonné pour le mint du plan, que le programme valide (propriétaire, mint et init_id) et rejette s'il est périmé ou réinitialisé.

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

Notes

  • amount est exprimé en unités de base. Pour un token à 6 décimales, 5_000_000 correspond à 5 tokens.
  • Le SDK TypeScript récupère les conditions du plan en temps réel lors de subscribe si vous ne les fournissez pas.
  • L'SubscribeBuilder Rust nécessite les conditions du plan attendues. Récupérez et décodez d'abord le compte du plan, puis transmettez ces champs via SubscribeData.
  • Seul le marchand ou un portefeuille répertorié dans pullers peut collecter les paiements.
  • L'abonné signe les transactions de configuration, d'annulation et de révocation. Le marchand ou le collecteur autorisé signe les transactions de collecte.

Is this page helpful?

Table des matières

Modifier la page
© 2026 Fondation Solana. Tous droits réservés.