إنتقل إلى المحتوى الرئيسي

المرحلة 2: اشتراكات بسحب الـ allowance

· 6 دقائق قراءة
OrcaRail
Crypto payment rails for web2 apps

20 أبريل 2026 — تنقل المرحلة 2 الفوترة المتكررة إلى SubscriptionHub على السلسلة. يوقّع الدافع approve واحدًا في محفظته، ويمكن لأي طرف (بما في ذلك الـ keeper لدينا) استدعاء charge(id) عند استحقاق فترة. تنتقل الأموال مباشرةً من الدافع إلى حصص التاجر؛ ولا تحتفظ بها OrcaRail أبدًا.

باختصار​

  • يسجّل SubscriptionHub.createSubscription(...) الجدول على السلسلة.
  • يوقّع الدافع approve(USDC, hub, cap) مرة واحدة في محفظته المعتادة. وهي الخطوة نفسها الموثقة لدينا أصلًا في الخصم التلقائي.
  • يمكن لأي EOA استدعاء charge(id). يتولى المُجدوِل لدينا في NestJS ذلك افتراضيًا؛ وسيدعمه Chainlink/Gelato بدءًا من المرحلة 3.
  • حماية من إعادة التنظيم (reorg) عبر lastChargedAt رتيب. لا خصم مزدوج.
  • ينتقل المشتركون الحاليون اختياريًا عند التجديد.

ما الذي تغيّره هذه المرحلة​

اليوم، يستخدم api/src/scheduler/processors/subscription-auto-charge.processor.ts حسابًا ذكيًا تتحكم به OrcaRail للسحب من الدافع عبر الـ approve القائم. يعمل ذلك، لكن الحساب الذكي ملكنا؛ أي أن الثقة تمر عبرنا.

بعد المرحلة 2:

يبقى الـ allowance من الدافع إلى عقد، لكن العقد قابل للتدقيق وغير قابل للتغيير، ويمكن لأي طرف، لا نحن فقط، أن يكون الـ keeper.

العقود الجديدة​

SubscriptionHub​

نسخة واحدة لكل شبكة. واجهة مصغّرة:

function createSubscription(
bytes32 id,
address payer,
IERC20 token,
uint256 amount,
uint64 interval,
uint256 cap,
Split calldata split
) external;

function charge(bytes32 id) external; // any caller
function cancel(bytes32 id) external; // payer only
function subscription(bytes32 id) view returns (Subscription memory);

event SubscriptionCreated(bytes32 indexed id, address indexed payer, ...);
event Charged(bytes32 indexed id, uint256 amount, uint64 nextChargeAt);
event ChargeSkipped(bytes32 indexed id, bytes32 reason);
event Canceled(bytes32 indexed id);

الثوابت التي تفرضها charge()​

  • block.timestamp >= nextChargeAt — لا يمكن الخصم مبكرًا.
  • block.timestamp > lastChargedAt — رتيب، فلا يمكن لإعادة التنظيم أن تسبب خصمًا مزدوجًا.
  • amountCharged + amount <= cap — يفرض حد الموافقة إضافةً إلى allowance في ERC-20.
  • token.allowance(payer, hub) >= amount — revert صريح مع InsufficientAllowance لنص webhook واضح.
  • token.balanceOf(payer) >= amount — كذلك، مع InsufficientBalance.

يُعاد استخدام Split من المرحلة 1: FeeSplitter نفسه ودلالة نقاط الأساس نفسها.

تغييرات API​

التغيير الكبير أن api/src/scheduler/processors/subscription-auto-charge.processor.ts يتوقف عن السحب من حسابنا الذكي ويبدأ باستدعاء SubscriptionHub.charge(id) عبر EOA خاص بالـ keeper.

بشكل ملموس:

  • حساب EOA الخاص بالـ keeper محفظة ساخنة لكل شبكة لا تحتوي إلا على الغاز. لا يملك أي صلاحية على أموال التاجر.
  • إذا كانت للشبكة سياسة paymaster من Alchemy (راجع api/src/withdrawals/services/gas-strategy)، يستخدم الـ keeper المسار المدعوم؛ وإلا فيدفع من رصيد الغاز الخاص به.
  • عند الـ revert، يحلل المعالج محدد الخطأ (selector) ويحدّث حالات SubscriptionAutoChargeEntity نفسها التي نستخدمها اليوم (past_due وinsufficient_balance وغيرها).
  • تُرسل الـ webhooks ورسائل البريد من api/src/mail/mail-templates/subscription-*.hbs دون تغيير.

