كتابة عقود ذكية بواسطة لغة Cairo
بعد أن قمنا بتعلم أساسيات لغة Cairo في الدرس السابق دعونا نقوم بتعلم طريقة كتابة العقود الذكية في Starknet بإستخدام لغة Cairo.
بالضبط! نحن بالحاجة إلى معرفة بعض الأوامر والشروط لكتابة عقود ذكية بإستخدام الأساسيات التي طرحها في الدرس السابق.
بيئة التطوير
افضل طريقة للتعلم هي البناء ورؤية النتائج بشكل مباشر فلذلك في هذا الدرس لن نقوم بإستخدام اي من الادوات السابق التي قمنا بإعدادها بل سنقوم بإستخدام موقع Remix الذي تطرقنا إليه في الكورسات التعليمية السابقة أثناء تعلم لغة Solidity.
لماذا Remix؟ في هذا الدرس سنقوم بكتابة عقود ذكية على Starknet فنحن بالحاجة إلى رفع العقود الذكية بكل سهولة دون اي تعقيد واختبارها بشكل مجاني. بواسطة Remix سنقوم بتثبيت أداة Starknet والتي ستوفر بيئة عمل كاملة للتعامل مع العقود الذكية ونشرها بكل سهولة.
ستذهب إلى موقع Remix من هنا وستقوم بإعداد البيئة هكذا.
بمجرد فتح الموقع ستكون الواجهة بهذا الشكل:

البيئة التلقائية هذه تعمل فقط على مشاريع Ethereum وفي هذا الدرس نقوم بالتفاعل مع Starknet فستذهب الى Plugins عن طريق النقر على الزر في الاسفل على اليسار. بعد النقر عليها ستقوم بالبحث عن Starknet ومن ثم النقر على زر Activate لتفعيل بيئة Starknet:

بعد الإنتهاء من تفعيل الأداة مباشرة سيظهر لك طلب الموافقة على إعطاء الصلاحيات للأداة ستقوم بالنقر على الزر Accept:

بعد ذلك ستعود إلى قسم الملفات للتحقق من تغير البيئة إلى cairo_scarb_sample:

يمكنك الآن إنشاء ملف بإسم contract.cairo في المجلد hello_world:

دعونا نعود إلى تعلم كتابة العقود الذكية ومن ثم بعد ذلك سنقوم بتوضيح طريقة التفاعل مع العقد ونشره على Starknet واختباره.
كتابة العقود الذكية
ليس هناك أي اختلاف في طريقة كتابة كود لغة Cairo ولكن أثناء كتابة العقود الذكية هناك بعض القواعد الصارمة المتعلقة في Starknet التي يجب تنفيذها من اجل ان يتم إنشاء العقد الذكي وتخزين البيانات وتشغيل الدوال وغيرها…
عند البدء في كتابة العقد الذكي سيتم إستخدام الوحدات - Modules ومن ثم في دخل هذه الوحدات نقوم بإضافة الدوال والهياكل التي نريد تشغيله
يمكنك متابعة هذا الفيديو لفهم طريقة كتابة عقود ذكية بإستخدام لغة Cairo:
تعريف العقد الذكي:
#[starknet::contract] mod Contract { }
عند بناء عقد ذكي نقوم بإنشاء mod والتي تعبر عن الوحدات ومن ثم إضافة إسم العقد الذكي. بالنسبة إلى السطر الأول يتم إضافته من أجل إعلام مُشغل Starknet بالتعامل مع الوحدة (mod) على أساس أنها عقد ذكي وليس مجرد mod.
تخزين البيانات في العقد الذكي في Starknet يختلف عن Solidity. في Solidity كل متغير نقوم بتعريفه هو خانة في الذاكرة, ولكن الأمر يختلف في starknet لسبب انه غير ممكن تعريف المتغيرات في الوحدات في Cairo ولهذا السبب سنقوم بإستخدام struct بإسم Storage من Starknet وكل متغير نقوم بتعريفه في struct فهو خانة/غرفه في ذاكرة العقد الذكي أو blockchain.
سيصبح العقد الذكي بهذا الشكل:
#[starknet::contract] mod Contract { #[storage] struct Storage { stored_date: u128, } }
قمنا بإنشاء struct بإسم Storage من أجل تعريف أماكن لتخزين البيانات في blockchain. وبالنسبة للتعليق #[storage] الغرض منه هو إخبار مُترجم starknet بأن يقوم بتشغيل الكود المطلوب ويسمح له بالتفاعل مع حالة blockchain مثل السماح في تخزين البيانات في المتغيرات بعد ذلك وكل ما نريد فعله. وبعد ذلك قمنا بتعريف خانة في blockchain تُدعى stored_data لتخزين قيمة عددية من النوع u128.
نحن بالحاجة إلى إنشاء دالة تعمل على تخزين البيانات التي نريد تخزينها في المتغير stored_data بحيث يصبح العقد الذكي بهذا الشكل:
#[starknet::contract] mod Contract { #[storage] struct Storage { stored_data: u128 } #[external(v0)] fn set(ref self: ContractState, number: u128) { self.stored_data.write(number); } }
بما أن الهدف من الدالة التي نريد إنشائها هو تخزين البيانات وليس إرجاع البيانات قمنا بإنشاء دالة خارجية تُدعى set في السطر 9 و قمنا بتمرير 2 بيانات وهم:
- self: هل فكرت كيف يمكننا تخزين البيانات في blockchain؟ الأمر بالحاجة في الوصول الى حالة العقد بالكامل ولهذا السبب تم إنشاء self وجعلنا نوعها ContractState والذي يسمح إلى self بتخزين حالة العقد الذكي بالكامل من دوال خارجية ومتغيرات. وقبل كتابة self قمنا بإضافة ref والتي تسمح لنا بأن نحصل على مرجع او نسخة من حالة/بيانات العقد الذكي والتعديل عليها دون أي مشاكل.
- number: هي القيمة التي نريد إدخالها في الدالة ومن ثم تخزينها.
بعد إنشاء الدالة قمنا بإضافة أمر تخزين قيمة number في المتغير stored_data. وكما تلاحظ من أجل الوصول إلى المتغير قمنا بإستخدام self ومن ثم اسم المتغير stored_data. وبما أن ما نريد القيام به هو تخزين البيانات إلى العقد قمنا بإضافة write(number) والتي تعني كتابة قيمة number في المتغير stored_data.
بالنسبة إلى السطر 8: تم إضافة #[external(v0)] والتي تقوم بإخبار starknet بأنها دالة خارجية سيتم تشغيلها بشكل عام للجميع.
ربما يبدو الأمر غريب ولكن قم بقرأة ذلك مجدداً وفي الامثلة القادمة وعند تشغيل العقد ستقوم بفهم كل شيء.
بعد أن قمنا بإنشاء دالة للتخزين في المتغير stored_data. نحن بالحاجة الآن إلى دالة من أجل استدعاء البيانات من المتغير بحيث يصبح العقد الذكي بهذا الشكل:
#[starknet::contract] mod Contract { #[storage] struct Storage { stored_data: u128 } #[external(v0)] fn set(ref self: ContractState, number: u128) { self.stored_data.write(number); } #[external(v0)] fn get(self: @ContractState) -> u128 { self.stored_data.read() } }
الأمر مشابه للدالة السابقة ولكن بما أننا نريد إرجاع بيانات قمنا بإنشاء دالة تُدعى get وقمنا بتمرير self ولكن كما تلاحظ لم نقوم بإضافة ref مثل المره السابقه والسبب لأننا لا نريد التعديل على البيانات في العقد ولكن قمنا بإضافة @ قبل ContractState والتي تقوم بإعطاء نسخة من حالة العقد من متغيرات ودوال في الدالة get ولكن لا نستطيع التعديل على البيانات. فقط نأخذ نسخة من حالة العقد لعرض البيانات.
بعد إنشاء الدالة قمنا بإضافة أمر إستدعاء القيمة المخزنة في المتغير stored_data. وكما تلاحظ من أجل الوصول إلى المتغير قمنا بإستخدام self ومن ثم اسم المتغير stored_data. وبما أن ما نريد القيام به هو قراءة البيانات وليس كتابة قمنا بإضافة read() والتي تعني قراءة القيمة المخزنة في المتغير stored_data.
يمكننا أن نستنتج شيئاً:
- عندما نريد ان نجعل الدالة تقوم بالكتابة على blockchain (تخزين البيانات) سنقوم بتمرير self كقابل للتعديل على blockchain بهذا الشكل: ref self: ContractState.
- عندما نريد ان نجعل الدالة تقوم بالقراءة من blockchain (إستدعاء البيانات) سنقوم بتمرير self بشكل طبيعي وسنأخذ نسخة من بيانات/حالة العقد الذكي بهذا الشكل: self: @ContractState.
الأمر لا يكفي بهذا الشكل. يجب علينا تجربة العقد الذكي في Remix ورؤية كيف يعمل العقد. قم بنسخ الكود النهائي الذي في الأعلى وقم بلصقه على الملف contract.cairo الذي قمنا بإنشائه. ومن ثم ستقوم بالنقر على علامة Starknet الذي قمنا بتفعيلها في بداية الدرس، كما موضح في الفيديو:
الخطوات التي قمنا بتنفيذها:
- تجميع العقد الذكي عن طريق النقر على Compile contract.cairo.
- بعد الإنتهاء من تجميع الكود، قمنا بعمل Declare للعقد الذكي. بحيث تقوم declare بإرسال الكود إلى الشبكة ولكن هذا لا يعني أنه قد انتهى كل شيء.
- بعد الإنتهاء من إرسال الكود إلى الشبكة نريد إنشاء مثيل له على الشبكة عن طريق النقر على Deploy حتى نتمكن من التفاعل مع الكود كعقد ذكي.
التفاعل مع العقد الذكي
بعد الإنتهاء من نشر العقد الذكي ستلاحظ انه يمكنك القراءة والكتابة على العقد الذكي بهذا الشكل يبدو.

