メインコンテンツまでスキップ

lesson-2_スマートコントラクトを作成しよう

👏 スマートコントラクトを作成する

コーディングの前に今回作成するto-doアプリに必要となる機能を整理しておきましょう。

  1. to-doを作成する機能

  2. to-doを更新する機能

  3. to-doの完了・未完了を切り替える機能

  4. to-doを削除する機能

それでは、以上の4点を意識しながら、コーディングに進みましょう。

contractsフォルダの中にTodoContract.solファイルを作成してください。

TodoContract.solのファイル内に以下のコードを記載します。

// TodoContract.sol
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.9;

contract TodoContract {
uint256 public taskCount = 0;

struct Task {
uint256 index;
string taskName;
bool isComplete;
}

mapping(uint256 => Task) public todos;
//1.to-doを作成する機能
event TaskCreated(string task, uint256 taskNumber);
//2.to-doを更新する機能
event TaskUpdated(string task, uint256 taskId);
//3.to-doの完了・未完了を切り替える機能
event TaskIsCompleteToggled(string task, uint256 taskId, bool isComplete);
//4.to-doを削除する機能
event TaskDeleted(uint256 taskNumber);
}

では、コードを詳しく見ていきましょう。

// TodoContract.sol
// SPDX-License-Identifier: GPL-3.0

これは「SPDXライセンス識別子」と呼ばれます。

詳細については、こちらを参照してみてください。

// TodoContract.sol
pragma solidity ^0.8.9;

これは、コントラクトで使用するSolidityコンパイラのバージョンです。

上記の場合「このコントラクトを実行するときは、Solidityコンパイラのバージョン0.8.6のみを使用し、それ以下のものは使用しません」という意味です。

// TodoContract.sol
contract TodoContract {
...
}

contractは、ほかの言語でいうところの「class」のようなものなのです。

// TodoContract.sol
uint256 public taskCount = 0;

taskCountは、スマートコントラクト内のToDoアイテムの総数を格納する符号なしpublic整数です。

// TodoContract.sol
struct Task {
uint256 index;
string taskName;
bool isComplete;
}

struct Taskは、各ToDoに関する情報(メタデータ)を格納するためのデータ構造です。これには、To-doのidtaskNameisCompleteのブール値などが含まれています。

  • id : To-doを識別するためのid
  • taskName : To-doのタイトル
  • isComplete : To-doが完了したかどうかの状態(完了したらtrue、完了してないならfalse)
// TodoContract.sol
mapping(uint256 => Task) public todos;

mapping(uint256 => Task) public todos;は、すべてのToDoを格納するマッピングで、キーはidで、値は上記のTaskです。

// TodoContract.sol
//1.to-doを作成する機能
event TaskCreated(string task, uint256 taskNumber);
//2.to-doを更新する機能
event TaskUpdated(string task, uint256 taskId);
//3.to-doの完了・未完了を切り替える機能
event TaskIsCompleteToggled(string task, uint256 taskId, bool isComplete);
//4.to-doを削除する機能
event TaskDeleted(uint256 taskNumber);

TaskCreatedTaskUpdatedTaskIsCompleteToggledTaskDeletedは、ブロックチェーン上に発生するイベントで、dAppはこれをリスニングし、それに応じて機能することができます。

次に以下のコードをTodoContract内のevent TaskDeleted(uint256 taskNumber);下に追加してください。

1. to-doを作成する機能

// TodoContract.sol
function createTask(string memory _taskName) public {
todos[taskCount] = Task(taskCount, _taskName, false);
taskCount++;
emit TaskCreated(_taskName, taskCount - 1);
}

では、コードを詳しく見ていきましょう。

// TodoContract.sol
function createTask(string memory _taskName) public {
 ...
}

createTask関数は、to-doの_taskNameを受け取ります。

// TodoContract.sol
todos[taskCount] = Task(taskCount, _taskName, false);

taskCount_taskNameで新しいTask構造を作成し、todosマップの現在のtaskCountの値に代入することができます。

// TodoContract.sol
taskCount++;

