Web3 DApp 開発の技術スタックを理解することで、Web3 dApp 開発のコア技術スタック、dApp 開発における RPC の役割、dRPC を使用してアカウントを作成し、API キー、エンドポイント、エンドポイント分析を生成し、dRPC アカウントに資金を追加し、残高を確認する方法を学習しているはずです。
スマート コントラクトの展開における dRPC の役割は、Ethereum ノードのセットアップ プロセスを簡素化し、開発者が 1 行のコードで簡単に操作および展開できるようにすることです。
この記事では、dRPC エンドポイントと API キーを使用して、コーヒー支払いスマート コントラクトを記述、コンパイル、テストし、Ethereum Sepolia Testnet にデプロイします。
機能は次のとおりです:
さあ、手を汚してみましょう。
ルート ディレクトリの下にフォルダーを作成し、 contracts
という名前を付けます。
contracts
フォルダーの下にファイルを作成し、 coffee.sol
という名前を付けます。
スマート コントラクトを記述するには、Solidity を使用します。Solidity ファイルは、Solidity ソース コードの標準ファイル拡張子であるため、
.sol
拡張子で命名されます。
次のソース コードをcoffee.sol
に追加します。
// SPDX-License-Identifier: MIT pragma solidity >=0.8.0 <0.9.0; contract Coffee { uint256 public constant coffeePrice = 0.0002 ether; uint256 public totalCoffeesSold; uint256 public totalEtherReceived; // Custom error definitions error QuantityMustBeGreaterThanZero(); error InsufficientEtherSent(uint256 required, uint256 sent); error DirectEtherTransferNotAllowed(); // Event to log coffee purchases event CoffeePurchased(address indexed buyer, uint256 quantity, uint256 totalCost); // Function to buy coffee function buyCoffee(uint256 quantity) external payable { if (quantity <= 0) { revert QuantityMustBeGreaterThanZero(); } uint256 totalCost = coffeePrice * quantity; if (msg.value > totalCost) { revert InsufficientEtherSent(totalCost, msg.value); } // Update the total coffees sold and total ether received totalCoffeesSold += quantity; totalEtherReceived += totalCost; console.log("Total ether received updated:", totalEtherReceived); console.log("Total coffee sold updated:", totalCoffeesSold); // Emit the purchase event emit CoffeePurchased(msg.sender, quantity, totalCost); // Refund excess Ether sent if (msg.value > totalCost) { uint256 refundAmount = msg.value - totalCost; payable(msg.sender).transfer(refundAmount); } } // Fallback function to handle Ether sent directly to the contract receive() external payable { revert DirectEtherTransferNotAllowed(); } // Public view functions to get totals function getTotalCoffeesSold() external view returns (uint256) { console.log("getTotalCoffeesSold :", totalCoffeesSold); return totalCoffeesSold; } function getTotalEtherReceived() external view returns (uint256) { console.log("getTotalEtherReceived :", totalEtherReceived); return totalEtherReceived; } }
//SPDX-License-Identifier: MIT
: このライセンス識別子は、コードがマサチューセッツ工科大学 (MIT)ライセンスに基づいてライセンスされていることを示します。
pragma solidity >=0.8.0 <0.9.0;
: コードが 0.8.0 (含む) から 0.9.0 (含まない) までの Solidity バージョン用に記述されていることを指定します。 uint256 public constant coffeePrice = 0.0002 ether; uint256 public totalCoffeesSold; uint256 public totalEtherReceived;
coffeePrice
: 0.0002 ether
の定数値として設定します。totalCoffeesSold
: 販売されたコーヒーの数を追跡します。totalEtherReceived
: 契約によって受信された Ether の合計を追跡します。Solidity のカスタム エラーは、プログラミング言語によって提供されるデフォルトのエラー メッセージではなく、特定のユース ケースに合わせて調整されたエラー メッセージです。ユーザー エクスペリエンスの向上に役立つだけでなく、スマート コントラクトのデバッグや保守にも役立ちます。
Solidity でカスタム エラーを定義するには、次の構文を使用できます。
error
: このキーワードはカスタムエラーを定義するために使用されます error QuantityMustBeGreaterThanZero(); error InsufficientEtherSent(uint256 required, uint256 sent); error DirectEtherTransferNotAllowed();
QuantityMustBeGreaterThanZero()
: 数量がゼロより大きいことを確認します。InsufficientEtherSent(uint256 required, uint256 sent)
: 送信された Ether が十分であることを確認します。DirectEtherTransferNotAllowed()
: 契約への直接 Ether 転送を防止します。イベントは、発行時にトランザクション ログに渡された引数を保存するコントラクトの一部です。イベントは通常、EVM のログ機能を使用して、呼び出し元のアプリケーションにコントラクトの現在の状態を通知するために使用されます。イベントは、コントラクトに加えられた変更をアプリケーションに通知し、関連するロジックを実行するために使用できます。
event CoffeePurchased(address indexed buyer, uint256 quantity, uint256 totalCost);
CoffeePurchased(address indexed buyer, uint256 quantity, uint256 totalCost)
: コーヒーの購入を記録します。関数は、特定のタスクを実行する自己完結型のコード モジュールです。同じコードを書き直すという冗長性が排除されます。代わりに、開発者は必要に応じてプログラム内の関数を呼び出すことができます。
function buyCoffee(uint256 quantity) external payable { if (quantity <= 0) { revert QuantityMustBeGreaterThanZero(); } uint256 totalCost = coffeePrice * quantity; if (msg.value > totalCost) { revert InsufficientEtherSent(totalCost, msg.value); } // Update the total coffees sold and total ether received totalCoffeesSold += quantity; totalEtherReceived += totalCost; console.log("Total ether received updated:", totalEtherReceived); console.log("Total coffee sold updated:", totalCoffeesSold); // Emit the purchase event emit CoffeePurchased(msg.sender, quantity, totalCost); // Refund excess Ether sent if (msg.value > totalCost) { uint256 refundAmount = msg.value - totalCost; payable(msg.sender).transfer(refundAmount); } } receive() external payable { revert DirectEtherTransferNotAllowed(); } function getTotalCoffeesSold() external view returns (uint256) { console.log("getTotalCoffeesSold :", totalCoffeesSold); return totalCoffeesSold; } function getTotalEtherReceived() external view returns (uint256) { console.log("getTotalEtherReceived :", totalEtherReceived); return totalEtherReceived; }
buyCoffee(uint256 quantity) external payable
: コーヒーの購入を処理し、次の操作を実行します。receive() external payable
: 誰かが契約アドレスに直接資金を送金した場合に、直接の Ether 転送を元に戻します。getTotalCoffeesSold() external view returns (uint256)
: 販売されたコーヒーの合計数を返します。getTotalEtherReceived() external view returns (uint256)
: 受信した Ether の合計を返します。ここでは、Hardhat を使用してスマート コントラクトをコンパイルします。
次のコマンド プロンプトを使用して Hardhat をインストールします。
npm install --save-dev hardhat
インストールが成功すると、以下の応答が返されます。
このコマンド プロンプトを使用してハードハットを初期化するのと同じディレクトリで、次の操作を実行します。
npx hardhat init
下矢印ボタンを使用して「 Create a Javascript project
を選択し、Enter キーを押します。
ルートフォルダにインストールするにはEnterキーを押してください
キーボードのy
を使用して、 @nomicfoundation/hardhat-toolbox
依存関係を含むすべてのプロンプトを受け入れます。
下記の応答は初期化に成功したことを示しています
いくつかの新しいフォルダーとファイルがプロジェクトに追加されたことがわかります。たとえば、
Lock.sol
、iginition/modules
、test/Lock.js
、hardhat.config.cjs
などです。これらについては心配しないでください。
唯一役に立つのは、
iginition/modules
とhardhat.config.cjs
です。これらが何に使われるかは後でわかります。contractsフォルダーの下のLock.sol
とcontracts
iginition/modules
フォルダーの下のLock.js
は削除してもかまいません。
次のコマンド プロンプトを使用して契約をコンパイルします。
npx hardhat compile
Coffee.json
ファイル内には、スマート コントラクトと対話するときに呼び出す JSON 形式の ABI コードがあります。 { "_format": "hh-sol-artifact-1", "contractName": "Coffee", "sourceName": "contracts/coffee.sol", "abi": [ { "inputs": [], "name": "DirectEtherTransferNotAllowed", "type": "error" }, { "inputs": [ { "internalType": "uint256", "name": "required", "type": "uint256" }, { "internalType": "uint256", "name": "sent", "type": "uint256" } ], "name": "InsufficientEtherSent", "type": "error" }, { "inputs": [], "name": "QuantityMustBeGreaterThanZero", "type": "error" }, { "anonymous": false, "inputs": [ { "indexed": true, "internalType": "address", "name": "buyer", "type": "address" }, { "indexed": false, "internalType": "uint256", "name": "quantity", "type": "uint256" }, { "indexed": false, "internalType": "uint256", "name": "totalCost", "type": "uint256" } ], "name": "CoffeePurchased", "type": "event" }, { "inputs": [ { "internalType": "uint256", "name": "quantity", "type": "uint256" } ], "name": "buyCoffee", "outputs": [], "stateMutability": "payable", "type": "function" }, { "inputs": [], "name": "coffeePrice", "outputs": [ { "internalType": "uint256", "name": "", "type": "uint256" } ], "stateMutability": "view", "type": "function" }, { "inputs": [], "name": "getTotalCoffeesSold", "outputs": [ { "internalType": "uint256", "name": "", "type": "uint256" } ], "stateMutability": "view", "type": "function" }, { "inputs": [], "name": "getTotalEtherReceived", "outputs": [ { "internalType": "uint256", "name": "", "type": "uint256" } ], "stateMutability": "view", "type": "function" }, { "inputs": [], "name": "totalCoffeesSold", "outputs": [ { "internalType": "uint256", "name": "", "type": "uint256" } ], "stateMutability": "view", "type": "function" }, { "inputs": [], "name": "totalEtherReceived", "outputs": [ { "internalType": "uint256", "name": "", "type": "uint256" } ], "stateMutability": "view", "type": "function" }, { "stateMutability": "payable", "type": "receive" } ], "bytecode": "", "deployedBytecode": "", "linkReferences": {}, "deployedLinkReferences": {} }
スマート コントラクトの構築中に自動テスト スクリプトを記述することは非常に重要であり、強く推奨されます。これは 2 要素認証 (2FA) のように機能し、ライブ ネットワークに展開する前にスマート コントラクトが期待どおりに動作することを確認します。
test
フォルダーの下に新しいファイルを作成し、 Coffee.
という名前を付けます。ファイル内に、以下のコードを貼り付けます。
const { loadFixture } = require("@nomicfoundation/hardhat-toolbox/network-helpers.js"); const { expect } = require("chai"); const pkg = require("hardhat"); const ABI = require('../artifacts/contracts/coffee.sol/Coffee.json'); const { web3 } = pkg; describe("Coffee Contract", function () { // Fixture to deploy the Coffee contract async function deployCoffeeFixture() { const coffeeContract = new web3.eth.Contract(ABI.abi); coffeeContract.handleRevert = true; const [deployer, buyer] = await web3.eth.getAccounts(); const rawContract = coffeeContract.deploy({ data: ABI.bytecode, }); // Estimate gas for the deployment const estimateGas = await rawContract.estimateGas({ from: deployer }); // Deploy the contract const coffee = await rawContract.send({ from: deployer, gas: estimateGas.toString(), gasPrice: "10000000000", }); console.log("Coffee contract deployed to: ", coffee.options.address); return { coffee, deployer, buyer, rawContract }; } describe("Deployment", function () { // Test to check initial values after deployment it("Should set the initial values correctly", async function () { const { coffee } = await loadFixture(deployCoffeeFixture); const totalCoffeesSold = await coffee.methods.totalCoffeesSold().call(); const totalEtherReceived = await coffee.methods.totalEtherReceived().call(); expect(totalCoffeesSold).to.equal("0"); expect(totalEtherReceived).to.equal("0"); }); }); describe("Buying Coffee", function () { // Test to check coffee purchase and event emission it("Should purchase coffee and emit an event", async function () { const { coffee, buyer } = await loadFixture(deployCoffeeFixture); const quantity = 3; const totalCost = web3.utils.toWei("0.0006", "ether"); // Buyer purchases coffee const receipt = await coffee.methods.buyCoffee(quantity).send({ from: buyer, value: totalCost }); // Check event const event = receipt.events.CoffeePurchased; expect(event).to.exist; expect(event.returnValues.buyer).to.equal(buyer); expect(event.returnValues.quantity).to.equal(String(quantity)); expect(event.returnValues.totalCost).to.equal(totalCost); }); // Test to check revert when quantity is zero it("Should revert if the quantity is zero", async function () { const { coffee, buyer } = await loadFixture(deployCoffeeFixture); expect( coffee.methods.buyCoffee(0).send({ from: buyer, value: web3.utils.toWei("0.0002", "ether") }) ).to.be.revertedWith("QuantityMustBeGreaterThanZero"); }); // Test to check if totalCoffeesSold and totalEtherReceived are updated correctly it("Should update totalCoffeesSold and totalEtherReceived correctly", async function () { const { coffee, buyer } = await loadFixture(deployCoffeeFixture); const quantity = 5; const totalCost = web3.utils.toWei("0.001", "ether"); await coffee.methods.buyCoffee(quantity).send({ from: buyer, value: totalCost }); const totalCoffeesSold = await coffee.methods.totalCoffeesSold().call(); const totalEtherReceived = await coffee.methods.totalEtherReceived().call(); expect(totalCoffeesSold).to.equal(String(quantity)); expect(totalEtherReceived).to.equal(totalCost); }); }); describe("Fallback function", function () { // Test to check revert when ether is sent directly to the contract it("Should revert if ether is sent directly to the contract", async function () { const { coffee, buyer } = await loadFixture(deployCoffeeFixture); expect( web3.eth.sendTransaction({ from: buyer, to: coffee.options.address, value: web3.utils.toWei("0.001", "ether"), }) ).to.be.revertedWith("DirectEtherTransferNotAllowed"); }); }); });
このコードは、コーヒー スマート コントラクトの機能をテストします。デプロイメント、コーヒーの購入、コントラクトへの直接 Ether 転送の処理のテストが含まれます。
内訳は次のとおりです。
deployCoffeeFixture
async function deployCoffeeFixture() { const coffeeContract = new web3.eth.Contract(ABI.abi); coffeeContract.handleRevert = true; const [deployer, buyer] = await web3.eth.getAccounts(); const rawContract = coffeeContract.deploy({ data: ABI.bytecode, }); const estimateGas = await rawContract.estimateGas({ from: deployer }); const coffee = await rawContract.send({ from: deployer, gas: estimateGas.toString(), gasPrice: "10000000000", }); console.log("Coffee contract deployed to: ", coffee.options.address); return { coffee, deployer, buyer, rawContract }; }
describe("Deployment", function () { it("Should set the initial values correctly", async function () { const { coffee } = await loadFixture(deployCoffeeFixture); const totalCoffeesSold = await coffee.methods.totalCoffeesSold().call(); const totalEtherReceived = await coffee.methods.totalEtherReceived().call(); expect(totalCoffeesSold).to.equal("0"); expect(totalEtherReceived).to.equal("0"); }); });
totalCoffeesSold
とtotalEtherReceived
がゼロに設定されていることを確認します。describe("Buying Coffee", function () { it("Should purchase coffee and emit an event", async function () { const { coffee, buyer } = await loadFixture(deployCoffeeFixture); const quantity = 3; const totalCost = web3.utils.toWei("0.0006", "ether"); const receipt = await coffee.methods.buyCoffee(quantity).send({ from: buyer, value: totalCost }); const event = receipt.events.CoffeePurchased; expect(event).to.exist; expect(event.returnValues.buyer).to.equal(buyer); expect(event.returnValues.quantity).to.equal(String(quantity)); expect(event.returnValues.totalCost).to.equal(totalCost); }); it("Should revert if the quantity is zero", async function () { const { coffee, buyer } = await loadFixture(deployCoffeeFixture); expect( coffee.methods.buyCoffee(0).send({ from: buyer, value: web3.utils.toWei("0.0002", "ether") }) ).to.be.revertedWith("QuantityMustBeGreaterThanZero"); }); it("Should update totalCoffeesSold and totalEtherReceived correctly", async function () { const { coffee, buyer } = await loadFixture(deployCoffeeFixture); const quantity = 5; const totalCost = web3.utils.toWei("0.001", "ether"); await coffee.methods.buyCoffee(quantity).send({ from: buyer, value: totalCost }); const totalCoffeesSold = await coffee.methods.totalCoffeesSold().call(); const totalEtherReceived = await coffee.methods.totalEtherReceived().call(); expect(totalCoffeesSold).to.equal(String(quantity)); expect(totalEtherReceived).to.equal(totalCost); }); });
CoffeePurchased
イベントが発行されることをテストします。totalCoffeesSold
とtotalEtherReceived
が正しく更新されていることを確認します。describe("Fallback function", function () { it("Should revert if ether is sent directly to the contract", async function () { const { coffee, buyer } = await loadFixture(deployCoffeeFixture); expect( web3.eth.sendTransaction({ from: buyer, to: coffee.options.address, value: web3.utils.toWei("0.001", "ether"), }) ).to.be.revertedWith("DirectEtherTransferNotAllowed"); }); });
テスト スクリプトを記述したら、次の操作を行います。
console.log()
を呼び出して、ログ メッセージとコントラクト変数を出力できます。これを使用するには、次のようにコントラクト コードにhardhat/console.sol
をインポートする必要があります。 //SPDX-License-Identifier: MIT pragma solidity >=0.8.0 <0.9.0; import "hardhat/console.sol"; contract Coffee { //... }
契約をテストするには、ターミナルで次のコマンドを実行します。
npx hardhat test
以下のような出力が得られるはずです:
これは、スマート コントラクトが期待どおりに機能していることを示しています。
npx hardhat test
を実行すると、スマート コントラクトが自動的にコンパイルされ、テストされます。ぜひ試してみて、コメント セクションでお知らせください。
ここでは、スマート コントラクトをSepolia テストネットにデプロイします。テストネットを使用すると、大きなコストをかけずに、Ethereum メインネットを模倣した環境でスマート コントラクトをテストできます。dApp の機能に問題がなければ、Ethereum メインネットに再デプロイできます。
dotenv パッケージとこれらの依存関係をインストールします。
npm install dotenv npm install --save-dev @nomicfoundation/hardhat-web3-v4 'web3@4'
これにより、Web3.Js と Dotenv が 'node_modules' フォルダーに含まれ、プロジェクトに追加されます。
それらをhardhat.config.cjs
ファイルにインポートします。
require('dotenv').config(); require("@nomicfoundation/hardhat-toolbox"); require("@nomicfoundation/hardhat-web3-v4"); const HardhatUserConfig = require("hardhat/config"); module.exports = { solidity: "0.8.24", } };
ルート フォルダーに.env
ファイルを作成します。
MetaMask ウォレットからアカウントの秘密鍵と dRPC API キーを取得します。
それらを.env
ファイルに保存します。
DRPC_API_KEY=your_drpc_api_key PRIVATE_KEY=your_wallet_private_key
hardhat.config.cjs
ファイルを更新して、Sepolia テストネット構成を含めます。
require('dotenv').config(); require("@nomicfoundation/hardhat-toolbox"); require("@nomicfoundation/hardhat-web3-v4"); const HardhatUserConfig = require("hardhat/config"); const dRPC_API_KEY = process.env.VITE_dRPC_API_KEY; const PRIVATE_KEY = process.env.VITE_PRIVATE_KEY; module.exports = { solidity: "0.8.24", networks: { sepolia: { url: `https://lb.drpc.org/ogrpc?network=sepolia&dkey=${dRPC_API_KEY}`, accounts: [`0x${PRIVATE_KEY}`], } } };
ignition/module
フォルダーの下に新しいスクリプト ファイルを作成し、 deploy.cjs
という名前を付けます。次のコードを追加して、スマート コントラクトをデプロイします。
const { buildModule } = require("@nomicfoundation/hardhat-ignition/modules"); const CoffeeModule = buildModule("CoffeeModule", (m) => { const coffee = m.contract("Coffee"); return { coffee }; }); module.exports = CoffeeModule;
ターミナルで次のコマンドを実行して、スマート コントラクトをデプロイします。
npx hardhat ignition deploy ./ignition/modules/deploy.cjs --network sepolia
コマンド プロンプトを実行すると、 Confirm deploy to network sepolia (11155111)? (y/n)
ので、 y
と入力します。デプロイが成功すると、ターミナルにデプロイされたスマート コントラクトのアドレスが表示されます。
また、 deployed_addresses.json
ファイル内のコントラクト アドレスにアクセスすることもできます。
おめでとうございます。スマート コントラクトを Sepolia テストネットに正常にデプロイしました。🎉
この記事では、Hardhat CLI を使用して支払いスマート コントラクトを作成し、テスト、コンパイル、およびデプロイする方法を説明しました。
次の記事では、この dApp のフロントエンドの構築方法を学びます。この UI は次の要素で構成されます。
デフォルト メッセージを超えて: Solidity でのカスタム エラーのマスター
Solidity のカスタム エラーをマスターする: デフォルト メッセージを超えてスマート コントラクトを向上させる