كل المقالات

سلسلة دفتر الأستاذ، الجزء الثاني: الأرصدة وعدم قابلية التغيير

·
15 سبتمبر 2026
·
وقت القراءة: 17 د
·
••• مشاهدة
·
••• إعجاب

مترجمة عن المقالة الأصلية باللغة الإنجليزية. اقرأها بالإنجليزية

سلسلة دفتر الأستاذ، الجزء الثاني: الأرصدة وعدم قابلية التغيير

انتهى الجزء الأول من «سلسلة دفتر الأستاذ» هذه في عالم أبسط، حيث كانت كل حركة تُسوّى بمجرد تسجيلها. لكن الواقع الآن أن المال الحقيقي لا يتصرف بهذه الطريقة. لنأخذ Tobi مثالًا، فهو أحد مستخدميك، ولديه 100 USDC في تطبيقك، و 50 USDC أخرى عالقة في إيداع لم يُؤكَّد بعد، وعملية سحب بقيمة 30 USDC في طريقها إلى محفظة خارجية. ما الذي يجب أن يُظهره رصيده في تلك اللحظة، وكم يمكنه أن ينفق منه فعليًا؟

بحسب ما يحسبه التطبيق، قد يُظهر 100 أو 120 أو 150، والإجابة الصادقة عن المبلغ الذي يمكنه إنفاقه هي 70. هذه المسافة بين ما هو مسجّل، وما هو متوقع، وما هو قابل للإنفاق في تلك اللحظة هي موضوع هذا الجزء الثاني.

لماذا لم يعد رصيد واحد كافيًا

لا شيء في نموذج القيد المزدوج يتغير هنا. كل حركة ما زالت متوازنة، والقيود ما زالت تُكتب مرة واحدة ولا تُمس أبدًا. ما يتغير هو أن المال صار الآن يقضي وقتًا في حالات مختلفة. دفعة البطاقة تستغرق أيامًا حتى تُسوّى، وإيداع العملات المشفرة يبقى غير مؤكد حتى تصل تأكيدات الكتل إلى عدد محدد، وعملية الصرف تكون في الطريق لدى مزود قد يفشل، ولا أحد يعلم.

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

الأرصدة الثلاثة

يجيب دفتر الأستاذ الذي ينمذج المال الموجود في الطريق بثلاثة أرقام، وكلها ما زالت مجرد مجاميع، على القيود وعلى الإيداعات التي ما زالت تنتظر الترحيل.

  1. المُرحَّل (Posted): وهو مجموع كل الحركات المسوّاة عبر جميع حسابات Tobi، أي محفظته إضافة إلى أي حسابات تحتفظ بمال محجوز لعمليات السحب الخاصة به، وهي الحقائق النهائية. الرصيد المرحّل لـ Tobi هو 100 USDC.

  2. المعلّق (Pending): وهذا هو الرصيد الذي يسبب الارتباك في أغلب الأحيان، لأن عبارة «الرصيد المعلّق» تُستخدم لتعني شيئين مختلفين. الأول هو النشاط المعلّق وحده، +50 USDC واردة و -30 USDC صادرة، أي 80 USDC في الطريق، وصافيها +20. والثاني هو الرصيد المرحّل مضافًا إليه ذلك النشاط، 100 + 50 - 30 = 120 USDC، وهو ما سيستقر عليه الرصيد على الأرجح. هذان سؤالان مختلفان، فلا تخلط بينهما أبدًا في نقطة نهاية واحدة.

  3. المتاح (Available): وهو ما يستطيع Tobi إنفاقه فعليًا الآن. القيمة المرحّلة ناقص القيم الصادرة، 100 - 30 = 70 USDC. المال الوارد الذي لم يُسوَّ ليس متاحًا، فمبلغ 50 USDC الذي ما زال غير مؤكد على السلسلة لا وجود له للإنفاق في هذه المحفظة حتى يُسوّى.

