Free

جعل Claude يرحّل تكامل x402 مكتوباً يدوياً إلى gem المجتمع

الهجرة من اليدوي إلى gem: -622/+317 سطراً صافياً. الـ controller ينزل من 30 سطر سباكة بروتوكول إلى 4. فخاخ: importmap يُسقط pin بصمت، YAML يقرأ 0x... كعدد صحيح.


diff لأحد commits:

19 files changed, 317 insertions(+), 622 deletions(-)

محذوف:

app/services/x402/facilitator_client.rb        53 سطراً
app/services/x402/payment_handler.rb           86 سطراً
test/services/x402/facilitator_client_test.rb  112 سطراً
test/services/x402/payment_handler_test.rb     108 سطراً

مضاف: سطر واحد في Gemfile، config/initializers/x402.rb (29 سطراً)، طريقتا record_x402! على Purchase/Subscription + اختبارات الـ model المقابلة.

هذا ليس refactor — هذا استبدال الجزء الذي كتبته بالجزء الذي كتبه شخص آخر. النسخة اليدوية كانت تعمل لأسبوعين. الدفع لمرة واحدة، الاشتراك، تسجيل tx_hash — كلها تعمل. إذن لماذا الترحيل؟

هذا المقال عن كيفية جعل Claude يقوم بهذا النوع من الترحيل، ومتى يستحق ذلك.


الخلفية: كيف بدت النسخة اليدوية

x402 هو بروتوكول HTTP 402 Payment Required. يوقّع العميل تفويض EIP-3009، ويتحقق الخادم وَيسوّي معاملة on-chain عبر facilitator.

PaymentHandler اليدوي كان تقريباً:

handler = X402::PaymentHandler.new
payment_payload = handler.decode_payment_signature(params[:payment_signature])
requirements = {
  scheme: "exact",
  network: X402::PaymentHandler::NETWORK.call,
  maxAmountRequired: (plan.price_cents * 10_000).to_s,
  payTo: X402::PaymentHandler::WALLET_ADDRESS.call,
  token: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
  description: "#{plan.key} subscription"
}

verify_result = handler.facilitator.verify(payment_payload, requirements)
unless verify_result["isValid"]
  render json: { error: verify_result["invalidReason"] || "Verification failed" }, status: :unprocessable_entity and return
end

settle_result = handler.facilitator.settle(payment_payload, requirements)
unless settle_result["success"]
  render json: { error: settle_result["errorReason"] || "Settlement failed" }, status: :unprocessable_entity and return
end

نحو 30 سطراً من سباكة البروتوكول داخل الـ controller: فك ترميز التوقيع، بناء requirements، verify، settle، معالجة الأخطاء. عنوان عقد USDC مثبّت في الكود. الواجهة الأمامية مثل ذلك — window.ethereum.request مكتوب يدوياً، تبديل chain يدوي، تركيب header X-PAYMENT يدوي.

المحفّز: المكتبات نضجت

جعل Claude يمسح أسبوعياً نظام البروتوكولات التي تعتمد عليها عادة جيدة — خصوصاً لبروتوكول مثل x402 ظهر حديثاً. يمكن لـ Claude أن يتابع تطور gem x402-rails (جانب Ruby) وx402-fetch (جانب JS)، وأن يرى المجتمع يتشكّل.

حتى يوم ما:

أنت: "هل x402-rails وx402-fetch ناضجتان الآن؟ إن كانتا كذلك، فرحِّل."

يقرأ Claude README وchangelogs ويعود بالتقرير: بروتوكول v1 مستقر، وضع non-optimistic يعطي نتيجة settlement، الـ facilitator الافتراضي payai.network. الترحيل ممكن.

بعد الترحيل: الـ controller يصبح 4 أسطر

نفس action subscribe بعد الترحيل:

def subscribe
  plan = Plan.find(params[:plan])

  if Current.user.subscriptions.active.exists?(plan: plan.key)
    render json: { success: true, plan: plan.key, already_active: true }
    return
  end

  x402_paywall(amount: plan.price_dollars)
  return if performed? # رندر الـ gem إما 402 أو خطأ، وتوقف بالفعل

  settlement = request.env["x402.settlement_result"]
  payment    = request.env["x402.payment"]
  return render_failure("settlement failed") unless settlement&.success?

  Subscription.record_x402!(user: Current.user, plan: plan, payment: payment, settlement: settlement)
end

