توسعه dApp با استفاده از کتابخانه های جاوا اسکریپتی اتریوم Web3.js و ethers.js

کتابخانه های جاوا اسکریپتی اتریوم:

Web3.js و ethers.js کتابخانه های جاوا اسکریپتی هستند که به دولوپر اجازه تعامل با بلاک چین را می دهند. در این مطلب به منظور توضیح چگونگی ارسال تراکنش ها با استفاده از هر دو کتابخانه Web3.js و ethers.js و از طریق API مربوط به Infura، به صورت گام به گام یک dApp را توسعه می دهیم.

پیش نیاز ها:

پیش فرض ما در این آموزش این است که آشنایی کافی برای توسعه قراردادهای هوشمند در سالیدیتی وجود دارد.

پیش از شروع اجازه بدهید علت انتخاب ما برای استفاده از Infura را توضیح بدهیم. برای دولوپرها یا تیم هایی که احتیاج به زیرساخت های مقیاس پذیر بر اساس تقاضا را دارند، Infura امکان اتصال dApps به testnet و mainnet اتریوم را با یک دستور یک خطی فراهم می کند. به این تریتب، به علت عدم اجبار در داشتن هاست، و نگهداری از نود های اتریوم خود، که هر دو پیچیده و پرهزینه هستند، Infura امکان صرفه جویی برای تیم های توسغه دهنده را فراهم می آورد. این تیم ها قادر خواهند بود بجای نگهداری از زیرساخت گران قیمت تمام تمرکز خود را متوجه توسعه محصول اصلی کنند.

شروع پروژه: قسمت اول

توجه:

اگر قبلا تنظیمات یک پروژه را انجام داده اید می توانید مستقیما به سراغ بخش دوم: Web3 در قسمت دوم بروید. در غیر اینصورت، در این بخش ما مرور سریعی خواهیم داشت بر چگونگی ایجاد یک پروژه با استفاده از Infura و Truffle و اتصال آن پروژه به React، جایی که با Web3 و Ethers تعامل خواهیم داشت.

بخش اول: Truffle

در این آموزش ما برای ساخت بنای پروژه از Truffle استفاده می کنیم. Truffle یک محیط توسعه است که امکان تعامل با EVM و تعامل با قرارداد های هوشمند را می دهد. Truffle با ارائه ابزارهای کامپایل، توسعه، تست، و اشکال زدایی، توسعه قرارداد های هوشمند روی اتریوم را آسان تر می کند. Truffle در ترکیب با Ganache به دولوپر ها امکان لوکال تست را پیش از قرار دادن قرارداد ها روی شبکه های عمومی مانند Rinkedby و Mainnet می دهد.

بیایید کار خود را با ایجاد یک پرژه Truffle در خط فرمان شروع کنیم:

~$ truffle init

بیایید نگاهی به تغییراتی بیاندازیم که با اجرای ایم دستور در مسیر پروژه می افتد. همان طور که می بینید بلاک های اجرایی در مسیر پروژه ما ساخته شده اند. در این آموزش تمرکز ما بر روی فایل های contracs، migrations، و truffle-config خواهد بود.

در مرحله بعد، برای اینکه بتوانید تراکنش های خود را قبل از ارسال به نودهای Infura امضا کنید، می توانید Truffle HD Wallet را به پروژه خود اضافه کنید.

برای افزودن این wallet دستور زیر را اجرا کنید:

~$ npm install --save @truffle/hdwallet-provider

در این مرحله احتیاج به نصب Ganache خواهید داشت:

~$ npm install -g ganache-cli

Ganache یک بلاکچین اتریوم شخصی است که شما می توانید برای اجرای تست ها، اجرای دستورات، و تشخیص وضعیت در حالی که عملکرد بلاکچین را کنترل می کنید، از آن استفاده کنید. این ابزار می تواند در تمام چرخه توسعه مفید واقع شود.

می توانید آن را به عنوان localhost:XXXX خود تصور کنید: هر چیزی که اتفاق می افتد به صورت لوکال باقی خواهد ماند، که یک زمین بازی فوق العاده را فراهم می کند، که می توانید پیش از استفاده از Infura و دیپلوی کردن روی testnet از آن استفاده کنید.

برای دسترسی به truffle console( که بزودی به آن خواهیم پرداخت)، باید اطمینان حاصل کنیم که فایل truffe-config به روز است. در غیراینصورت با چنین خطایی مواجه می شویم:

اگر ترجیح می دهید که برای Ganache به جای نسخه CLI از نسخه GUI استفاده کنید می توانید آن را از این آدرس دانلود کنید:

نتیجه اجرا Ganache GUI در حالت quick-start چیزی مشابه این تصویر خواهد بود:

برای اینکه بتوانیم به local Ganache testnet متصل شویم فایل truffle-config باید به صورت زیر باشد:

development: {

host: "127.0.0.1", // Localhost (default: none)

port: 8545, // Standard Ethereum port (default: none)

network_id: "*" // Any network (default: none)

},