الرصيد المتاح هنا رقم واحد لأن هذه المحفظة تعمل بسياسة واحدة. كثير من الأنظمة تقسّمه، فما يستطيع Tobi إنفاقه وما يستطيع سحبه ينتهي بهما الأمر رقمين مختلفين. يمكن أن يكون إيداع البطاقة قابلًا للإنفاق داخل التطبيق فورًا ويبقى مقفلًا أمام المدفوعات لأيام، لأنه لولا ذلك لأودع أحدهم ببطاقة مسروقة، وسحبها على شكل عملات مشفرة في اللحظة نفسها، ولكنت أنت من يبقى عالقًا مع طلب استرداد المبلغ (chargeback). بعض الشركات تسلك الاتجاه المعاكس وتمنح ائتمانًا مؤقتًا، أي مالًا يُتاح قبل أن يُسوّى بناءً على بعض المعايير، وهكذا تعمل الإيداعات الفورية، وفي الولايات المتحدة تحدد Regulation CC حتى الجداول الزمنية التي يجب أن تلتزم بها البنوك بشأن موعد إتاحة الأموال المودعة. ثم تحمل أرصدة التجار فوق كل ذلك احتياطيات نقدية وحجوزات للمخاطر.

لكن للائتمان المؤقت ثمنًا. فأنت تقيّد المبلغ دائنًا لـ Tobi لحظة ظهور الإيداع، DR deposits_in_transit 50 / CR tobi:wallet 50، فينفقه، ثم يفشل الإيداع لأي سبب غريب، وإذا بمحفظته الآن بالسالب. لذا يجب أن يسمح دفتر الأستاذ بالأرصدة السالبة في حسابات محددة بحكم التصميم، وأن يتتبعها كذمة مدينة، أي مال بات Tobi مدينًا لك به. ومن هنا تأتي خسائر الاحتيال ومخاطر الائتمان، لذا تقرر مسبقًا أي أنواع الحسابات يُسمح لها بأن تصبح سالبة. بعض أنظمة دفاتر الأستاذ، مثل النظام الذي بنيناه على سبيل المثال، فيها علَم لهذا الغرض بالضبط، debits_must_not_exceed_credits، فعّله على حساب وسيُرفض أي تحويل قد يدفع قيوده المدينة إلى تجاوز قيوده الدائنة.

تتبّع المال

والآن، لنأخذ حركات Tobi واحدة تلو الأخرى. وكما في الجزء الأول، ما زالت كل حركة تُسجَّل كمعاملة متوازنة.

يُكتشف إيداعه البالغ 50 USDC بعد تأكيد واحد. العملات موجودة على السلسلة لكنها ليست نهائية كما تعلم، فما زال بإمكان الشبكة أن تتخلص من الكتلة التي تحملها وتستبدل بها كتلة أخرى، وهذا ما يُسمّى «reorg»، وهذا مال تراه لكنك لا تحتفظ به بعد. لا يُرحَّل أي شيء إلى دفتر الأستاذ في هذه المرحلة. بدلًا من الترحيل، يحصل الإيداع على تاريخه الخاص الذي يُضاف إليه فقط، ففي كل مرة يتغير فيها شيء يخصه، تأكيد جديد أو حالة جديدة، يُضاف صف جديد، ويقرأ الرصيد المعلّق لـ Tobi مبلغ 50 من هناك. لا يمس أي قيد حساب الحفظ أو محفظته. حساب الحفظ هو ما تجري مطابقته مع الرصيد النهائي على السلسلة، وهذا بالضبط ما يتحقق منه المدققون وأنظمة إثبات الاحتياطي، كما هو معتاد طبعًا.

الأرصدة الآن: المرحّل 100، والنشاط المعلّق +50، والمتاح 100.

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

DR custody       50
CR tobi:wallet   50

والآن، في عملية التثبيت نفسها يحصل تاريخ الإيداع على صفه الخاص بالقيد الدائن، وينتقل مبلغ 50 من المعلّق إلى المتاح. الأرصدة الآن: المرحّل 150، والنشاط المعلّق 0، والمتاح 150. لم يُعدَّل أي شيء، فدفتر الأستاذ ببساطة سمع الآن فقط بهذا الإيداع، حين أصبح نهائيًا بالفعل.