to-doが作成されるたびにtaskCountが1ずつ増えるようにしています。

// TodoContract.sol
emit TaskCreated(_taskName, taskCount - 1);

すべてが完了したら、TaskCreatedイベントをemitする必要があります。

次に以下のコードをTodoContract内のcreateTask関数の下に追加してください。

2.to-doを更新する機能

// TodoContract.sol
function updateTask(uint256 _taskId, string memory _taskName) public {
Task memory currTask = todos[_taskId];
todos[_taskId] = Task(_taskId, _taskName, currTask.isComplete);
emit TaskUpdated(_taskName, _taskId);
}

では、コードを詳しく見ていきましょう。

// TodoContract.sol
function updateTask(uint256 _taskId, string memory _taskName) public {
...
}

updateTask関数は、更新されるto-doの_taskIdと更新されたtaskNameを受け取ります。

// TodoContract.sol
Task memory currTask = todos[_taskId];
todos[_taskId] = Task(_taskId, _taskName, currTask.isComplete);
  • これらの値で新しいTask構造を作成し、受け取った_taskIdに対応するtodosマップに割り当てることができます。

  • 更新中に、そのto-doのisCompleteの値を保持しておく必要があります。まず、マップから現在のタスクを取得し、それを変数に格納し、そのisComplete値を新しいタスク・オブジェクトに使用します。

// TodoContract.sol
emit TaskUpdated(_taskName, _taskId);

すべてが完了したら、TaskUpdatedイベントをemitする必要があります。

次に以下のコードをTodoContract内のupdateTask関数の下に追加してください。

3.to-doの完了・未完了を切り替える機能

// TodoContract.sol
function toggleComplete(uint256 _taskId) public {
Task memory currTask = todos[_taskId];
todos[_taskId] = Task(_taskId, currTask.taskName, !currTask.isComplete);

emit TaskIsCompleteToggled(
currTask.taskName,
_taskId,
!currTask.isComplete
);
}

では、コードを詳しく見ていきましょう。

// TodoContract.sol
function toggleComplete(uint256 _taskId) public {
...
}

toggleComplete関数には、更新するto-doの_taskIdを受け取ります。

// TodoContract.sol
Task memory currTask = todos[_taskId];
todos[_taskId] = Task(_taskId, currTask.taskName, !currTask.isComplete);
  • todosマップからTaskオブジェクトを取得し、その値で新しいTaskオブジェクトを作成します。

  • isCompleteを現在のisCompleteの反対の値として設定します。

// TodoContract.sol
emit TaskIsCompleteToggled(
currTask.taskName,
_taskId,
!currTask.isComplete
);

すべてが完了したら、TaskIsCompleteToggledイベントをemitする必要があります。

最後に以下のコードをTodoContract内のtoggleComplete関数の下に追加してください。

4.to-doを削除する機能

// TodoContract.sol
function deleteTask(uint256 _taskId) public {
delete todos[_taskId];
emit TaskDeleted(_taskId);
}

では、コードを詳しく見ていきましょう。

// TodoContract.sol
function deleteTask(uint256 _taskId) public {
...
}

deleteTaskは、削除するto-doの_taskをパラメータとして受け取ります。

// TodoContract.sol
delete todos[_taskId];

受け取った_taskに対応するtodosマップからTaskオブジェクトを削除します。

// TodoContract.sol
emit TaskDeleted(_taskId);

すべてが完了したら、TaskDeletedイベントをemitする必要があります。

✅ 動作確認をする

TodoContractに実装した各機能が、期待する動作を行うかを確認するために、テストを作成しましょう。

packages/contract/testディレクトリの中に、test.jsファイルを作成して、以下のコードを記載してください。

const hre = require("hardhat");
const { expect } = require("chai");

