見出し画像

Lisk SDK (lisk-client)を使いながらJavaScriptを勉強してみない? - その5 -

おばんです!
モンハンが楽しみな万博おじです🤗

ということで、今回は6回目、内容は「トランザクションの生成と送信」です。

  1. 準備

  2. JavaScriptの基本:パスフレーズを生成

  3. 文字列操作:パスフレーズからアドレスを取得

  4. 非同期処理:APIからアカウント情報を取得

  5. ループ処理:APIからトランザクション情報を取得

  6. トランザクションの生成と送信

  7. トランザクション手数料の取得

はじめに

いよいよLSKを送信します。
今回は内容のほとんどがlisk-clientとLiskサービスAPIの使用方法なので難易度はちょっと高めかもしれませんがのんびりご覧ください😉

前回

今回のお勉強用ソースコード

<!DOCTYPE html>
<html lang="ja">
	<head>
		<meta charset="utf-8"/>
		<meta name="viewport" content="width=device-width, initial-scale=1.0">
		<title>画面タイトルです</title>
		<script src="https://js.lisk.com/lisk-client-5.2.2.min.js" defer></script>
		<style>
			html { font-size: 10px; }
			
			body { font-size: 1.6rem; }

			input[type="text"],
			input[type="number"],
			input[type="password"],
			textarea,
			button {
				font-size: 1.6rem;
				padding: 5px;
			}
		</style>
	</head>
	<body>
		<div>
			<input type="password" id="enter-passphrase" style="width: 750px;" placeholder="パスフレーズを入力してください" oninput="checkPassphrase(this.value)" />
		</div>
		<div>
			<button type="button" id="btn-login" style="width: 150px;" onclick="login()" disabled="true">ログイン</button>
			<button type="button" style="width: 150px;" onclick="createAccount()">アカウントを作成</button>
		</div>
		<div>
			<a href="https://testnet-faucet.lisk.com/" target="_blank" rel="noopener noreferrer">テストネット用のLSKを受け取ります</a>
		</div>

		<div>
			<input type="text" id="enter-recipient" style="width: 750px;" placeholder="[必須] 送信先アドレスを入力してください (例:lsk9g3k58b3gzcykjyaob9ekbt3a7b3e586h4gkxj)" disabled="true"/>
		</div>
		<div>
			<input type="number" id="enter-amount" style="width: 750px;" placeholder="[必須] 送信枚数を入力してください (0以上の数値)" disabled="true"/>
		</div>
		<div>
			<input type="text" id="enter-memo" style="width: 750px;" placeholder="[任意] メモを入力してください" disabled="true"/>
		</div>
		<div>
			<button type="button" id="btn-send" style="width: 150px;" onclick="send()" disabled="true">送信</button>
		</div>

		<hr>
			<h4>テスト用パスフレーズ:</h4>
			<div>
				abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about
			</div>
		<hr>
		<!-- (1)パスフレーズの表示場所 -->
		<h4>パスフレーズ:</h4>
		<div id="lisk-passphrase"></div>

		<!-- (2)アドレスの表示場所 -->
		<h4>アドレス:</h4>
		<div id="lisk-address"></div>
		<div id="lisk-bufferAddress"></div>

		<!-- (3)公開鍵の表示場所 -->
		<h4>公開鍵:</h4>
		<div id="lisk-publicKey"></div>
		
		<!-- (4)残高の表示場所 -->
		<h4>残高:</h4>
		<div id="lisk-balance"></div>
		
		<!-- (5)残高の表示場所 -->
		<h4>トランザクション(直近10件):</h4>
		<div id="lisk-transactions"></div>

		<script>
			/*
			 * アカウント情報初期化
			 */
			function clearAccountInfo() {
				document.querySelector("#lisk-address").innerHTML = "";
				document.querySelector("#lisk-balance").innerHTML = "";
				document.querySelector("#lisk-passphrase").innerHTML = "";
				document.querySelector("#lisk-bufferAddress").innerHTML = "";
				document.querySelector("#lisk-publicKey").innerHTML = "";
				document.querySelector("#lisk-transactions").innerHTML = "";
			}

			/*
			 * アカウント作成処理
			 */
			function createAccount() {
				// (0)アカウント情報初期化
				clearAccountInfo();

				// (1)パスフレーズを生成して画面に表示
				const mnemonic = lisk.passphrase.Mnemonic.generateMnemonic();
				document.querySelector("#lisk-passphrase").innerHTML = mnemonic;

				// (2)アドレスと公開鍵を取得して画面に表示
				const addressAndPublicKey = lisk.cryptography.getAddressAndPublicKeyFromPassphrase(mnemonic);
				const bufferAddress = addressAndPublicKey.address;
				const publicKey = addressAndPublicKey.publicKey;
				document.querySelector("#lisk-bufferAddress").innerHTML = `(${bufferAddress.toString("hex")})`;
				document.querySelector("#lisk-publicKey").innerHTML = publicKey.toString("hex");

				// (3)アドレスを取得して画面に表示
				const address = lisk.cryptography.getLisk32AddressFromAddress(bufferAddress);
				document.querySelector("#lisk-address").innerHTML = address;

				// (4)残高は0LSKとする
				document.querySelector("#lisk-balance").innerHTML = "0LSK";
			}

			/*
			 * ログイン 
			 */
			async function login() {
				// (0)アカウント情報初期化
				clearAccountInfo();

				// (1)入力されたパスフレーズを取得
				const passphrase = document.querySelector("#enter-passphrase").value;

				// (2)パスフレーズからlsk始まりのアドレスを取得
				const address = lisk.cryptography.getLisk32AddressFromPassphrase(passphrase);

				// (3)Lisk Service API の accounts を使用してアカウント情報を取得
				const response = await fetch(`https://testnet-service.lisk.com/api/v2/accounts?address=${address}`);
				const json = await response.json();

				// (4)見つからなかった場合は終了
				if (json.error) {
					alert("アカウントが見つかりませんでした。");
					return;
				}

				// (5)見つかった場合は画面に表示
				const account = json.data[0];
				document.querySelector("#lisk-address").innerHTML = account.summary.address;
				document.querySelector("#lisk-balance").innerHTML = `${lisk.transactions.convertBeddowsToLSK(account.summary.balance)}LSK`;
				document.querySelector("#lisk-passphrase").innerHTML = "ひみつ";
				
				// (6)公開鍵とバッファアドレスはパスフレーズから取得して表示
				const addressAndPublicKey = lisk.cryptography.getAddressAndPublicKeyFromPassphrase(passphrase);
				document.querySelector("#lisk-bufferAddress").innerHTML = `(${addressAndPublicKey.address.toString("hex")})`;
				document.querySelector("#lisk-publicKey").innerHTML = addressAndPublicKey.publicKey.toString("hex");

				// (7)トランザクション情報取得して表示
				showTransactions(address);
			}

			/*
			 * トランザクション情報表示
			 */
			async function showTransactions(address) {
				// (1)指定のアドレスで送信または受信したトランザクション情報を取得
				const response = await fetch(`https://testnet-service.lisk.com/api/v2/transactions?address=${address}&offset=0&limit=10`);
				const json = await response.json();

				// (2)見つからなかった場合は終了
				if (json.error || json.data.length === 0) {
					return;
				}
				
				let html_transactions = "";
				for (data of json.data) {
					html_transactions += `
						<div>ID:${data.id}</div>
						<div>タイプ:${data.moduleAssetName}</div>
						<div>送信者:${data.sender.address === address? "あなた": data.sender.address}</div>
						${data.asset.recipient === undefined? "":
							`<div>受信者:${data.asset.recipient.address === address? "あなた": data.asset.recipient.address}</div>`
						}
						${data.asset.amount === undefined? "":
							`<div>${data.sender.address === address? "送信":"受信"}枚数:${lisk.transactions.convertBeddowsToLSK(data.asset.amount)}LSK</div>`
						}
						<div>手数料:${lisk.transactions.convertBeddowsToLSK(data.fee)}LSK</div>
						${data.asset.data === undefined? "":
							`<div>データ:${data.asset.data}</div>`
						}
						<hr>
					`;
				}
				document.querySelector("#lisk-transactions").innerHTML = html_transactions;
			}

			/*
			 * パスフレーズチェック
			 */
			function checkPassphrase(val) {
				// (1)現在の入力値をチェック
				const ret = lisk.passphrase.Mnemonic.validateMnemonic(val);

				// (2)パスフレーズが正しくない場合はログインボタンを入力不可、正しい場合は入力可に変更
				document.querySelector("#btn-login").disabled = !ret;
				
				document.querySelector("#btn-send").disabled = !ret;
				document.querySelector("#enter-recipient").disabled = !ret;
				document.querySelector("#enter-amount").disabled = !ret;
				document.querySelector("#enter-memo").disabled = !ret;
			}

			/*
			 * 送信処理
			 */
			async function send() {
				// 画面の入力値を取得
				const passphrase = document.querySelector("#enter-passphrase").value;
				const amount = document.querySelector("#enter-amount").value;
				const recipient = document.querySelector("#enter-recipient").value;
				const memo = document.querySelector("#enter-memo").value;

				// 入力チェック
				try {
					lisk.cryptography.validateLisk32Address(recipient);
				} catch(_err) {
					alert("送信先アドレスが不正です。");
					return;
				}

				if (amount.length === 0 || amount < 0) {
					alert("送信枚数が不正です。");
					return;
				}

				// アカウント情報取得
				const address = lisk.cryptography.getLisk32AddressFromPassphrase(passphrase);
				const accountsResponse = await fetch(`https://testnet-service.lisk.com/api/v2/accounts?address=${address}`);
				const accounts = await accountsResponse.json();
				if (accounts.error) {
					alert(`送信に失敗しました。\n${accounts.message}`);
					return;
				}
				const account = accounts.data[0];

				// 送信処理用のスキーマ情報を取得
				const schemeResponse = await fetch(`https://testnet-service.lisk.com/api/v2/transactions/schemas?moduleAssetId=2:0`);
				const schemes = await schemeResponse.json();
				const scheme = schemes.data[0].schema;

				// Liskネットワーク情報を取得
				const networkResponse = await fetch(`https://testnet-service.lisk.com/api/v2/network/status`);
				const network = await networkResponse.json();
				const networkIdentifier = network.data.networkIdentifier;

				// 送信トランザクション設定
				const tokenTransferTx = {
					moduleID: 2,
					assetID: 0,
					nonce: BigInt(account.sequence.nonce),
					fee: BigInt(lisk.transactions.convertLSKToBeddows("0.1")),
					signatures: [],
					senderPublicKey: lisk.cryptography.getPrivateAndPublicKeyFromPassphrase(passphrase).publicKey,
					asset: {
						amount: BigInt(lisk.transactions.convertLSKToBeddows(amount)),
						recipientAddress: lisk.cryptography.getAddressFromLisk32Address(recipient),
						data: memo
					}
				}

				// 送信トランザクションをパスフレーズで署名
				const signedTx = lisk.transactions.signTransaction(
					scheme,
					tokenTransferTx,
					lisk.cryptography.hexToBuffer(networkIdentifier),
					passphrase
				);

				// 署名後のトランザクションをバイト配列にしたあと16進数表記の文字列に変換				const tx = lisk.cryptography.bufferToHex(lisk.transactions.getBytes(scheme, signedTx));
				const res = await fetch(`https://testnet-service.lisk.com/api/v2/transactions?transaction=${tx}`, {method: 'POST'});
				const result = await res.json();
				if (result.error) {
					alert(`送信に失敗しました。\n${result.message}`);
					return;
				}
				alert(`送信に成功しました!\nトランザクションID:${result.transactionId}`);
			}
		</script>
	</body>
