استراتيجية الـ keeper
الدالة SubscriptionHub.charge(id) (EVM) وتعليمة charge في Anchor (Solana) عامة ولا تتطلب صلاحيات. يستطيع أي EOA استدعاءها. والـ «keeper» مجرد مُجدوِل يفعل ذلك.
تشرح هذه الصفحة كيف تنفّذ OrcaRail عمليات الخصم، وما الذي يحدث عند عدم توفر الـ keeper، وما أهداف SLO التي ننشرها.
الـ keeper الأساسي: مُجدوِل NestJS
يوجد الـ keeper الأساسي في api/src/scheduler/processors/subscription-auto-charge.processor.ts. وهو معالج BullMQ:
- يقرأ الاشتراكات المستحقة من قاعدة البيانات (نسخة مطابقة للحالة على السلسلة يحافظ عليها المُفهرِس).
- يجمّعها حسب chain id.
- يوقّع ويرسل معاملة
charge(ids[])لكل سلسلة باستخدام EOA keeper خاص بكل سلسلة. - عند النجاح، ينتظر التأكيد ويحدّث قاعدة البيانات عبر أحداث المُفهرِس.
- عند الـ revert، يحلّل محدِّد الـ revert ويربطه بحالة في
SubscriptionAutoChargeEntity:ChargeSkipped(InsufficientAllowance)←past_due+ بريد الـ allowanceChargeSkipped(InsufficientBalance)←past_due+ بريد الرصيدChargeSkipped(CapExceeded)←canceled(على الدافع إعادة الموافقة)ChargeSkipped(NotDue)← غير ضار (تقريب في المُفهرِس)؛ إعادة المحاولة بعد دورة واحدة
الـ keeper الاحتياطي: Chainlink Automation (المفضّل)
بدءًا من المرحلة 3، يسجّل كل نشر لـ SubscriptionHub Upkeep في Chainlink Automation:
- تعيد
checkUpkeep(bytes)القيمةperformData = abi.encode(ids[])للمعرّفات التي يتحقق فيهاisDue(id) && !chargedThisBlock(id). - تستدعي
performUpkeep(bytes)الدالةcharge(ids[])دفعةً واحدة. - يُموَّل بـ LINK من خزينة المنصة.
- مُعدّ في وضع «conditional» بحد غاز يناسب حجم الدفعة.
الـ keeper الاحتياطي: Gelato Web3 Functions (بديل)
إذا لم يتوفر Chainlink على سلسلة ما (أو لم يناسب تسعيره)، تستطلع Gelato Web3 Function دالة العرض isDue() في المركز وترسل charge(ids[]) عند الاستحقاق. يُدفع لاحقًا من 1Balance في Gelato.
الـ keeper على Solana
يستخدم فرع Solana في NestJS نمط المُجدوِل نفسه مع keeper يوقّع بـ SOL. لا يوجد اليوم على Solana مكافئ لـ Chainlink Automation بتغطية مماثلة؛ الخيارات الاحتياطية:
- Clockwork (إذا كانت حالة صيانته مقبولة وقت الطرح): cron أصلي على Solana.
- نسخة ثانية من NestJS في منطقة مختلفة كتكرار أبسط.
رسوم Solana منخفضة بما يكفي لتكون مراقبة رصيد الغاز أقل أهمية منها في EVM.
إدارة الغاز
EVM
- السلاسل التي لديها سياسة paymaster من Alchemy: يستدعي الـ keeper الدالة
charge()كـ UserOperation مموّلة عبر المنطق القائم في api/src/withdrawals/services/gas-strategy. - السلاسل دون paymaster: يموّل الـ keeper الغاز ذاتيًا من رصيده الساخن، وتعيد محفظة المنصة تعبئته يوميًا.
- سقف سعر الغاز: تُؤجَّل عمليات الخصم كتلة واحدة إذا تجاوز سعر الغاز حدًا خاصًا بكل سلسلة.
- غاز يدعمه التاجر (اختياري): يمكن للتجار ضبط
gas_refund_address؛ ويُسحب التعويض عبر منطق تقسيم الرسوم.
Solana
- يعمل الـ keeper برصيد SOL ثابت لكل سلسلة.
- تُضبط رسوم الأولوية ديناميكيًا بناءً على الكتل الأخيرة؛ وترفع كل إعادة محاولة الأولوية درجة واحدة.
أهداف SLO
| المقياس | الهدف | ينطبق على |
|---|---|---|
تنفيذ charge() خلال 10 دقائق من nextChargeAt | 99.5% | كل السلاسل المدعومة |
تنفيذ charge() خلال ساعة من nextChargeAt | 99.9% | كل السلاسل المدعومة |
| رصيد الغاز الساخن لـ EOA الـ keeper فوق حد التنبيه | 99.99% | كل السلاسل المدعومة |
فهرسة أحداث Paid / Charged خلال دقيقتين | 99.5% | كل السلاسل المدعومة |
معدل revert في charge() (حالات revert غير الضارة فقط) | أقل من 0.1% | كل السلاسل المدعومة |
تستثني حالات الـ revert غير الضارة ChargeSkipped(InsufficientAllowance) وChargeSkipped(InsufficientBalance) وChargeSkipped(CapExceeded)؛ فهذه حالات متوقعة من جهة الدافع.
المراقبة والتنبيهات
باستخدام مسار Sentry + Datadog القائم:
- معدل revert في
charge()في الساعة أعلى من الحد ← PagerDuty - رصيد العملة الأصلية لـ EOA الـ keeper أقل من
gasPrice × gasLimit × 100← PagerDuty - رصيد upkeep في Chainlink أقل من الحد ← تحذير
1Balanceفي Gelato أقل من الحد ← تحذير- تأخر أحداث المُفهرِس أكثر من 5 دقائق ← تحذير
لوحات المتابعة: واحدة لكل سلسلة، إضافة إلى ملخص عام. أدلة التشغيل للأعطال مرتبطة من جدول المناوبة.
الحماية من الـ reorg
- تفرض
SubscriptionHub.charge(id)الشرطblock.timestamp > lastChargedAtبشكل رتيب. أي reorg قد يكرر خصمًا يُرفض بـAlreadyChargedThisPeriod. - يستخدم برنامج Solana
last_charged_atبالطريقة نفسها؛ حالات reorg على Solana أندر بكثير، لكن الثابت نفسه قائم.
التدخل اليدوي
يستطيع التجار في حالات الطوارئ (مثل توقف NestJS وChainlink في الوقت نفسه) تشغيل charge(id) يدويًا عبر إجراء في لوحة التحكم. تُوقَّع المعاملة من محفظة التاجر نفسه؛ وليست احتجازية.
ما لا يستطيع الـ keeper فعله
قائمة صريحة، لتوضيح النطاق:
- إنشاء اشتراكات دون توقيع الدافع.
- رفع
capأوamountأوintervalلاشتراك قائم. - نقل الأموال إلى عنوان غير موجود في
Splitالخاص بالاشتراك. - تجاوز
nextChargeAtأوlastChargedAt. - تعطيل مسارات الإيقاف المؤقت أو الإلغاء أو الاسترداد.
يحتفظ الـ keeper بالغاز ولا شيء غيره.