Πρόγραμμα Συνδρομής

Ένα πρόγραμμα συνδρομής επιτρέπει σε έναν έμπορο να δημοσιεύσει όρους χρέωσης που μπορούν να αποδεχτούν οι χρήστες. Αφού ένας χρήστης εγγραφεί, ο έμπορος ή ένας εγκεκριμένος συλλέκτης μπορεί να εισπράξει έως και το ποσό του προγράμματος σε κάθε περίοδο χρέωσης.

Αυτός ο οδηγός παρουσιάζει την πλήρη ροή ως δομικά στοιχεία. Ο έμπορος δημιουργεί ένα πρόγραμμα, ο συνδρομητής το αποδέχεται και ο έμπορος ή ο συλλέκτης εισπράττει πληρωμές από το αποτελούμενο PDA συνδρομής.

Εγκατάσταση

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

Δημιουργία Προγράμματος

Ο έμπορος κατέχει το πρόγραμμα. Το PDA του προγράμματος προκύπτει από τη διεύθυνση του εμπόρου και το planId.

Ένας χορηγός μπορεί να χρηματοδοτήσει το rent του πλάνου περνώντας τον προαιρετικό payer ενώ ο έμπορος παραμένει ιδιοκτήτης του πλάνου. Η διαγραφή του πλάνου επιστρέφει τα χρήματα στον ιδιοκτήτη, όχι στον πληρωτή, επομένως ελέγχετε τη χορηγία εκτός αλυσίδας.

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

Ενημέρωση Προγράμματος

Ο έμπορος μπορεί να ενημερώσει τα μεταβλητά πεδία του προγράμματος μετά τη δημιουργία. Οι υπάρχοντες συνδρομητές διατηρούν τους όρους που αποδέχτηκαν, ενώ οι νέοι συνδρομητές αποδέχονται τους τρέχοντες όρους του προγράμματος.

Ισχύουν μερικοί κανόνες:

  • Ένα πεπερασμένο endTs μπορεί μόνο να συντομευτεί, ποτέ να επεκταθεί ή να διαγραφεί (PlanEndTsCannotExtend).
  • Σε ένα πλάνο Sunset μπορείτε να αφαιρέσετε pullers (το νέο σύνολο πρέπει να είναι υποσύνολο του τρέχοντος) για να ανακαλέσετε έναν παραβιασμένο puller· η κατάσταση, το endTs και τα μεταδεδομένα παραμένουν δεσμευμένα.
  • Οι επεξεργασίες λειτουργούν κατά την τελευταία περίοδο χρέωσης ενός πλάνου, εφόσον το endTs παραμένει αμετάβλητο.
  • Η εντολή φέρει την κατάσταση του πλάνου που παρατηρήθηκε κατά την υπογραφή (expectedCreatedAt, expectedEndTs, expectedPullers, expectedMetadataUri). Εάν το ενεργό πλάνο δεν ταιριάζει πλέον, η ενημέρωση απορρίπτεται (StalePlanApproval), οπότε μια ξεπερασμένη υπογεγραμμένη ενημέρωση δεν μπορεί να επαναφέρει αφαιρεμένους pullers ή να αναιρέσει μεταγενέστερες επεξεργασίες. Το updatePlan του plugin client ανακτά την ενεργή κατάσταση αυτόματα· όταν δημιουργείτε χειροκίνητα, ανακτήστε το πλάνο και περάστε τα πεδία μόνοι σας.
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();

Εγγραφή

Ο συνδρομητής αποδέχεται τους τρέχοντες όρους του πλάνου. Το PDA συνδρομής προκύπτει από το PDA του πλάνου και τη διεύθυνση του συνδρομητή.

Για να συνδυάσετε την αρχικοποίηση της αρχής και την εγγραφή σε μία μόνο συναλλαγή, περάστε το sentinel UNKNOWN_INIT_ID (που εξάγεται από το TypeScript SDK) ως expectedSubscriptionAuthorityInitId. Το πρόγραμμα αποδέχεται την αρχή μόνο εάν δημιουργήθηκε στο τρέχον slot, επομένως το sentinel λειτουργεί για νέες εγγραφές· ένας επιστρέφων χρήστης του οποίου η αρχή δημιουργήθηκε σε προηγούμενο slot πρέπει να περάσει το πραγματικό initId (το plugin client το ανακτά αυτόματα) ή η κλήση αποτυγχάνει με 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,
});