</html>

前回からの変更点

HTMLで変わったのは以下の通りです

  • 送信先アドレス入力欄の追加(enter-recipient)

  • 送信枚数入力欄の追加(enter-amount)

  • メモ欄の追加(enter-memo)

  • 送信ボタンの追加(btn-send)

  • パスフレーズ入力欄(enter-passphrase)の幅を変更

JavaScriptで変わったのは以下の通りです

  • 送信処理(send)の追加

  • パスフレーズチェック処理(checkPassphrase)で追加した送信先アドレス欄などを有効なパスフレーズの場合にのみ入力可とするように変更

画面を開くとこんな感じ

変更後の画面
LSKを送信後
10秒ほどたってから再度ログインボタンをおすと送信されたことがわかりますね!

ソースコードの説明:HTML

新しく出てくる内容はありません🙂

ソースコードの説明:JavaScript(lisk-client)

lisk.cryptography.validateLisk32Address

正しいアドレスかどうかを判定します。
間違ったアドレスの場合は例外が発生します。

ℹ️例外については後述

lisk.transactions.convertLSKToBeddows

LSK単位の値をブロックチェーンで保持する単位に変換します。
(100000000倍)

ℹ️その3でlisk.transactions.convertBeddowsToLSKの時にちらっと出てきましたね😊