تحصل تجربة الدافع في pay على تعديلين بسيطين:

  1. تشير خطوة approve() الآن بالـ spender إلى عنوان SubscriptionHub بدلًا من محفظتنا الساخنة. هذا تغيير في نص واحد وقيمة إعداد واحدة؛ والتدفق الذي يراه الدافع مطابق.
  2. زر «إلغاء» يستدعي SubscriptionHub.cancel(id) ثم يطلب approve(hub, 0) على السلسلة للتنظيف الكامل. يبقى توثيق إلغاء الخصم التلقائي الحالي صحيحًا.

الإيقافات​

  • مسار السحب الاحتجازي في subscription-auto-charge.processor.ts — مع العلم المفعّل، لا يعمل إلا فرع SubscriptionHub.charge.
  • الحساب الذكي المملوك لـ OrcaRail بصفته approval_spender_address — تخزّن الاشتراكات الجديدة عنوان المركز بدلًا منه.
  • يبقى الحقل approval_spender_address في استجابة API؛ لكنه يشير الآن إلى مكان جديد قابل للتدقيق.

خطة الترحيل​

لا يُنقل المشتركون الحاليون قسرًا أبدًا. نقدم مسارين:

  1. عند التجديد: في الدورة التالية، يطلب شريط في لوحة التحكم من الدافع إعادة توقيع approve(hub, cap) على SubscriptionHub الجديد. نقرة واحدة. يمكن ترك الـ allowance القديم للمحفظة الساخنة القديمة أو إلغاؤه؛ لم يعد مهمًا.
  2. بمبادرة المستخدم: زر «الانتقال إلى النموذج غير الاحتجازي» في صفحة تفاصيل الاشتراك يفعل الشيء نفسه فورًا.

الاشتراكات التي لا تنتقل أبدًا تبقى على المسار الاحتجازي حتى الإلغاء. ويُوقف هذا المسار في المرحلة 7 مع إشعار مسبق كافٍ.

المخاطر ووسائل الحد منها​

  • ارتباك الدافع عند الترحيل. الحل: يحاكي قالب البريد لغة subscription-auto-charge-approved.hbs الحالي؛ وشريط في لوحة التحكم؛ وسؤال شائع في /docs/non-custodial/migration-guide.
  • انقطاع الـ keeper يفوّت خصمًا. الحل: charge() لا يتطلب إذنًا؛ يمكن لأي نسخة من Chainlink Automation أو Gelato أن تكون احتياطيًا؛ كما يمكن للتاجر استدعاء charge() يدويًا. الخطة الكاملة في المرحلة 3.
  • خصم مزدوج بسبب إعادة التنظيم. الحل: ثابت lastChargedAt الرتيب؛ واختبار وحدة مع إعادة تنظيم على fork.
  • إلغاء الدافع للتفويض بين الدورات. الحل: تصدر charge() الحدث ChargeSkipped(AllowanceRevoked)؛ ويُرسل قالب البريد الحالي subscription-insufficient-allowance.hbs؛ وينتقل الاشتراك إلى past_due.
  • انحراف قائمة الرموز المسموح بها. الحل: لا يقبل المركز إلا الرموز التي تحقق SubscriptionHub.isAllowed(token)؛ والتغييرات تتطلب التوقيع المتعدد.

قائمة الحالة​

  • تنفيذ SubscriptionHub مع كل الثوابت
  • اختبارات fuzz لحالات السقف والـ allowance وإعادة التنظيم والتخطي
  • إنجاز تدقيق إضافي يغطي SubscriptionHub
  • تجهيز EOA للـ keeper لكل شبكة، مع مراقبة الغاز عبر Sentry + Datadog
  • تحويل وجهة approve في تطبيق الدفع إلى SubscriptionHub
  • تحديث نقطة النهاية approve-auto-charge لقبول allowances يكون فيها المركز هو الـ spender
  • شريط في لوحة التحكم للاشتراكات الحالية
  • ترحيل المجموعة التجريبية واستقرارها لمدة 14 يومًا

ما التالي​

التوسع إلى شبكات متعددة وkeeper ثانٍ: المرحلة 3.


التوثيق المرجعي: حالة المرحلة 2 · دليل الترحيل · مرجع العقود · لماذا سحب الـ allowance