9
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

はじめに

こんにちは。だい(@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です。

image.png

エミュレータの起動が確認できたらCtrl+Cで停止しておきます。

symbol-sdkをプロジェクトに追加

Symbol SDKは、SymbolブロックチェーンをFlutterで扱うためのパッケージです。プロジェクトに追加する手順は以下の通りです。

まず、pubspec.yamlファイルに以下を追記します。

pubspec.yaml
dependencies:
  flutter:
    sdk: flutter

  symbol_sdk:
  http:

ターミナルで以下のコマンドを実行、依存関係を取得します。

% fvm flutter pub get

スクリーンショット 2024-06-18 0.19.15.png

lib/config.dartを以下の内容で作成します。

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を以下のコードに置き換えます。

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(),
          ],
        ),
      ),
    );
  }
}

ディレクトリ構成は以下のようになります。

スクリーンショット 2024-06-19 15.45.56.png

以下のコマンドを実行してエミュレータで確認します。

% fvm flutter run

エミュレータが起動すると最初に起動した画面とは違い、真ん中に「Send Transaction」というボタンが表示されていると思います。

スクリーンショット 2024-06-18 2.35.19.png

「Send Transaction」を押すとトランザクションが送信されます。
トランザクションが送信されたらボタンの下にURLが表示されるので、コピーしてブラウザに貼り付けます。

スクリーンショット 2024-06-18 9.12.39.png

image.png

エクスプローラーでトランザクションが確認できれば成功です!

コードの解説

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}');

以下、全てのコードです。

main.dart
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も修正が必要です。

pubspec.yaml
dependencies:
  flutter:
    sdk: flutter

  symbol_sdk: ^1.0.16
  http:
  symbol_rest_client: ^1.0.0

依存関係を新たに追加したので、以下のコマンドを実行する必要があります。

% fvm flutter pub get
9
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?