lisk.cryptography.getPrivateAndPublicKeyFromPassphrase

パスフレーズから秘密鍵および公開鍵を取得します。

ℹ️公開鍵を取得したいだけなら既にその3で勉強しているlisk.cryptography.getAddressAndPublicKeyFromPassphraseを使ってもいいですよ😊

lisk.cryptography.getAddressFromLisk32Address

lsk始まりのアドレスからBuffer(バッファタイプ)のアドレスに変換します。

ℹ️変換したBufferアドレスは16進数表記にして画面上に()付きで表示していますね😊

lisk.transactions.signTransaction

作成したトランザクション情報に署名します。

ℹ️マルチシグネチャアカウントの場合はlisk.transactions.signMultiSignatureTransactionを使用して署名します。
署名例はこちらをどうぞ

lisk.cryptography.bufferToHex

Bufferを16進数表記に変換します。

ℹ️これの代わりにtoString("hex")としても大丈夫。

lisk.cryptography.hexToBuffer

16進数表記の文字列をBufferに変換します。

lisk.transactions.getBytes

バイト配列を取得します。

ソースコードの説明:JavaScript

try { [例外発生の可能性のある処理] } catch([変数]) { [例外時処理] }

発生する例外を受け取り何らかの処理を行う場合に使用します。

ℹ️プログラムが異常終了してはいけない場合はcatch内でログ出力などを行い、ユーザーには何かしらの異常があったことがわかるような通知をしてあげるといいと思います。