Είσπραξη Πληρωμής

Ο έμπορος ή ένας εξουσιοδοτημένος puller υπογράφει την είσπραξη. Όταν το πλάνο χρησιμοποιεί λίστα επιτρεπόμενων προορισμών, ο κάτοχος του token account του παραλήπτη πρέπει να figurerer αναφέρεται στο 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();

Ακύρωση και Ανάκληση

Η ακύρωση σημειώνει τη συνδρομή ως λήγουσα. Η ανάκληση κλείνει το PDA της συνδρομής αφού παρέλθει η προθεσμία λήξης της ακύρωσης. Ο συνδρομητής υπογράφει και τις δύο συναλλαγές.

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

Άμεση Ακύρωση

Όταν τόσο ο συνδρομητής όσο και ο τρέχων ιδιοκτήτης του πλάνου υπογράφουν, το cancelSubscriptionNow λήγει τη συνδρομή τη στιγμή της ακύρωσης αντί στο τέλος της περιόδου χρέωσης. Μπορεί επίσης να συντομεύσει μια εκκρεμή ακύρωση με περίοδο χάριτος. Η έγκριση είναι δεσμευμένη στην αρχή περιόδου που παρατηρήθηκε κατά την υπογραφή· εάν η συνδρομή έχει αλλάξει έκτοτε, η συναλλαγή απορρίπτεται (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();

Επαναφορά Συνδρομής

Μια ακυρωμένη συνδρομή μπορεί να επανενεργοποιηθεί πριν ανακληθεί. Μόλις ανακληθεί, ο λογαριασμός συνδρομής κλείνει και ο συνδρομητής πρέπει να εγγραφεί εκ νέου. Η επαναφορά απαιτεί το SubscriptionAuthority του συνδρομητή για το mint του πλάνου, το οποίο το πρόγραμμα επικυρώνει (ιδιοκτήτης, mint και init_id) και απορρίπτει αν είναι παρωχημένο ή έχει επανεκκινηθεί.

Η εντολή φέρει τη λήξη που παρατήρησε ο συνδρομητής κατά την υπογραφή (expectedExpiresAtTs)· μια αναντιστοιχία απορρίπτεται (StaleSubscriptionApproval), επομένως μια ξεπερασμένη υπογεγραμμένη επανέναρξη δεν μπορεί να διαγράψει μια μεταγενέστερη ακύρωση που ο συνδρομητής δεν ενέκρινε ποτέ.

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

Σημειώσεις

  • Το amount είναι σε βασικές μονάδες. Για ένα token 6 δεκαδικών, το 5_000_000 σημαίνει 5 tokens.
  • Το TypeScript SDK ανακτά τους ενεργούς όρους πλάνου κατά τη διάρκεια του subscribe όταν τους παραλείπετε.
  • Το SubscribeBuilder της Rust χρειάζεται τους αναμενόμενους όρους πλάνου. Ανακτήστε και αποκωδικοποιήστε πρώτα τον λογαριασμό πλάνου και στη συνέχεια διαβιβάστε αυτά τα πεδία μέσω του SubscribeData.
  • Μόνο ο έμπορος ή ένα πορτοφόλι που αναφέρεται στο pullers μπορεί να εισπράξει πληρωμές.
  • Ο συνδρομητής υπογράφει τις συναλλαγές εγκατάστασης, ακύρωσης και ανάκλησης. Ο έμπορος ή ο εγκεκριμένος συλλέκτης υπογράφει τις συναλλαγές είσπραξης.

Is this page helpful?

Πίνακας Περιεχομένων

Επεξεργασία Σελίδας
© 2026 Ίδρυμα Solana. Με επιφύλαξη παντός δικαιώματος.