tsunagi-functions（0.1.0.3.5）をLaravelで使う

# はじめに

こんにちは。だい（@ishidad2）です。
晴れ時々SymbolということでSymbolネタの記事を書いていこうと思います。
今回は@nem_takanobu(Taka Nobu)さんが公開されているtsunagi-functionsを使ってPHPからトランザクションを飛ばしてみようと思います。
ちなみに、PHPから飛ばす書いたのですが色々な諸事情あり、Laravel10を使って試してみました。
なお、Lravel10のインストール方法などは割愛させてもらいます。

それでは行ってみましょう！

# Base32 ライブラリのインストール

SymbolHelpers関数内でBase32ライブラリを使うのでインストールしておきます。

```bash
composer require christian-riesen/base32
```

# `app/Helpers/SymbolHelpers.php`を作成

<details><summary>ソースコード</summary>

```php
<?php

namespace App\Helpers;

use Base32\Base32;

class SymbolHelpers
{
    public static function loadCatjson($tx, $network)
    {
        $jsonFile = '';
        if ($tx["type"] === "AGGREGATE_COMPLETE" || $tx["type"] === "AGGREGATE_BONDED") {
            $jsonFile = "aggregate.json";
        } else {
            $jsonFile = strtolower($tx["type"]) . ".json";
        }

        $res = file_get_contents($network["catjasonBase"] . $jsonFile);
        return json_decode($res, true);
    }

    public static function loadLayout($tx, $catjson, $isEmbedded)
    {
        $prefix = $isEmbedded ? "Embedded" : "";

        $layoutName = '';
        if ($tx["type"] === "AGGREGATE_COMPLETE") {
            $layoutName = "AggregateCompleteTransactionV2";
        } elseif ($tx["type"] === "AGGREGATE_BONDED") {
            $layoutName = "AggregateBondedTransactionV2";
        } else {
            $layoutName = $prefix . self::toCamelCase(strtolower($tx["type"])) . "TransactionV1";
        }

        $conditions = ["prefix" => $prefix, "layout_name" => $layoutName];
        $factory = array_filter($catjson, function ($item) use ($conditions) {
            return isset($item['factory_type']) && $item['factory_type'] == $conditions["prefix"] . "Transaction" && $item["name"] === $conditions["layout_name"];
        });

        return array_values($factory)[0]["layout"];
    }

    public static function toCamelCase($str)
    {
        return str_replace(' ', '', ucwords(str_replace('_', ' ', $str)));
    }

    public static function prepareTransaction($tx, $layout, $network)
    {
        $preparedTx = $tx;
        $preparedTx["network"] = $network["network"];
        $preparedTx['version'] = ($tx["type"] === "AGGREGATE_COMPLETE" || $tx["type"] === "AGGREGATE_BONDED") ? 2 : 1;

        if (isset($preparedTx['message'])) {
            $preparedTx['message'] = "00" . bin2hex($tx['message']);
        }

        if (isset($preparedTx['name'])) {
            $preparedTx['name'] = bin2hex($tx['name']);
        }

        if (isset($preparedTx['value'])) {
            $preparedTx['value'] = bin2hex($tx['value']);
        }

        if (isset($tx['mosaics'])) {
            $ids = array_column($preparedTx['mosaics'], 'mosaic_id');
            array_multisort($ids, SORT_ASC, $preparedTx['mosaics']);
        }

        foreach ($layout as $layer) {
            if (isset($layer["size"]) && !is_numeric($layer["size"])) {
                $size = 0;

                if (isset($layer["element_disposition"]) && isset($preparedTx[$layer["name"]])) {
                    $size = strlen($preparedTx[$layer["name"]]) / ($layer["element_disposition"]["size"] * 2);
                } elseif (strpos($layer["size"], '_count') !== false) {
                    $size = isset($preparedTx[$layer["name"]]) ? count($preparedTx[$layer["name"]]) : 0;
                }

                $preparedTx[$layer["size"]] = $size;
            }
        }

        if (isset($tx["transactions"])) {
            $txes = [];
            foreach ($tx["transactions"] as $eTx) {
                $eCatjson = self::loadCatjson($eTx, $network);
                $eLayout = self::loadLayout($eTx, $eCatjson, true);
                $ePreparedTx = self::prepareTransaction($eTx, $eLayout, $network);
                array_push($txes, $ePreparedTx);
            }
            $preparedTx["transactions"] = $txes;
        }

        return $preparedTx;
    }

    //トランザクション解析
		public static function parseTransaction($tx,$layout,$catjson,$network) {

			$parsed_tx = []; //return
			foreach($layout as $layer){

				$layer_type = $layer["type"];
				$layer_disposition = "";
				if(isset($layer["disposition"])){
					$layer_disposition = $layer["disposition"];
				}
				$filter_item = array_filter($catjson, function($cj) use($layer_type){
					return $cj["name"] === $layer_type;
				});
				$catitem = array_values($filter_item);

				if(count($catitem) > 0 ){
					$catitem = $catitem[0];
				}

				if(isset($layer["condition"])){
					if($layer["condition_operation"] === "equals"){
						if($layer["condition_value"] !== $tx[$layer["condition"]]){

							continue;
						}
					}
				}

				if($layer_disposition === "const"){
					continue;

				}else if($layer_type === "EmbeddedTransaction"){

					$tx_layer = $layer;
					$items = [];
					foreach($tx["transactions"] as $e_tx){ //小文字のeはembeddedの略
						$e_catjson = self::loadCatjson($e_tx,$network);//catjsonの更新
						$e_layout = self::loadLayout($e_tx,$e_catjson,true); //isEmbedded:true

						$e_parsed_tx = self::parseTransaction($e_tx,$e_layout,$e_catjson,$network); //再帰
						array_push($items,$e_parsed_tx);
					}
					$tx_layer["layout"] = $items;
					array_push($parsed_tx,$tx_layer);
					continue;

				}else if(isset($catitem["layout"]) && isset($tx[$layer["name"]]) ){

					$tx_layer = $layer;
					$items = [];
					foreach($tx[$layer["name"]] as $item){

						$filter_value = array_filter($catjson, function($cj) use($layer_type){
							return $cj["name"] === $layer_type;
						});
						$filter_layer = array_values($filter_value)[0];

						$item_parsed_tx = self::parseTransaction($item,$filter_layer["layout"],$catjson,$network); //再帰
						array_push($items,$item_parsed_tx);
					}
					$tx_layer["layout"] = $items;
					array_push($parsed_tx,$tx_layer);
					continue;

				}else if($layer_type === "UnresolvedAddress"){

					//アドレスに30個の0が続く場合はネームスペースとみなします。
					if(isset($tx[$layer["name"]]) && !is_array($tx[$layer["name"]]) && strpos($tx[$layer["name"]],'000000000000000000000000000000') !== false){

						$filter_value = array_filter($catjson, function($cj){
							return $cj["name"] === "NetworkType";
						});

						$network_type = array_values($filter_value)[0];
		//				print_r($network_type);
		//				print_r($tx["network"]);

						$filter_network = array_filter($network_type["values"], function($cj) use($tx){
							return $cj["name"] === $tx["network"];
						});
						$network_value = array_values($filter_network)[0]["value"];

						$prefix = dechex($network_value + 1);
						$tx[$layer["name"]] =  $prefix . $tx[$layer["name"]];
					}
				}else if(isset($catitem["type"]) && $catitem["type"] === "enum"){

					if(strpos($catitem["name"],'Flags') !== false){

						$value = 0;
						foreach($catitem["values"] as $item_layer){

							if(strpos($tx[$layer["name"]],$item_layer["name"]) !== false){

								$value += $item_layer["value"];
							}
						}
						$catitem["value"] = $value;

					}else if(strpos($layer_disposition,'array') !== false ){

						$values = [];
						foreach($tx[$layer["name"]] as $item){

							$filter_value = array_filter($catitem["values"], function($cj) use($item){
								return $cj["name"] === $item;
							});

							$item_value = array_values($filter_value)[0]["value"];
							array_push($values,$item_value);
						}
						$tx[$layer["name"]] = $values;
					}else{

						//NetworkType
						$conditions = ["tx" => $tx,"layer_name" => $layer["name"] ];
						$filter_value = array_filter($catitem["values"], function($cj) use($conditions){

							return $cj["name"] === $conditions["tx"][$conditions["layer_name"]];
						});
						$catitem["value"] = array_values($filter_value)[0]["value"];
					}
				}

				//layerの配置
				if(strpos($layer_disposition,'array') !== false ){

					if($layer_type === "byte"){

						$size = 0;
						if(isset($tx[$layer["size"]])){
							$size = $tx[$layer["size"]];
						}

						if(isset($layer["element_disposition"])){ //message

							$sub_layout = $layer;
							$items = [];
							for($count = 0; $count < $size; $count++){
								$tx_layer = [];
								$tx_layer["signedness"] = $layer["element_disposition"]["signedness"];
								$tx_layer["name"] = "element_disposition";
								$tx_layer["size"] = $layer["element_disposition"]["size"];
								$tx_layer["value"] = substr($tx[$layer["name"]],$count * 2, 2);
								$tx_layer["type"] = $layer_type;
								array_push($items,$tx_layer);
							}
							$sub_layout["layout"] = $items;
							array_push($parsed_tx, $sub_layout);

						}else{print_r("not yet");}
					}else if(isset($tx[$layer["name"]])){

						$sub_layout = $layer;
						$items = [];
						foreach($tx[$layer["name"]] as $tx_item){

							$filter_layer = array_filter($catjson, function($cj) use($layer_type){
								return $cj["name"] === $layer_type;
							});
							$tx_layer = array_values($filter_layer)[0];
							$tx_layer["value"] = $tx_item;
							if($layer_type === "UnresolvedAddress"){
								//アドレスに30個の0が続く場合はネームスペースとみなします。
								if(strpos($tx_item,'000000000000000000000000000000') !== false){

									$filter_value = array_filter($catjson, function($cj){
										return $cj["name"] === "NetworkType";
									});
									$network_type = array_values($filter_value)[0];

									$filter_network = array_filter($network_type["values"], function($cj) use($tx){
										return $cj["name"] === $tx["network"];
									});

									$network_value = array_values($filter_network)[0]["value"];

									$prefix = dechex($network_value + 1);
									$tx_layer["value"] =  $prefix . $tx_layer["value"];

								}
							}
							array_push($items,$tx_layer);
						}
						$sub_layout["layout"] = $items;
						array_push($parsed_tx,$sub_layout);

					}// else{console.log("not yet");}
				}else{ //reserved またはそれ以外(定義なし)

					$tx_layer = $layer;
					if(count($catitem) > 0){

						//catjsonのデータを使う
						if(isset($catitem["signedness"])){
							$tx_layer["signedness"]	= $catitem["signedness"];
						}
						if(isset($catitem["size"])){
							$tx_layer["size"]  = $catitem["size"];

						}
						if(isset($catitem["type"])){
							$tx_layer["type"]  = $catitem["type"];

						}
						if(isset($catitem["value"])){
							$tx_layer["value"] = $catitem["value"];
						}
					}

					//txに指定されている場合上書き(enumパラメータは上書きしない)
					if(isset($layer["name"]) && isset($tx[$layer["name"]]) ){
						if(isset($catitem["type"]) && $catitem["type"] === "enum"){

						}else{
							$tx_layer["value"] = $tx[$layer["name"]];
						}
					}else{

					}
		//			print_r("push tx_layer".PHP_EOL);
		//			print_r($tx_layer);
					array_push($parsed_tx,$tx_layer);
				}
			}

			$layer_size = array_filter($parsed_tx, function($pf){
				return $pf["name"] === "size";
			} );

			if(isset($layer_size[0]["size"])){

		//		print_r($parsed_tx);
				$parsed_tx[array_keys($layer_size)[0]]["value"] = self::countSize($parsed_tx);
			}
			return $parsed_tx;
		}

		//サイズ計算
		public static function countSize($item,$alignment = 0) {
			$total_size = 0;
			//レイアウトサイズの取得
			if(isset($item)  && isset($item["layout"])){
				foreach( $item["layout"] as $layer){
					$item_alignment;
					if(isset($item["alignment"])){
						$item_alignment = $item["alignment"];
					}else{
						$item_alignment = 0;
					}
					$total_size += self::countSize($layer,$item_alignment); //再帰
				}
			//レイアウトを構成するレイヤーサイズの取得
			}else if(array_values($item) === $item){

				$layout_size = 0;
				foreach($item as $key => $value){

					$layout_size += self::countSize($item[$key],$alignment);//再帰
				}

				if(isset($alignment)  && $alignment > 0){
					$layout_size = floor(($layout_size  + $alignment - 1) / $alignment ) * $alignment;
				}
				$total_size += $layout_size;
			}else{

				if(isset($item["size"])){

					$total_size += $item["size"];
				}else{
					print_r("no size:" + $item["name"]);
				}
			}

			return $total_size;
		}

		//トランザクション構築
		public static function buildTransaction($parsed_tx) {

			$built_tx = $parsed_tx;

			$layer_payload_size = array_filter($built_tx, function($bf){
				return $bf["name"] === "payload_size";
			});

			if(count($layer_payload_size) > 0 ){

				$filter_transactions =  array_filter($built_tx, function($bf){
					return $bf["name"] === "transactions";
				});
				$transactions = array_values($filter_transactions)[0];
				$built_tx[array_keys($layer_payload_size)[0]]["value"] = self::countSize($transactions);
			}

			//Merkle Hash Builder
			$layer_transactions_hash =  array_filter($built_tx, function($bf){
				return $bf["name"] === "transactions_hash";
			});

			if(count($layer_transactions_hash) > 0){

				$hashes = [];
				$filter_transactions =  array_filter($built_tx, function($bf){
					return $bf["name"] === "transactions";
				});

				$transactions = array_values($filter_transactions)[0];
				foreach($transactions["layout"] as $e_tx){


					$digest = hash('sha3-256',
						sodium_hex2bin(
							self::hexlifyTransaction($e_tx)
						)
					);
					array_push($hashes,$digest);
				}

				$num_remaining_hashes = count($hashes);
				while (1 < $num_remaining_hashes) {

					$i = 0;
					while ($i < $num_remaining_hashes) {
						$hasher = hash_init('sha3-256');
						hash_update($hasher,sodium_hex2bin($hashes[$i]));

						if ($i + 1 < $num_remaining_hashes) {
							hash_update($hasher,sodium_hex2bin($hashes[$i+1]));
						} else {
							// if there is an odd number of hashes, duplicate the last one
							hash_update($hasher,sodium_hex2bin($hashes[$i]));
							$num_remaining_hashes += 1;
						}
						$hashes[intval($i / 2)] = hash_final($hasher,false);
						$i += 2;
					}
					$num_remaining_hashes = intval($num_remaining_hashes / 2);

				}
				$built_tx[array_keys($layer_transactions_hash)[0]]["value"] = $hashes[0];
			}

			return $built_tx;
		}

		//トランザクションを16進数でシリアライズ
		public static function hexlifyTransaction($item,$alignment = 0) {

			$hex = "";
			if(isset($item["layout"])){
				foreach($item["layout"] as $layer){
					$item_alignment;
					if(isset($item["alignment"])){
						$item_alignment = $item["alignment"];
					}else{
						$item_alignment = 0;
					}
					$hex .= self::hexlifyTransaction($layer,$item_alignment); //再帰
				}
			}else if(array_values($item) === $item){

				$sub_layout_hex = "";
				foreach($item as $sub_layout){
					$sub_layout_hex .= self::hexlifyTransaction($sub_layout,$alignment);//再帰
				}

				if(isset($alignment) && $alignment > 0){
					$aligned_size = floor(( strlen($sub_layout_hex) + ($alignment * 2) - 2)/ ($alignment * 2) ) * ($alignment * 2);
					$sub_layout_hex = $sub_layout_hex . str_repeat ("0",$aligned_size - strlen($sub_layout_hex));
				}
				$hex .= $sub_layout_hex;
			}else{
				$size = $item["size"];
				if(!isset($item["value"])){
					if($size >= 24){
						$item["value"] = str_repeat("00",$size);
					}else{
						$item["value"] = 0;
					}
				}

				if($size==1){
					if($item["name"] === "element_disposition"){
						$hex = $item["value"];
					}else{
						$hex = bin2hex(pack('C', $item["value"]));
					}
				}else if($size==2){
					$hex = bin2hex(pack('v', $item["value"]));
				}else if($size==4){
					$hex = bin2hex(pack('V', $item["value"]));
				}else if($size==8){

					//0xffffffffffffffff を 00000000000000000としてしまう現象回避
					if(sprintf('%016X',$item["value"]) == "00000000000000000" && $item["value"] > 0){

						$hex = "ffffffffffffffff";

					}else{
						$hex = bin2hex(pack('P', $item["value"]));
					}
				}else if($size==24 || $size==32 || $size==64){
					$hex = $item["value"];
				}else{
					print_r("unknown size order");
				}
			}
			return $hex;
		}

		//トランザクション署名
		public static function signTransaction($built_tx,$private_key,$network) {

			$sign_secret = sodium_hex2bin($private_key);
			$verifiable_data = self::getVerifiableData($built_tx);

			$payload = $network["generationHash"] . self::hexlifyTransaction($verifiable_data);
			$signature = sodium_bin2hex(sodium_crypto_sign_detached(sodium_hex2bin($payload), $sign_secret));

			return $signature; 
		}

		//検証データ取得
		public static function getVerifiableData($built_tx) {

			$filter_layer = array_filter($built_tx,function($fb){
				return $fb["name"] === "type";
			});
			$type_layer = array_values($filter_layer)[0];

			if(in_array($type_layer["value"], [16705,16961])){
				return array_slice($built_tx,5,6);
			}else{
				return array_slice($built_tx,5);
			}
		}

		//トランザクションのハッシュ値計算
		public static function hashTransaction($signer,$signature,$built_tx,$network) {

			$hasher = hash_init('sha3-256');
			hash_update($hasher,sodium_hex2bin($signature));
			hash_update($hasher,sodium_hex2bin($signer));
			hash_update($hasher,sodium_hex2bin($network["generationHash"]));
			hash_update($hasher,sodium_hex2bin(self::hexlifyTransaction(self::getVerifiableData($built_tx))));

			$tx_hash = hash_final($hasher,false);

			return $tx_hash;
		}

		//トランザクション更新
		public static function updateTransaction($built_tx,$name,$type,$value) {

			$layer = array_filter($built_tx,function($fb) use($name){
				return $fb["name"] === $name;
			});

			$built_tx[array_keys($layer)[0]][$type] = $value;
			return $built_tx;
		}


		//連署
		public static function cosignTransaction($tx_hash,$private_key) {

			$sign_secret = sodium_hex2bin($private_key);
			$signature = sodium_bin2hex(sodium_crypto_sign_detached(sodium_hex2bin($tx_hash), $sign_secret));

			return $signature;
		}


    public static function generateAddressId($address)
    {
        return bin2hex(Base32::decode($address));
    }

    public static function generateNamespaceId($name, $parentNamespaceId = 0)
    {
        $NAMESPACE_FLAG = 1 << 63;

        $hasher = hash_init('sha3-256');
        hash_update($hasher, pack('V', $parentNamespaceId & 0xFFFFFFFF));
        hash_update($hasher, pack('V', ($parentNamespaceId >> 32) & 0xFFFFFFFF));
        hash_update($hasher, $name);

        $digest = unpack("C*", hex2bin(hash_final($hasher, false)));
        $result = self::digestToBigint($digest);

        return $result | $NAMESPACE_FLAG;
    }

    public static function generateKey($name)
    {
        $NAMESPACE_FLAG = 1 << 63;

        $hasher = hash_init('sha3-256');
        hash_update($hasher, $name);

        $digest = unpack("C*", hex2bin(hash_final($hasher, false)));
        $result = self::digestToBigint($digest);

        return $result | $NAMESPACE_FLAG;
    }

    public static function generateMosaicId($ownerAddress, $nonce)
    {
        $NAMESPACE_FLAG = 1 << 63;

        $hasher = hash_init('sha3-256');
        hash_update($hasher, pack('V', $nonce));
        hash_update($hasher, hex2bin($ownerAddress));
        $digest = unpack("C*", hex2bin(hash_final($hasher, false)));
        $result = self::digestToBigint($digest);

        if ($result & $NAMESPACE_FLAG) {
            $result -= $NAMESPACE_FLAG;
        }

        return $result;
    }

    public static function convertAddressAliasId($namespaceId)
    {
        return bin2hex(pack('P', $namespaceId)) . "000000000000000000000000000000";
    }

    private static function digestToBigint($digest)
    {
        $result = 0;
        for ($i = 0; $i < 8; $i++) {
            $result += $digest[$i + 1] << 8 * $i;
        }
        return $result;
    }
}
```