اگر با این فایل آشنا نیستید، در واقع یک فایل جاوا اسکریپتی است و می تواند هر کدی که برای ایجاد تنظیمات پروژه شما مورد نیاز است را اجرا کند.

سپس در یک ترمینال مجزا دستور زیر را اجرا کنید:

~$ ganache-cli

اگر Ganache به درستی نصب شده باشد با اجرای این دستور می توانیم آن را در ترمینال مشاهده کنیم. برای تست کارکرد اتصال به testnet، دستور زیر را اجرا کنید:

~$ truffle console

در این مرحله احتیاج خواهیم داشت که تنظیماتی بر رووی hdwallet-provider که پیش از این نصب کردیم اعمال کنیم. صرفا برای یادآموری، hdwallet-provider یک اتصال شبکه راحت و آسان برای پیکربندی از طریق Infura به اتریوم است.

دستور زیر را در کنسول اجرا کنید:

~$ const HDWalletProvider = require('@truffle/hdwallet-provider');

(HDWalletProvider در نتیجه اجرا undefined را بر خواهد گرداند)

در مرحله بعد mnemonic خود را تعریف کنید:(mnemonic در اینجا یک رشته تصادفی شامل 12 کلمه است)

~$ const mnemonic = '12 words here';

(mnemonic در نتیجه اجرا undefined را بر خواهد گرداند)

در این مرحله wallet خود را اضافه خواهیم کرد:

~$ const wallet = new HDWalletProvider(mnemonic, "http://localhost:8545");

در نتیجه، با اجرای دستور wallet لیست حساب ها/آدرس هایی که می توانیم از آنها استفاده کنیم را مشاهده می کنیم:

~$ wallet

در نتیجه اجرای این دستور می توانید ببینید که یک wallet ساخته اید که می توانید از آن در ترمینال خود استفاده کنید. شما باید چیزی شبیه تصویر زیر در ترمینال خود مشاهده کنید:

بیایید به نصب ارتباط مورد نظرمان به یک testnet (که در اینجا Rinkedby خواهد بود) ادامه دهیم. ما برای دیپلوی کردن قرارداد هایمان بر روی testnet از Infura استفاده خواهیم کرد تا بتوانیم واسط کاربری برنامه خود را بسازیم ودر آن با قرارداد هوشمند Storage تعامل داشته باشیم.

نکته مهم: ما در اینجا از اولین آدرس از آرایه آدرس ها که wallet در اختیار ما قرار می دهد برای دیپلوی قرارداد هوشمند خود بر روی tetnet استفاده خواهیم کرد. باید اطمینان حاصل کنیم که این آدرس دارای موجودی است. Eth های تستی که در بخش بعد از Rinkedby دریافت می کنیم برای دیپلوی قرارداد استفاده خواهند شد.

در تصویر زیر یک مثال از محل دسترسی به آدرس ذکر شده در ترمینال را مشاهده می کنید. دقت کنید که آدرسی که در اختیار شما قرار می گیرد با تصویر زیر متفاوت خواهد بود.

4 پسندیده

بخش دوم: Rinkedby

در این بخش اطمینان حاصل می کنیم که می توانیم تراکنش های خود را به Rinkedby ارسال کنیم، که مستلزم یک کیف پول حاوی Eth های Rinkedby خواهد بود. پیش از این مرحله ما توانستیم کیف پول خود را داشته باشیم. در اینجا از یک faucet معروف که در لینک زیر مشاهده می کنید اساتفاده خواهیم کرد:

https://faucet.rinkeby.io/

توسعه دهنده ها می توانند با ارسال Eth تست از طریق faucet ها به آدرس های مشخص، از موجودی این آدرس ها در فرآیند تست قراردادهای خود استفاده کنند.

با دریافت Rinkedby Eth تست طبق مراحل موجود در در لینک بالا، آدرس مورد نظر ما باید موجودی جدید را نشان دهد و ما می توانیم به مرحله بعد که انجام تنظیمات Infura است برویم.

شما می توانید موجودی آدرس خود را همچنین با رفتن به آدرس https://rinkeby.etherscan.io/ مشاهده کنید.

بخش سوم: Infura

برای استفاده از Infura قدم اول ثبت نام برای یک حساب کاربری است:

شما می توانید تنها با یک نام کاربری و یک پسورد کار خود را شروع کنید!

بعد از ثبت نام شما در داشبورد خود یک پروژه خواهید ساخت، که چیزی شبیه تصویر زیر خواهد بود:

PROJECT ID همان چیزی است که ما برای تأیید اعتبار درخواستهای API خود به Infura از آن استفاده خواهیم کرد. اطمینان حاصل کنید که گزینه Rinkeby را در Endpoints انتخاب کرده اید، زیرا در این آموزش ما از این شبکه استفاده خواهیم کرد.

مطمئن شوید که دیگر در کنسول truffle نیستید!

در مسیر پروژه دستور زیر را اجرا کنید:


~$ npm install --save dotenv

اجرای این دستور این امکان را به ما می دهد تا بدون اینکه مستقیماً شناسه پروژه infura یا mnemonic خود را اضافه کنیم ، به اطلاعات حساس خود در فایل truffle-config خود دسترسی پیدا کنیم.