معنى «نهائي» يعتمد على السلسلة. اثنا عشر تأكيدًا مفهوم يخص إثبات العمل (proof of work). أما في إثبات الحصة (proof of stake) على Ethereum فالإشارة الحقيقية هي نقطة التحقق النهائية (finalized checkpoint)، أي ما بين حقبتين وثلاث حقب (epochs)، نحو 15 دقيقة في اليوم العادي. ولدى Solana مستويات الالتزام الخاصة بها، ومنذ أن انطلقت ترقية Alpenglow في أكتوبر 2026، صار confirmed و finalized يعنيان الشيء نفسه. وعلى rollup مثل Base أو OP، فإن تأكيد المُسلسِل (sequencer) لمعاملتك ليس إلا الخطوة الأولى، إذ تأتي نهائية L1 بعد نحو 20 دقيقة. وفي النهاية تصبح النهائية حدًا أدنى تضبطه لكل سلسلة، ويجب أن يسجّل كل إيداع الحد الأدنى الذي سُوّي بموجبه، حتى تعرف بالضبط، في اليوم الذي تسيء فيه سلسلة ما التصرف، أي الإيداعات قُيّدت بموجب أي قاعدة. وإذا اعتبر المزود إيداعًا مكتملًا بينما هو ما زال دون حدك الأدنى، فاحتجزه للمراجعة ولا تقيّد شيئًا. وفي حال كنت تستخدم نظامًا مثل Fireblocks فما عليك إلا ضبط ذلك في لوحة التحكم.

والآن، فإن الـ reorg قبل النهائية هو الحالة البسيطة، والإيداع لم يفشل. فالمعاملة عادةً تعود إلى الـ mempool وتصل مجددًا في كتلة لاحقة، لذا يعود الإيداع ببساطة إلى حالة غير مؤكد ويبدأ عدد تأكيداته من جديد. إن وسمته بالفشل في تلك اللحظة، فسيحدث أحد أمرين عندما يصل مجددًا، إما أن يتجاهله نظامك فلا يُقيَّد لـ Tobi أبدًا بينما يحتفظ حساب الحفظ بعملات لا تدين بها لأحد، أو أن يظهر وكأنه إيداع جديد تمامًا فيُقيَّد لـ Tobi مرتين. لا يفشل الإيداع إلا عندما يثبت أنه اختفى، على Ethereum مثلًا عندما تستخدم معاملة أخرى من المرسل نفسه ذلك الـ nonce وتصبح تلك المعاملة نهائية، وحتى عندها لا يوجد ما يُعكس لأن دفتر الأستاذ لم يره أصلًا.

هذه الحالة الثانية هي سبب الأهمية الكبيرة لمفتاح الإيداع. فمن الممكن أن تحمل معاملة واحدة عدة تحويلات USDC، لذا فالمفتاح هو السلسلة، و hash المعاملة، وموضع التحويل داخل تلك المعاملة. على Ethereum هذا هو موضعه في إيصال المعاملة (receipt). لا تلجأ إلى logIndex في هذه الحالة، فهو يعدّ عبر الكتلة بأكملها، لذا يتغير في اللحظة التي تصل فيها معاملة من كتلة مُتخلَّص منها إلى كتلة أخرى. وقيد التفرد الذي يضمن أن كل إيداع يُقيَّد مرة واحدة يقع على معاملة دفتر الأستاذ التي ترحّله. ضعه على التاريخ وسيرفض صف التأكيد الثاني، لأن كل صف هناك يحمل المفتاح نفسه.

نعود إلى Tobi، فقد مرّ مبلغ 50 الخاص به. والآن يطلب سحب 30 USDC. وقبل استدعاء أي مزود، يُحجز المال:

DR tobi:wallet        30
CR tobi:withdrawals   30

الأرصدة الآن: المرحّل 150، والنشاط المعلّق -30، والمتاح 120. ما زال المرحّل 150 لأنه يُقرأ عبر جميع حسابات Tobi، فقد انخفضت المحفظة إلى 120 والتقط حساب السحوبات مبلغ 30. الحجز قيد مزدوج حقيقي، ولهذا ينخفض المتاح قبل ترحيل السحب، فمبلغ 30 محجوز بالفعل رغم أنه لم يغادر المبنى بعد.