BigInt

最大9007199254740991、最小−9007199254740991までの整数値を格納することができます。

function send

送信処理です。
送信先のアドレスと送信枚数の入力チェックを行います。
また、LiskサービスAPIから各種情報を取得し、送信処理用のトランザクションを生成します。
トランザクションはパスフレーズで署名を行い、バイト配列を経由して16進数表記の文字列にしたものをLiskサービスAPIを使って処理しています。

生成するトランザクションは以下のような形式でなくてはいけません。

{
	moduleID: 2,
	assetID: 0,
	nonce: BigInt,
	fee: BigInt,
	signatures: [],
	senderPublicKey: Buffer,
	asset: {
		amount: BigInt,
		recipientAddress: Buffer,
		data: String
	}
}

moduleID:2固定
 ※処理内容によって変わります(投票処理の場合は5)

assetID:0固定
 ※処理内容によって変わります(投票処理の場合は1)

nonce:アカウント情報のnonceと同値を設定(BigInt)
 ※For文で連続実行する場合は+1ずつカウントアップすること

fee:送信手数料を設定(BigInt)
 ※0.1LSKの場合は10000000を設定
 ※動的手数料にする方法は次回

senderPublicKey:送信者の公開鍵を設定(Buffer)

amount:送信枚数を設定(BigInt)
 ※1LSKの場合は100000000を設定

recipientAddress:送信先のアドレスを設定(Buffer)
 ※lsk始まりのアドレスではなくBufferアドレス

data:任意入力欄
 ※入力なし("")でもOK

ソースコードの説明:Liskサービス API

