خطة الاشتراك

تتيح خطة الاشتراك للتاجر نشر شروط الفوترة التي يمكن للمستخدمين قبولها. بعد اشتراك المستخدم، يمكن للتاجر أو السّاحب المُعتمد تحصيل ما يصل إلى مبلغ الخطة في كل فترة فوترة.

يعرض هذا الدليل التدفق الكامل كوحدات بناء. ينشئ التاجر خطة، ويقبلها المشترك، ثم يقوم التاجر أو السّاحب بتحصيل المدفوعات من حساب 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 يمكنك إزالة المسحوبات (يجب أن تكون المجموعة الجديدة مجموعة فرعية من المجموعة الحالية) لإلغاء صلاحية ساحب مخترق؛ تبقى الحالة وendTs والبيانات الوصفية مجمّدة.
  • تعمل التعديلات خلال فترة الفوترة الأخيرة للخطة طالما ظل endTs دون تغيير.
  • تحمل التعليمة حالة الخطة المرصودة عند التوقيع (expectedCreatedAt، expectedEndTs، expectedPullers، expectedMetadataUri). إذا لم تعد الخطة الحالية مطابقة، يُرفض التحديث (StalePlanApproval)، وبالتالي لا يمكن للتحديث الموقَّع القديم استعادة المسحوبين المحذوفين أو التراجع عن التعديلات اللاحقة. يقوم عميل الإضافة updatePlan بجلب الحالة الحالية تلقائيًا؛ أما عند البناء اليدوي، فيجب جلب الخطة وتمرير الحقول بنفسك.
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 الخطة وعنوان المشترك.

لتجميع تهيئة السلطة والاشتراك في معاملة واحدة، مرِّر القيمة الحارسة UNKNOWN_INIT_ID (المُصدَّرة من TypeScript SDK) بوصفها expectedSubscriptionAuthorityInitId. يقبل البرنامج السلطة فقط إذا كانت قد أُنشئت في slot الحالي، لذا تصلح القيمة الحارسة للتسجيلات الجديدة؛ أما المستخدم العائد الذي أُنشئت سلطته في slot سابق، فيجب عليه تمرير initId الفعلي (يجلبه عميل الإضافة تلقائيًا) وإلا فشلت العملية بخطأ 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,
});

تحصيل دفعة

يوقّع التاجر أو أحد المسحوبات المدرجة في القائمة البيضاء على عملية التحصيل. عندما تستخدم الخطة قائمة مسموح بها للوجهات، يجب إدراج مالك token account المستلم في 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 الخاص بالمنت المرتبط بالخطة، والذي يتحقق منه البرنامج (المالك، والمنت، و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 بالوحدات الأساسية. بالنسبة للرمز ذي 6 خانات عشرية، يعني 5_000_000 قيمة 5 رمزًا.
  • يجلب TypeScript SDK شروط الخطة المباشرة أثناء subscribe عند حذفها.
  • يحتاج SubscribeBuilder في Rust إلى شروط الخطة المتوقعة. احرص على جلب حساب الخطة وفك ترميزه أولًا، ثم مرِّر تلك الحقول عبر SubscribeData.
  • يمكن للتاجر فقط أو أي محفظة مدرجة في pullers تحصيل المدفوعات.
  • يوقّع المشترك معاملات الإعداد والإلغاء والسحب. أما التاجر أو المحصّل المعتمد فيوقّع معاملات التحصيل.

Is this page helpful?

جدول المحتويات

تعديل الصفحة