پس از نصب ، می خواهیم یک فایل .env ایجاد کنیم که حاوی اطلاعات حساس ما باشد. یادآوری: این اطلاعات نباید بصورت پابلیک قابل مشاهده باشد. این فایل حاوی Infura PROJECT ID و رشته mnemonic ما خواهد بود. برای درک عمیق تر اهمیت حفظ اسرار ، به این مقاله نگاهی بیندازید!

فایل .env باید چیزی شبیه تصویر زیر باشد(البته با اطلاعات شما):


INFURA_PROJECT_ID=INSERT YOUR PROJECT ID HERE (no quotations)

MNEMONIC="your mnemonic here"

بعد از این مرحله ما یک بار دیگر به سراغ فایل truffle-config می رویم و همه connection ها را برای Infura تنظیم می کنیم. این کار به ما این امکان را می دهد که برنامه خود را به زیرساخت Rinkeby Infura هدایت کنیم.

اکنون دسترسی به HDWalet Provider خود را که قبلاً نصب کرده ایم را با کد:


require(“dotenv”).config();

اضافه می کنیم تا بتوانیم به اطلاعات حساس از فایل .env دسترسی داشته باشیم. این اطلاعات جدید را در ابتدای فایل truffle-config قرار می دهیم.

توجه: اگر از Infura استفاده می کنید پارامتر from اجباری خواهد بود، زیرا Infura برای حفاظت از امنیت کاربران ما، به ما اجازه ساخت کیف پول از طریق API های خودمان را نمی دهد. اگر از Ganache یا Geth PRC استفاده می کنید، این پارامتر اختیاری خواهد بود.

کد ما باید در این مرحله به صورت زیر باشد:


require("dotenv").config();

const HDWalletProvider = require("@truffle/hdwallet-provider");

module.exports = {

networks: {

development: {

host: "127.0.0.1", // Localhost (default: none)

port: 8545, // Standard Ethereum port (default: none)

network_id: "*", // Any network (default: none)

},

rinkeby: {

provider: () =>

new HDWalletProvider(

process.env.MNEMONIC,

`https://rinkeby.infura.io/v3/${process.env.INFURA_PROJECT_ID}`

),

network_id: 4, // Rinkeby's id

gas: 5500000, // Rinkeby has a lower block limit than mainnet

confirmations: 2, // # of confs to wait between deployments. (default: 0)

timeoutBlocks: 200, // # of blocks before a deployment times out (minimum/default: 50)

skipDryRun: true, // Skip dry run before migrations? (default: false for public nets )

},

},

};

با نگاهی به قطعه کد بالا مشاهده می کنید که ما Rinkedby را به عنوان نتورکی که به آن متصل خواهیم شد تعریف کرده و HDWallet، mnemonic، و Infura PROJECT ID خود را به آن تخصیص می دهیم.

اکنون که تنظیمات ما به منظور دیپلوی قرارداد به Rinkeby با استفاده از Infura کامل شد ، بیایید ادامه دهیم و قرارداد خود را بنویسیم!

2 پسندیده

بخش چهارم: قرارداد

بیایید یک قرارداد امتحان شده و واقعی به پروژه خود اضافه کنیم.

این قرارداد به کاربر اجازه می دهد تا یک UINT (عدد صحیح مثبت بدون علامت) را از طریق تابع set () ذخیره کند. سپس امکان بازگرداندن / مشاهده UINT را دارید که با فراخوانی تابع get () انجام می شود.

ما یک فایل .sol جدید در فولدر قرارداد خود ایجاد می کنیم و قرارداد زیر را به نام SimpleStorage.sol به آن اضافه می کنیم:


pragma solidity >=0.5.8 <0.7.0;

contract SimpleStorage {

uint256 storedData;

function set(uint x) public {

storedData = x;

}

function get() public view returns (uint) {

return storedData;

}

}

همچنین برای آگاهی از اضافه شدن یک قرارداد ، migrations را به روز می کنیم.

اگر با این موضوع آشنایی ندارید، migration ها فایل های جاوا اسکریپتی هستند که به شما امکان دیپلوی قراردادهایتان به شبکه اتریوم را می دهند. از قراردادهای کامپایل شده با عنوان artifact یاد می شود. زمانی که ما پروژه خود را migrate میکنیم، اسکریپت های migration نیاز به دسترسی به artifact ها خواهد داشت.

در اینجا ما یک فایل جدید حاوی کد زیر و با نام deployed_contracts_2.js در فولدر Migration ایجاد میکنیم:


const SimpleStorage = artifacts.require("SimpleStorage.sol");

module.exports = function(deployer) {

deployer.deploy(SimpleStorage);

};

حالا ما به اسکریپت های migration دسترسی به قراردادهای کامپایل شده را داده ایم.

بخش پنجم: دیپلوی

در یک ترمینال دستور زیر را اجرا کنید:


~$ truffle compile