بعد النقر على call قام بتشغيل دالة get وقام بإظهار البيانات في الاسفل كما تلاحظ هناك قيمة بهذا الشكل "0x0" السبب هو يقوم بعرض البيانات من النوع hex وأثناء تحويلها الى int تعني 0. يمكنك تجربتها في المربع التالي:
ومن أجل الكتابة ستقوم بتحديد على Write وستقوم بتشغيل دالة set وتمرير القيمة بشكل طبيعي للغاية.
Constructors
إذا كنت مطور Solidity او قد قمت بإستخدام OOP في لغات البرمجة الأخرى ستكون قد قمت بإستخدام Constructor بإستمرار.
يتم إستخدام دالة Constructor في العقود الذكية لمرة واحدة فقط وهي اثناء نشر العقد الذكي.
قم بإنشاء ملف بإسم constructors.cairo على Remix و دعونا نقوم بالتحقق من المثال التالي:
#[starknet::contract] mod Constructors { #[storage] struct Storage { stored_number: u128, } #[constructor] fn constructor(ref self: ContractState, number: u128) { self.stored_number.write(number); } #[external(v0)] fn get_number(self: @ContractState) -> u128 { self.stored_number.read() } }
كما تلاحظ قمنا بالإعلان عن عقد ذكي يُدعى constructors ومن ثم الإعلان عن struct Storage لتخزين قيمة عددية في المتغير stored_number على blockchain كما قمنا بفعل هذا في المثال السابق.
ولكن في هذا العقد الذكي نريد تخزين البيانات في المتغير stored_number أثناء نشر العقد الذكي فقط باعتبارها قيمة ثابتة ولا نريد من أحد التعديل عليها بعد ذلك. (هناك الكثير من حالات الإستخدام التي ستحتاجها من دالة constructor في المستقبل). قمنا بإعلام مترجم Starknet بأن الدالة هي في الواقع دالة constructor عن طريقة #[constructor] أعلى الدالة وقمنا بتمرير حالة العقد في self وبما ان الدالة ستقوم بالتعديل على بيانات العقد الذكي اضفنا ref ومن ثم قمنا بتمرير number في الدالة وهي القيمة التي نريد تخزينها في المتغير stored_number.
في النهاية قمنا بإنشاء دالة خارجية تُدعئ get_number وقمنا بتمرير نسخة من حالة العقد في self عن طريق العلامة @ لقراءة القيمة من المتغير stored_number.
قُم الان بتجميع الكود الخاص بك و إرساله إلى شبكة Starknet عن طريق النقر على Declare. ستلاحظ انه أثناء النقر نشر العقد الذكي (Deploy) سيطلب منك إدخال قيمة عددية وهذا بفضل دالة constructor. قُم بإدخال قيمة عددية ومن ثم النقر على Deploy من أجل التفاعل مع العقد.
ملاحظة: أثناء إستدعاء القيمة بواسطة Remix سيظهر القيمة في الأسفل من النوع hex. يمكنك نسخها وتحويلها إلى قيمة عددية في الاسفل هنا:
التعيين - Mapping
تعمل Mappings في Cairo مثل hashmaps أو القواميس في لغات البرمجة الأخرى. يتم استخدامها لتخزين البيانات في أزواج لكل قيمة مفتاح.
بالضبط! مفتاح -> قيمة. أحد أهم طرق التخزين التي يجب عليك معرفتها والتي ستقوم باستخدامها بشكل كبير أثناء بناء العقود الذكية لكونها تجعل التعامل مع البيانات بسيطة للغاية بحيث يمكنك الوصول إلى البيانات عن طريق المفتاح بشكل مباشر بالإضافة إلى عدم الحاجة إلى صرف الكثير من الغازات.
في هذا المثال سنقوم بإنشاء عقد ذكي يقوم بتخزين بيانات الاشخاص بحيث نقوم بتغطية العديد من الأفكار بواسطة هذا المثال.
#[starknet::contract] mod Mappings { use starknet::ContractAddress; #[storage] struct Storage { myMap: LegacyMap<ContractAddress, felt252>, } #[external(v0)] fn set_map(ref self: ContractState, address: ContractAddress, name: felt252) { self.myMap.write(address, name); } #[external(v0)] fn get_map(self: @ContractState, address: ContractAddress) -> felt252 { self.myMap.read(address) } }
دعونا نقوم بتوضيح كل سطر:
- السطر 1 - 2: قمنا بالإعلان وإنشاء عقد ذكي يُدعى mappings.
- السطر 3: قمنا باستدعاء ContractAddress من starknet والتي يتم استخدامها كنوع من البيانات وهي address.
- السطر 5 - 8: قمنا بإنشاء struct Storage والذي يعمل على تخزين المتغيرات في blockchain. وفي السطر 7 قمنا بإنشاء متغير myMap والذي يقوم بتخزين زوج بيانات (mapping) بحيث يصبح لكل قيمة مفتاح خاص بها. وكما تلاحظ من إجل الإعلان عن mapping قمن باستخدام كلمة LegacyMap ومن ثم قمنا بجعل نوع المفتاح هو ContractAddress والقيمة لكل مفتاح سنقوم بتخزين اسم مالك address فلذلك تم وضع نوع القيمة felt252.
- السطر 10 - 13: قمنا بإنشاء دالة set_map خارجية والتي تعمل على تخزين البيانات على blockchain وقمنا بتمرير self كمرجعية ref لحالة العقد و address الذي نريد تخزين كمفتاح ومن ثم name الذي نريد تخزينه كقيمة للمفتاح. وفي السطر 12 كما تلاحظ أثناء الكتابة على المتغير myMap وإدخال البيانات التي قمنا بتمريرها في الدالة قمنا بإضافة اولاً address كمفتاح وبجانبه name وهي القيمة.
- السطر 15 - 18: قمنا بإنشاء دالة set_map خارجية والتي تعمل على إرجاع البيانات من blockchain وقمنا بتمرير self و address الذي عن طريقه سنقوم بإستدعاء القيمة المرتبطة به بكل سهوله. وفي السطر 17 كما تلاحظ أثناء قراءة القيمة من المتغير myMap قمنا بإدخال address إليها من أجل جلب القيمة المتعلقة بهذا المفتاح (address).
قُم الان بتجميع الكود الخاص بك وإرساله إلى شبكة Starknet عن طريق النقر على Declare ومن ثم نشره على الشبكة عن طريق النقر على Deploy.
ملاحظة: أثناء إستدعاء القيمة بواسطة Remix سيظهر القيمة في الأسفل من النوع hex. يمكنك نسخها وتحويلها إلى قيمة نصية في الاسفل هنا:
إستخدام trait و impl في العقود الذكية
من المهم للغاية أثناء كتابة العقود الذكية التفكير في إستخدام trait و impl من اجل تنظيم العقد الذكي الخاص بك والواجهات من أجل الإستخدام الخارجي.
سنقوم ببناء العقد الذكي الاول بواسطة trait و impl ومن ثم ستقوم بتحويل بقية العقود الذكية كتمرين بسيط من أجل الإحتفاظ بهذا في ذاكرتك.
قم بإنشاء ملف بإسم contract2.cairo على Remix و دعونا نقوم بالتحقق من المثال التالي:
#[starknet::interface] trait IContract<TContractState> { fn set(ref self: TContractState, number: u128); fn get(self: @TContractState) -> u128; } #[starknet::contract] mod Contract { use super::IContract; #[storage] struct Storage { stored_data: u128, } #[abi(embed_v0)] impl ContractImpl of IContract <ContractState> { fn set(ref self: ContractState, number: u128) { self.stored_data.write(number); } fn get(self: @ContractState) -> u128 { self.stored_data.read() } } }
يبدو كل شيء واضح مع بعض الإختلافات, دعونا نقوم بتوضيح ذلك:
- السطر 1 - 5: قمنا بإنشاء trait بإسم IContract وقمنا بطلب تمرير قيمة بإسم TContractState والتي سنقوم بتمرير حالة العقد والبيانات أثناء إستخدام الواجهة (trait) وفي السطر 3 و 4 قمنا بإنشاء الدالتين التي نريد تجميعهم في trait واستخدامهم بعد ذلك. وكما تلاحظ في السطر 1 قمنا بإعلام مُشغل statknet بأن يقوم بالتعامل مع trait وكأنها واجهة عن طريق كتابة #[starknet::interface].
- السطر 9: قمنا باستدعاء الواجهة IContract باستخدام الكلمة use وبما أن الواجهة خارج العقد الذكي قمنا بإضافة كلمة super.
- السطر 11 - 14: قمنا بإنشاء هيكل التخزين (struct Storage) وتعريف متغير بإسم stored_data من أجل تخزين قيمة عددية في blockchain.
- السطر 16 - 25: الان قمنا بإنشاء impl بإسم ContractImpl وقمنا بإستدعاء الواجهة IContract في impl وتمرير حالة العقد (ContractState) الى الواجهة ومن ثم قمنا في بناء الدالتين الخاصة بنا كما قمنا في الاعلى. وكما تلاحظ في السطر 16 قمنا بإعلام مُشغل starknet بأن يتعامل مع impl بشكل خارجي بحيث نتمكن من التفاعل مع الدوال وتخزين البيانات بشكل خارجي بواسطة #[abi(embed_v0)]
قُم الان بتجميع الكود الخاص بك وإرساله إلى شبكة Starknet عن طريق النقر على Declare ومن ثم نشره على الشبكة عن طريق النقر على Deploy.
ملاحظة: أثناء إستدعاء القيمة بواسطة Remix سيظهر القيمة في الأسفل من النوع hex. يمكنك نسخها وتحويلها إلى قيمة عددية في الاسفل هنا:
يمكنك متابعة الجزء الثاني من ورشة العمل للحصول على افكار أكثر:
من أجل تثبيت ذلك في عقلك قم ببناء بعض الأفكار العشوائية او تحويل الامثلة السابقة كما قمنا الان وانتقل الى الدرس القادم المليء بالإثارة.
كما هو الحال دائمًا، إذا كانت لديك أي أسئلة أو شعرت بالتعثر أو أردت فقط أن تقول مرحبًا، فقم بالإنضمام على Telegram او Discord وسنكون أكثر من سعداء لمساعدتك!