ربما لاحظت أن الإيداع بقي خارج دفتر الأستاذ حتى أصبح نهائيًا، بينما دخل السحب إلى دفتر الأستاذ قبل أن يغادر أي شيء. يعود الأمر إلى ما يفعله كل منهما بالرصيد المتاح. الحجز يقتطع مما يستطيع Tobi إنفاقه، لذا يجب أن يُثبَّت مع فحص الرصيد معًا، وإلا فقد ينفق طلبان مبلغ 30 نفسه. أما الإيداع المعلّق فلا يضيف شيئًا يستطيع إنفاقه بعد، لذا يمكنه الانتظار في الخارج حتى يصبح نهائيًا. والائتمان المؤقت هو الاستثناء الوحيد، ولهذا يُرحَّل الإيداع هناك فورًا.

يُسلّم المزود المبلغ، وتكتمل عملية الصرف:

DR tobi:withdrawals   30
CR custody            30

الأرصدة الآن: المرحّل 120، والنشاط المعلّق 0، والمتاح 120. الإكمال لا يفعل سوى تصفية الحجز، فحساب الحفظ يدفع لقناة الدفع وتبقى المحفظة دون مساس. ولو أن الترحيل خصم من المتاح مرة أخرى، لرأى Tobi رصيده 150 يصبح 90 بينما هو يدفع 30.

والآن، تعمّدت أن أُبقي ذلك السحب نظيفًا، فقد حجز 30 وسُوّي بـ 30. لكن السحوبات الحقيقية نادرًا ما تكون كذلك. رسوم الغاز لا تُعرف حتى تُعدَّن المعاملة، والمزودون يأخذون رسومهم الخاصة، وغاز تحويل USDC يُدفع بعملة ETH، وهي أصل مختلف. لذا فإن المبلغ الذي تحجزه والمبلغ الذي يُسوّى في النهاية لا يتطابقان تقريبًا أبدًا، فتحجز حدًا أعلى، أي 30 مضافًا إليها أقصى ما قد تبلغه الرسوم، ولنقل 2:

DR tobi:wallet        32
CR tobi:withdrawals   32

ثم سوِّ المبالغ الفعلية وحرّر الباقي، في معاملة واحدة:

DR tobi:withdrawals      32
CR custody            30.00
CR revenue:fees        0.40
CR tobi:wallet         1.60

انخفض المتاح بمقدار 32 أثناء وجود المبلغ في الطريق، ويستقر عند 119.60، وما زال صافي حساب السحوبات صفرًا. الغاز الذي فرضته الشبكة فعليًا هو مصروفك أنت، يُدفع من ETH الخاص بك في حركة منفصلة، DR expense:gas / CR treasury:eth، وهو يتوازن وحده لأن فحص الجزء الأول يعمل لكل أصل على حدة. ومبلغ 0.40 أصبح الآن إيرادك، لذا يُنقل خارج حساب حفظ أموال العملاء أيضًا، DR treasury:usdc 0.40 / CR custody 0.40. أنت تُبقي حساب الحفظ لأموال العملاء فقط. وإن دفعت الغاز منه، فستأكل كل عملية سحب قليلًا من أموال الجميع، وأول مكان سيظهر فيه ذلك هو مهمة cron الخاصة باحتياطياتك، أي ما تحتفظ به لكل أصل مقابل ما تدين به للمستخدمين، حين تعود بنتيجة ناقصة.

وقنوات البطاقات تجعل هذا أصعب:

ماذا يحدث عندما يفشل السحب

هناك ثلاثة مسارات لهذا، واثنان منها فقط حالات فشل.

خطأ بسيط: يُرفض السحب المعلّق قبل أن يُرحَّل، ربما لأن العنوان لم يجتز الفحص. فيُحرَّر الحجز:

DR tobi:withdrawals   30
CR tobi:wallet        30