https://testnet-service.lisk.com/api/v2/transactions/schemas

テストネットのトランザクション用のスキーマ情報を取得する際に使用するLisk公式のLiskサービスAPIです。
詳しくはこちらをご覧ください。

ℹ️この取得結果を見て生成するトランザクションに必要な項目や型を確認しましょう。

以下のようなJSONが返却されます。

{
    "data": [
        {
            "moduleAssetId": "2:0",
            "moduleAssetName": "token:transfer",
            "schema": {
                "$id": "lisk/transfer-asset",
                "title": "Transfer transaction asset",
                "type": "object",
                "required": [
                    "amount",
                    "recipientAddress",
                    "data"
                ],
                "properties": {
                    "amount": {
                        "dataType": "uint64",
                        "fieldNumber": 1
                    },
                    "recipientAddress": {
                        "dataType": "bytes",
                        "fieldNumber": 2
                    },
                    "data": {
                        "dataType": "string",
                        "fieldNumber": 3
                    }
                }
            }
        },
        ....
    ]
}

https://testnet-service.lisk.com/api/v2/network/status

テストネットのネットワーク情報を取得する際に使用するLisk公式のLiskサービスAPIです。
詳しくはこちらをご覧ください。

ℹ️トランザクションの送信に必要なnetworkIdentifierはこのAPIで取得できます。
テストネット、メインネットごとに固定なので固定値としてプログラム内で持っておいてもいいです。

以下のようなJSONが返却されます。

{
    "data": {
        "genesisHeight": 14075260,
        "height": 16694403,
        "finalizedHeight": 16694241,
        "networkVersion": "3.1",
        "networkIdentifier": "15f0dacc1060e91818224a94286b13aa04279c640bd5d6f193182031d133df7c",
        "milestone": "4",
        "currentReward": "100000000",
        "rewards": {
            "milestones": [
                "500000000",
                "400000000",
                "300000000",
                "200000000",
                "100000000"
            ],
            "offset": 2160,
            "distance": 3000000
        },
        ....
    }
}

https://testnet-service.lisk.com/api/v2/transactions

テストネットのトランザクション情報を送信する際に使用するLisk公式のLiskサービスAPIです。
詳しくはこちらをご覧ください。

ℹ️前回の記事を見ていた人は「おや?」と思うかもしれませんね。
そうです、トランザクション情報を取得するAPIと同じですね!
これは処理方法がGETかPOSTかの違いで変わります。

POSTの場合はトランザクションを送信し、GETの場合はトランザクション情報を取得します。
※{"method": "POST"}を省略または{"method": "GET"}とするとGETになります。
※今回と前回のお勉強用ソースコードを比較すると、今回はfetchに{"method": "POST"}を指定していて、前回はfetchに何も指定していないことがわかると思います。

以下のようなJSONが返却されます。

{
  "message": "Transaction payload was successfully passed to the network node",
  "transactionId": "...."
}

おわりに

今回はここまで!
LSKの送信をやってみましたがいかがでしたでしょうか?
APIやlisk-client周りがいろいろ出てきたので難しかったかもしれませんね😅
ただここまで出来ればLisk Desktopを開かずにサクッとLSKを送信する処理を作ることもできますよ😉(定期的にどこかに送信するような場合とか便利です)
それではお疲れさまでした!
次回もよろしくおねがいしまーす🙂

万博おじについて

Liskに関するツールなど開発したりノード管理したりしています。
何かあればTwitter等でご連絡ください。

個人アカウント
Twitter:ys_mdmg
GitHub:lisknonanika
Discord:ys_mdmg#5646
Lisk Explorer:lisk observer, lisk scan

デリゲートアカウント(共同管理)
Twitter:liskcommulab
Discord:CommuLab#0097
Lisk Explorer:lisk observer, lisk scan

管理
ノード:Mainnet / Testnet
Lisk Service:Mainnet / Testnet
デリゲートサイト:Lisk CommuLab

個人やデリゲート宛ての寄付ありがとうございます!
ノード管理や開発資金に充てさせて頂いています😊

この記事が気に入ったらサポートをしてみませんか?