كل المقالات
7 دقائق قراءة

كشف دفعة USDT الواردة على TON: الطريقة الموثوقة

كشف دفعة USDT على TON: تفكيك transfer_notification للجيتون عبر get_transactions، وقيمة decimals من get_jetton_info، ومطابقة المبلغ والحماية من التكرار.

USDT على TONكشف الدفعاتtransfer_notificationجيتونات TEP-74الحماية من التكرارget_transactions

كل من بنى يومًا نظام استقبال مدفوعات على TON اصطدم بالفخّ نفسه: الدفعة وصلت، والخادم الخلفي «لم يرَها». أرسل العميل 50 USDT، والمحفظة تُظهر الوارد، والفاتورة ما زالت معلّقة في حالة «قيد الانتظار». وبعد عشر دقائق تصل تذكرة دعم غاضبة. وبعد ساعة أخرى يتبيّن أنك قيّدت الدفعة مرتين، لأن المُستطلِع (poller) عاود المعالجة عند إعادة المحاولة. الكشف الموثوق عن دفعة USDT الواردة (incoming USDT payment) على TON ليس «فحص الرصيد مرة كل دقيقة»، بل تفكيك رسائل داخلية بعينها خاصة بالجيتون، مع حماية من التكرار وحماية من التزوير. وفيما يلي نمرّ خطوة خطوة على الطريقة الصحيحة — بالأدوات الحقيقية لخادم MCP TONNode، التي يمكن استدعاؤها مجانًا محليًا.

لماذا كشف دفعة USDT على TON ليس مسألة رصيد محفظة

المقاربة الساذجة هي قراءة رصيد المحفظة دوريًا، فإن ارتفع اعتُبرت الدفعة مقبولة. وهذا أشبه بصندوق نقد لا يجيد إلا النظر إلى المبلغ الإجمالي داخل الدرج: دخلت +10 USDT — لكن ممّن؟ ومقابل أي حساب؟ عن طلب قديم أم جديد؟ وإن دفع عميلان 10 لكل منهما في اللحظة نفسها، فلن ترى إلا +20 دفعة واحدة.

وعلى TON تنهار هذه المقاربة فورًا لأسباب عدة:

  • USDT ليست عملة أصلية، بل جيتون (معيار TEP-74). فالأموال لا تصل إلى المحفظة الرئيسية مباشرةً، بل إلى محفظة الجيتون التابعة لها — وهي عقد منفصل مرتبط بعقد رئيسي (master) محدد للجيتون. أما رصيد المحفظة الرئيسية بـGRAM فلا يتغيّر إطلاقًا في هذه الأثناء.
  • فارق الرصيد لا يخبرك بمن دفع ولا مقابل ماذا. فالرصيد لا يحتفظ بأي ارتباط بالفاتورة.
  • التسابق والتجميع. بين استطلاعين تصل عدة دفعات — فترى الفارق الإجمالي، لا الأحداث المنفصلة.
  • القيد المزدوج. إعادة محاولة الاستطلاع أو إعادة تشغيل العامل (worker) — وتُحتسب الدفعة الواحدة مرتين.
  • decimals. قيمتها في USDT هي 6، لا 9 كما جرت العادة في الجيتونات. ومقسوم واحد خاطئ يعني أن العميل «قُيِّد له» بفارق ألف ضعف.

الطريق الصحيح ألّا تعمل على الرصيد، بل على تدفّق معاملات محفظة الجيتون الخاصة بالمستلم، مع تفكيك كل حدث تحويل على حِدة. وكل عمليات القراءة متاحة مجانًا محليًا:

{ "mcpServers": { "ton": { "command": "npx", "args": ["-y", "@tonnode/mcp"] } } }

كيف تبدو دفعة USDT الواردة من الداخل: internal_transfer للجيتون (op 0x178d4519)

حين يحوّل إليك أحدهم USDT، تجري سلسلة من الأحداث:

  1. محفظة الجيتون الخاصة بالمُرسِل تخصم المبلغ وترسل إلى محفظة الجيتون الخاصة بك رسالةً داخلية internal_transfer (op 0x178d4519).
  2. محفظة الجيتون الخاصة بك تقيّد الأموال و — فقط إذا أرفق المُرسِل forward_ton_amount > 0 — ترسل إضافةً إلى ذلك رسالةً داخلية transfer_notification (op 0x7362d09c) إلى محفظتك الرئيسية.