توجه: نیم نگاهی به تمام این فرآیند داشته باشید! در این مرحله باید در ساختار فایل پروژه خود فولدر جدیدی به نام build را ببینید که حاوی دو فایل JSON است.

(اینها در وقع artifact های ما هستند که در زمان ساخت واسط کاربری به آنها مراجعه خواهیم کرد.)

بعد از کامپایل موفقیت آمیز دستور زیر را اجرا کنید:


~$ truffle migrate --network rinkeby

ما توانستیم که قرارداد های خود را با استفاده از Infura روی Renkedby testnet با موفقیت دیپلوی کنیم!

شما می توانید آدرس قرارداد را از اینجا بردارید و در rinkeby.etherscan.io جزییات آن را مشاهده کنید:

توجه: اگر به مشکلی در ارتباط با عدم موجودی کافی مواجه شدید، از ارسال مقدار کافی Rinkedby Eth به حسابی که تصمیم دارید از طریق آن قرارداد خود را دیپلوی کنید، اطمینان حاصل کنید. برای توضیحات بیشتر به بخش دوم: Rinkedby مراجعه کنید.

2 پسندیده

ساخت اپلیکیشن: قسمت دوم

بخش اول: React

در این مرحله می توانیم همه کارهای انجام شده در قسمت های قبلی را در یک فولدر مجزا قرار دهیم تا مدیریت بخش های مختلف پروژه آسان تر باشد. به این ترتیب همه ی فولدر های build / contracts / migrations / test / env / truffle-config در یک فولدر قرار میگیرند.

حالا که قرارداد ما دیپلوی شده است، می توانیم به سراغ واسط کاربری برویم. در اینجا ما از npx برای ایجاد boilerplate react application استفاده می کنیم.
نکته جالب! npm ابزاری است که برای نصب پکیج ها استفاده می شود. npx ابزاری است که برای اجرای پکیج ها استفاده می شود.

~$ npx create-react-app infura_experiments

بیایید کارهایی را که در مراحل قبل انجام دادیم را (contracts + migrations + truffle-config file)، به مسیر پروژه react اضافه کنیم. در همان سطح فولدر Source که از این به بعد آن را به عنوان src می شناسیم. به این ترتیب همه بخش های اصلی پروژه را می توانیم در کنار هم داشته باشیم.

بخش دوم: Web3

Web3.js مجموعه ای از کتابخانه هاست که به شما امکان تعامل با یک نود لوکال یا ریموت در شبکه اتریوم با استفاده از HTTP، IPC یا WebSocket را می دهد.

برای نصب Web3.js دستور زیر را اجرا کنید:

~$ npm install web3

فایل app.js در فولدر src،جایی است که باید Web3 را import کنید:

import Web3 from "web3";

توجه: کلاس Web3 یک پکیج برای تمامی ماژول های مربوط به اتریوم است. جزییات بیشتر در این مورد را می توانید در لینک زیر ببینید:
https://web3js.readthedocs.io/en/v1.3.0/

برای شروع باید از داشتن یک Provider اطمینان حاصل کنیم! کتابخانه web3.js به یک شی Provider نیاز دارد که شامل پروتکل اتصال به نودی است که می خواهید به آن متصل شوید.

import React, { useState } from "react";

import Web3 from "web3";

import "./App.css";

const web3 = new Web3(Web3.givenProvider);

در فولدر src یک فولدر جدید به نام ABI بسازید و یک فایل abi.js در آن قرار دهید. حالا به فایل SimpleStorage.json در فولدر build بروید و اطلاعات ABI را به این فایل بیاورید.

فایل SimpleStorage.JSON باید چیزی شبیه کد زیر باشد:

فایل abi/abi.js باید چیزی شبیه کد زیر در فرمت JSON باشد:

و کد فایل:

export const SimpleStorage = [

{

constant: false,

inputs: [

{

name: "x",

type: "uint256",

},

],

name: "set",

outputs: [],

payable: false,

stateMutability: "nonpayable",

type: "function",

},

{

constant: true,

inputs: [],

name: "get",

outputs: [

{

name: "",

type: "uint256",

},

],

payable: false,

stateMutability: "view",

type: "function",

},

];

در این مرحله ما به آدرسی که قرارداد در آن دیپلوی شده است احتیاج داریم.

ممکن است اینجا کمی گیج شده باشید که از کجا می توانید این آدرس را بدست بیاورید. برای این کار به فایل SimpleStorage.json در فولدر build بروید و کلمه address را جستجو کنید:

یک متغیر برای ذخیره آدرس قرارداد تعریف کنید:

const contractAddress = "your contract address here";

کد های زیر را در ادامه وارد کنید:

const storageContract = new web3.eth.Contract(SimpleStorage, contractAddress);

متغیر جدیدی که به نام storageContract تعریف کردیم حاوی آدرس قرارداد (برای آنکه بداند قرارداد در چه آدرسی قرار دارد) و ABI (برای اینکه بداند چگونه باید با قرارداد تعامل داشته باشد) خواهد بود.

برای ایجاد بستر تعامل با قرارداد لازم است SimpleStorage که حاوی اطلاعات ABI است را import کنیم/

