はじめに
こんにちは。だい(@ishidad2)です。
晴れ時々Symbolということで久々にSymbolネタの記事を書いていこうと思います。
今回は@Toshi_maさんが作成されているFlutter用のSymbolSDKを使ってスマホからトランザクションを飛ばしてみようと思います。
なお、DartやFlutterのインストールなどは事前に済んでいることが前提条件となります。
環境
FVMとは、Flutter SDKのバージョンをプロジェクト毎に管理するツールです。
以下を参考に必要であればインストールをしてください。(Flutterのバージョン管理が必要なければ不要です)
以下、私の実行環境です。
これより先の記述は筆者の環境であるMacでの説明になります。それ以外のWindows等では当てはまらない説明があるかもしれません。適宜読み替えてください。
% fvm --version
2.4.1
% dart --version
Dart SDK version: 3.3.1 (stable) (Wed Mar 6 13:09:19 2024 +0000) on "macos_arm64"
% fvm flutter doctor
Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel stable, 3.19.3, on macOS 14.5 23F79 darwin-arm64 (Rosetta), locale ja-JP)
[✓] Android toolchain - develop for Android devices (Android SDK version 34.0.0)
[✓] Xcode - develop for iOS and macOS (Xcode 15.1)
[✓] Chrome - develop for the web
[✓] Android Studio (version 2022.3)
[✓] VS Code (version 1.90.0)
[✓] Connected device (2 available)
[✓] Network resources
Flutterプロジェクト作成
% fvm flutter create sym_mobile
% cd sym-mobile
実行
以下のコマンドでプロジェクトを実行します。
% flutter run
Connected devices:
macOS (desktop) • macos • darwin-arm64 • macOS 14.5 23F79 darwin-arm64 (Rosetta)
Chrome (web) • chrome • web-javascript • Google Chrome 126.0.6478.61
No wireless devices were found.
[1]: macOS (macos)
[2]: Chrome (chrome)
Please choose one (or "q" to quit):
デバイスを選択する画面が表示されたら、AndroidまたはiOSのエミュレータを起動してください。エミュレータの起動方法については以下の記事を参考にしてください。
エミュレータが起動して以下のようになっていればOKです。
エミュレータの起動が確認できたらCtrl+C
で停止しておきます。
symbol-sdkをプロジェクトに追加
Symbol SDKは、SymbolブロックチェーンをFlutterで扱うためのパッケージです。プロジェクトに追加する手順は以下の通りです。
まず、pubspec.yamlファイルに以下を追記します。
dependencies:
flutter:
sdk: flutter
symbol_sdk:
http:
ターミナルで以下のコマンドを実行、依存関係を取得します。
% fvm flutter pub get
lib/config.dart
を以下の内容で作成します。
const String privateKey = 'E0EA41F92F586D679939B6490369C4F6F91ED407057F301AD535E35859FFA0B5'; // ⚠️注意⚠️: テスト用のキーです。実際のアプリケーションでは、環境変数またはセキュアなストレージを使用してください。
const String recipientAddress = 'TARDV42KTAIZEF64EQT4NXT7K55DHWBEFIXVJQY';
const String message = 'Hello, Symbol!!';
const String nodeUrl = 'http://sym-test-01.opening-line.jp:3000';
const String explorer = 'https://testnet.symbol.fyi/transactions';
lib/main.dart
を以下のコードに置き換えます。
import 'package:flutter/material.dart';
import 'package:symbol_sdk/index.dart';
import 'package:symbol_sdk/CryptoTypes.dart' as ct;
import 'package:symbol_sdk/symbol/index.dart';
import 'package:http/http.dart' as http;
import 'config.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Symbol Transaction Sender',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(title: 'Symbol Transaction Sender'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
final String title;
@override
MyHomePageState createState() => MyHomePageState();
}
class MyHomePageState extends State<MyHomePage> {
String transactionHash = '';
void sendTransaction() async {
try {
var facade = SymbolFacade(Network.TESTNET);
var keyPair = KeyPair(ct.PrivateKey(privateKey));
var deadline = Timestamp(facade.network.fromDatetime(DateTime.now().toUtc()).addHours(2).timestamp);
var tx = TransferTransactionV1(
network: NetworkType.TESTNET,
deadline: deadline,
signerPublicKey: PublicKey(keyPair.publicKey.bytes),
recipientAddress: UnresolvedAddress(recipientAddress),
message: MessageEncorder.toPlainMessage(message),
);
tx.fee = Amount(tx.size * 100);
var signature = facade.signTransaction(keyPair, tx);
var payload = facade.attachSignature(tx, signature);
setState(() {
transactionHash = facade.hashTransaction(tx).toString();
});
print('Transaction Hash: $transactionHash');
var response = await http.put(
Uri.parse('$nodeUrl/transactions'),
headers: {'Content-Type': 'application/json'},
body: payload,
);
print('Response: ${response.body}');
} catch (e) {
print('Error: $e');
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'Press the button to send a Symbol transaction:',
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: sendTransaction,
child: const Text('Send Transaction'),
),
const SizedBox(height: 20),
transactionHash.isNotEmpty
? GestureDetector(
child: SelectableText(
'$explorer/$transactionHash',
style: const TextStyle(color: Colors.blue),
),
)
: Container(),
],
),
),
);
}
}
ディレクトリ構成は以下のようになります。
以下のコマンドを実行してエミュレータで確認します。
% fvm flutter run
エミュレータが起動すると最初に起動した画面とは違い、真ん中に「Send Transaction」というボタンが表示されていると思います。
「Send Transaction」を押すとトランザクションが送信されます。
トランザクションが送信されたらボタンの下にURLが表示されるので、コピーしてブラウザに貼り付けます。
エクスプローラーでトランザクションが確認できれば成功です!
コードの解説
Flutterについての詳しい解説はここでは行いません。
Symbolのトランザクションの生成とアナウンス方法について解説を行います。
モジュールのインポート
Symbolのというより、お約束ごとですね。
必要なモジュールを以下のように読み込みます。
import 'package:flutter/material.dart';
import 'package:symbol_sdk/index.dart';
import 'package:symbol_sdk/CryptoTypes.dart' as ct;
import 'package:symbol_sdk/symbol/index.dart';
import 'package:http/http.dart' as http;
import 'config.dart';
トランザクションの生成
トランザクションの生成部分は以下です。
ここでTransferTransactionV1を生成しています。
var tx = TransferTransactionV1(
network: NetworkType.TESTNET,
deadline: deadline,
signerPublicKey: PublicKey(keyPair.publicKey.bytes),
recipientAddress: UnresolvedAddress(recipientAddress),
message: MessageEncorder.toPlainMessage(message),
);
詳しいことはAPIリファレンスを参照してもらえたらと思いますが、TransferTransactionV1のConstructorは以下になります。
TransferTransactionV1({
Signature? signature,
PublicKey? signerPublicKey,
int? version,
NetworkType? network,
TransactionType? type,
Amount? fee,
Timestamp? deadline,
UnresolvedAddress? recipientAddress,
List<UnresolvedMosaic>? mosaics,
Uint8List? message
})
トランザクション手数料の設定
tx.fee = Amount(tx.size * 100);
トランザクションへの署名とペイロードの導出
var signature = facade.signTransaction(keyPair, tx);
var payload = facade.attachSignature(tx, signature);
ノードへアナウンス
bodyにPayloadを詰め込んで/transactions
エンドポイントへアナウンスをします。
var response = await http.put(
Uri.parse('$nodeUrl/transactions'),
headers: {'Content-Type': 'application/json'},
body: payload,
);
最後に
FlutterでもSymoblのトランザクションが簡単に発行できました。
これまでは自力でペイロードを作成するかバックエンドを準備してトランザクションをアナウンスしていたところがSDKを使うことで手軽に簡単に行うことができるようになりました。
これは開発が捗りますね〜
余談
トランザクションを渡したら自動判定して適切なエンドポイントへアナウンスしてくれるパッケージとか誰か作ってくれないかな〜 |ω・`)チラリ
なんと、さっそく作ってくれた人がw
きっかけは下の投稿から。
ということで、以下サンプルコードを置いておきます。(main.dart
の変更点)
+ import 'package:symbol_rest_client/api.dart';
tx.fee = Amount(tx.size * 100);
var signature = facade.signTransaction(keyPair, tx);
+ var payload = facade.attachSignature(tx, signature, isPlainPayload: true); // symbol-sdkをv1.0.16以上に上げる必要があります
+ final transactionPayload = TransactionPayload(payload:payload);
+ final apiInstance = TransactionRoutesApi(ApiClient(basePath: nodeUrl));
+ final response = await apiInstance.announceTransaction(transactionPayload);
- var payload = facade.attachSignature(tx, signature);
setState(() {
transactionHash = facade.hashTransaction(tx).toString();
});
- print('Transaction Hash: $transactionHash');
- var response = await http.put(
- Uri.parse('$nodeUrl/transactions'),
- headers: {'Content-Type': 'application/json'},
- body: payload,
-);
-print('Response: ${response.body}');
以下、全てのコードです。
import 'package:flutter/material.dart';
import 'package:symbol_rest_client/api.dart';
import 'package:symbol_sdk/index.dart';
import 'package:symbol_sdk/CryptoTypes.dart' as ct;
import 'package:symbol_sdk/symbol/index.dart';
import 'config.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Symbol Transaction Sender',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(title: 'Symbol Transaction Sender'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
final String title;
@override
MyHomePageState createState() => MyHomePageState();
}
class MyHomePageState extends State<MyHomePage> {
String transactionHash = '';
void sendTransaction() async {
try {
var facade = SymbolFacade(Network.TESTNET);
var keyPair = KeyPair(ct.PrivateKey(privateKey));
var deadline = Timestamp(facade.network.fromDatetime(DateTime.now().toUtc()).addHours(2).timestamp);
var tx = TransferTransactionV1(
network: NetworkType.TESTNET,
deadline: deadline,
signerPublicKey: PublicKey(keyPair.publicKey.bytes),
recipientAddress: UnresolvedAddress(recipientAddress),
message: MessageEncorder.toPlainMessage(message),
);
tx.fee = Amount(tx.size * 100);
var signature = facade.signTransaction(keyPair, tx);
// 純粋なペイロードを取得する
var payload = facade.attachSignature(tx, signature, isPlainPayload: true);
final transactionPayload = TransactionPayload(payload:payload);
final apiInstance = TransactionRoutesApi(ApiClient(basePath: nodeUrl));
final response = await apiInstance.announceTransaction(transactionPayload);
setState(() {
transactionHash = facade.hashTransaction(tx).toString();
});
print(response);
} catch (e) {
print('Error: $e');
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'Press the button to send a Symbol transaction:',
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: sendTransaction,
child: const Text('Send Transaction'),
),
const SizedBox(height: 20),
transactionHash.isNotEmpty
? GestureDetector(
child: SelectableText(
'$explorer/$transactionHash',
style: const TextStyle(color: Colors.blue),
),
)
: Container(),
],
),
),
);
}
}
pubspec.yaml
も修正が必要です。
dependencies:
flutter:
sdk: flutter
symbol_sdk: ^1.0.16
http:
symbol_rest_client: ^1.0.0
依存関係を新たに追加したので、以下のコマンドを実行する必要があります。
% fvm flutter pub get