وهنا يكمن المفترق الأساسي. transfer_notification إشعار مريح بمعنى «وصلك جيتون»، لكنه يذهب إلى المحفظة الرئيسية المالِكة، وفقط عند forward_ton_amount > 0. فإن وضع المُرسِل صفرًا، ستقيّد محفظة الجيتون الأموال على أي حال، لكن لن يصل المالك أي إشعار.

أما في سجل محفظة الجيتون نفسها فالقيد موجود دائمًا — بصفته internal_transfer واردًا، بصرف النظر عن forward_ton_amount. ولذلك يُبنى الكشف الموثوق على استطلاع سجل محفظة الجيتون الخاصة بالمستلم، وتفكيك internal_transfer تحديدًا. وهذه بنيته وفق TEP-74:

internal_transfer#178d4519
  query_id:           uint64
  amount:             (VarUInteger 16)     // الوحدات الخام للجيتون
  from:               MsgAddress           // عنوان المالك الدافع (الشخص)
  response_address:   MsgAddress
  forward_ton_amount: (VarUInteger 16)
  forward_payload:    (Either Cell ^Cell)  // تعليق/memo

نقطتان مهمتان:

  • حقل from هو عنوان الدافع الحقيقي (المالك الشخص)، لا عنوان محفظة الجيتون الخاصة به. وهو تحديدًا ما ينبغي تسجيله بصفته المُرسِل. وانتبه: العنوان على مستوى الرسالة (tx.in_msg.source) هنا هو محفظة الجيتون الخاصة بالدافع، أما الدافع الشخص فيقع في حقل from داخل جسم الرسالة.
  • المبلغ في حقل amount بالوحدات الخام، وسنعود إلى تحويله أدناه.

الخلاصة المعمارية الأهم

يُبنى الكشف الموثوق لا على انتظار الإشعار في المحفظة الرئيسية، بل على استطلاع سجل محفظة الجيتون الخاصة بالمستلم نفسها. ففي سجلّها يظهر القيد دائمًا، بصرف النظر عن forward_ton_amount. وإن كنت تنصت إضافةً إلى ذلك للمحفظة الرئيسية، فستلتقط هناك transfer_notification (op 0x7362d09c، والمُرسِل في حقل sender) — لكن هذه مجرد مكافأة لحالة forward_ton_amount > 0، لا مصدر الحقيقة الوحيد.

نقرأ السجل عبر get_transactions ونفكّك internal_transfer

أولًا نحتاج إلى عنوان محفظة الجيتون الخاصة بالمستلم. لا تشتقّه دون اتصال ولا تثق بالرمز (ticker) — بل اسأل العقد الرئيسي لـUSDT نفسه: فاستدعاء get_wallet_address (عبر run_get_method) على العقد الرئيسي لـUSDT يعيد حتميًا عنوان محفظة جيتون USDT الخاصة بك، المرتبطة بالعقد الرئيسي الحقيقي. ثم نستطلع معاملات محفظة الجيتون هذه عبر get_transactions.

الأمر النصي للوكيل داخل حلقة الاستطلاع:

استدعِ run_get_method على العقد الرئيسي لـ USDT
EQCxE6mUtQJKFnGfaROTKOt1lZbDiiX1kCixRv7Nw2Id_sDs بالطريقة
get_wallet_address ووسيطها عنوان المالك UQ...myservice.
أعد عنوان محفظة جيتون USDT الخاصة بنا. ثم عبر get_transactions
اقرأ آخر معاملات محفظة الجيتون هذه. ولكل
internal_transfer وارد (op 0x178d4519) أعد query_id
وamount (خام) وحقل from (عنوان الدافع) والتعليق النصي
من forward_payload، إضافةً إلى lt وhash المعاملة.

شبه كود لمعالجة معاملة واحدة:

for (const tx of txs) {
  const body = tx.in_msg?.decoded;          // جسم الرسالة الواردة بعد التفكيك
  if (body?.op !== 0x178d4519) continue;    // ليست internal_transfer — نتخطّاها

  const rawAmount = BigInt(body.amount);    // وحدات خام، غير مقروءة بشريًا
  const payer     = body.from;              // عنوان المالك الدافع
  const memo      = parseComment(body.forward_payload);
  const key       = `${tx.lt}:${tx.hash}`;  // معرّف لإزالة التكرار
  // …المطابقة والقيد أدناه
}

ومن المفيد كذلك التحقّق مرة واحدة من حالة الحساب الرئيسي عبر get_account_state (هل هو نشط، ومتى كانت آخر معاملة) — فهذا يساعد على معرفة ما إذا كان المُستطلِع «حيًّا» وما إذا كان قد تأخّر.

decimals تحسم كل شيء: get_jetton_info وتحويل المبلغ الخام (USDT = 6 وليس 9)

حقل amount في التحويل مُقدَّر بـالوحدات الخام للجيتون — عدد صحيح بلا فاصلة عشرية. ولتحويله إلى مبلغ مقروء بشريًا تحتاج إلى decimals. وهنا تحديدًا ينكسر أكبر عدد من عمليات الدمج.

معظم الجيتونات لها decimals = 9. أما USDT (Tether) على TON فـdecimals = 6. أي:

1 USDT = 1 000 000 raw   (10^6 وليس 10^9)

وإن قسمت بحكم العادة مبلغ USDT الخام على 10^9، فستتحوّل دفعة العميل البالغة 50 USDT عندك إلى 0.05 — أي أقل بألف مرة. والخطأ في الاتجاه المعاكس يقيّد بألف ضعف بالسهولة نفسها. لا تُثبّت المقسوم في الكود — بل خذ decimals من بيانات التعريف على السلسلة عبر get_jetton_info:

const info = await getJettonInfo(USDT_MASTER); // name, symbol, decimals, الإصدار
const decimals = info.decimals;                // لـUSDT = 6
const human = Number(rawAmount) / 10 ** decimals; // 50000000 → 50.0

احتفظ بـdecimals في الكاش بحسب عنوان العقد الرئيسي، لا بحسب الرمز: فالرمز يُزوَّر، وقيمة decimals يجب ربطها بعقد بعينه. والقاعدة بسيطة: decimals دائمًا من get_jetton_info، والمبلغ يبقى في BigInt/خام حتى لحظة العرض.

نتحقّق من المصدر والمبلغ والتعليق — ونستبعد الجيتونات المزيّفة

والآن الأهم أمنيًا. هذه هي الفحوص التي لا يجوز القيد من دونها.

1. العقد الرئيسي — USDT الحقيقي وحده. بإمكان أي شخص إصدار جيتون باسم «USDT» ورمز «USD₮» وإرسال تحويل إليك بقيمة 1000 «USDT». وإن كنت تطابق الدفعة بالرمز — فسيخدعونك في خمس دقائق. والارتباط الموثوق الوحيد بـTether الحقيقي هو العمل مع محفظة الجيتون التي حسبها العقد الرئيسي الحقيقي لـUSDT لعنوان المالك لديك:

Tether USD₮ master: EQCxE6mUtQJKFnGfaROTKOt1lZbDiiX1kCixRv7Nw2Id_sDs

ولهذا تحديدًا نأخذ عنوان محفظة الجيتون من الاستدعاء على السلسلة get_wallet_address على هذا العقد الرئيسي (انظر أعلاه)، ونستطلع السجل عند هذا العقد — ولا عقد سواه. وأي internal_transfer يُقيَّد عليه يأتي حتمًا من محفظة جيتون شقيقة شرعية تابعة للعقد الرئيسي نفسه: فـ«USDT» المزيّف يعيش على محافظ عقد رئيسي آخر، ولا يصل إلى عنوانك أصلًا. ولذلك، في ظل هذه البنية، لا حاجة إلى مقارنة tx.in_msg.source بأي شيء — فالحماية يكفلها اختيارُ نقطة المراقبة نفسه. وانتبه: parse_address مجرد تحويل بين صيغ EQ/UQ/raw دون اتصال، وهي لا تحسب عنوان محفظة الجيتون؛ فذلك يتطلب استدعاء get_wallet_address على السلسلة تحديدًا. ومن المفيد استعمالها لتوحيد صيغ العناوين قبل المقارنة، حتى لا تُحتسب تمثيلات مختلفة للعنوان الواحد عناوين مختلفة.