کد ما در این مرحله باید به صورت زیر باشد:

import React, { useState } from "react";

import "./App.css";

import { SimpleStorage } from "./abi/abi";

import Web3 from "web3";

const web3 = new Web3(Web3.givenProvider);

const contractAddress = "your contract address here";

const storageContract = new web3.eth.Contract(SimpleStorage, contractAddress);

بخش سوم: منطق

بعد از این می توانیم با استفاده از Web3 چند فانکشنالیتی به اپلیکیشن خود اضافه کنیم. با این کار به کاربر اجازه می دهیم که فانکشن های موجود در قرارداد ما شامل Set و Get را فراخوانی کند و در هنگام تغییر وضعیت (که در اینجا یک UINT است)، کیف پول MetaMask را صدا بزند، همان طور که می تواند تغییرات اعمال شده را بازیابی کند.

در اینجا ما از hooks برای نگهداری متغیر هایی که با قرارداد ما و همچنین واسط کاربری تعامل دارند استفاده می کنیم. برای این کار در تابع app متغیر های زیر را تعریف می کنیم.

const [number, setUint] = useState(0);

const [getNumber, setGet] = useState("your uint is...");

برای آشنایی بیشتر با hook ها می توانید به لینک زیر مراجعه کنید:

نکته بعدی که باید مد نظر داشته باشیم این است که dApp ما مجوز دسترسی به موجودی کاربر برای پرداخت بهای gas مصرفی برای ارسال تراکنش ها را داشته باشد. تراکنش ها نیاز دارند که توابع موجود در قرارداد ما را فراخوانی کنند.

اینجاست که ارتباط ما با MetaMask برای امضای تراکنش ها که منجر به تغییر مقدار UINT موجود در قرارداد ما می شوند مفید واقع می شود و بهای gas مورد نیاز برای مانینگ تراکنش را می پردازد (نگران نباشید! این اتفاقات همچنان بر روی testnet می افتند).

کد های زیر به ترتیب:

const accounts = await window.ethereum.enable();

درخواست اجازه دسترسی به کیف پول کاربر را می دهد،

const account = accounts[0];

آدرس کاربر فعلی را می گیرد.

سپس با پارامترهای ورودی توابع را از طریق methods.set() و با استفاده از آدرس تعریف شده (آدرس کاربر)، تابع set را فراخوانی می کنیم:

const gas = await storageContract.methods.set(number).estimateGas();

const post = await storageContract.methods.set(number).send({

from: account,

gas,

});

در اینجا ما به یک number ارجاع می دهیم:

const [number, setUint] = useState(0);

در numberGet این موضوع خیلی سرراست تر است:

const post = await storageContract.methods.get().call();

setGet(post);

};

ما UINT را از numberSet بازیابی و ذخیره می کنیم.

تا اینجا کد dApp ما باید به این صورت درآمده باشد:

function App() {

const [number, setUint] = useState(0);

const [getNumber, setGet] = useState("your uint is...");

const numberSet = async (t) => {

t.preventDefault();

const accounts = await window.ethereum.enable();

const account = accounts[0];

const gas = await storageContract.methods.set(number).estimateGas();

const post = await storageContract.methods.set(number).send({

from: account,

gas,

});

};

const numberGet = async (t) => {

t.preventDefault();

const post = await storageContract.methods.get().call();

setGet(post);

};

آبجکت web3.eth.contract تعامل با قراردادهای هوشمند روی اتریوم را آسان می کند. زمانیکه شما یک آبجکت قرارداد ایجاد می کنید، به آن JSON interface مربوط به قرارداد دیپلوی شده خود را می دهید و Web3 به طور اتوماتیک تمامی فراخوانی ها را برای شما تبدیل فبه راخوانی های ABI سطح پایین بر روی RPC میکند.
در واقع شما با استفاده از این امکانات می توانید با قراردادهای هوشمند خود به صورت آبجکت های جاوا اسکریپتی رفتار کنید.
به یاد داشته باشید ، هر زمان که ما به storageContract مراجعه می کنیم ، آن را مبنای استفاده از web3.eth قرار می دهیم. به این ترتیب ما می توانیم با قرارداد ها تعامل داشته باشیم و همان طور که در کدهای بالا میبینید، با POST مقدار دهی انجام دهیم و در numberGet با GET مقدار را بخوانیم.این توابع مربوط به قرارداد که ما آنها را در اینجا فراخوانی می کنیم را خودتان می توانید در ABI ببینید.
const post در واقع یک uint را دریافت می کند و تراکنش را از طریق کیف پول شما در MetaMask روی شبکه Rinkrdby تایید می کند(post بهای gas را پرداخت می کند).
ما تراکنش های مربوط به قرارداد هوشمند خود را با ارسال پارامترهای ورودی به تابع به methods.set() و مقدار تخمینی gas و آدرس حساب کاربر را به send()، ایجاد می کنیم.
بر اساس منطق قرارداد، بیایید یک تابع return ابتدایی ایجاد کنیم.

return (
   <div className="cargo">
     <div className="case">
       <form className="form" onSubmit={numberSet}>
         <label>
           Set your uint:
           <input
             className="input"
             type="text"
             name="name"
             onChange={(t) => setUint(t.target.value)}
           />
         </label>
         <button className="button" type="submit" value="Confirm">
           Confirm
         </button>
       </form>
       <br />
       <button className="button" onClick={numberGet} type="button">
         Retrieve
       </button>
       <div className="result">{getNumber}</div>
     </div>
   </div>
 );
}
}
export default App;