يعود المتاح إلى 150، والمرحّل لم يتحرك قط لأن المال بقي مكانه.

الحالة المكلفة: رُحّلت عملية الصرف، وأكدتها السلسلة، وبعد أيام أعادتها منصة التداول التي ذهبت إليها. هذه الإعادة حركة مالية جديدة قائمة بذاتها، وهي على السلسلة مجرد تحويل وارد آخر، لذا تسلك المسار نفسه الذي يسلكه أي إيداع، تاريخها الخاص، وحدّ النهائية الأدنى لديك، ومطابقتها مع عملية الصرف بعينها التي تعيدها، وعندها فقط تُرحَّل. ورحّل أيضًا ما وصل فعلًا، فمنصة التداول التي تعيدها قد تحتفظ برسومها الخاصة، فيغادر 30 وربما يعود إليك 29.50:

DR custody       29.50
CR tobi:wallet   29.50

تبقى عملية الصرف الأصلية في التاريخ، وكذلك عملية الإعادة.

ثم هناك المسار الثالث، وهو الذي يوقع معظم الناس في الخسارة: انتهت مهلة المزود ولا أحد يعرف ما إذا كانت عملية الصرف قد وصلت. لا يمكنك أن تعامل الحالة المجهولة على أنها فشل. عاملها كفشل، وحرّر مبلغ 30، ثم تصل على السلسلة على أي حال، وهكذا ببساطة تكون قد دفعت مرتين، مبروك!

المعيار الفعلي هو أن النتيجة المجهولة تبقى معلّقة حتى يجيب المزود.

لكن بعض المزودين لا يجيبون أبدًا، ويعتمد ذلك على كيفية بناء أنظمتهم. لذا لا يمكن للحالة المجهولة أن تبقى معلّقة إلى الأبد أيضًا، بل تنزل على سلّم، وكل درجة فيه إما أن تحسمها بمعاملة في دفتر الأستاذ أو تسلّمها إلى الدرجة التالية:

  1. اسأل المزود عن الحالة مباشرة «إعادة استعلام»، ولا تكتفِ بانتظار الـ webhook.
  2. طابقها مع سجلات المزود ومع السلسلة نفسها، والخيار لك.
  3. اشطبها، بموافقة عبر لوحة الإدارة الخلفية لديك.

ولكي تعرف أصلًا أن شيئًا ما عالق، لا يمكن لحساب المقاصة أن يبقى رقمًا واحدًا. لنقل إن Tobi أرسل عمليتي سحب بقيمة 30 لكل منهما، فسُوّيت إحداهما ولم تُسوَّ الأخرى قط. يُظهر tobi:withdrawals مبلغ 30، وبما أن كلتيهما كانت 30، فلا شيء في ذلك الرقم يخبرك أيهما العالقة. لذا فإن كل قيد يسوّي شيئًا أو يحرره يشير إلى القيد بعينه الذي يغلقه، وهذه هي مطابقة البنود المفتوحة (open item matching)، مستعارة مباشرة من الذمم المدينة، ويُطلَق تنبيه على أي شيء يبقى مفتوحًا بعد تجاوز اتفاقية مستوى الخدمة (SLA) لقناة الدفع لديك.

التاريخ يبقى

بالنظر إلى كل ما فعله مال Tobi أعلاه، لم يُحدَّث أي قيد موجود قط، ولا حتى صف واحد من تاريخ الإيداع. تحركت الحالة لأن معاملات جديدة وصلت فوق المعاملات القديمة، وهذا هو النصف الثاني من هذه المقالة.

صُممت هذه الأنواع من الأنظمة لتتلقى التصحيحات، قيد فيه صفر زائد، أو حركة سُجّلت في الحساب الخطأ. والإغراء في الساعة 2:45 فجرًا هو تنفيذ UPDATE. أنت تصلح البيانات بمعاملة تصحيحية، وليس بتحديث صف أبدًا. يجب أن يعكس التصحيح المعاملة الأصلية مع قلب الطرفين، وأن يشير إليها، حاملًا سببًا ومصدرًا حتى يشرح السجل نفسه بنفسه عند النظر إليه:

function reverse(original: Txn, reason: string, source: string): Txn {
  return {
    id: randomUUID(),
    referenceId: original.id,
    effectiveAt: original.effectiveAt,
    reason,
    source,
    entries: original.entries.map((e) => ({
      accountId: e.accountId,
      side: e.side === "DR" ? "CR" : "DR",
      amountMinor: e.amountMinor,
      assetId: e.assetId,
    })),
  };
}

يُنسخ كل حقل باسمه عن قصد، فلو نشرت القيد الأصلي باستخدام ...e لانتهى الأمر بالتصحيح حاملًا id و timestamps الخاصة بالأصلي أيضًا. صافي المعاملتين معًا صفر، والرصيد النهائي لا يخفي أبدًا كيف حدث ما حدث. يستطيع فريق الدعم إعادة تشغيل تاريخ Tobi، صفوف الإيداع الخاصة به ثم دفتر الأستاذ، ومشاهدة الإيداع، والتأكيد، والحجز، والتحرير، بالترتيب. وقاعدة بياناتك تدعم هذا بالطريقة نفسها التي فعلتها قاعدة الجزء الأول، فأي UPDATE أو DELETE على جدول القيود يُرفض! ولا مجال للتردد في هذا.

هناك أمر واحد لا تمنعه reverse() بصيغتها المكتوبة، وهو عكس المعاملة نفسها مرتين، شغّلها مرتين وسيستعيد Tobi ماله مرتين. يجب ألا يتجاوز مجموع عمليات عكس المعاملة المعاملة الأصلية أبدًا. يمكن لفهرس فريد بسيط أن يمنع عكسًا كاملًا ثانيًا، لكنه لا يستطيع جمع أي شيء، لذا فإن عكسًا كاملًا مضافًا إليه عكس جزئي سيمرّ رغم ذلك. والفحص الحقيقي يقرأ ما عُكس بالفعل ويكتب عملية العكس الجديدة كمعاملة ذرّية واحدة، وهذه مشكلة تخص الجزء الثالث، وسنتحدث عنها.

يجب أن يتفق الرصيد المخزّن

قال الجزء الأول إن الرصيد المخزّن ذاكرة تخزين مؤقت يجب أن تتفق مع السجل. ومع ثلاثة أرصدة يرتفع حجم الرهان، لأن ذاكرة التخزين المؤقت هي ما يوافق على الإنفاق. يجب أن يعكس الرصيد المستخدم للموافقة على السحب الحجوزات والترحيلات المثبَّتة، ويجب أن يتحرك في عملية التثبيت نفسها التي تكتب القيود، وإلا فإن قراءة تحدث بينهما سترى مالًا موعودًا به بالفعل.

ما زال اختبار الجزء الأول صالحًا، احذف كل رقم مخزّن وأعد تشغيل دفتر الأستاذ إضافة إلى تاريخ الإيداعات، وستستعيد الأرصدة الثلاثة نفسها، ويمكن دائمًا التحقق من الجزء المعلّق بمسح جديد للسلسلة. ومع نقاط التحقق المذكورة أعلاه، تبدأ إعادة التشغيل من آخر نقطة تحقق موثّقة بدلًا من أول قيد كُتب على الإطلاق، وهذا ما يُبقي هذا الاختبار عمليًا على نطاق واسع. وعندما يختلف رقم مخزّن عن رقم أُعيد تشغيله، تقارن مهمة تسوية بينهما وتطلق تنبيهًا عند الانحراف. ثم تكتشف أيهما يكذب. إن كانت القيود صحيحة وذاكرة التخزين المؤقت خاطئة، فأعد بناء ذاكرة التخزين المؤقت من القيود، ولا مكان لأي حركة مالية جديدة في هذا الإصلاح. وإن كانت القيود نفسها خاطئة، فرحّل معاملة تصحيحية مثل التي أعلاه. وفي الحالتين، تحقّق قبل أن تثق بالرقم مجددًا. وهذا التنبيه غير قابل للتفاوض.