2. التعليق (memo) للمطابقة مع الفاتورة. التعليق النصي يقع في forward_payload بالصيغة القياسية: بادئة op 0x00000000 (أربع بايتات صفرية) + سلسلة UTF-8. وبه تربط التحويل الوارد بطلب بعينه.

3. المبلغ والدافع. قارن المبلغ بعد إعادة حسابه بالمبلغ المتوقَّع وفق الفاتورة؛ وعند الحاجة ثبّت from (عنوان الدافع) للسجل ومكافحة الاحتيال.

// 1. المصدر مضمون: نحن نقرأ سجل محفظة الجيتون
//    التي أعاد عنوانَها get_wallet_address
//    للعقد الرئيسي الحقيقي لـ USDT — أي أن أي تحويل هنا حقيقي.

// 2. التعليق → الفاتورة
const invoice = invoices.get(memo);
if (!invoice) continue;

// 3. هل يتطابق المبلغ؟
if (human < invoice.expectedAmount) markUnderpaid(invoice);

// 4. الدافع — للسجلات/مكافحة الاحتيال
log({ payer, memo, human, lt: tx.lt, hash: tx.hash });

خاصية عدم التكرار (idempotency): إزالة المكرّرات بـlt + hash كي لا تُقيَّد الدفعة مرتين

الاستطلاع يتقاطع دائمًا مع نفسه: يعمل وفق جدول زمني، ويتعطّل، ويُعيد المحاولة، وتتداخل نوافذ الاستطلاع. وبلا إزالة تكرار ستقيّد عاجلًا أم آجلًا دفعة واحدة مرتين.

في TON تُعرَّف كل معاملة تعريفًا فريدًا بزوج الزمن المنطقي (lt) + hash. وهذا هو مفتاحك الطبيعي للحماية من التكرار:

const key = `${tx.lt}:${tx.hash}`;
if (await seen.has(key)) continue;   // عولجت من قبل — نخرج
await creditInvoice(invoice, human); // القيد
await seen.add(key);                 // نثبّتها في معاملة قاعدة البيانات نفسها

ويمكن استعمال query_id من التحويل مفتاحًا كذلك، لكن (lt, hash) يصلح لأي معاملة واردة إلى محفظة الجيتون، بما فيها حالة forward_ton_amount = 0. احتفظ بالمفاتيح المعالَجة بشكل دائم، ونفّذ القيد في معاملة قاعدة بيانات واحدة مع فحص التفرّد — عندئذٍ لن تُنشئ العمليات المتوازية قيدًا مكرّرًا.

ومن المستحسن إجراء مطابقة حسابية (reconciliation) دوريًا: كل N دقيقة قارن الرصيد الفعلي لمحفظة الجيتون (عبر get_jetton_balance — فهي تحسب العنوان على السلسلة وتعيد الرصيد باستدعاء واحد) بمجموع كل الدفعات المقيَّدة. والفارق إشارة إلى أن تحويلًا ما فُقد أو تكرّر في مكان ما، حتى لو تعطّلت دورة استطلاع واحدة.

سعة تمرير مستقرّة تحت الحمل: مفتاحك الخاص بدل الحدود العامة

الاستطلاع في نظام المدفوعات هو تدفّق طلبات دائم ومنتظم: N محفظة × وتيرة الاستطلاع. وهنا تصير البنية التحتية العامة عنق الزجاجة.