جزء البروتوكول كله داخل الـ gem. x402_paywall(amount:) يعالج في سطر:

  • الطلب الأول بلا header X-PAYMENT → يرندر الـ gem 402 + PaymentRequirements
  • يوقّع عميل x402-fetch تفويض EIP-3009 ويعيد المحاولة مع X-PAYMENT
  • يستدعي الـ gem مسارَي /verify و/settle للـ facilitator (non-optimistic، أي ينتظر settle قبل الرجوع)
  • performed? يكتشف أن الـ gem قد رندر مسبقاً فنعمل return؛ وإلا فإن request.env["x402.settlement_result"] وrequest.env["x402.payment"] يحتويان النتيجة

التهيئة في config/initializers/x402.rb (29 سطراً):

X402.configure do |config|
  config.wallet_address = Rails.application.credentials.dig(:x402, :wallet_address)
  config.facilitator    = Rails.application.credentials.dig(:x402, :facilitator_url) ||
                          "https://facilitator.payai.network"

  # production → Base الشبكة الرئيسية (USDC حقيقي). dev/test → Base Sepolia (USDC مجاني للاختبار)
  config.chain = Rails.env.production? ? "base" : "base-sepolia"

  config.currency   = "USDC"
  config.version    = 1
  config.optimistic = false # انتظر settle الـ facilitator قبل الرجوع، لنسجّل tx_hash متزامناً
end

هذا هو لبّ حركة "اليدوي → مكتبة": 139 سطراً من services + 220 سطراً من اختبارات services، مكتوبة يدوياً، تُستبدل بـinitializer 29 سطراً + استدعاء controller 4 أسطر.

الواجهة الأمامية: viem + x402-fetch، لكن بدون vendor

في JS، النسخة اليدوية كانت تركب التوقيع بنفسها وتستدعي window.ethereum.request مباشرة. بعد الترحيل: viem وx402-fetch.

لكن هاتين الحزمتين مجمّعتين بمئات الكيلوبايتات. عمل vendor (نسخ dist/ من npm إلى vendor/javascript/) يُفجّر حجم المستودع. الحل: importmap + CDN من jsdelivr + تحميل كسول:

# config/importmap.rb
pin "viem",        to: "https://cdn.jsdelivr.net/npm/viem/+esm",        preload: false
pin "viem/chains", to: "https://cdn.jsdelivr.net/npm/viem/chains/+esm", preload: false
pin "x402-fetch",  to: "https://cdn.jsdelivr.net/npm/x402-fetch/+esm",  preload: false

preload: false هو المفتاح: لا يدخل في <link rel="modulepreload"> الخاص بالرسم الأول، فمعظم الصفحات لا تنزله أصلاً.

في controller Stimulus، حمِّل عند أول نقرة pay:

async loadDeps() {
  if (this._deps) return this._deps
  const [{ wrapFetchWithPayment }, { createWalletClient, custom }, { base, baseSepolia }] =
    await Promise.all([
      import("x402-fetch"),
      import("viem"),
      import("viem/chains")
    ])
  this._deps = { wrapFetchWithPayment, createWalletClient, custom, base, baseSepolia }
  return this._deps
}

المستخدمون بلا محفظة لا يحمّلون 300+ كيلوبايت أبداً. المستخدمون الذين لديهم MetaMask ويضغطون "ادفع" ينتظرون مرة من jsdelivr (بذاكرة CDN)، والنقرات التالية فورية.

أُصلحت 3 مشاكل في التنفيذ القديم بالمرور

النسخة اليدوية منسوخة من تنفيذ مرجعي في مشروع آخر. أثناء الترحيل، جعلت Claude يمسح الروائح المتراكمة. ظهرت 3:

1. توقف عن استخدام selectedAddress

الكود القديم:
js
const address = window.ethereum.selectedAddress

selectedAddress deprecated في MetaMask الحديث. الطريقة الصحيحة:

const accounts = await window.ethereum.request({ method: "eth_requestAccounts" })
const address = accounts[0]

يُظهر eth_requestAccounts أيضاً مربع حوار الاتصال — إن لم يربط المستخدم محفظته بالموقع من قبل، فهذا مدخل التفويض.

2. لا تطابق الأخطاء بالنصوص

قديم:
js
if (error.message.includes("User rejected")) { ... }
if (error.message.includes("chain")) { ... }

مطابقة النصوص تُكسَر دوماً عند أول تغيير في نصوص المحفظة التالية. انتقل إلى أكواد مُحدَّدة الأنواع:

// معيار EIP-1193: 4001 = user rejected
if (error.code === 4001) { this.#showError(this.errorRejectedValue); return }
// أكواد خاصة تعبر عبر flow
if (error.code === "CHAIN_SWITCH") { ... }
if (error.code === "PAYMENT_REQUIRED") { ... }

عند رمي أخطاء خاصة بك، ألصق code:

throw Object.assign(new Error("no_account"), { code: "NO_ACCOUNT" })

3. نصوص الواجهة عبر i18n، لا إنجليزية مدفونة

الكود القديم كان يحشر "Connecting wallet..." وجميع النصوص الأخرى في JS. نقلتها إلى صفات data-value مُحقنة من ERB:

<button data-controller="x402-payment"
        data-x402-payment-label-connecting-value="<%= t('paywall.x402.connecting') %>"
        data-x402-payment-label-signing-value="<%= t('paywall.x402.signing') %>"
        data-x402-payment-error-rejected-value="<%= t('paywall.x402.error.rejected') %>"
        ...>
  <%= t('paywall.x402.pay_button') %>
</button>

يقرأ JS this.labelConnectingValue. 19 لغة تُترجم باستقلال. صفر تغييرات في JS.

فخّان حقيقيان

الترحيل اصطدم بفخّين لا علاقة لهما ببروتوكول x402 نفسه وغير موجودين في README الـ gem.

الفخ 1: importmap يُسقط pin بصمت عند غياب ملف vendor

gem x402-rails يشحن عدة Stimulus controllers خاصة به. بعد تثبيت الـ gem، أدت الضغطة على زر الدفع إلى:

Uncaught Error: no Stimulus controller registered for "x402-pay"

نبشت. importmap.rb كان يحتوي بوضوح:

pin "@hotwired/stimulus", to: "@hotwired--stimulus.js" # @3.2.2

لكن vendor/javascript/@hotwired--stimulus.js لم يكن موجوداً. لا يُظهر importmap خطأً في هذه الحالة — يُسقط الـ pin بصمت ببساطة. فلا يجد controller الـ gem Stimulus، فيفشل في التسجيل، فيموت كل controller يلي.

الإصلاح: ضع ملف vendor:

./bin/importmap pin @hotwired/stimulus

ينزل هذا حزمة npm إلى vendor/javascript/. هذا النوع من الفشل الصامت نموذجي لما يفوّت Claude — يرى pin في importmap.rb ويفترض أن كل شيء بخير، دون أن يتحقق من تلقاء نفسه من وجود الملف المقابل في vendor/javascript/. المرة القادمة في هذا التشخيص، اجعل Claude يتحقق من الطرفين.

الفخ 2: credentials.yml يُحلل 0x... كعدد صحيح

credentials الإنتاج، مكتوبة بسذاجة:

x402:
  wallet_address: 0xAbCd...

بعد الـ deploy، كل نقرة x402 ترجع 422، والخطأ يقول إن wallet_address لا يطابق regex عنوان EVM.

حلّل YAML قيمة 0xAbCd... كـعدد صحيح ست عشري. من جانب Ruby، Rails.application.credentials.dig(:x402, :wallet_address) أعاد Integer لا String. والـ .to_s اللاحق عند الدخول إلى PaymentRequirements حوّله إلى سلسلة عشرية — لم يعد عنواناً صالحاً.

الإصلاح حرف واحد — إضافة علامات اقتباس:

x402:
  wallet_address: "0xAbCd..."

هذا الفخ لا يمسكه Claude من البداية؛ عليك العودة من رسالة الخطأ إلى طبقة parsing YAML. بمجرد تعلمه، ضع لاحقاً علامات اقتباس انعكاسياً حول كل قيمة تبدأ بـ 0x في YAML.

شكل الاختبارات تغيّر (هذه أهم إشارة)

بعد الترحيل، عدد ملفات الاختبار لم ينخفض، لكن الموقع تحوّل:

محذوف:
- test/services/x402/facilitator_client_test.rb (112 سطراً)
- test/services/x402/payment_handler_test.rb (108 أسطر)

مُضاف:
- test/models/purchase_test.rb كسب 40 سطراً لاختبار record_x402!
- test/models/subscription_test.rb كسب 69 سطراً لاختبار record_x402!

اختبارات طبقة الخدمة (كيف يعمل البروتوكول) — اختفت كلها. استبدلت باختبارات طبقة الـ model (كيف تُسجَّل البيانات بعد دفع ناجح).

منطقي — سلوك البروتوكول ملك الـ gem الذي يختبر نفسه. أنت تحتاج فقط إلى اختبار الجزء الذي كتبته: كيف يُدرج صف Purchase / Subscription بعد وصول نتيجة settlement، وكيف يُحفظ tx_hash.

هذه أيضاً الإشارة الصلبة لـ"هل أُرحِّل؟": إن كانت اختباراتك تحتوي كتلاً كبيرة تؤكد "شكل payload الذي أرسله صحيح" أو "عندما يعيد الـ facilitator isValid=false، أعالجه هكذا" — ذلك سلوك البروتوكول، يعود إلى المكتبة. إذا كان أي ملف اختبار تحت test/services/ يتجاوز 100 سطر، فمن المرجح أن ذلك service يختبر بروتوكولاً / واجهة خارجية ينبغي أن تكون مكتبة.

متى تترك Claude يقوم بهذا النوع من الترحيل

ليس كل "المجتمع أخرج gem" يستحق الترحيل. اجعل Claude يسأل أولاً:

  1. رقم إصدار المكتبة. مكتبة 0.x API ما يزال يتحرك؛ 1.x هو وقت التثبيت.
  2. دلتا الكود ≥ 200 سطر. في مرتي هذه، صافي -305 سطراً. دون 100 سطر صافٍ، switching cost لا يستحق.
  3. توحيد الاختبارات حقيقي. إن بقيت اختباراتك بعد الترحيل تؤكد 90% من نفس الأشياء بمجموعة stubs جديدة — السلوك لم ينتقل إلى المكتبة، فقط اسم الـ API تغير. لا تُرحّل.
  4. الإعدادات تتوحد. في النسخة اليدوية، عنوان عقد USDC واسم الشبكة وURL الـ facilitator منتشرة في 3 أماكن. بعد الترحيل: كلها في initializer 29 سطراً. هذه قيمة.
  5. مسار الترقية واضح. كيف تُرقى المكتبة؟ هل هناك عرف changelog للتغييرات الكاسرة؟ إن لم يكن، فلُف المكتبة بمحوّل خاص بك حتى لا تتسرب الـ gem إلى 50 موضع استدعاء.

عند استيفاء هذه الخمسة، prompt الترحيل جملة واحدة:

"x402-rails gem v1 استقرّ. استبدل PaymentHandler + FacilitatorClient الحاليين. احتفظ بنفس endpoints وأشكال الاستجابة — أريد فقط أن يدخل عمل البروتوكول إلى الـ gem. انقل الاختبارات إلى طبقة الـ model وفقاً لذلك."

سيقوم Claude: يقرأ وثائق الـ gem → يكتب initializer → يعيد كتابة controller → يحذف service القديم → يُعيد بناء الاختبارات. في الطريق يطلب تأكيداً مرتين أو ثلاثاً (مثلاً "أتريد الإبقاء على هذا السلوك؟"). عند الانتهاء، شغّل bin/rails test، فإن كان الكل أخضر، commit.

الخلاصة

البصيرة الحقيقية ليست "المكتبات تتفوق على اليدوي". اليدوي أحياناً هو الصواب — تخصيص البروتوكول، حساسية الكمون، الامتثال.

نقطة القرار الفعلية:

ذلك الملف في مجلد services/ لديك — الذي يجب تغييره في كل تحديث للبروتوكول — هل هناك الآن gem مخصصة للحفاظ على ذلك الشيء؟

إن كان الجواب نعم، فذلك ليس منطق عملك. إنه قط شارد "مُروَّض بروتوكولياً" تبنيته في مشروعك. أطعمته أسبوعين ويعمل جيداً — لكنه ليس لك. اطلب من Claude أن يعيده إلى المجتمع. ما تحتفظ به هو كتابة نتيجة البروتوكول إلى model الخاص بك — هذا الجزء خاص بمشروعك.

بعد الترحيل، يحتوي مجلد x402 لدي فقط: initializer 29 سطراً + استدعاء controller 4 أسطر + طريقتا record_x402!. 139 سطراً من خدمات مكتوبة يدوياً، و220 سطراً من اختبارات الخدمة المصاحبة لها — اختفت كلها. كود أقل. نفس السلوك. اختبارات أكثر إحكاماً. هذا ترحيل ناجح.