بخش چهارم: CSS

زمان آن رسیده که کمی به شکل ظاهری پروژه توجه کنیم. این مرحله کاملاً اختیاری است اما باعث می شود پروژه کمتر ابتدایی به نظر برسد. در صورت تمایل می توانید هر گونه تغییراتی در این مرحله ایجاد کنید!

.cargo {
 text-align: center;
 display: flex;
 justify-content: center;
 background-color: #f2f1f5;
 height: 100vh;
}
.pack {
 min-height: 50vh;
 width: 50vw;
 display: flex;
 flex-direction: column;
 align-items: center;
 justify-content: center;
}
.form {
 height: 20vh;
 width: 20vw;
 display: flex;
 justify-content: space-evenly;
 flex-direction: column;
}
.button {
 width: 20vw;
 height: 5vh;
}
.result {
 padding-top: 20px;
}

حالا زمان اجرای پروژه و تعامل با آن از طریق Web3 و Infura فرا رسیده است!

~$ yarn start

بخش پنجم: کد نهایی Web3

import React, { useState } from "react";
import "./App.css";
import { SimpleStorage } from "./abi/abi";
import Web3 from "web3";
 
const web3 = new Web3(Web3.givenProvider);
const contractAddress = "your contract address";
const storageContract = new web3.eth.Contract(SimpleStorage, contractAddress);
 
function App() {
 const [number, setUint] = useState(0);
 const [getNumber, setGet] = useState("your uint is...");
 
 const numberSet = async (t) => {
   t.preventDefault();
   const accounts = await window.ethereum.enable();
   const account = accounts[0];
   const gas = await storageContract.methods.set(number).estimateGas();
   const post = await storageContract.methods.set(number).send({
     from: account,
     gas,
   });
 };
 
 const numberGet = async (t) => {
   t.preventDefault();
   const post = await storageContract.methods.get().call();
   setGet(post);
 };
 return (
   <div className="cargo">
     <div className="case">
       <form className="form" onSubmit={numberSet}>
         <label>
           Set your uint:
           <input
             className="input"
             type="text"
             name="name"
             onChange={(t) => setUint(t.target.value)}
           />
         </label>
         <button className="button" type="submit" value="Confirm">
           Confirm
         </button>
       </form>
       <br />
       <button className="button" onClick={numberGet} type="button">
         Retrieve
       </button>
       <div className="result">{getNumber}</div>
     </div>
   </div>
 );
}
 
export default App;

بخش ششم: Ethers

