گیس کی فیس کو سپانسر کرنا: اپنے صارفین کے لیے ٹرانزیکشن کے اخراجات کیسے پورے کریں
تعارف
اگر ہم چاہتے ہیں کہ ایتھیریم مزید ایک ارب لوگوں (opens in a new tab) کی خدمت کرے، تو ہمیں رکاوٹوں کو دور کرنے اور اسے استعمال میں ہر ممکن حد تک آسان بنانے کی ضرورت ہے۔ اس رکاوٹ کی ایک وجہ گیس کی فیس ادا کرنے کے لیے ETH کی ضرورت ہے۔
اگر آپ کے پاس ایک غیر مرکزی ایپلی کیشن (dapp) ہے جو صارفین سے پیسے کماتی ہے، تو یہ مناسب ہو سکتا ہے کہ صارفین کو اپنے سرور کے ذریعے ٹرانزیکشنز جمع کروانے دیں اور ٹرانزیکشن کی فیس خود ادا کریں۔ چونکہ صارفین اب بھی اپنے والیٹس میں EIP-712 اجازت نامے کے پیغام (opens in a new tab) پر دستخط کرتے ہیں، اس لیے وہ ایتھیریم کی سالمیت کی ضمانتوں کو برقرار رکھتے ہیں۔ دستیابی اس سرور پر منحصر ہے جو ٹرانزیکشنز کو ریلے کرتا ہے، اس لیے یہ زیادہ محدود ہے۔ تاہم، آپ چیزوں کو اس طرح ترتیب دے سکتے ہیں کہ صارفین براہ راست سمارٹ کنٹریکٹ تک بھی رسائی حاصل کر سکیں (اگر انہیں ETH مل جائے)، اور دوسروں کو اپنے سرورز قائم کرنے دیں اگر وہ ٹرانزیکشنز کو سپانسر کرنا چاہتے ہیں۔
اس ٹیوٹوریل میں بتائی گئی تکنیک صرف اسی وقت کام کرتی ہے جب آپ سمارٹ کنٹریکٹ کو کنٹرول کرتے ہوں۔ دیگر تکنیکیں بھی ہیں، بشمول اکاؤنٹ کی تجرید (opens in a new tab) جو آپ کو دوسرے سمارٹ کنٹریکٹس کے لیے ٹرانزیکشنز کو سپانسر کرنے کی اجازت دیتی ہیں، جنہیں میں امید کرتا ہوں کہ مستقبل کے ٹیوٹوریل میں شامل کروں گا۔
نوٹ: یہ پروڈکشن لیول کا کوڈ نہیں ہے۔ یہ اہم حملوں کا شکار ہو سکتا ہے اور اس میں بڑی خصوصیات کی کمی ہے۔ اس گائیڈ کے کمزوریوں کے سیکشن میں مزید جانیں۔
پیشگی شرائط
اس ٹیوٹوریل کو سمجھنے کے لیے آپ کو پہلے سے درج ذیل سے واقف ہونا چاہیے:
- Solidity
- JavaScript
- React اور WAGMI۔ اگر آپ ان یوزر انٹرفیس ٹولز سے واقف نہیں ہیں، تو ہمارے پاس اس کے لیے ایک ٹیوٹوریل موجود ہے۔
نمونہ ایپلی کیشن
یہاں نمونہ ایپلی کیشن Hardhat کے Greeter کنٹریکٹ کی ایک قسم ہے۔ آپ اسے GitHub پر (opens in a new tab) دیکھ سکتے ہیں۔ سمارٹ کنٹریکٹ پہلے ہی Sepolia (opens in a new tab) پر، پتہ 0xC87506C66c7896366b9E988FE0aA5B6dDE77CFfA (opens in a new tab) پر تعینات کیا جا چکا ہے۔
اسے عملی طور پر دیکھنے کے لیے، ان اقدامات پر عمل کریں۔
-
ریپوزٹری کو کلون کریں اور ضروری سافٹ ویئر انسٹال کریں۔
1git clone https://github.com/qbzzt/260301-gasless.git2cd 260301-gasless/server3npm install -
.envمیں ترمیم کریں تاکہPRIVATE_KEYکو ایک ایسے والیٹ پر سیٹ کیا جا سکے جس میں Sepolia پر ETH موجود ہو۔ اگر آپ کو Sepolia ETH کی ضرورت ہے، تو فوسٹ استعمال کریں۔ مثالی طور پر، یہ نجی کلید اس کلید سے مختلف ہونی چاہیے جو آپ کے براؤزر والیٹ میں ہے۔ -
سرور شروع کریں۔
1npm run dev -
http://localhost:5173(opens in a new tab) URL پر ایپلی کیشن کو براؤز کریں۔ -
والیٹ سے منسلک ہونے کے لیے Connect with Injected پر کلک کریں۔ والیٹ میں منظور کریں، اور اگر ضروری ہو تو Sepolia میں تبدیلی کو منظور کریں۔
-
ایک نیا سلام (greeting) لکھیں اور Update greeting via sponsor پر کلک کریں۔
-
پیغام پر دستخط کریں۔
-
تقریباً 12 سیکنڈ انتظار کریں (Sepolia پر بلاک کا وقت)۔ انتظار کے دوران آپ ٹرانزیکشن دیکھنے کے لیے سرور کے کنسول میں URL دیکھ سکتے ہیں۔
-
دیکھیں کہ سلام تبدیل ہو گیا ہے، اور آخری بار اپ ڈیٹ کرنے والے پتے کی ویلیو اب آپ کے براؤزر والیٹ کا پتہ ہے۔
یہ سمجھنے کے لیے کہ یہ کیسے کام کرتا ہے، ہمیں یہ دیکھنے کی ضرورت ہے کہ یوزر انٹرفیس میں پیغام کیسے بنتا ہے، سرور اسے کیسے ریلے کرتا ہے، اور سمارٹ کنٹریکٹ اس پر کیسے کارروائی کرتا ہے۔
یوزر انٹرفیس
یوزر انٹرفیس WAGMI (opens in a new tab) پر مبنی ہے؛ آپ اس کے بارے میں اس ٹیوٹوریل میں پڑھ سکتے ہیں۔
یہاں بتایا گیا ہے کہ ہم پیغام پر کیسے دستخط کرتے ہیں:
1const signGreeting = useCallback(React ہک useCallback (opens in a new tab) ہمیں اس وقت کارکردگی کو بہتر بنانے کی سہولت دیتا ہے جب جزو (component) کو دوبارہ ڈرا کیا جاتا ہے، اسی فنکشن کو دوبارہ استعمال کر کے۔
1 async (greeting) => {2 if (!account) throw new Error("Wallet not connected")اگر کوئی اکاؤنٹ نہیں ہے، تو ایک ایرر (error) ظاہر کریں۔ ایسا کبھی نہیں ہونا چاہیے کیونکہ UI بٹن جو اس عمل کو شروع کرتا ہے جو signGreeting کو کال کرتا ہے، اس صورت میں غیر فعال ہوتا ہے۔ تاہم، مستقبل کے پروگرامرز اس حفاظتی اقدام کو ہٹا سکتے ہیں، اس لیے یہاں بھی اس شرط کو چیک کرنا ایک اچھا خیال ہے۔
1 const domain = {2 name: "Greeter",3 version: "1",4 chainId,5 verifyingContract: contractAddr,6 }ڈومین سیپریٹر (opens in a new tab) کے لیے پیرامیٹرز۔ یہ ویلیو مستقل (constant) ہے، اس لیے ایک بہتر آپٹمائزڈ عمل درآمد میں، ہم اسے ہر بار فنکشن کال ہونے پر دوبارہ گننے کے بجائے صرف ایک بار کیلکولیٹ کر سکتے ہیں۔
nameایک صارف کے پڑھنے کے قابل نام ہے، جیسے کہ اس dapp کا نام جس کے لیے ہم دستخط تیار کر رہے ہیں۔versionورژن ہے۔ مختلف ورژنز ہم آہنگ (compatible) نہیں ہوتے۔chainIdوہ چین ہے جسے ہم استعمال کر رہے ہیں، جیسا کہ WAGMI کی طرف سے (opens in a new tab) فراہم کیا گیا ہے۔verifyingContractوہ کنٹریکٹ کا پتہ ہے جو اس دستخط کی تصدیق کرے گا۔ ہم نہیں چاہتے کہ ایک ہی دستخط متعدد کنٹریکٹس پر لاگو ہو، اس صورت میں کہ کئیGreeterکنٹریکٹس ہوں اور ہم چاہتے ہوں کہ ان کے سلام مختلف ہوں۔
1
2 const types = {3 GreetingRequest: [4 { name: "greeting", type: "string" },5 ],6 }ڈیٹا کی قسم جس پر ہم دستخط کرتے ہیں۔ یہاں، ہمارے پاس ایک ہی پیرامیٹر ہے، greeting، لیکن حقیقی زندگی کے سسٹمز میں عام طور پر زیادہ ہوتے ہیں۔
1 const message = { greeting }اصل پیغام جس پر ہم دستخط کرنا اور بھیجنا چاہتے ہیں۔ greeting فیلڈ کا نام بھی ہے اور اس متغیر (variable) کا نام بھی جو اسے بھرتا ہے۔
1 const signature = await signTypedDataAsync({2 domain,3 types,4 primaryType: "GreetingRequest",5 message,6 })دراصل دستخط حاصل کریں۔ یہ فنکشن غیر مطابقت پذیر (asynchronous) ہے کیونکہ صارفین ڈیٹا پر دستخط کرنے میں (کمپیوٹر کے نقطہ نظر سے) کافی وقت لیتے ہیں۔
1 const r = `0x${signature.slice(2, 66)}`2 const s = `0x${signature.slice(66, 130)}`3 const v = parseInt(signature.slice(130, 132), 16)4
5 return {6 req: { greeting },7 v,8 r,9 s,10 }11 },یہ فنکشن ایک واحد ہیکسا ڈیسیمل ویلیو واپس کرتا ہے۔ یہاں ہم اسے فیلڈز میں تقسیم کرتے ہیں۔
1 [account, chainId, contractAddr, signTypedDataAsync],2)اگر ان میں سے کوئی بھی متغیر تبدیل ہوتا ہے، تو فنکشن کی ایک نئی مثال (instance) بنائیں۔ account اور chainId پیرامیٹرز کو صارف والیٹ میں تبدیل کر سکتا ہے۔ contractAddr چین Id کا ایک فنکشن ہے۔ signTypedDataAsync کو تبدیل نہیں ہونا چاہیے، لیکن ہم اسے ایک ہک (opens in a new tab) سے امپورٹ کرتے ہیں، اس لیے ہم یقین سے نہیں کہہ سکتے، اور اسے یہاں شامل کرنا بہتر ہے۔
اب جب کہ نئے سلام پر دستخط ہو چکے ہیں، ہمیں اسے سرور پر بھیجنے کی ضرورت ہے۔
1 const sponsoredGreeting = async () => {2 try {یہ فنکشن ایک دستخط لیتا ہے اور اسے سرور پر بھیجتا ہے۔
1 const signedMessage = await signGreeting(newGreeting)2 const response = await fetch("/server/sponsor", {اس سرور میں /server/sponsor پاتھ پر بھیجیں جہاں سے ہم آئے ہیں۔
1 method: "POST",2 headers: { "Content-Type": "application/json" },3 body: JSON.stringify(signedMessage),4 })معلومات کو JSON-encoded بھیجنے کے لیے POST کا استعمال کریں۔
1 const data = await response.json()2 console.log("Server response:", data)3 } catch (err) {4 console.error("Error:", err)5 }6 }جواب (response) آؤٹ پٹ کریں۔ پروڈکشن سسٹم پر ہم صارف کو بھی جواب دکھائیں گے۔
سرور
مجھے اپنے فرنٹ اینڈ کے طور پر Vite (opens in a new tab) استعمال کرنا پسند ہے۔ یہ خود بخود React لائبریریوں کو پیش کرتا ہے اور فرنٹ اینڈ کوڈ تبدیل ہونے پر براؤزر کو اپ ڈیٹ کرتا ہے۔ تاہم، Vite میں بیک اینڈ ٹولنگ شامل نہیں ہے۔
اس کا حل index.js (opens in a new tab) میں ہے۔
1 app.post("/server/sponsor", async (req, res) => {2 ...3 })4
5 // باقی سب کچھ Vite کو سنبھالنے دیں6 const vite = await createViteServer({7 server: { middlewareMode: true }8 })9
10 app.use(vite.middlewares)پہلے ہم ان درخواستوں کے لیے ایک ہینڈلر رجسٹر کرتے ہیں جنہیں ہم خود ہینڈل کرتے ہیں (POST سے /server/sponsor تک)۔ پھر ہم دیگر تمام URLs کو ہینڈل کرنے کے لیے ایک Vite سرور بناتے اور استعمال کرتے ہیں۔
1 app.post("/server/sponsor", async (req, res) => {2 try {3 const signed = req.body4
5 const txHash = await sepoliaClient.writeContract({6 address: greeterAddr,7 abi: greeterABI,8 functionName: 'sponsoredSetGreeting',9 args: [signed.req, signed.v, signed.r, signed.s],10 })11 } ...12 })یہ صرف ایک معیاری viem (opens in a new tab) بلاک چین کال ہے۔
سمارٹ کنٹریکٹ
آخر میں، Greeter.sol (opens in a new tab) کو دستخط کی تصدیق کرنے کی ضرورت ہے۔
1 constructor(string memory _greeting) {2 greeting = _greeting;3
4 DOMAIN_SEPARATOR = keccak256(5 abi.encode(6 keccak256(7 "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"8 ),9 keccak256(bytes("Greeter")),10 keccak256(bytes("1")),11 block.chainid,12 address(this)13 )14 );15 }کنسٹرکٹر ڈومین سیپریٹر (opens in a new tab) بناتا ہے، جو اوپر دیے گئے یوزر انٹرفیس کوڈ کی طرح ہے۔ بلاک چین پر عمل درآمد بہت زیادہ مہنگا ہے، اس لیے ہم اسے صرف ایک بار کیلکولیٹ کرتے ہیں۔
1 struct GreetingRequest {2 string greeting;3 }یہ وہ سٹرکچر ہے جس پر دستخط کیے جاتے ہیں۔ یہاں ہمارے پاس صرف ایک فیلڈ ہے۔
1 bytes32 private constant GREETING_TYPEHASH =2 keccak256("GreetingRequest(string greeting)");یہ سٹرکچر شناخت کنندہ (identifier) (opens in a new tab) ہے۔ اسے یوزر انٹرفیس میں ہر بار کیلکولیٹ کیا جاتا ہے۔
1 function sponsoredSetGreeting(2 GreetingRequest calldata req,3 uint8 v,4 bytes32 r,5 bytes32 s6 ) external {یہ فنکشن ایک دستخط شدہ درخواست وصول کرتا ہے اور سلام کو اپ ڈیٹ کرتا ہے۔
1 // EIP-712 ڈائجسٹ کا حساب لگائیں2 bytes32 digest = keccak256(3 abi.encodePacked(4 "\x19\x01",5 DOMAIN_SEPARATOR,6 keccak256(7 abi.encode(8 GREETING_TYPEHASH,9 keccak256(bytes(req.greeting))10 )11 )12 )13 );EIP 712 (opens in a new tab) کے مطابق ڈائجسٹ (digest) بنائیں۔
1 // دستخط کنندہ کو بازیافت کریں2 address signer = ecrecover(digest, v, r, s);3 require(signer != address(0), "Invalid signature");دستخط کنندہ کا پتہ حاصل کرنے کے لیے ecrecover (opens in a new tab) کا استعمال کریں۔ نوٹ کریں کہ ایک غلط دستخط کے نتیجے میں بھی ایک درست پتہ مل سکتا ہے، بس وہ ایک بے ترتیب (random) پتہ ہوگا۔
1 // گریٹنگ کو ایسے لاگو کریں جیسے دستخط کنندہ نے اسے کال کیا ہو2 greeting = req.greeting;3 emit SetGreeting(signer, req.greeting);4 }سلام کو اپ ڈیٹ کریں۔
کمزوریاں
یہ پروڈکشن لیول کا کوڈ نہیں ہے۔ یہ اہم حملوں کا شکار ہو سکتا ہے اور اس میں بڑی خصوصیات کی کمی ہے۔ یہاں کچھ کمزوریاں اور ان کو حل کرنے کا طریقہ بتایا گیا ہے۔
ان میں سے کچھ حملوں کو دیکھنے کے لیے، Attacks ہیڈنگ کے نیچے موجود بٹنوں پر کلک کریں اور دیکھیں کہ کیا ہوتا ہے۔ Invalid signature بٹن کے لیے، ٹرانزیکشن کا جواب دیکھنے کے لیے سرور کنسول چیک کریں۔
سرور پر ڈینائل آف سروس (Denial of service)
سب سے آسان حملہ سرور پر ڈینائل آف سروس (denial-of-service) (opens in a new tab) حملہ ہے۔ سرور انٹرنیٹ پر کہیں سے بھی درخواستیں وصول کرتا ہے اور ان درخواستوں کی بنیاد پر ٹرانزیکشنز بھیجتا ہے۔ حملہ آور کو درست یا غلط دستخطوں کا ایک گچھا جاری کرنے سے روکنے کے لیے بالکل کچھ نہیں ہے۔ ہر ایک کی وجہ سے ایک ٹرانزیکشن ہوگی۔ بالآخر سرور کے پاس گیس کی ادائیگی کے لیے ETH ختم ہو جائے گا۔
اس مسئلے کا ایک حل یہ ہے کہ شرح کو فی بلاک ایک ٹرانزیکشن تک محدود کر دیا جائے۔ اگر مقصد بیرونی ملکیت والے اکاؤنٹس (externally owned accounts) کو سلام دکھانا ہے، تو اس سے کوئی فرق نہیں پڑتا کہ بلاک کے وسط میں سلام کیا ہے۔
ایک اور حل یہ ہے کہ پتوں کا ریکارڈ رکھا جائے اور صرف درست صارفین کے دستخطوں کی اجازت دی جائے۔
غلط سلام کے دستخط
جب آپ Signature for wrong greeting پر کلک کرتے ہیں، تو آپ ایک مخصوص پتے (0xaA92c5d426430D4769c9E878C1333BDe3d689b3e) اور سلام (Hello) کے لیے ایک درست دستخط جمع کرواتے ہیں۔ لیکن یہ اسے ایک مختلف سلام کے ساتھ جمع کرواتا ہے۔ اس سے ecrecover الجھن کا شکار ہو جاتا ہے، جو سلام تو بدل دیتا ہے لیکن اس کا پتہ غلط ہوتا ہے۔
اس مسئلے کو حل کرنے کے لیے، پتے کو دستخط شدہ سٹرکچر (opens in a new tab) میں شامل کریں۔ اس طرح، ecrecover کا بے ترتیب پتہ دستخط میں موجود پتے سے میل نہیں کھائے گا، اور سمارٹ کنٹریکٹ پیغام کو مسترد کر دے گا۔
ری پلے حملے (Replay attacks)
جب آپ Replay attack پر کلک کرتے ہیں، تو آپ وہی "میں 0xaA92c5d426430D4769c9E878C1333BDe3d689b3e ہوں، اور میں چاہوں گا کہ سلام Hello ہو" والا دستخط جمع کرواتے ہیں، لیکن درست سلام کے ساتھ۔ نتیجے کے طور پر، سمارٹ کنٹریکٹ یہ سمجھتا ہے کہ پتے (جو آپ کا نہیں ہے) نے سلام کو واپس Hello میں تبدیل کر دیا ہے۔ ایسا کرنے کی معلومات ٹرانزیکشن کی معلومات (opens in a new tab) میں عوامی طور پر دستیاب ہے۔
اگر یہ ایک مسئلہ ہے، تو اس کا ایک حل نانس (opens in a new tab) شامل کرنا ہے۔ پتوں اور نمبروں کے درمیان ایک میپنگ (mapping) (opens in a new tab) رکھیں، اور دستخط میں ایک نانس فیلڈ شامل کریں۔ اگر نانس فیلڈ پتے کی میپنگ سے میل کھاتی ہے، تو دستخط کو قبول کریں اور اگلی بار کے لیے میپنگ میں اضافہ کریں۔ اگر ایسا نہیں ہوتا، تو ٹرانزیکشن کو مسترد کر دیں۔
ایک اور حل یہ ہے کہ دستخط شدہ ڈیٹا میں ٹائم سٹیمپ (timestamp) شامل کیا جائے اور دستخط کو اس ٹائم سٹیمپ کے بعد صرف چند سیکنڈ کے لیے درست تسلیم کیا جائے۔ یہ آسان اور سستا ہے، لیکن ہمیں ٹائم ونڈو کے اندر ری پلے حملوں کا خطرہ ہوتا ہے، اور اگر ٹائم ونڈو سے تجاوز ہو جائے تو جائز ٹرانزیکشنز کی ناکامی کا خطرہ ہوتا ہے۔
دیگر غائب خصوصیات
کچھ اضافی خصوصیات ہیں جو ہم پروڈکشن سیٹنگ میں شامل کریں گے۔
دوسرے سرورز سے رسائی
فی الحال، ہم کسی بھی پتے کو sponsorSetGreeting جمع کروانے کی اجازت دیتے ہیں۔ لامرکزیت کے مفاد میں، یہ بالکل وہی ہو سکتا ہے جو ہم چاہتے ہیں۔ یا شاید ہم یہ یقینی بنانا چاہتے ہیں کہ سپانسر شدہ ٹرانزیکشنز ہمارے سرور کے ذریعے جائیں، اس صورت میں ہم سمارٹ کنٹریکٹ میں msg.sender کو چیک کریں گے۔
بہر حال، یہ ایک شعوری ڈیزائن کا فیصلہ ہونا چاہیے، نہ کہ صرف اس مسئلے کے بارے میں نہ سوچنے کا نتیجہ۔
ایرر ہینڈلنگ (Error handling)
ایک صارف سلام جمع کرواتا ہے۔ ہو سکتا ہے کہ یہ اگلے بلاک پر اپ ڈیٹ ہو جائے۔ ہو سکتا ہے کہ نہ ہو۔ ایررز (Errors) پوشیدہ ہوتے ہیں۔ پروڈکشن سسٹم پر، صارف کو ان صورتوں کے درمیان فرق کرنے کے قابل ہونا چاہیے:
- نیا سلام ابھی تک جمع نہیں کروایا گیا ہے
- نیا سلام جمع کروا دیا گیا ہے، اور یہ عمل میں ہے
- نیا سلام مسترد کر دیا گیا ہے
نتیجہ
اس مقام پر، آپ کو کچھ مرکزیت (centralization) کی قیمت پر، اپنے dapp صارفین کے لیے گیس کے بغیر تجربہ بنانے کے قابل ہونا چاہیے۔
تاہم، یہ صرف ان سمارٹ کنٹریکٹس کے ساتھ کام کرتا ہے جو ERC-712 کو سپورٹ کرتے ہیں۔ مثال کے طور پر، ایک ERC-20 ٹوکن کی منتقلی کے لیے، یہ ضروری ہے کہ ٹرانزیکشن پر صرف ایک پیغام کے بجائے مالک کے دستخط ہوں۔ اس کا حل اکاؤنٹ کی تجرید (ERC-4337) (opens in a new tab) ہے۔ مجھے امید ہے کہ میں مستقبل میں اس کے بارے میں ایک ٹیوٹوریل لکھوں گا۔
میرے مزید کام کے لیے یہاں دیکھیں (opens in a new tab)۔
صفحہ کی آخری اپ ڈیٹ: ۳ مارچ، ۲۰۲۶