</details>

長いので折りたたんでいます。([ここ](https://github.com/xembook/tsunagi-functions/blob/main/php/tsunagi-functions-0.1.0.3.5.php)のコードをヘルパー用に修正しました。Claudeくんが...ｗ)

# オートロードの設定

`composer.json`ファイルを開き、"autoload" セクションに以下を追加します。
既に記載がある場合は変更しなくてOKです。

```json
"autoload": {
    "psr-4": {
        "App\\": "app/"
    }
}
```

# オートローダーを更新

ターミナルで以下のコマンドを実行して、オートローダーを更新します。

```bash
$ composer dump-autoload
```

# ヘルパー関数をインポート

これらのヘルパー関数を使用したい場所で、以下のようにクラスをインポートします。

```php
use App\Helpers\SymbolHelpers;
```

関数を呼び出す際は、以下のように静的メソッドとして使用します。

```php
$layout = SymbolHelpers::loadLayout($tx1, $catjson, false);
$prepared_tx = SymbolHelpers::prepareTransaction($tx1, $layout, $network);
$parsed_tx = SymbolHelpers::parseTransaction($prepared_tx, $layout, $catjson, $network);
```

# やってみる

コマンドを作って試してみたいと思います。

```bash
$ php artisan make:command sendTransaction
```

`app/Console/Commands/sendTransaction.php`を以下のように編集します。

```php

<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use App\Helpers\SymbolHelpers;

class sendTransaction extends Command
{
    protected $signature = 'app:send-transaction {private_key} {recipient_address} {amount} {message?}';
    protected $description = 'Send a transaction using Symbol Helpers';

    public function handle()
    {
        try {
            $private_key = $this->argument('private_key');
            $recipient_address = $this->argument('recipient_address');
            $amount = $this->argument('amount');
            $message = $this->argument('message') ?? 'Transaction sent via CLI';
            $sign_secret = sodium_hex2bin($private_key);
            $sign_public = sodium_crypto_sign_publickey_from_secretkey($sign_secret);
            $signer = sodium_bin2hex($sign_public);

            $this->info('Private key: ' . substr(sodium_bin2hex($sign_secret), 0, 64));
            $this->info('Public key (hex): ' . $signer);

            $network = [
                "version" => 1,
                "network" => "TESTNET",
                "generationHash" => "49D6E1CE276A85B70EAFE52349AACCA389302E7A9754BCF1221E79494FC665A4",
                "epochAdjustment" => 1667250467,
                "catjasonBase" => "https://xembook.github.io/tsunagi-functions/catjson/0.2.0.3/",
            ];

            $deadline_time = ((time()  + 7200) - 1667250467) * 1000;

            $tx1 = [
                "type" => "TRANSFER",
                "signer_public_key" => $signer,
                "fee" => 25000,
                "deadline" => $deadline_time,
                "recipient_address" => SymbolHelpers::generateAddressId($recipient_address),
                "mosaics" => [
                    ["mosaic_id" => 0x72C0212E67A08BCE, "amount" => intval($amount)],
                ],
                "message" => $message,
            ];

            $catjson = SymbolHelpers::loadCatjson($tx1, $network);

            if (empty($catjson)) {
                $this->error("Failed to load catjson");
                return 1;
            }

            $this->info("Catjson loaded successfully");

            $layout = SymbolHelpers::loadLayout($tx1, $catjson, false);
            $prepared_tx = SymbolHelpers::prepareTransaction($tx1, $layout, $network);
            $parsed_tx = SymbolHelpers::parseTransaction($prepared_tx, $layout, $catjson, $network);

            $built_tx = SymbolHelpers::buildTransaction($parsed_tx);
            $signature = SymbolHelpers::signTransaction($built_tx, $private_key, $network);
            $built_tx = SymbolHelpers::updateTransaction($built_tx, "signature", "value", $signature);
            $tx_hash = SymbolHelpers::hashTransaction($tx1["signer_public_key"], $signature, $built_tx, $network);
            $this->info("Transaction hash: " . $tx_hash);
            $payload = SymbolHelpers::hexlifyTransaction($built_tx);
            $this->info("Payload: " . $payload);

            $node = "http://sym-test-03.opening-line.jp:3000";
            $params = json_encode(["payload" => $payload]);
            $header = ["Content-Type: application/json"];

            $context = stream_context_create([
                "http" => [
                    "method"  => 'PUT',
                    "header"  => implode("\r\n", $header),
                    "content" => $params,
                ],
            ]);

            $json_response = file_get_contents($node . "/transactions", false, $context);
            $this->info("Transaction sent. Response:");
            $this->info($json_response);

            $this->info($node . "/transactionStatus/" . $tx_hash);
            $this->info($node . "/transactions/confirmed/" . $tx_hash);
            $this->info("https://testnet.symbol.fyi/transactions/" . $tx_hash);
        } catch (\Exception $e) {
            $this->error("An error occurred: " . $e->getMessage());
            $this->error("File: " . $e->getFile() . " Line: " . $e->getLine());
            $this->error("Stack trace:");
            $this->error($e->getTraceAsString());
            return 1;
        }
    }
}

```

# 実行

実行方法は以下の通りです。

```bash
$ php artisan app:send-transaction <private_key> <recipient_address> <amount> [<message>]
```

`private_key`の部分は少しコツ必要で（[こちら](https://qiita.com/nem_takanobu/items/d69f86487fce527dbc29#%E3%82%A2%E3%82%AB%E3%82%A6%E3%83%B3%E3%83%88%E4%BD%9C%E6%88%90)に書いてるある通り）php sodiumの仕様でSecretkeyが sign_secret + sign_public という構成をとっています。


実際のコマンドは以下のようになります。

```bash
$ php artisan app:send-transaction ED949592C90CA58A16CB5BEC303DB011A48373063DDB0C4CFD6DFD01F14A9007D47E477DA7CAE6127779523270F91BD000D7D0E06DA56192FE911460DC39081C TA3KA2ZLNBFRQZUYBRRODVI6236L5T2SFOG7JGI 1000000 "Hello PHP! Welcome to Symbol world!"
```

:::note alert
今回はテスト用に秘密鍵を載せていますが秘密鍵は絶対に公開しないでください。
:::
# 結果

以下のようなログが表示されます。

```bash
Private key: ed949592c90ca58a16cb5bec303db011a48373063ddb0c4cfd6dfd01f14a9007
Public key (hex): d47e477da7cae6127779523270f91bd000d7d0e06da56192fe911460dc39081c
Catjson loaded successfully
Transaction hash: 0fb3f9e864204f110a46a03c2de8a9f101872021ba6ce25b54798fe7e3faca65
Payload: d4000000000000003b6174650bb8072587127b5de4baefee7635732841328a0289f651727e4ab301052e7f965f6550122ac90b91fc874a24d5d1cf237987be42faeedd28ea714e0bd47e477da7cae6127779523270f91bd000d7d0e06da56192fe911460dc39081c0000000001985441a861000000000000102e5d8a0c0000009836a06b2b684b1866980c62e1d51ed6fcbecf522b8df4992400010000000000ce8ba0672e21c07240420f00000000000048656c6c6f20504850212057656c636f6d6520746f2053796d626f6c20776f726c6421
Transaction sent. Response:
{"message":"packet 9 was pushed to the network via /transactions"}
http://sym-test-03.opening-line.jp:3000/transactionStatus/0fb3f9e864204f110a46a03c2de8a9f101872021ba6ce25b54798fe7e3faca65
http://sym-test-03.opening-line.jp:3000/transactions/confirmed/0fb3f9e864204f110a46a03c2de8a9f101872021ba6ce25b54798fe7e3faca65
https://testnet.symbol.fyi/transactions/0fb3f9e864204f110a46a03c2de8a9f101872021ba6ce25b54798fe7e3faca65
```

もし、トランザクションの内容にエラーがある場合は`http://sym-test-03.opening-line.jp:3000/transactionStatus/[hash]`を調べてみてください。

成功したら以下のようなトランザクションが記録されます。

https://testnet.symbol.fyi/transactions/0FB3F9E864204F110A46A03C2DE8A9F101872021BA6CE25B54798FE7E3FACA65

# 最後に

今回はLaravelのバックエンドでSymbolトランザクションを実行したかったので試してみましたが、なかなかいい感じにできました。
途中、デッドラインがうまく指定できていなかったり、catjsonの指定が古いままでエラーが発生して進まないなど色々と壁にぶつかりましたが無事トランスファートランザクションが送信できたのでよかったです。
引き続き、[複雑なトランザクション](https://qiita.com/nem_takanobu/items/c136b80b97988da2d0e6#%E3%83%88%E3%83%A9%E3%83%B3%E3%82%B6%E3%82%AF%E3%82%B7%E3%83%A7%E3%83%B3%E3%82%BF%E3%82%A4%E3%83%97%E5%88%A5%E8%A8%98%E8%BF%B0%E4%BE%8B)も試してみたいと思います。

# ソースコード

今回使用したソースコードは以下に置いておきます。

https://github.com/ishidad2/tsunagi-functions-laravel-helpers

# エラーコード

https://qiita.com/nem_takanobu/items/276509fec42076564e77