describe("TodoContract", () => {
// declare contract variable
let contract;

// deploy contract before all of the tests
before(async () => {
const contractFactory = await hre.ethers.getContractFactory("TodoContract");
contract = await contractFactory.deploy();
});

// check creating function
it("create function is working on chain", async () => {
// check if you can create multiple tasks
const receipt = await (await contract.createTask("make lunch")).wait();
await contract.createTask("do the dises");
await contract.createTask("have luch with friends");

// check if you can read tasks
expect((await contract.readTask(0))[1]).to.equal("make lunch");
expect((await contract.readTask(1))[1]).to.equal("do the dises");
expect((await contract.readTask(2))[1]).to.equal("have luch with friends");

// check if event "TaskCreated" works
expect(
receipt.events?.filter((x) => {
return x.event === "TaskCreated";
})[0].args[0]
).to.equal("make lunch");
});

it("update function is working on chain", async () => {
// check if you can update tasks
const receipt = await (await contract.updateTask(0, "make dinner")).wait();
await contract.updateTask(1, "clean up the rooms");
expect((await contract.readTask(0))[1]).to.equal("make dinner");
expect((await contract.readTask(1))[1]).to.equal("clean up the rooms");

// check if event "TaskUpdated" works
expect(
receipt.events?.filter((x) => {
return x.event === "TaskUpdated";
})[0].args[0]
).to.equal("make dinner");
});

// check toggling function
it("toggleComplete function is working on chain", async () => {
// check if you can make a task completed
const formerState = (await contract.readTask(0))[2];
const receipt = await (await contract.toggleComplete(0)).wait();
expect((await contract.readTask(0))[2]).to.equal(!formerState);

// check if event "TaskIsCompleteToggled" works
expect(
receipt.events?.filter((x) => {
return x.event === "TaskIsCompleteToggled";
})[0].args[0]
).to.equal("make dinner");
});

// check deleting function
it("delete function is working on chain", async () => {
// check if you can delete a task
const receipt = await (await contract.deleteTask(0)).wait();
expect((await contract.readTask(0))[1]).to.equal("");

// check if event "TaskDeleted" works
expect(
receipt.events
?.filter((x) => {
return x.event === "TaskDeleted";
})[0]
.args[0].toNumber()
).to.equal(0);
});
});

簡単にテストの内容を解説します。 以下の部分でTodoContractのデプロイを行います。before()を用いることで、テストの前に一度だけ実行されるようになります。

// deploy contract before all of the tests
before(async () => {
const contractFactory = await hre.ethers.getContractFactory("TodoContract");
contract = await contractFactory.deploy();
});

以降のコードで、実際にコントラクトの関数を呼び出して期待する結果となるかどうかを確認しています。 例として、createTask関数のテストを見てみましょう。ToDoを3つ作成し、それぞれのToDoが正しく登録されているかを確認しています。

  // check creating function
it('create function is working on chain', async () => {
// check if you can create multiple tasks
const receipt = await (await contract.createTask('make lunch')).wait();
await contract.createTask('do the dises');
await contract.createTask('have luch with friends');

// check if you can read tasks
expect((await contract.readTask(0))[1]).to.equal('make lunch');
expect((await contract.readTask(1))[1]).to.equal('do the dises');
expect((await contract.readTask(2))[1]).to.equal('have luch with friends');

また、ToDo: make lunchを作成した際のイベントが正しく発火しているかも確認しています。

    const receipt = await (await contract.createTask('make lunch')).wait();
...
// check if event "TaskCreated" works
expect(
receipt.events?.filter((x) => {
return x.event === 'TaskCreated';
})[0].args[0],
).to.equal('make lunch');
});

それでは、以下のコマンドでテストを実行してみましょう。

yarn test

全てのテストにパスした場合、このように表示されます。

スマートコントラクトの開発は以上で完了です。

あとは、コンパイルとデプロイの作業を進めるだけです。頑張っていきましょう!

🙋‍♂️ 質問する

ここまでの作業で何かわからないことがある場合は、Discordの#polygonで質問をしてください。

ヘルプをするときのフローが円滑になるので、エラーレポートには下記の3点を記載してください ✨

1. 質問が関連しているセクション番号とレッスン番号
2. 何をしようとしていたか
3. エラー文をコピー&ペースト
4. エラー画面のスクリーンショット

次のレッスンに進んで、スマートコントラクトのコンパイルとデプロイの作業を開始しましょう 🎉