

كيف يعمل
خمس خطوات إلى العقد
أنشئ عقدًا ذكيًا بمساعدة معالج بسيط.
أنشئ عقدًا ذكيًا بمساعدة معالج بسيط.
- الخطوة 1دورك في العقداختر دورك في العقد. في العقد دوران فقط: العميل ومقدم الخدمة. لا يوجد تحكيم من طرف ثالث.9:41LTE
دورك في العقد
- الخطوة 2اختيار العملةاختر عملة العقد. GRAM — العملة الأصلية لشبكة TON — مناسبة للصفقات السريعة والصغيرة، لأنها متقلبة إلى حد كبير. USDT عملة أكثر استقرارًا للصفقات الكبيرة والطويلة.9:41LTE
اختيار العملة
- الخطوة 3مبلغ العقدأدخل مبلغ العقد بالعملة المختارة. سيُحوَّل هذا المبلغ إلى مقدم الخدمة بعد إنجاز العمل.9:41LTE
مبلغ العقد
مبلغ العقد هو ثمن العمل. يودعه العميل.
من 10% إلى 90%. القيمة الافتراضية: 30%.
0,4% من المبلغ، على ألا تتجاوز 15 GRAM.
- الخطوة 4عناوين المحافظالصق عناوين المحافظ. لاحقًا سيجري إيداع الأموال وسحبها عبر هذه المحافظ.9:41LTE
عناوين المحافظ
- الخطوة 5مراجعة العقدراجع بيانات العقد المُدخلة قبل النشر.9:41LTE
مراجعة العقد
دورك في العقد
بعد الإنشاء
بطاقة العقد
أنت العميل، والعملة USDT. ست لقطات للبطاقة نفسها كما في لوحة التحكم الحقيقية، مع كل الإجراءات المتاحة في كل خطوة.
أُنشئ للتو
الأرصدة صفر. يمكنك إيداع مبلغ الصفقة، أو المبلغ مع زيادة تصل إلى 30% مباشرة، أو حذف العقد ما دام لم يودع أحد شيئًا.
تم الإيداع وبانتظار الطرف الآخر
مبلغ الصفقة موجود في العقد، ووديعة مقدم الخدمة لم تصل بعد. ما دامت الحالة ليست «نشط»، يمكنك سحب أموالك. لا يزال بإمكانك إضافة الزيادة حتى 30%.
أودع مقدم الخدمة
المبلغان المتفق عليهما موجودان، والعقد نشط. المبالغ مجمّدة، ولا يمكن سحب USDT. يمكنك قبول العمل، أو اقتراح حصة أخرى، أو إضافة الزيادة (وديعة إضافية من العميل).
أُضيفت وديعة 20%
يوجد +200 USDT فوق مبلغ الصفقة. يتبقى 100 USDT حتى سقف 30%. السحب لا يزال مغلقًا: العقد النشط يحتفظ بمبلغ العقد إضافةً إلى الزيادة.
اقتُرح 80% لمقدم الخدمة.
سيحصل مقدم الخدمة على 800 USDT إذا وافق. اختفى زر «قبول العمل»، ولن يظهر مجددًا إلا إذا حددت 100%. يمكن استبدال الاقتراح وزيادة الوديعة الإضافية.
وافق مقدم الخدمة
اكتمل العقد: ذهب 800 USDT إلى مقدم الخدمة، و4 USDT إتاوة للخدمة. تبقّى لك 400 USDT يمكنك سحبها.
Q&A
الأسئلة الشائعة
أين الطرف الثالث؟
لا يوجد. مقدم الخدمة يخاطر بوديعته، لذا من مصلحته إنجاز العمل حتى النهاية.
افتراضيًا، وديعة مقدم الخدمة 30% من مبلغ الصفقة. قد يبدو أن لديه الأفضلية: فهو يخاطر بـ30% وأنت بـ100%. لكن المهم ليس النسب، بل المال. فهذه الـ30% هي أمواله الخاصة، وقد يكون فقدانها مؤلمًا له بقدر ما يؤلمك فقدان الـ100% الخاصة بك. لا أحد يضحّي بماله بلا سبب.
خطر آخر: استلم العميل العمل وقد لا يدفع. هنا خياران. الأول — عندما يكون العمل شبه جاهز، اقترح التسوية مسبقًا. الثاني — اطلب من العميل إيداع زيادة حتى 30% فوق المبلغ (وديعة). بعد إتمام العقد يستعيد العميل وديعته، لذا لديه سبب لإغلاق الصفقة (العقد).
لماذا عليّ أن أثق بكم؟
لا داعي للثقة. العقد الذكي علني. عنوان عقدك هو تجزئة (hash) للبيانات الأولية والكود. إذا استُبدل الكود، يصبح العنوان مختلفًا.
يمكن فتحه والاطلاع عليه (مثلًا في Tonviewer). الأمر الوحيد أنك تحتاج إلى فهم الكود — وهذه هي المشكلة الأساسية. لكنه في المقابل دليل بنسبة 100%. يمكنك أيضًا إعطاء الكود لنموذج ذكاء اصطناعي. في الأسفل كود العقد مع أبسط شرح ممكن.
ContractUSDT
هذه قائمة الاستيرادات: الثوابت والدوال المساعدة.
1import "@contracts/common-messages"2import "@contracts/errors"3import "@contracts/contract_USDT/usdt-utils"4import "fees-management"5import "messages"6import "storage"وصف موجز للعقد.
8contract ContractUSDT {9 author: "Alexandr_K"10 version: "0.0.4"11 description: "Escrow contract USDT"12 storage: StorageUsdt13 incomingMessages: AllowedMessage14}قائمة الإجراءات المتاحة على العقد.
16type AllowedMessage =17 | TransferNotificationForRecipient18 | ChangePercent19 | AcceptPercent20 | RefundUsdt21 | ErrRefund22 | ErrMode23 | BurnContractدالة مساعدة: هل العقد نشط أم لا. عندما يودع الجميع مبالغهم يصبح العقد نشطًا، ولم يعد السحب ممكنًا بهذه البساطة.
28fun StorageUsdt.isActive(self, extra: ExtraFieldsUsdt): bool {29 if (extra.completed) {30 return false;31 }32 return self.clientAmount >= extra.clientAmountInit &&33 self.providerDeposit >= extra.providerDepositInit;34}دالة للباقي. تُرفق مع كل رسالة كمية صغيرة من GRAM لمعالجة المعاملة. وتعيد الدالة ما يزيد.
67fun sendExcessesTon(to: address, amount: coins) {68 if (amount > MIN_MESSAGE_VALUE_BOUND) {69 val excessesMsg = createMessage({70 bounce: BounceMode.NoBounce,71 dest: to,72 value: amount,73 });74 excessesMsg.send(SEND_MODE_IGNORE_ERRORS);75 }76}تحسب عنوان محفظة الجيتون. ويعتمد على عنوان العقد.
78fun contractUsdtWallet(): address {79 return calcAddressOfJettonWallet(contract.getAddress());80}إذا فشل التحويل، تعود الأموال ويصحح العقد سجلاته: من يملك كم.
105fun onBouncedMessage(in: InMessageBounced) {106 in.bouncedBody.skipBouncedPrefix();107 val msg = lazy BounceOpToHandle.fromSlice(in.bouncedBody);108 var storage = lazy StorageUsdt.load();109 match (msg) {110 TransferBounce => {111 if (msg.queryId == QueryRole.Client as uint64) {112 storage.clientAmount += msg.jettonAmount;113 } else if (msg.queryId == QueryRole.Provider as uint64) {114 storage.providerDeposit += msg.jettonAmount;115 } else if (msg.queryId == QueryRole.Admin as uint64) {116 var extra: ExtraFieldsUsdt = lazy storage.extra.load();117 extra.royaltyInit += msg.jettonAmount;118 storage.extra = extra.toCell();119 } else {120 // ErrRefund (QueryRole.None) и неизвестные id: учёт не уменьшали121 return;122 }123 }124 }125 storage.save();126}إذا كان نص الرسالة فارغًا، لا نفعل شيئًا.
131fun onInternalMessage(in: InMessage) {132 if (in.body.isEmpty()) {133 return;134 }بعد ذلك يأتي تعريف المتغيرات.
137val msg = lazy AllowedMessage.fromSlice(in.body);138var storage: StorageUsdt = lazy StorageUsdt.load();139val tonBalanceBeforeMsg = contract.getOriginalBalance() - in.valueCoins;140val storageFee = MIN_TONS_FOR_STORAGE_THIS_CONTRACT -141min(tonBalanceBeforeMsg, MIN_TONS_FOR_STORAGE_THIS_CONTRACT);142var msgValue = in.valueCoins - storageFee - CONTRACT_GAS_CONSUMPTION;إيداع USDT: يستقبل التحويلات ويسجّل كم يملك كل طرف من USDT.
145TransferNotificationForRecipient => {146 assert (in.senderAddress == contractUsdtWallet()) throw Errors.InvalidWallet;147 assert (msg.transferInitiator != null) throw Errors.InvalidData;148 if (msg.transferInitiator == storage.clientAddress) {149 storage.clientAmount += msg.jettonAmount;150 storage.save();151 } else if (msg.transferInitiator == storage.providerAddress) {152 storage.providerDeposit += msg.jettonAmount;153 storage.save();154 }155 sendExcessesTon(msg.transferInitiator, msgValue);156}يغيّر نسبة الدفع. إذا أُنجز العمل إلى النصف فقط، يمكن اقتراح 50%. وافق مقدم الخدمة — نُفّذ العقد. لم يوافق — واصلا التفاوض.
162ChangePercent => {163 assert (msgValue > 0) throw Errors.NotEnoughGas;164 assert (in.senderAddress == storage.clientAddress) throw Errors.UnauthorizedSender;165 assert (msg.percent >= 0 && msg.percent <= 100) throw Errors.WrongPercent;166 var extra: ExtraFieldsUsdt = lazy storage.extra.load();167 assert (extra.completed == false) throw Errors.ContractIsCompleted;168 assert (storage.isActive(extra)) throw Errors.ContractIsNotActive;169 extra.percent = msg.percent;170 if (msg.percent == 100) {171 extra.completed = true;172 storage.providerDeposit += extra.clientAmountInit - extra.royaltyInit;173 storage.clientAmount -= extra.clientAmountInit;174 }175 storage.extra = extra.toCell();176 storage.save();177 sendExcessesTon(in.senderAddress, msgValue);178}إذا وافق مقدم الخدمة، يدفع له العقد حصته مع وديعته، والباقي لك. عند 0% تُلغى الصفقة كلها ويستعيد الجميع أموالهم. وفي هذه الحالة لا تُدفع للعقد عمولة 0.4%.
183AcceptPercent => {184 assert (msgValue > 0) throw Errors.NotEnoughGas;185 assert (in.senderAddress == storage.providerAddress) throw Errors.UnauthorizedSender;186 var extra: ExtraFieldsUsdt = lazy storage.extra.load();187 assert (extra.completed == false) throw Errors.ContractIsCompleted;188 assert (extra.percent != 100) throw Errors.WrongPercent;189 assert (msg.percent == extra.percent) throw Errors.WrongPercent;190 extra.completed = true;191 val percentValue = extra.clientAmountInit / 100 * msg.percent;192 if (msg.percent == 0) {193 extra.royaltyInit = 0;194 }195 storage.providerDeposit += percentValue - extra.royaltyInit;196 storage.clientAmount -= percentValue;197 storage.extra = extra.toCell();198 storage.save();199 sendExcessesTon(in.senderAddress, msgValue);200}أكبر كتلة — تدير استرداد الأموال.
208RefundUsdt => {209 msgValue -= (MIN_MESSAGE_VALUE_BOUND + MIN_FORWARD_TON_AMOUNT);210 assert (msgValue > 0) throw Errors.NotEnoughGas;211 assert (msg.jettonAmount > 0) throw Errors.InvalidData;212 var extra: ExtraFieldsUsdt = lazy storage.extra.load();213 val transferRecipient: address = msg.addressRefund != null214 ? msg.addressRefund215 : in.senderAddress;216 val isActive = storage.isActive(extra);217 var queryId: uint64;218 var jettonAmount: coins;219 if (in.senderAddress == storage.clientAddress) {220 queryId = QueryRole.Client;221 if (isActive) {222 val deposit: uint72 = (extra.clientAmountInit / 100) * 30;223 val locked = extra.clientAmountInit + deposit;224 assert (storage.clientAmount > locked) throw Errors.NotEnoughJettons;225 jettonAmount = min(storage.clientAmount - locked, msg.jettonAmount);226 } else {227 jettonAmount = min(storage.clientAmount, msg.jettonAmount);228 }229 storage.clientAmount -= jettonAmount;230 } else if (in.senderAddress == storage.providerAddress) {231 queryId = QueryRole.Provider;232 if (isActive) {233 assert (234 storage.providerDeposit > extra.providerDepositInit235 ) throw Errors.NotEnoughJettons;236 jettonAmount = min(237 storage.providerDeposit - extra.providerDepositInit,238 msg.jettonAmount,239 );240 } else {241 jettonAmount = min(storage.providerDeposit, msg.jettonAmount);242 }243 storage.providerDeposit -= jettonAmount;244 } else if (in.senderAddress == extra.adminAddress) {245 queryId = QueryRole.Admin;246 val maxRefundUsdt: uint72 = extra.completed ? extra.royaltyInit : 0;247 jettonAmount = min(maxRefundUsdt, msg.jettonAmount);248 extra.royaltyInit -= jettonAmount;249 storage.extra = extra.toCell();250 } else {251 throw Errors.UnauthorizedSender;252 }253 assert (jettonAmount > 0) throw Errors.NotEnoughJettons;254 storage.save();255 sendAskToTransferUsdt(256 queryId,257 jettonAmount,258 transferRecipient,259 msgValue + MIN_MESSAGE_VALUE_BOUND + MIN_FORWARD_TON_AMOUNT,260 );261}وضع الخطأ. إذا فُقدت المحفظة أو مرت الأموال خارج السجل، يمكن لطرف أن ينقل السيطرة إلى الطرف الآخر. حالة «خارج السجل» مستحيلة بالمعنى الدقيق، إلا إذا تمّت عمدًا. لكن فليبقَ الخيار موجودًا. أما نسيان المحفظة أو فقدانها فممكن تمامًا.
271ErrMode => {272 assert (msgValue > 0) throw Errors.NotEnoughGas;273 var extra: ExtraFieldsUsdt = lazy storage.extra.load();274 var noticeTo: address? = null;275 var noticeAboutClient: bool = false;276 if (in.senderAddress == storage.clientAddress) {277 if (msg.value == ErrModeVal.ProviderFullRights as int8) {278 extra.errMode = ErrModeVal.ProviderFullRights;279 } else if (msg.value == ErrModeVal.BlockErrorMode as int8) {280 extra.errMode = ErrModeVal.BlockErrorMode;281 } else if (282 msg.value == ErrModeVal.ClientFullRightsAfterHalfYear as int8 &&283 extra.errMode != ErrModeVal.BlockErrorMode as int8284 ) {285 extra.errMode = ErrModeVal.ClientFullRightsAfterHalfYear;286 extra.errModeAt = blockchain.now() as uint64;287 noticeTo = storage.providerAddress;288 noticeAboutClient = true;289 }290 } else if (in.senderAddress == storage.providerAddress) {291 if (msg.value == ErrModeVal.ClientFullRights as int8) {292 extra.errMode = ErrModeVal.ClientFullRights;293 } else if (msg.value == ErrModeVal.BlockErrorMode as int8) {294 extra.errMode = ErrModeVal.BlockErrorMode;295 } else if (296 msg.value == ErrModeVal.ProviderFullRightsAfterHalfYear as int8 &&297 extra.errMode != ErrModeVal.BlockErrorMode as int8298 ) {299 extra.errMode = ErrModeVal.ProviderFullRightsAfterHalfYear;300 extra.errModeAt = blockchain.now() as uint64;301 noticeTo = storage.clientAddress;302 noticeAboutClient = false;303 }304 } else {305 throw Errors.UnauthorizedSender;306 }307 if (noticeTo != null) {308 assert (msgValue > HALF_YEAR_NOTICE_VALUE) throw Errors.NotEnoughGas;309 msgValue -= HALF_YEAR_NOTICE_VALUE;310 }311 storage.extra = extra.toCell();312 storage.save();313 if (noticeTo != null) {314 sendHalfYearControlNotice(noticeTo, noticeAboutClient);315 }316 sendExcessesTon(in.senderAddress, msgValue);317}10enum ErrModeVal {11 ClientFullRights = 112 ProviderFullRights = 213 ClientFullRightsAfterHalfYear = 16 // клиент получает полные права через полгода14 ProviderFullRightsAfterHalfYear = 26 // исполнитель — через полгода15 BlockErrorMode = 9916}ErrRefund — سحب طارئ للجيتونات التي وصلت دون إشعار ولم تُسجَّل. أو عندما تُفقد المحفظة (بالأدق: تُفقد كلمة مرورها).
321ErrRefund => {322 msgValue -= (MIN_MESSAGE_VALUE_BOUND + MIN_FORWARD_TON_AMOUNT);323 assert (msgValue > 0) throw Errors.NotEnoughGas;324 assert (msg.jettonAmount > 0) throw Errors.InvalidData;325 val extra: ExtraFieldsUsdt = lazy storage.extra.load();326 if (327 !extra.allowsErrRefund(328 in.senderAddress,329 storage.clientAddress,330 storage.providerAddress,331 )332 ) {333 throw Errors.UnauthorizedSender;334 }335 val transferRecipient: address = msg.addressRefund != null336 ? msg.addressRefund337 : in.senderAddress;338 sendAskToTransferUsdt(339 QueryRole.None,340 msg.jettonAmount as coins,341 transferRecipient,342 msgValue + MIN_MESSAGE_VALUE_BOUND + MIN_FORWARD_TON_AMOUNT,343 );344}36fun ExtraFieldsUsdt.allowsErrRefund(37 self,38 sender: address,39 client: address,40 provider: address,41): bool {42 if (sender == client) {43 if (self.errMode == ErrModeVal.ClientFullRights as int8) {44 return true;45 }46 return self.errMode == ErrModeVal.ClientFullRightsAfterHalfYear as int8 &&47 (blockchain.now() as uint64) >= self.errModeAt + HALF_YEAR;48 }49 if (sender == provider) {50 if (self.errMode == ErrModeVal.ProviderFullRights as int8) {51 return true;52 }53 return self.errMode == ErrModeVal.ProviderFullRightsAfterHalfYear as int8 &&54 (blockchain.now() as uint64) >= self.errModeAt + HALF_YEAR;55 }56 return false;57}Burn — تدمير العقد كي لا تبقى قشرة فارغة عالقة في الشبكة. يمكن أن يحرقه من أنشأه (العميل أو مقدم الخدمة) أو المسؤول — وفقط عندما يكون رصيدا العميل ومقدم الخدمة صفرًا.
351BurnContract => {352 assert (msgValue > 0) throw Errors.NotEnoughGas;353 val extra: ExtraFieldsUsdt = lazy storage.extra.load();354 val deployer = extra.deployerAddress(storage.clientAddress, storage.providerAddress);355 assert (356 in.senderAddress == deployer || in.senderAddress == extra.adminAddress357 ) throw Errors.UnauthorizedSender;358 assert (359 storage.clientAmount == 0 && storage.providerDeposit == 0360 ) throw Errors.BalancesNotEmpty;361 if (extra.completed && extra.royaltyInit > 0) {362 val jettonTon = MIN_MESSAGE_VALUE_BOUND + MIN_FORWARD_TON_AMOUNT;363 msgValue -= jettonTon;364 assert (msgValue > 0) throw Errors.NotEnoughGas;365 sendAskToTransferUsdt(366 QueryRole.Admin,367 extra.royaltyInit as coins,368 extra.adminAddress,369 msgValue + jettonTon,370 );371 }372 val burn = createMessage({373 bounce: BounceMode.NoBounce,374 dest: in.senderAddress,375 value: 0,376 });377 burn.send(SEND_MODE_CARRY_ALL_BALANCE | SEND_MODE_DESTROY);378}59fun ExtraFieldsUsdt.deployerAddress(self, client: address, provider: address): address {60 if (self.mode == QueryRole.Client as int8) {61 return client;62 }63 assert (self.mode == QueryRole.Provider as int8) throw Errors.UnauthorizedSender;64 return provider;65}الكود المصدري الموثّق للعقود الثلاثة. تجزئة الكود تطابق ما هو على الشبكة. كل رابط يفتح المصدر على TON Verifier.
ContractMinterContractUSDTContractTON