গ্যাস ফি স্পনসর করা: কীভাবে আপনার ব্যবহারকারীদের জন্য লেনদেন খরচ কভার করবেন
ভূমিকা
আমরা যদি চাই ইথিরিয়াম আরও এক বিলিয়ন মানুষকে (opens in a new tab) পরিষেবা দিক, তবে আমাদের বাধাগুলো দূর করতে হবে এবং এটি ব্যবহার করা যতটা সম্ভব সহজ করতে হবে। এই বাধার একটি উৎস হলো গ্যাস ফি পরিশোধ করার জন্য ETH-এর প্রয়োজনীয়তা।
আপনার যদি এমন একটি ডিএ্যাপ থাকে যা ব্যবহারকারীদের থেকে অর্থ উপার্জন করে, তবে ব্যবহারকারীদের আপনার সার্ভারের মাধ্যমে লেনদেন জমা দিতে দেওয়া এবং লেনদেন ফি নিজে পরিশোধ করা যৌক্তিক হতে পারে। যেহেতু ব্যবহারকারীরা এখনও তাদের ওয়ালেট-এ একটি 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 -
Sepolia-তে ETH আছে এমন একটি ওয়ালেট-এ
PRIVATE_KEYসেট করতে.envসম্পাদনা করুন। আপনার যদি Sepolia ETH-এর প্রয়োজন হয়, তবে একটি ফাসেট ব্যবহার করুন। আদর্শভাবে, এই প্রাইভেট কি আপনার ব্রাউজার ওয়ালেট-এ থাকা কি থেকে আলাদা হওয়া উচিত। -
সার্ভার চালু করুন।
1npm run dev -
http://localhost:5173(opens in a new tab) URL-এ অ্যাপ্লিকেশনটি ব্রাউজ করুন। -
একটি ওয়ালেট-এর সাথে কানেক্ট করতে Connect with Injected-এ ক্লিক করুন। ওয়ালেট-এ অনুমোদন করুন এবং প্রয়োজনে Sepolia-তে পরিবর্তন অনুমোদন করুন।
-
একটি নতুন গ্রিটিং লিখুন এবং 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")যদি কোনো একাউন্ট না থাকে, তবে একটি এরর রেইজ করুন। এটি কখনই হওয়া উচিত নয় কারণ যে UI বোতামটি signGreeting কল করার প্রক্রিয়া শুরু করে তা সেই ক্ষেত্রে নিষ্ক্রিয় থাকে। তবে, ভবিষ্যতের প্রোগ্রামাররা সেই সুরক্ষা সরিয়ে ফেলতে পারে, তাই এখানেও এই শর্তটি পরীক্ষা করা একটি ভালো ধারণা।
1 const domain = {2 name: "Greeter",3 version: "1",4 chainId,5 verifyingContract: contractAddr,6 }ডোমেইন সেপারেটর (opens in a new tab)-এর জন্য প্যারামিটার। এই ভ্যালুটি ধ্রুবক, তাই একটি আরও ভালোভাবে অপ্টিমাইজ করা ইমপ্লিমেন্টেশনে, আমরা ফাংশনটি কল করার প্রতিবার পুনরায় গণনা করার পরিবর্তে এটি একবার গণনা করতে পারি।
nameহলো একটি ব্যবহারকারী-পঠনযোগ্য নাম, যেমন ডিএ্যাপ-এর নাম যার জন্য আমরা সিগনেচার তৈরি করছি।versionহলো সংস্করণ। বিভিন্ন সংস্করণ সামঞ্জস্যপূর্ণ নয়।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 })প্রকৃতপক্ষে সিগনেচার পান। এই ফাংশনটি অ্যাসিনক্রোনাস কারণ ব্যবহারকারীরা ডেটা সাইন করতে দীর্ঘ সময় (কম্পিউটারের দৃষ্টিকোণ থেকে) নেয়।
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)যদি এই ভেরিয়েবলগুলোর কোনোটি পরিবর্তিত হয়, তবে ফাংশনটির একটি নতুন ইনস্ট্যান্স তৈরি করুন। account এবং chainId প্যারামিটারগুলো ব্যবহারকারী দ্বারা ওয়ালেট-এ পরিবর্তন করা যেতে পারে। contractAddr হলো চেইন আইডির একটি ফাংশন। 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-এনকোড করে পাঠাতে POST ব্যবহার করুন।
1 const data = await response.json()2 console.log("Server response:", data)3 } catch (err) {4 console.error("Error:", err)5 }6 }রেসপন্স আউটপুট করুন। একটি প্রোডাকশন সিস্টেমে আমরা ব্যবহারকারীকেও রেসপন্স দেখাব।
সার্ভার
আমি আমার ফ্রন্ট-এন্ড হিসেবে 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)। তারপর আমরা অন্যান্য সমস্ত URL হ্যান্ডেল করার জন্য একটি 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)");এটি হলো স্ট্রাকচার আইডেন্টিফায়ার (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) অনুসারে ডাইজেস্ট তৈরি করুন।
1 // স্বাক্ষরকারী পুনরুদ্ধার করুন2 address signer = ecrecover(digest, v, r, s);3 require(signer != address(0), "Invalid signature");সাইনার এডড্রেস পেতে ecrecover (opens in a new tab) ব্যবহার করুন। মনে রাখবেন যে একটি খারাপ সিগনেচার এখনও একটি বৈধ এডড্রেস তৈরি করতে পারে, কেবল একটি র্যান্ডম এডড্রেস।
1 // গ্রিটিং এমনভাবে প্রয়োগ করুন যেন স্বাক্ষরকারী এটি কল করেছে2 greeting = req.greeting;3 emit SetGreeting(signer, req.greeting);4 }গ্রিটিং আপডেট করুন।
ঝুঁকি
এটি প্রোডাকশন-স্তরের কোড নয়। এটি উল্লেখযোগ্য আক্রমণের জন্য ঝুঁকিপূর্ণ এবং এতে প্রধান বৈশিষ্ট্যগুলোর অভাব রয়েছে। এখানে কিছু দেওয়া হলো, সাথে কীভাবে সেগুলো সমাধান করা যায়।
এই আক্রমণগুলোর কিছু দেখতে, Attacks শিরোনামের অধীনে বোতামগুলোতে ক্লিক করুন এবং দেখুন কী ঘটে। Invalid signature বোতামের জন্য, লেনদেন রেসপন্স দেখতে সার্ভার কনসোল চেক করুন।
সার্ভারে ডিনায়াল-অফ-সার্ভিস
সবচেয়ে সহজ আক্রমণ হলো সার্ভারে একটি ডিনায়াল-অফ-সার্ভিস (opens in a new tab) আক্রমণ। সার্ভার ইন্টারনেটের যেকোনো জায়গা থেকে রিকোয়েস্ট গ্রহণ করে এবং সেই রিকোয়েস্টগুলোর ওপর ভিত্তি করে লেনদেন পাঠায়। একজন আক্রমণকারীকে একগুচ্ছ সিগনেচার, বৈধ বা অবৈধ, ইস্যু করা থেকে বিরত রাখার মতো কিছুই নেই। প্রতিটি একটি লেনদেন ঘটাবে। শেষ পর্যন্ত সার্ভারের গ্যাস ফি পরিশোধ করার জন্য ETH শেষ হয়ে যাবে।
এই সমস্যার একটি সমাধান হলো প্রতি ব্লক-এ একটি লেনদেনের হার সীমিত করা। যদি উদ্দেশ্য হয় এক্সটার্নালি ওনড একাউন্ট-এ গ্রিটিং দেখানো, তবে ব্লক-এর মাঝখানে গ্রিটিং কী তা কোনো ব্যাপার নয়।
আরেকটি সমাধান হলো এডড্রেসগুলোর ট্র্যাক রাখা এবং কেবল বৈধ গ্রাহকদের থেকে সিগনেচার অনুমোদন করা।
ভুল গ্রিটিং সিগনেচার
যখন আপনি Signature for wrong greeting-এ ক্লিক করেন, তখন আপনি একটি নির্দিষ্ট এডড্রেস (0xaA92c5d426430D4769c9E878C1333BDe3d689b3e) এবং গ্রিটিং (Hello)-এর জন্য একটি বৈধ সিগনেচার জমা দেন। কিন্তু এটি একটি ভিন্ন গ্রিটিং-এর সাথে জমা দেয়। এটি ecrecover-কে বিভ্রান্ত করে, যা গ্রিটিং পরিবর্তন করে কিন্তু ভুল এডড্রেস থাকে।
এই সমস্যা সমাধানের জন্য, সাইন করা স্ট্রাকচারে (opens in a new tab) এডড্রেস যোগ করুন। এইভাবে, ecrecover র্যান্ডম এডড্রেস সিগনেচারের এডড্রেস-এর সাথে মিলবে না, এবং স্মার্ট কন্ট্রাক্ট মেসেজটি প্রত্যাখ্যান করবে।
রিপ্লে আক্রমণ
যখন আপনি Replay attack-এ ক্লিক করেন, তখন আপনি একই "আমি 0xaA92c5d426430D4769c9E878C1333BDe3d689b3e, এবং আমি চাই গ্রিটিং Hello হোক" সিগনেচার জমা দেন, কিন্তু সঠিক গ্রিটিং-এর সাথে। ফলস্বরূপ, স্মার্ট কন্ট্রাক্ট বিশ্বাস করে যে এডড্রেস (যা আপনার নয়) গ্রিটিংটি আবার Hello-তে পরিবর্তন করেছে। এটি করার তথ্য লেনদেন তথ্যে (opens in a new tab) সর্বজনীনভাবে উপলব্ধ।
যদি এটি একটি সমস্যা হয়, তবে একটি সমাধান হলো একটি নন্স (opens in a new tab) যোগ করা। এডড্রেস এবং সংখ্যার মধ্যে একটি ম্যাপিং (opens in a new tab) রাখুন, এবং সিগনেচারে একটি নন্স ফিল্ড যোগ করুন। যদি নন্স ফিল্ড এডড্রেস-এর ম্যাপিংয়ের সাথে মিলে যায়, তবে সিগনেচার গ্রহণ করুন এবং পরের বারের জন্য ম্যাপিং বৃদ্ধি করুন। যদি তা না হয়, তবে লেনদেন প্রত্যাখ্যান করুন।
আরেকটি সমাধান হলো সাইন করা ডেটাতে একটি টাইমস্ট্যাম্প যোগ করা এবং সেই টাইমস্ট্যাম্পের পর কেবল কয়েক সেকেন্ডের জন্য সিগনেচারটিকে বৈধ হিসেবে গ্রহণ করা। এটি সহজ এবং সস্তা, তবে আমরা টাইম উইন্ডোর মধ্যে রিপ্লে আক্রমণের ঝুঁকি নিই, এবং টাইম উইন্ডো অতিক্রম করলে বৈধ লেনদেন ব্যর্থ হওয়ার ঝুঁকি থাকে।
অন্যান্য অনুপস্থিত বৈশিষ্ট্য
একটি প্রোডাকশন সেটিংয়ে আমরা আরও কিছু বৈশিষ্ট্য যোগ করব।
অন্যান্য সার্ভার থেকে অ্যাক্সেস
বর্তমানে, আমরা যেকোনো এডড্রেস-কে একটি sponsorSetGreeting জমা দেওয়ার অনুমতি দিই। বিকেন্দ্রীকরণের স্বার্থে এটি ঠিক আমরা যা চাই তা হতে পারে। অথবা হয়তো আমরা নিশ্চিত করতে চাই যে স্পনসর করা লেনদেনগুলো আমাদের সার্ভারের মাধ্যমে যায়, সেক্ষেত্রে আমরা স্মার্ট কন্ট্রাক্ট-এ msg.sender চেক করব।
যাই হোক না কেন, এটি একটি সচেতন ডিজাইনের সিদ্ধান্ত হওয়া উচিত, কেবল বিষয়টি নিয়ে চিন্তা না করার ফলাফল নয়।
এরর হ্যান্ডলিং
একজন ব্যবহারকারী একটি গ্রিটিং জমা দেয়। হয়তো এটি পরবর্তী ব্লক-এ আপডেট হয়। হয়তো হয় না। এররগুলো অদৃশ্য। একটি প্রোডাকশন সিস্টেমে, ব্যবহারকারীর এই ক্ষেত্রগুলোর মধ্যে পার্থক্য করতে সক্ষম হওয়া উচিত:
- নতুন গ্রিটিং এখনও জমা দেওয়া হয়নি
- নতুন গ্রিটিং জমা দেওয়া হয়েছে, এবং এটি প্রক্রিয়াধীন রয়েছে
- নতুন গ্রিটিং প্রত্যাখ্যান করা হয়েছে
উপসংহার
এই পর্যায়ে, আপনি কিছু কেন্দ্রীকরণের বিনিময়ে আপনার ডিএ্যাপ ব্যবহারকারীদের জন্য একটি গ্যাসলেস অভিজ্ঞতা তৈরি করতে সক্ষম হবেন।
তবে, এটি কেবল সেই স্মার্ট কন্ট্রাক্টগুলোর সাথে কাজ করে যা ERC-712 সমর্থন করে। উদাহরণস্বরূপ, একটি ERC-20 টোকেন ট্রান্সফার করার জন্য, কেবল একটি মেসেজের পরিবর্তে মালিকের দ্বারা লেনদেন সাইন করা প্রয়োজন। এর সমাধান হলো একাউন্ট অ্যাবস্ট্রাকশন (ERC-4337) (opens in a new tab)। আমি আশা করি ভবিষ্যতে এটি সম্পর্কে একটি টিউটোরিয়াল লিখব।
আমার আরও কাজের জন্য এখানে দেখুন (opens in a new tab)।
পেজ সর্বশেষ আপডেট: 3 মার্চ, 2026