اکنون که یک dApp ساده با استفاده از truffle ، Infura و Web3 ساخته ایم ، بیایید ادامه دهیم و همان پروژه را این بار ا استفاده از Ethers.js انجام دهیم!
چند نکته که چرا ممکن است بخواهید از Ethers استفاده کنید:

  • ارتباط با نود های اتریوم از طریق JSON-RPC، INFURA، MetaMask، و …
  • بسیار کم حجم(88 کیلو بایت در حالت فشرده، 284 کیلو بایت در حالت عادی)
  • مستندات بسیار زیاد(https://docs.ethers.io/)
  • مجموعه بزرگی از تست کیس ها که در حال خاضر از آنها نگهداری می شود و موارد جدید به آنها اضافه می شود
  • کاملا آماده برای استفاده با TypeScript، همراه با فایل های تعریف و منابع کامل TypeScript
  • اتصال به یک نود مانند Infura از طریق آن بسیار ساده است
  • Ethers داری متدی است که به شما اجازه می دهد یک provider ایجاد کنید، که بصورت اتوماتیک با یک ارتباط پیش فرض به آن provider متصل می شود(با استفاده از Infura)
  • MIT Licennse (شامل تمامی وابستگی ها)
  • کاملا اوپن سورس

برای استفاده از ethers بیایید در یک ترمینال دستور زیر را اجرا کنیم:

~$ npm install --save ethers

مطمئن شوید که کتابخانه ethers را بعد از نصب import کنید:

import React, { useState } from "react";
import { simpleStorage } from "./abi/abi";
import { ethers } from 'ethers';
import "./App.css";

بعد از آن مطمئن شوید که می توانید قراردادتان را فراخوانی کنید:

const storageContract = new ethers.Contract(contractAddress, abi, signer);

ما در این مرحلهبرای امضای تراکنش هایمان(هر آنچه که read-only نیست) احتیاج به یک signer داریم.
نکته: اگر تصمیم ندارید با استفاده از provider پیش فرض متصل شوید، می توانید به عنوان مثال به Infura وصل شوید. جزییات بیشتر در لینک زیر می توانید ببینید:
https://docs.ethers.io/ethers.js/v5-beta/api-providers.html
برای مثال:

let provider = new ethers.providers.Web3Provider(web3.currentProvider);
let infuraProvider = new ethers.providers.InfuraProvider('rinkeby');

در مرحله بعد signer ،contractAddress، و storageContract را اضافه می کنیم.

نکته 1: اگر نگاهی به بخش دوم بیاندازید متوجه تفاوت های کوچکی در تعاریف ما برای ethers می شوید.

نکته 2: در پایان این بخش یک مقایسه کوتااه خواهیم داشت که اختلافات Web3 و ethers را نشان می دهد.

import React, { useState } from "react";
import "./App.css";
import { SimpleStorage } from "./abi/abi";

import { ethers } from "ethers";

let abi = SimpleStorage;
const provider = new ethers.providers.Web3Provider(window.ethereum);

const signer = provider.getSigner();

const contractAddress = "0x62Db0a2161e304819f4d54d54B90A3Feae6dDc72";

const storageContract = new ethers.Contract(contractAddress, abi, signer);

از آنجا که singer برای ما یک مفهوم جدید است می توانیم آن را به عنوان انتزاعی از حساب اتریوم در نظر بگیریم که می تواند برای امضای پیام ها و تراکنش ها و ارسال تراکنش های امضا شده به شبکه اتریوم و اجرای عملیات تغییر وضعیت استفاده شود.

بخش هفتم: منطق

ما همچنان از hooks استفاده می کنیم بنابراین تنها تفاوت مهم را در numberGet و numberSet خواهیم دید. ما طبق روال قبل account ای که برای اتصال به dApp استفاده شده را گرفته و حالا تغییراتی روی gas و POST اعمال می کنیم.
Web3 به ما اجازه تعریف بعضی متغیر ها با استفاده از متد هایی مانند:

const gas = await storageContract.methods.set(number).estimateGas();

را میداد.
با این وجود ethes به نظر چیزی شبیه کد زیر است:

const gas = await storageContract.estimateGas.set(number);

کد ما تا این مرحله:

function App() {
 const [number, setUint] = useState(0);
 const [getNumber, setGet] = useState("your uint is...");
 
 
 const numberSet = async (t) => {
   t.preventDefault();
 
   const accounts = await window.ethereum.enable();
   const account = accounts[0];
   const gas = await storageContract.estimateGas.set(number);
   const post = await storageContract.set(number);
 };
 
 const numberGet = async (t) => {
   t.preventDefault();
   const post = await storageContract.get();
   setGet(parseInt(post));
 };

بزرگ ترین تفاوت در اینجا این است که مقدار بازگشتی ethres یک big number خواهد بود، بنابراین ما نمی توانیم مانند Web3 به setGet(post) اعتماد کنیم. چند راه حل برای این مساله (بسته به اینکه شما بخواهید که متد post شما با یک BigNumber کار کند) وجود دارد اما در مثال زیر ما از parseInt استفاده خواهیم کرد:

const numberGet = async (t) => {
   t.preventDefault();
   const post = await storageContract.get();
   setGet(parseInt(post));
 };

بخش هشتم: کد نهایی ethers

تابع return ما در هر دو پروژه یکسان خواهد بود.
کد کامل App.js:

import React, { useState } from "react";
import "./App.css";
import { SimpleStorage } from "./abi/abi";
 
import { ethers } from "ethers";
 
let abi = SimpleStorage;
const provider = new ethers.providers.Web3Provider(window.ethereum);
 
const signer = provider.getSigner();
 
const contractAddress = "your contract address";
 
const storageContract = new ethers.Contract(contractAddress, abi, signer);
 
function App() {
 const [number, setUint] = useState(0);
 const [getNumber, setGet] = useState("your uint is...");
 
 
 const numberSet = async (t) => {
   t.preventDefault();
 
   const accounts = await window.ethereum.enable();
   const account = accounts[0];
   const gas = await storageContract.estimateGas.set(number);
   const post = await storageContract.set(number);
 };
 
 const numberGet = async (t) => {
   t.preventDefault();
   const post = await storageContract.get();
   setGet(parseInt(post));
 };
 return (
   <div className="cargo">
     <div className="case">
       <form className="form" onSubmit={numberSet}>
         <label>
           Set your uint:
           <input
             className="input"
             type="text"
             name="name"
             onChange={(t) => setUint(t.target.value)}
           />
         </label>
         <button className="button" type="submit" value="Confirm">
           Confirm
         </button>
       </form>
       <br />
       <button className="button" onClick={numberGet} type="button">
         Retrieve
       </button>
       <div className="result">{getNumber}</div>
     </div>
   </div>
 );
}
 
export default App;

مقایسه اجمالی

اتصال به اتریوم:

// web3
var Web3 = require('web3');
var web3 = new Web3('http://localhost:8545');
 
// ethers
var ethers = require('ethers');
const url = "http://127.0.0.1:8545";
const provider = new ethers.providers.JsonRpcProvider(url);

اتصال به MetaMask

// web3
const web3 = new Web3(Web3.givenProvider);
 
// ethers
const provider = new ethers.providers.Web3Provider(window.ethereum);

ایجاد signer:

در ethers یک signer در واقع انتزاعی از یک حساب اتریوم است و می تواند برای امضای پیام ها و تراکنش ها و ارسال تراکنش های امضا شده به شبکه اتریوم استفاده شود.
در Web3، یک account می تواند برای امضای پیام ها و تراکنش ها استفاده شود:

// web3
const account = web3.eth.accounts.create();
 
// ethers (create random new account)
const signer = ethers.Wallet.createRandom();
 
// ethers (connect to JSON-RPC accounts)
const signer = provider.getSigner();
 
Signing a message
// web3 (using a private key)
signature = web3.eth.accounts.sign('Some data', privateKey)
 
// web3 (using a JSON-RPC account)
// @TODO
 
// ethers
signature = await signer.signMessage('Some data')
دیپلوی قرارداد ها
// web3
const contract = new web3.eth.Contract(abi);
contract.deploy({
   data: bytecode,
   arguments: ["my string"]
})
.send({
   from: "0x12598d2Fd88B420ED571beFDA8dD112624B5E730",
   gas: 150000,
   gasPrice: "30000000000000"
}), function(error, transactionHash){ ... })
.then(function(newContract){
    console.log('new contract', newContract.options.address) 
});
 
// ethers
const signer = provider.getSigner();
const factory = new ethers.ContractFactory(abi, bytecode, signer);
const contract = await factory.deploy("hello world");
console.log('contract address', contract.address);
 
// wait for contract creation transaction to be mined
await contract.deployTransaction.wait();

تعامل با قرارداد ها

// web3
const contract = new web3.eth.Contract(abi, contractAddress);
// read only query
contract.methods.getValue().call();
// state changing operation
contract.methods.changeValue(42).send({from: ....})
.on('receipt', function(){
    ...
});
 
// ethers
// pass a provider when initiating a contract for read only queries
const contract = new ethers.Contract(contractAddress, abi, provider);
const value = await contract.getValue();
 
 
// pass a signer to create a contract instance for state changing operations
const contract = new ethers.Contract(contractAddress, abi, signer);
const tx = await contract.changeValue(33);
 
// wait for the transaction to be mined
const receipt = await tx.wait();

توابع overload شده:

شما می توانید چندین تعریف برای یک فانکشن در یک محدوده داشته باشید. این تعریف ها باید با هم در نوع و/یا تعداد پارامترهای ورودی متفاوت باشند. این ها overload های یک فانکشن هستند: فانکشن هایی با نام یکسان و پارامترهای متفاوت.
در ethers سینتکس های فراخوانی یک فانکشن overload شده و یک فانکشن overload نشده یک قرارداد متفاوت هستند که می توانید در کد زیر مشاهده کنید:

// web3
message = await contract.methods.getMessage('nice').call();
 
 
// ethers
const abi = [
  "function getMessage(string) public view returns (string)",
  "function getMessage() public view returns (string)"
]
const contract = new ethers.Contract(address, abi, signer);
 
// for ambiguous functions (two functions with the same
// name), the signature must also be specified
message = await contract['getMessage(string)']('nice');

BigNumber

یک BigNumber آبجکتی است که امکان عملیات ریاضی امن روی اعدادی با اندازه بزرگ را فراهم می آورد.
تبدیل به BigNumber:

// web3
web3.utils.toBN('123456');
 
// ethers (from a number; must be within safe range)
ethers.BigNumber.from(123456)
 
// ethers (from base-10 string)
ethers.BigNumber.from("123456")
 
// ethers (from hex string)
ethers.BigNumber.from("0x1e240")

Hash

محاسبه Keccak256 hash یک رشته UTF-8 در Web3 و ethers:

//web3
web3.utils.sha3('hello world');
web3.utils.keccak256('hello world');
 
// ethers (hash of a string)
ethers.utils.id('hello world')
 
// ethers (hash of binary data)
ethers.utils.keccak256('0x4242')

شما موفق شدید! هدف ما نشان دادن چگونگی استفاده از Web3.js و ethers برای خواندن و نوشتن داده در یک testnet با استفاده از Infura بود. ما مسائل زیادی را در این آموزش پوشش دادیم:

  • ما از truffle و Ganache برای شروع پروژه استفاده کردیم
  • با موفقیت قرارداد های خود را کامپایل و migrate کردیم
  • از Infura برای اتصال به testnet (در اینجا Rinkedby) استفاده کردیم
  • منطق backend خود را به React وصل کردیم
  • تفاوت های استفاده از web3.js و ethers.js را بررسی کردیم

امیدواریم که شما درک بهتری از تفاوت های موجود بین Web3.js و ethers.js پیدا کرده باشید. مانند همیشه، ما شما را تشویق می کنیم که در حین ساخت اپلیکیشن های غیر متمرکز شگفت انگیزتان به بررسی همه گزینه هایی که دارید ادامه دهید.
منبع:

5 پسندیده