اللايت-سيرفرات العامة من الإعداد العالمي مشتركة ومحدودة: فهي تحت الحمل ترد بـnot ready أو تنقضي مهلة ADNL دون رد. أما واجهات HTTP API العامة (toncenter وtonapi.io) فبلا مفتاح تحتمل نحو طلب واحد في الثانية، وعند التجاوز تعيد بصدق HTTP 429 «Too Many Requests». ولاحظ: رمز الحدّ الحقيقي هو 429 تحديدًا، لا «228» الطريفة التي تدور في مجتمع TON وليست رمزًا في أي واجهة برمجية. وبالنسبة إلى صندوق نقد يستطلع السجل كل بضع ثوانٍ، تعني هذه الحدود دفعاتٍ لا تُقيَّد أصلًا وأخرى يتأخّر قيدها. وأما كيفية اختيار مزوّد بمفتاح خاص فموضوع شرح منفصل.

وكل أدوات القراءة اللازمة لكشف المدفوعات — get_transactions وrun_get_method وget_jetton_balance وget_jetton_info وparse_address وget_account_state — متاحة مجانًا محليًا: npx -y @tonnode/mcp. وحزمة @tonnode/mcp مفتوحة المصدر (رخصة MIT) وتعمل ببروتوكول TON الأصلي ADNL دون طبقات HTTP وسيطة. وحين ينتقل الاستطلاع إلى الإنتاج وتحتاج إلى سعة تمرير مضمونة، يأتي دور النقطة الطرفية المستضافة بمفتاحك الخاص:

{
  "mcpServers": {
    "ton": {
      "type": "http",
      "url": "https://mcp.tonnode.io/mcp",
      "headers": { "Authorization": "Bearer tn_live_…" }
    }
  }
}

ولا تختلف الخطط إلا في حدّ الطلبات في الدقيقة — والأدوات الـ16 كلها متاحة في كل خطة:

  • Hobby — مجانية إلى الأبد، 60 طلبًا/دقيقة. ويُمنح المفتاح فور تسجيل الدخول، دون بطاقة بنكية.
  • Pro — 29 دولارًا شهريًا، 300 طلب/دقيقة.
  • Scale — 199 دولارًا شهريًا، 1200 طلب/دقيقة.

ولبدء استطلاع المدفوعات تكفي الـ60 طلبًا المجانية في الدقيقة للحفاظ على استطلاع مستقر بلا 429. وإن كنت تبني على هذا وكيل ذكاء اصطناعي يفكّك المدفوعات بنفسه — فآلية عمل ذلك مشروحة في دليل MCP لوكلاء الذكاء الاصطناعي على TON.

ابدأ بمفتاح Hobby المجاني — 60 req/min تكفي استطلاع المدفوعات، دون بطاقة بنكية: tonnode.io/dashboard?plan=hobby. وحين يكبر الحمل ولا يعود عامل واحد كافيًا — قارن حدود Pro وScale.


قائمة تحقّق للكشف الموثوق عن USDT على TON:

  1. استطلع سجل محفظة الجيتون الخاصة بالمستلم عبر get_transactions، لا رصيد المحفظة الرئيسية.
  2. خذ عنوان محفظة الجيتون هذه من الاستدعاء على السلسلة get_wallet_address (run_get_method) على العقد الرئيسي الحقيقي لـUSD₮ (EQCxE6mUtQJKFnGfaROTKOt1lZbDiiX1kCixRv7Nw2Id_sDs) — عندئذٍ يكون أي تحويل إليه USDT حقيقيًا بالضرورة.
  3. ابحث عن internal_transfer الواردة بـop 0x178d4519 — فهي تحديدًا ما يوجد دائمًا في سجل محفظة الجيتون، بصرف النظر عن forward_ton_amount. أما transfer_notification (0x7362d09c) فتذهب إلى المحفظة الرئيسية، وفقط عند forward_ton_amount > 0.
  4. خذ عنوان الدافع من حقل from، والمبلغ من amount (خام).
  5. خذ decimals من get_jetton_info. USDT = 6، لا 9.
  6. طابِق الفاتورة بالتعليق من forward_payload (op 0x00000000 + UTF-8).
  7. أزل التكرار بـ(lt, hash) — وقيّد مرة واحدة بالضبط.
  8. حافظ على سعة تمرير مستقرّة بمفتاحك الخاص، لا بالحدود العامة.

امنح وكيلك الوصول إلى TON

16 أداة MCP: قراءة ومبادلات غير وصائية وعبر السلاسل ومحافظ. الباقة المجانية — 60 طلب/دقيقة، بلا بطاقة.