今回の記事では、Solidity & Flutterを使って、Dappsを作る方法を解説します。
アプリの内容としては、「アプリ→メタマスクに接続→口座にトークンを発行する」というFaucetアプリを作るというものになっています。
画像ではこんな感じ。
トークンの発行、送付、メタマスクとの接続を学べるので、この記事を通してDappsの基本を学べると思います。
では、さっそく作っていきましょう。
あと、英語の記事も書いたので、よかったら見てください!
リンク)
Dappsとは
Dappsについて知らない人向けに、簡単にDappsについて説明させてください。
Dappsとは、スマートコントラクト(プログラム)を、ブロックチェーンに組み込むことで、
- ブロックチェーン上にあるので → 改ざんができない
- 分散して保存されているので → 破壊できない
- 中央管理者がいないので → 誰もシステムを止められない
といった特徴を持つアプリのことです。
これらの特徴から、Dappsは、世界に新しい価値を生み出すのではないかと期待されています。
詳しく知りたい方は、公式サイトからどうぞ。
メタマスク
メタマスクとは
仮想通貨やNFT、トークンなどを管理できる財布のひとつです。
今回はこのメタマスクを、
- スマートコントラクトのガス代の支払い
- トークンの受け取り・管理
に使います。
メタマスクをインストールする
まずは、上記のリンクからメタマスクの拡張機能をブラウザにインストール
そして、口座を作ってください。
注意:シークレットキー、リカバリーフレーズは公開しないようにしましょう。漏れると口座が乗っ取られる危険性があります。
また、スマートコントラクトを、ネットワークに展開&使用するには、ガス代を払う必要があります。
ブロックチェーンのネットワークには多くの種類があります。
メインネットではETH(リアルマネー)を使います。
ただ、今回のアプリでは、Goerliテストネットを使用します。テストネットでは、ガス代に必要な通貨を無料で受け取ることができます。
テストネットのトークンをもらう
まずは、ガス代に使うテストネット用の通貨を、無料でゲットしましょう。
そのために、メタマスクのネットワークを、Goerliテストネットワークに設定する必要があります。
しかし、最初はテストネットが表示されていないので、設定を変更しましょう。
メタマスクで、
→ネットワークを追加
→高度な設定
→テストネットワークを表示をオン。
これでテストネットが表示されます。
その後、Goerliテストネットワークを選択しておきましょう。
設定が終わったら、上のリンクからGoerli ETHをゲットしましょう。
→alchemyのアカウントを作る
→メタマスクのアドレスを入力
→0.2goerli ETHを無料でゲット!
faucetは蛇口という意味で、今回作るアプリもこのサイトの機能と同じです。
→アドレスが入力されたら、その口座にトークンを配布するという仕組み。
スマートコントラクト (Solidity)
次に、Solidityを使用して、スマートコントラクトを作成しましょう。
remixでスマートコントラクトを書く
今回は、オンラインエディタであるremixを使用します。
上のリンクからremixを開いてください。
新しいワークスペースを作成します。
テンプレートはなんでも良いですが、今回はERC20を使用し、名前は、FaucetTokenとしておきます。
FaucetTestToken.sol ファイルを作ります。
コードを以下のように書き換えます。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract FaucetTestToken is ERC20 {
constructor() ERC20("FaucetTestToken", "FTT") {}
function faucet (address recipient , uint amount) external {
_mint(recipient, amount * (10 ** decimals()));
}
}
今回は、トークンを発行するのにOpenZeppelinというライブラリを使っています。
faucet関数では、受取人のアドレスにトークンを送る(mint)という処理を行っています。
また今回は、Solidityの基礎部分のコードについて、説明は省略させていただきます。
わからない部分があったら、以下のサイトでSolidityの基礎を学べます。(どちらも日本語に対応)
公式サイト
https://ethereum.org/en/developers/learning-tools/
CryptZombies
https://cryptozombies.io/jp/course
特にCryptZombiesはおすすめです。
チャプター1-3(solidityの高度なコンセプト)まで終わらせれば、今回のコードは理解できると思います。
コンパイル
次に、コードをコンパイルします。
このとき、コンパイルした際に生成されるABIファイルは、Flutterでフロントエンドを作るときに必要になります。
デプロイ
最後に、スマートコントラクトをデプロイします。
このとき、Environment → Injected Provider (Goerli network)にしてからデプロイしましょう。
少し時間が立ったあと、デプロイに成功していることを確認したら、実際に試してみましょう。
faucet関数に、あなたのメタマスクのアドレス&振り込みたいトークンの数を入力し、transactを押します。
処理が終わった後、metamaskのActivityより、faucetの取引が確認できればスマートコントラクトを実行できています。
その際、コントラクトアドレスもコピーしておきます。
assetsに戻り、import token → token contract addressに、先ほどコピーしたコントラクトアドレスを入力します。
そして、Add custom tokenで、自分の保有しているトークンの量を見ることができます。
スマートコントラクトが正常に動作していることがわかったので、これからはflutterでフロントエンドを作っていきましょう。
その前に、dappのリクエストを処理&スマートコントラクトとやり取りするためのAPIを設定します。
infura
今回はエンドポイントに、infuraを使用します。
上のリンクより、アカウントを作成したら、CREATE NEW KEYで新たなプロジェクトを作成。
ENDPOINTSをGoerliに設定します。
このURLはFlutterでプロジェクトを作るときに使用します。
Flutter
最後に、Flutterプロジェクトを作っていきます。
完成品は、以下のgithubのリンクからダウンロードできますので、参照しながら記事をご覧ください。
それぞれのコードについて、簡単に解説していきます。
ライブラリ
まずは、pubspec.yamlに依存関係を追加します。
dependencies:
flutter:
sdk: flutter
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.2
web3dart: ^2.3.3
http: ^0.13.4
walletconnect_dart: ^0.0.11
url_launcher: ^6.1.8
web3dart
→トランザクションを送信したり、スマートコントラクトとやり取りするのに必要。
http
→Dappsとmetamaskの接続を確立するために必要。
walletconnect_dart
→アプリからmetamaskに接続するのに必要。
url_launcher
→urlからmetamaskを起動する際に必要。
abiファイル
デプロイされたスマートコントラクトと、アプリがやり取りするためには、abiファイルが必要です。
先ほど、コンパイルしたときに取得したabiファイルをコピーします。
flutterプロジェクトにassetsフォルダを作り、その中にcontract.jsonを作成し、abiファイルをペーストします。
これで、abiファイルの設定は完了です。
ここからは、homepage.dartの内容について説明していきます。
walletconnect_dart
walletconnect_dartの機能を使って、アプリとmetamaskを接続することができます。
挙動としては、
Dapp→ bridgeサーバー → wallet
という形で、bridgeサーバーを通して、metamask等のwalletとの通信を行っています。
コードの解説をします。homepage.dartの68行目から
final connector = WalletConnect(
bridge: 'https://bridge.walletconnect.org',
clientMeta: const PeerMeta(
name: 'WalletConnect',
description: 'WalletConnect Developer App',
url: 'https://walletconnect.org',
icons: [
'https://gblobscdn.gitbook.com/spaces%2F-LJJeCjcLrr53DcT1Ml7%2Favatar.png?alt=media'
],
),
);
connectorを作り、Dappとbridgeサーバーの接続を行っています。
公式を参照:
https://github.com/RootSoft/walletconnect-dart-sdk/tree/58581538e321537d42cd1cdb3d92cc6a1cfcf75a
homepage.dartの80行目から
var _session, session, _uri;
connectMetamaskWallet(BuildContext context) async {
if (!connector.connected) {
try {
session = await connector.createSession(
chainId: 5,
onDisplayUri: (uri) async {
_uri = uri;
await launchUrlString(uri, mode: LaunchMode.externalApplication);
});
print(session.accounts[0]);
setState(() {
_session = session;
});
} catch (exp) {
print(exp);
}
}
}
connectorを通して、ブロックチェーンネットワーク(GoerliネットワークのchainIdは5)とのsessionを確立しています。
web3dart
web3dartでは、トランザクションを送信したり、スマートコントラクトとやり取りしたりすることができます。
web3dartの使い方について解説していきます。
まずは、サーバーに対して、1度だけのやり取りだけでなく、複数回のやり取りを行うために、Clientを作成します。
Clientを使用することで、サーバーに対して、永続的な接続を開いたままにすることができます。
公式を参照:
https://github.com/xclud/web3dart
homepage.dartの101行目より、
late Client httpClient;
late Web3Client ethClient;
//change this to your Infura goerli endpoints
final String blockchainUrl =
"https://goerli.infura.io/v3/0170d757246f418f999960b7f36484f1";
var tokenBalance;
@override
void initState() {
httpClient = Client();
ethClient = Web3Client(blockchainUrl, httpClient);
super.initState();
}
このとき、blockchainUrlは、infuraで取得した、goerliネットワークのENDPOINTSを使用します。
(tokenBalanceは、ユーザーのERC20トークンを持っている数を保存する変数です。)
次に、スマートコントラクトを使用するために、getContract関数を用意します。
116行目より、
Future<DeployedContract> getContract() async {
//change this to your abi file.
String abiFile = await rootBundle.loadString("assets/contract.json");
//change this to your ContractAddress
String contractAddress = "0xF49CE5D5f85A9f1f7eDe1a4150c86Db9D97b337F";
final contract = DeployedContract(
ContractAbi.fromJson(abiFile, "FaucetTestToken"),
EthereumAddress.fromHex(contractAddress));
return contract;
}
abiファイルを用いて、スマートコントラクトの内容をアプリ側から理解することができます。
contractAddressは、metamaskから取得した、ご自身のものに書き換えてください。
次に、スマートコントラクト内の関数を使用するために、callFunction関数を作成します。
128行目より、
Future<List<dynamic>> callFunction(String name, List<dynamic> args) async {
final contract = await getContract();
final function = contract.function(name);
final result = await ethClient.call(
contract: contract, function: function, params: args);
return result;
}
nameは呼び出すスマートコントラクトの関数の名前で、paramsは引数を指定しています。
次は、実際にスマートコントラクト内の関数を呼び出す関数を作成します。
136行目より、
Future<void> getTokenBalance(String account) async {
final userAddress = EthereumAddress.fromHex(account);
List<dynamic> results = await callFunction("balanceOf", [userAddress]);
setState(() {
tokenBalance = (results[0] ~/ BigInt.from(pow(10, 18))).toInt();
});
}
利用者のアドレスのwalletに存在するFTTトークンの数を返す関数(getTokenBalance)を作成しました。
今回書いたスマートコントラクトでは、openzeppelinを使用しているので、ERC20に備わっている、balanceOfという標準関数を使用しています。
また、スマートコントラクトから帰ってくる変数が、10の18乗と大きいので、扱いやすいようにBigInt型で、小数点切り捨ての割り算をしてからInt型に戻しています。
また、162行目より、
Future<void> faucet(String account) async {
snackBar(label: "move to metamask and verify.");
final userAddress = EthereumAddress.fromHex(account);
//obtain our contract from abi in json file
final contract = await getContract();
// extract function from json file
final function = contract.function("faucet");
//get secret key from metamask
EthereumWalletConnectProvider provider =
EthereumWalletConnectProvider(connector);
//obtain private key for write operation
final credentials = WalletConnectEthereumCredentials(provider: provider);
//send transaction using the our private key, function and contract
await ethClient.sendTransaction(
credentials,
Transaction.callContract(
from: userAddress,
contract: contract,
function: function,
parameters: [userAddress, BigInt.from(1)]),
chainId: 5);
ScaffoldMessenger.of(context).removeCurrentSnackBar();
snackBar(label: "verifying transaction");
//set a 20 seconds delay to allow the transaction to be verified before trying to retrieve the balance
Future.delayed(const Duration(seconds: 20), () {
ScaffoldMessenger.of(context).removeCurrentSnackBar();
snackBar(label: "retrieving transaction");
getTokenBalance(account);
ScaffoldMessenger.of(context).clearSnackBars();
});
}
利用者のアドレスのwalletに、FTTトークンを1つ送る関数(faucet)を作成しました。
スマートコントラクトを使用するときには、ガス代を支払う必要があります。
そのときに、秘密鍵を入力する必要があるのですが、アプリ側で秘密鍵を保持するのは危険です。
なので、metamaskから秘密鍵を取得することで、アプリ内に保持しないDappsを作ることが可能です。
credentialsに秘密鍵を保存しているのですが、それをメタマスクから貰うために、20行目からのWalletConnectEthereumCredentialsクラスが必要になります。
また、スマートコントラクトを呼び出す際に、トークンを送る個数を、引数のparametersから、1つに指定しています。
以上で、Flutterについての簡単な説明を終わりたいと思います。
使用方法
テストをする際には実機を使うので、あらかじめ、アプリ版メタマスクをインストールしておいてください。
"Connect wallet"を押します。
メタマスクに移動するので、接続を承認します。すると、アプリに自動的に戻り、メタマスクが接続されます。
あなたのメタマスクのアドレス、chainID、持っているFTTトークンの数が表示されます。
FTTトークンを持っていても、最初は0と表示されます。仕様です。
Reloadを押すと、持っているFTTトークンの数が更新されます。
Get a Token!を押したあと、メタマスクに手動で移動し、トランザクションを承認します。
その後、アプリに手動で戻リます。時間が経って、処理が完了すると、トークンの数が更新されています。
アプリからトークンを発行して、ウォレットに送付することができました!!!
参照
改善点
シミュレータでアプリを動かす方法はわかりませんでした。メタマスクとかのアプリをシミュレータにインストールできるんでしょうか?
あとは、metamaskを接続した直後に、保有しているトークン数を更新する方法がわかりませんでした。なのでreloadボタンを採用しています。
非同期処理で、口座にあるトークンの数を受け取ったあと、画面に反映するのができていないと思うので、多分自分のflutterへの理解が足りていないことが原因です。
Get a Tokenボタンを押したあと、メタマスクに自動に行き、自動でアプリに戻ってくる処理を実装できませんでした。メタマスクを接続するときはできているので、これも実装できると思うのですが、自分の技術力不足です。。。
今後、調べて改善したいと思います。
まとめ
長い記事なのに読んでくださり、ありがとうございました。
今回、初めてqiitaに記事を書かさせていただきました。
アプリからメタマスクに接続してスマートコントラクトを使用する方法は、日本だけでなく、英語の記事でも情報が少なく、情報を得るのに苦労しました。
本当に情報がないので、後から作る人の助けになればと記事を書かさせていただきました。
読みにくかったり、自分の技術力不足があるとは思いますが、参考になっていたら幸いです。
ちかれた。
参考になってたら嬉しいです。ハートください💕