گیس فیس کو اسپانسر کرنا: اپنے صارفین کے لیے ٹرانزیکشن کے اخراجات کیسے پورے کریں
تعارف
اگر ہم چاہتے ہیں کہ Ethereum مزید ایک ارب لوگوں (opens in a new tab) کی خدمت کرے، تو ہمیں رکاوٹوں کو دور کرنے اور اسے استعمال میں ہر ممکن حد تک آسان بنانے کی ضرورت ہے۔ اس رکاوٹ کی ایک وجہ گیس فیس ادا کرنے کے لیے ETH کی ضرورت ہے۔
اگر آپ کے پاس ایک dapp ہے جو صارفین سے پیسے کماتی ہے، تو یہ مناسب ہو سکتا ہے کہ صارفین کو اپنے سرور کے ذریعے ٹرانزیکشنز جمع کرانے دیں اور ٹرانزیکشن فیس خود ادا کریں۔ چونکہ صارفین اب بھی اپنے والیٹس میں EIP-712 اجازت نامے کے پیغام (opens in a new tab) پر دستخط کرتے ہیں، اس لیے وہ Ethereum کی سالمیت کی ضمانتوں کو برقرار رکھتے ہیں۔ دستیابی اس سرور پر منحصر ہے جو ٹرانزیکشنز کو ریلے کرتا ہے، اس لیے یہ زیادہ محدود ہے۔ تاہم، آپ چیزوں کو اس طرح ترتیب دے سکتے ہیں کہ صارفین براہ راست اسمارٹ کانٹریکٹ تک بھی رسائی حاصل کر سکیں (اگر انہیں ETH مل جائے)، اور دوسروں کو اپنے سرورز ترتیب دینے دیں اگر وہ ٹرانزیکشنز کو اسپانسر کرنا چاہتے ہیں۔
اس ٹیوٹوریل میں بتائی گئی تکنیک صرف اسی وقت کام کرتی ہے جب آپ اسمارٹ کانٹریکٹ کو کنٹرول کرتے ہیں۔ دیگر تکنیکیں بھی ہیں، بشمول اکاؤنٹ ایبسٹریکشن (account abstraction) (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) پر تعینات (deploy) کیا جا چکا ہے۔
اسے عملی طور پر دیکھنے کے لیے، ان اقدامات پر عمل کریں۔
-
ریپوزٹری کو کلون کریں اور ضروری سافٹ ویئر انسٹال کریں۔
1git clone https://github.com/qbzzt/260301-gasless.git2cd 260301-gasless/server3npm install -
.envمیں ترمیم کریں تاکہPRIVATE_KEYکو ایک ایسے والیٹ پر سیٹ کیا جا سکے جس میں Sepolia پر ETH موجود ہو۔ اگر آپ کو Sepolia ETH کی ضرورت ہے، تو فاسٹ (faucet) استعمال کریں۔ مثالی طور پر، یہ پرائیویٹ کلید اس کلید سے مختلف ہونی چاہیے جو آپ کے براؤزر والیٹ میں ہے۔ -
سرور شروع کریں۔
1npm run dev -
URL
http://localhost:5173(opens in a new tab) پر ایپلی کیشن کو براؤز کریں۔ -
والیٹ سے منسلک ہونے کے لیے 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) ہمیں اس وقت کارکردگی کو بہتر بنانے کی سہولت دیتا ہے جب کمپوننٹ کو دوبارہ ڈرا کیا جاتا ہے، اسی فنکشن کو دوبارہ استعمال کر کے۔
1 async (greeting) => {2 if (!account) throw new Error("Wallet not connected")اگر کوئی اکاؤنٹ نہیں ہے، تو ایک ایرر (error) ظاہر کریں۔ ایسا کبھی نہیں ہونا چاہیے کیونکہ UI بٹن جو اس عمل کو شروع کرتا ہے جو signGreeting کو کال کرتا ہے، اس صورت میں غیر فعال (disabled) ہوتا ہے۔ تاہم، مستقبل کے پروگرامرز اس حفاظتی اقدام کو ہٹا سکتے ہیں، اس لیے یہاں بھی اس شرط کو چیک کرنا ایک اچھا خیال ہے۔
1 const domain = {2 name: "Greeter",3 version: "1",4 chainId,5 verifyingContract: contractAddr,6 }ڈومین سیپریٹر (domain separator) (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 فیلڈ کا نام بھی ہے اور اس ویری ایبل کا نام بھی جو اسے پُر کرتا ہے۔
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 کو تبدیل نہیں ہونا چاہیے، لیکن ہم اسے ایک ہک (hook) (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)پہلے ہم ان درخواستوں کے لیے ایک ہینڈلر رجسٹر کرتے ہیں جنہیں ہم خود ہینڈل کرتے ہیں (/server/sponsor پر POST)۔ پھر ہم دیگر تمام 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 }کنسٹرکٹر (constructor) اوپر دیے گئے یوزر انٹرفیس کوڈ کی طرح ڈومین سیپریٹر (opens in a new tab) بناتا ہے۔ بلاک چین پر عمل درآمد بہت زیادہ مہنگا ہے، اس لیے ہم اسے صرف ایک بار کیلکولیٹ کرتے ہیں۔
1 struct GreetingRequest {2 string greeting;3 }یہ وہ اسٹرکچر ہے جس پر دستخط کیے جاتے ہیں۔ یہاں ہمارے پاس صرف ایک فیلڈ ہے۔
1 bytes32 private constant GREETING_TYPEHASH =2 keccak256("GreetingRequest(string greeting)");یہ اسٹرکچر شناخت کنندہ (structure 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) میں عوامی طور پر دستیاب ہے۔
اگر یہ ایک مسئلہ ہے، تو اس کا ایک حل نونس (nonce) (opens in a new tab) شامل کرنا ہے۔ ایڈریسز اور نمبرز کے درمیان ایک میپنگ (mapping) (opens in a new tab) رکھیں، اور دستخط میں ایک نونس فیلڈ شامل کریں۔ اگر نونس فیلڈ ایڈریس کی میپنگ سے میل کھاتی ہے، تو دستخط کو قبول کریں اور اگلی بار کے لیے میپنگ میں اضافہ (increment) کریں۔ اگر ایسا نہیں ہوتا ہے، تو ٹرانزیکشن کو مسترد کر دیں۔
ایک اور حل یہ ہے کہ دستخط شدہ ڈیٹا میں ٹائم اسٹیمپ (timestamp) شامل کیا جائے اور دستخط کو اس ٹائم اسٹیمپ کے بعد صرف چند سیکنڈ کے لیے درست تسلیم کیا جائے۔ یہ آسان اور سستا ہے، لیکن ہمیں ٹائم ونڈو کے اندر ری پلے حملوں کا خطرہ ہوتا ہے، اور اگر ٹائم ونڈو سے تجاوز کر جائے تو جائز ٹرانزیکشنز کی ناکامی کا خطرہ ہوتا ہے۔
دیگر غائب خصوصیات
کچھ اضافی خصوصیات ہیں جو ہم پروڈکشن سیٹنگ میں شامل کریں گے۔
دوسرے سرورز سے رسائی
فی الحال، ہم کسی بھی ایڈریس کو sponsorSetGreeting جمع کرانے کی اجازت دیتے ہیں۔ ڈی سینٹرلائزیشن کے مفاد میں، یہ بالکل وہی ہو سکتا ہے جو ہم چاہتے ہیں۔ یا شاید ہم یہ یقینی بنانا چاہتے ہیں کہ اسپانسر شدہ ٹرانزیکشنز ہمارے سرور کے ذریعے ہی جائیں، اس صورت میں ہم اسمارٹ کانٹریکٹ میں msg.sender کو چیک کریں گے۔
دونوں صورتوں میں، یہ ایک شعوری ڈیزائن کا فیصلہ ہونا چاہیے، نہ کہ صرف اس مسئلے کے بارے میں نہ سوچنے کا نتیجہ۔
ایرر ہینڈلنگ (Error handling)
ایک صارف گریٹنگ جمع کراتا ہے۔ ہو سکتا ہے کہ یہ اگلے بلاک پر اپ ڈیٹ ہو جائے۔ ہو سکتا ہے کہ نہ ہو۔ ایررز (Errors) پوشیدہ ہوتے ہیں۔ پروڈکشن سسٹم پر، صارف کو ان صورتوں کے درمیان فرق کرنے کے قابل ہونا چاہیے:
- نیا گریٹنگ ابھی تک جمع نہیں کرایا گیا ہے
- نیا گریٹنگ جمع کرا دیا گیا ہے، اور یہ پروسیس میں ہے
- نیا گریٹنگ مسترد کر دیا گیا ہے
نتیجہ
اس مقام پر، آپ کو اپنے dapp صارفین کے لیے کچھ سینٹرلائزیشن کی قیمت پر گیس کے بغیر (gasless) تجربہ بنانے کے قابل ہونا چاہیے۔
تاہم، یہ صرف ان اسمارٹ کانٹریکٹس کے ساتھ کام کرتا ہے جو ERC-712 کو سپورٹ کرتے ہیں۔ مثال کے طور پر، ایک ERC-20 ٹوکن منتقل کرنے کے لیے، یہ ضروری ہے کہ ٹرانزیکشن پر صرف ایک پیغام کے بجائے مالک کے دستخط ہوں۔ اس کا حل اکاؤنٹ ایبسٹریکشن (ERC-4337) (opens in a new tab) ہے۔ مجھے امید ہے کہ میں مستقبل میں اس کے بارے میں ایک ٹیوٹوریل لکھوں گا۔
میرے مزید کام کے لیے یہاں دیکھیں (opens in a new tab)۔
صفحہ کی آخری اپ ڈیٹ: ۳ مارچ، ۲۰۲۶