Abonelik Planı

Abonelik planı, bir satıcının kullanıcıların kabul edebileceği faturalandırma koşullarını yayınlamasına olanak tanır. Bir kullanıcı abone olduktan sonra, satıcı veya onaylanmış bir tahsildar her faturalandırma döneminde plan tutarına kadar tahsilat yapabilir.

Bu kılavuz, tam akışı yapı taşları olarak göstermektedir. Satıcı bir plan oluşturur, abone bunu kabul eder ve satıcı veya tahsildar ortaya çıkan abonelik PDA'sından ödemeleri tahsil eder.

Kurulum

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

Plan Oluşturma

Satıcı, planın sahibidir. Plan PDA'sı, satıcı adresinden ve planId'den türetilir.

Bir sponsor, isteğe bağlı payer parametresini geçirerek planın rent bedelini karşılayabilir; bu durumda satıcı, planın sahibi olmaya devam eder. Planın silinmesi durumunda geri ödeme ödeyene değil, sahibine yapılır; bu nedenle sponsorluk işlemlerini zincir dışında kontrol altında tutun.

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

Planı Güncelleme

Satıcı, oluşturulduktan sonra değiştirilebilir plan alanlarını güncelleyebilir. Mevcut aboneler kabul ettikleri koşulları korurken, yeni aboneler mevcut plan koşullarını kabul eder.

Birkaç kural geçerlidir:

  • Sonlu bir endTs yalnızca kısaltılabilir, hiçbir zaman uzatılamaz veya temizlenemez (PlanEndTsCannotExtend).
  • Sunset planında, ele geçirilmiş bir çekiciyi iptal etmek için çekicileri kaldırabilirsiniz (yeni küme, mevcut kümenin bir alt kümesi olmalıdır); durum, endTs ve meta veriler dondurulmuş kalır.
  • Düzenlemeler, endTs değişmediği sürece bir planın son faturalandırma döneminde de geçerlidir.
  • Talimat, imzalama anında gözlemlenen plan durumunu taşır (expectedCreatedAt, expectedEndTs, expectedPullers, expectedMetadataUri). Canlı plan artık eşleşmiyorsa güncelleme reddedilir (StalePlanApproval); dolayısıyla bayat bir imzalı güncelleme, kaldırılmış çekicileri geri getiremez veya sonraki düzenlemeleri geri alamaz. Eklenti istemcisinin updatePlan işlevi canlı durumu sizin için getirir; manuel olarak oluştururken planı kendiniz çekip alanları kendiniz iletmeniz gerekir.
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();

Abone Ol

Abone, mevcut plan koşullarını kabul eder. Abonelik PDA'sı, plan PDA'sından ve abone adresinden türetilir.

Yetki başlatma ve aboneliği tek bir işlemde birleştirmek için expectedSubscriptionAuthorityInitId olarak TypeScript SDK tarafından dışa aktarılan UNKNOWN_INIT_ID sentinel değerini geçirin. Program yetkiyi yalnızca mevcut slot içinde oluşturulmuşsa kabul eder; bu nedenle sentinel değeri yeni kayıtlar için geçerlidir. Yetkisi daha önceki bir slot'ta oluşturulmuş olan geri dönen bir kullanıcının gerçek initId değerini geçirmesi gerekir (eklenti istemcisi bunu sizin için getirir); aksi takdirde çağrı StaleSubscriptionAuthority hatasıyla başarısız olur.

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

Ödeme Tahsil Et

Ödemeyi merchant veya beyaz listedeki bir çekici imzalar. Plan bir hedef izin listesi kullandığında, alıcı token account sahibi destinations listesinde yer almalıdır.

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

İptal Et ve İptal Yetki Kaldır

İptal etmek, aboneliği sona erecek şekilde işaretler. Yetki kaldırma ise iptal süresinin dolmasının ardından abonelik PDA'sını kapatır. Abone her iki işlemi de imzalar.

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

Hemen İptal Et

Hem abonenin hem de mevcut plan sahibinin imzalaması durumunda cancelSubscriptionNow, aboneliği fatura döneminin sonunda değil, iptal anında sona erdirir. Aynı zamanda bekleyen bir ek süre iptalini de kısaltabilir. Onay, imzalama anında gözlemlenen dönem başlangıcına bağlıdır; abonelik o tarihten bu yana değişmişse işlem reddedilir (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();

Aboneliği Sürdür

İptal edilmiş bir abonelik, yetkisi kaldırılmadan önce yeniden etkinleştirilebilir. Yetki kaldırıldıktan sonra abonelik hesabı kapatılır ve abonenin yeniden abone olması gerekir. Sürdürme işlemi, aboneden planın mint'i için SubscriptionAuthority gerektirir; program bu bilgiyi (sahip, mint ve init_id) doğrular ve bilgi güncel değilse ya da yeniden başlatılmışsa reddeder.

Talimat, abonenin imzalama anında gözlemlediği son kullanma tarihini taşır (expectedExpiresAtTs); bir uyuşmazlık reddedilir (StaleSubscriptionApproval). Bu sayede bayat bir imzalı yeniden başlatma, abonenin hiç onaylamadığı sonraki bir iptali geçersiz kılamaz.

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

Notlar

  • amount temel birimler cinsindendir. 6 ondalık basamaklı bir token için 5_000_000, 5 token anlamına gelir.
  • TypeScript SDK, subscribe sırasında plan koşullarını atladığınızda bunları canlı olarak çeker.
  • Rust SubscribeBuilder, beklenen plan koşullarına ihtiyaç duyar. Önce plan hesabını çekip çözümleyin, ardından bu alanları SubscribeData aracılığıyla iletin.
  • Yalnızca merchant veya pullers listesinde yer alan bir cüzdan ödeme toplayabilir.
  • Abone, kurulum, iptal ve yetki kaldırma işlemlerini imzalar. Merchant veya onaylı toplayıcı ise ödeme toplama işlemlerini imzalar.

Is this page helpful?

İçindekiler

Sayfayı Düzenle
© 2026 Solana Vakfı. Tüm hakları saklıdır.