المرحلة 2: اشتراكات بسحب الـ allowance
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 على تعديلين بسيطين:
- تشير خطوة
approve()الآن بالـ spender إلى عنوانSubscriptionHubبدلًا من محفظتنا الساخنة. هذا تغيير في نص واحد وقيمة إعداد واحدة؛ والتدفق الذي يراه الدافع مطابق. - زر «إلغاء» يستدعي
SubscriptionHub.cancel(id)ثم يطلبapprove(hub, 0)على السلسلة للتنظيف الكامل. يبقى توثيق إلغاء الخصم التلقائي الحالي صحيحًا.
الإيقافات
- مسار السحب الاحتجازي في
subscription-auto-charge.processor.ts— مع العلم المفعّل، لا يعمل إلا فرعSubscriptionHub.charge. - الحساب الذكي المملوك لـ OrcaRail بصفته
approval_spender_address— تخزّن الاشتراكات الجديدة عنوان المركز بدلًا منه. - يبقى الحقل
approval_spender_addressفي استجابة API؛ لكنه يشير الآن إلى مكان جديد قابل للتدقيق.
خطة الترحيل
لا يُنقل المشتركون الحاليون قسرًا أبدًا. نقدم مسارين:
- عند التجديد: في الدورة التالية، يطلب شريط في لوحة التحكم من الدافع إعادة توقيع
approve(hub, cap)علىSubscriptionHubالجديد. نقرة واحدة. يمكن ترك الـ allowance القديم للمحفظة الساخنة القديمة أو إلغاؤه؛ لم يعد مهمًا. - بمبادرة المستخدم: زر «الانتقال إلى النموذج غير الاحتجازي» في صفحة تفاصيل الاشتراك يفعل الشيء نفسه فورًا.
الاشتراكات التي لا تنتقل أبدًا تبقى على المسار الاحتجازي حتى الإلغاء. ويُوقف هذا المسار في المرحلة 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