ملاحظة جانبية: الرصيد المخزّن مجموع، والمجاميع تحتاج إلى مساحة. لعملة USDC عدد 6 خانات عشرية على Ethereum و 7 على Stellar، ولـ ETH عدد 18. يبلغ الحد الأقصى لعدد صحيح بإشارة بطول 64 بت نحو 9.22 × 10¹⁸، وهذا بوحدة wei لا يتجاوز ~9.22 ETH، أي إيداعًا واحدًا معقولًا. و bigint في Postgres هو بالضبط ذلك العدد الصحيح بطول 64 بت، الكلمة نفسها التي في bigint الخاص بـ TypeScript في الجزء الأول، لكن الحد مختلف جدًا. استخدم مبالغ بطول 128 بت، وهذا بالضبط ما نفعله، وهو ما يعني في Postgres النوع numeric بمقياس 0، واحتفظ بـ bigint بدلًا من number في TypeScript، وخزّن مقياس كل أصل مع الأصل نفسه، فالقيمة 30000000 لا تعني شيئًا حتى تعرف أنها USDC بعدد 6 خانات عشرية.

والآن، أما الحديث عن كيفية بقاء عملية التثبيت نفسها آمنة تحت الحمل العالي، والقفل، وما إلى ذلك، فذلك موضوع الجزء الثالث.

كم كان الرصيد في ذلك الوقت؟

هناك ساعتان على كل حركة، متى أصبحت سارية ومتى سجّلها نظامك. يمكن أن تكون تسوية بطاقة سارية بالأمس وتصل اليوم، ويمكن أن يظهر webhook المزود متأخرًا بساعات. أعد بناء رصيد الأمس اليوم وسيحتوي على حركات لم يكن النظام قد سمع بها بالأمس. ذلك الرقم هو ما تعرفه الآن عن الأمس، وليس ما كنت تعرفه حينها.

يجب على التصحيحات والإعادات أيضًا أن تختار كل منها تاريخ سريان. فتصحيح خطأ في القيد يحمل تاريخ إنشاء المعاملة الأصلية، لأن الأصلية لم تكن صحيحة قط، ولهذا تنسخ reverse() أعلاه original.effectiveAt. أما الإعادة فتحمل تاريخ اليوم، لأن عملية الصرف حدثت فعلًا والمال عاد حقًا لاحقًا. وإن أخطأت في هذين الاثنين وعكستهما، فسيخرج كل رصيد بتاريخ معيّن لتلك الأيام خاطئًا جدًا، وسيكلفك ذلك مجددًا.

ثم هناك الفترات المغلقة. بمجرد إغلاق فترة، وإرسال الكشوف، وتقديم التقارير، واحتساب الفوائد، لا يمكن لقيد متأخر يسري داخلها أن يغيّر تلك الأرقام بعد الآن. فترحّله في الفترة المفتوحة كتسوية خارج الفترة بتاريخ سريان يشير إلى الماضي، والبنوك العادية تسمي هذا تاريخ القيمة، أي التاريخ الذي يُحتسب منه، وتاريخ القيد، أي التاريخ الذي رُحّل فيه. والقيد بتاريخ قيمة سابق يعني أن الفوائد أو الرسوم حُسبت على الرصيد الخطأ، لذا تدخل إعادة الحساب أيضًا كمعاملة تسوية جديدة.

ما الذي لا يزال هذا لا يضمنه

النموذج هنا صحيح، فلدينا ثلاثة أرصدة، وتاريخ يُضاف إليه فقط، وذاكرة تخزين مؤقت متوافقة. والآن تصل عمليتا سحب بقيمة 80 USDC لكل منهما بفارق جزء من الألف من الثانية. تقرأ كلتاهما المتاح 120، وتجتاز كلتاهما الفحص، وتحجز كلتاهما. 160 خرجت من 120، والتاريخ سيسجّل ببساطة كيف خسرتها.

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

•••
شارك هذه المقالة:
سلسلة
سلسلة دفتر الأستاذ

ابقَ على اطلاع

احصل على أحدث المقالات في بريدك الوارد.