さあ、手を汚してみましょう。
ルート ディレクトリの下にフォルダーを作成し、 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
: このライセンス識別子は、コードがライセンスに基づいてライセンスされていることを示します。
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 のカスタム エラーは、プログラミング言語によって提供されるデフォルトのエラー メッセージではなく、特定のユース ケースに合わせて調整されたエラー メッセージです。ユーザー エクスペリエンスの向上に役立つだけでなく、スマート コントラクトのデバッグや保守にも役立ちます。
error
: このキーワードはカスタムエラーを定義するために使用されます error QuantityMustBeGreaterThanZero(); error InsufficientEtherSent(uint256 required, uint256 sent); error DirectEtherTransferNotAllowed();
QuantityMustBeGreaterThanZero()
: 数量がゼロより大きいことを確認します。InsufficientEtherSent(uint256 required, uint256 sent)
: 送信された Ether が十分であることを確認します。DirectEtherTransferNotAllowed()
: 契約への直接 Ether 転送を防止します。 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 をインストールします。
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": {} }
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: "", }); 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: "", }); 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
を実行すると、スマート コントラクトが自動的にコンパイルされ、テストされます。ぜひ試してみて、コメント セクションでお知らせください。
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: `//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 テストネットに正常にデプロイしました。🎉