トランザクション - Blockchain Symbol-SDK v3.2

# 前準備

変更しない値を一括で記述しておきます。アカウントはコードで作成したものでもウォレットで作成した物、どちらでもかまいません。

```SymConst.ts
import { Network } from 'symbol-sdk/symbol';

export class SymConst {
  /** RestGatewayURL */
  static readonly REST_GATEWAY_URL = 'https://sym-test-01.opening-line.jp:3001';
  /** ネットワーク識別 */
  static readonly NETWORK_IDENTIFIER = Network.TESTNET.name;
  /** カレントモザイクID */
  static readonly CURRENCY_MOSAIC_ID = BigInt('0x72C0212E67A08BCE');
  /** アリス秘密鍵 */
  static readonly ALICE_PRIVATE_KEY =
    'F10B0***********************************************************';
  /** ボブ秘密鍵 */
  static readonly BOB_PRIVATE_KEY =
    '6EBF9***********************************************************';
  /** ボブ公開鍵 */
  static readonly BOB_PUBLIC_KEY =
    '94EC711522B4B32A1B6A6ED61D86D1E3EE11AFB9B912A17F8983EED3808819FD';
  /** キャロル公開鍵 */
  static readonly CAROL_PUBLIC_KEY =
    '249B8ADE64EFF216D43BB655EF41DC1D7B8DDF96BD655749FFAF78BE3ACE7D77';
}
```

# トランザクション

簡単なトランスファートランザクションの発行です。


## アカウント生成

アリスからボブへの転送を考えているので、アリスは秘密鍵からアカウント生成します。ボブは公開鍵からのアドレスを生成します。

```mermaid
sequenceDiagram
    participant Alice
    participant Bob
    Alice->>Bob: 1XYM<br/>Hello, Symbol!!
```

```typescript
  // faced生成
  const facade = new SymbolFacade(SymConst.NETWORK_IDENTIFIER);

  // 送信者：アリス
  // 秘密鍵からアリスのキーペア生成
  const aliceKeyPair = new KeyPair(new PrivateKey(SymConst.ALICE_PRIVATE_KEY));

  // 受信者：ボブ
  // 文字列公開鍵からPublicKey生成
  const bobPublicKey = new PublicKey(
    Uint8Array.from(Buffer.from(SymConst.BOB_PUBLIC_KEY, 'hex'))
  );
  // ボブのアドレスを取得
  const bobAddress = new Address(
    facade.network.publicKeyToAddress(bobPublicKey)
  );
```


## トランザクションの生成

デッドラインは、承認され無かった場合の待ち時間で、未承認のまま期限が過ぎると取り消されます。最大6時間となっています。
転送モザイクは、配列になっていて複数モザイクを同時に転送できます。一度に転送できる上限があるのかは分かりませんが、1アカウントにつき1,000モザイクまでしか持てないので、1,000が上限になってくるかと思います。
メッセージは、平文(メッセージタイプ:`0x00`)で転送します。メッセージタイプの1バイトを含む1024バイトまで格納できます。

```typescript
  // デッドライン設定
  const deadlineTimestamp = facade.now().addHours(2).timestamp;

  // 転送モザイク設定
  const sendMosaics = [
    { mosaicId: SymConst.CURRENCY_MOSAIC_ID, amount: 1_000000n },
  ];

  // 平文メッセージ
  const messageData = new Uint8Array([
    0x00,
    ...new TextEncoder().encode('Hello, Symbol!!'),
  ]);
```

:::note alert
v3.2.0では`NetworkTimestamp`にバグがあり、タイムスタンプが正の整数で返ってきません。少し修正が必要です。
```diff:node_modules/symbol-sdk/src/NetworkTimestamp.js
		if (referenceDatetime < this.epoch)
			throw RangeError('timestamp cannot be before epoch');

-		const subtractDates = (lhs, rhs) => (lhs - rhs);
-		return (subtractDates(referenceDatetime, this.epoch) / this.timeUnits) | 0;
+		const differenceMilliseconds = referenceDatetime.getTime() - this.epoch.getTime();
+		return Math.trunc(differenceMilliseconds / this.timeUnits);
	}
}
```
:::

上記の値をセットして転送トランザクションを生成します。最後にトランザクション発行の手数料を設定しています。トランザクションサイズに100を掛ければ、大抵のノードが承認します。細かい手数料は`/network/fees/transaction`を参照してください。

```typescript
  // 転送トランザクション生成
  const transferTx = facade.transactionFactory.create({
    type: 'transfer_transaction_v1',
    signerPublicKey: aliceKeyPair.publicKey,
    recipientAddress: bobAddress,
    deadline: deadlineTimestamp,
    mosaics: sendMosaics,
    message: messageData,
  });
  transferTx.fee = new models.Amount(BigInt(transferTx.size * 100)); // 手数料
```


## 署名

アリスのキーペアで署名を生成して、トランザクションと合わせます。トランザクションハッシュもこのタイミングで取得出来ます。今回、特に用途はないです。

```typescript
  // アリス署名
  const transferTxSignature = facade.signTransaction(aliceKeyPair, transferTx);
  const transferTxPayloadJson = SymbolTransactionFactory.attachSignature(
    transferTx,
    transferTxSignature
  );
  const transferTxHash = facade.hashTransaction(transferTx);
```


## アナウンス

RESTにアナウンスします。一応レスポンスはありますが、「ノードに送信できたよ！」っていうレスポンスなので、ノード側でリジェクトされている可能性もあります。別途確認のコードを追加した方が良いです（今回は省略します）。アナウンス先のノードを接続先にしたウォレットを立ち上げておくと、エラーの場合、通知が出るのでテストする際は便利です。

```typescript
  // RESTにアナウンス
  const transactionsResponse = await fetch(
    new URL('/transactions', SymConst.REST_GATEWAY_URL),
    {
      method: 'PUT',
      headers: { 'Content-Type': 'application/json' },
      body: transferTxPayloadJson,
    }
  );

  // アナウンス結果を表示
  const transactionsResponseJson = await transactionsResponse.json();
  console.log(`restResponse      : ${transactionsResponseJson.message}`);
```

## コード例

```transferTx.ts
import {
  Address,
  KeyPair,
  NetworkTimestamp,
  SymbolFacade,
  SymbolTransactionFactory,
  models,
} from 'symbol-sdk/symbol';
import { SymConst } from './SymConst.js';
import { PrivateKey, PublicKey } from 'symbol-sdk';

try {
  // faced生成
  const facade = new SymbolFacade(SymConst.NETWORK_IDENTIFIER);

  // 送信者：アリス
  // 秘密鍵からアリスのキーペア生成
  const aliceKeyPair = new KeyPair(new PrivateKey(SymConst.ALICE_PRIVATE_KEY));

  // 受信者：ボブ
  // 文字列公開鍵からPublicKey生成
  const bobPublicKey = new PublicKey(
    Uint8Array.from(Buffer.from(SymConst.BOB_PUBLIC_KEY, 'hex'))
  );
  // ボブのアドレスを取得
  const bobAddress = new Address(
    facade.network.publicKeyToAddress(bobPublicKey)
  );

  // デッドライン設定
  const deadlineTimestamp = facade.now().addHours(2).timestamp;

  // 転送モザイク設定
  const sendMosaics = [
    { mosaicId: SymConst.CURRENCY_MOSAIC_ID, amount: 1_000000n },
  ];

  // 平文メッセージ
  const messageData = new Uint8Array([
    0x00,
    ...new TextEncoder().encode('Hello, Symbol!!'),
  ]);

  // 転送トランザクション生成
  const transferTx = facade.transactionFactory.create({
    type: 'transfer_transaction_v1',
    signerPublicKey: aliceKeyPair.publicKey,
    recipientAddress: bobAddress,
    deadline: deadlineTimestamp,
    mosaics: sendMosaics,
    message: messageData,
  });
  transferTx.fee = new models.Amount(BigInt(transferTx.size * 100)); // 手数料

  // 未署名トランザクションを表示
  console.log(
    `unsignedTx        : ${Buffer.from(transferTx.serialize())
      .toString('hex')
      .toUpperCase()}`
  );

  // アリス署名
  const transferTxSignature = facade.signTransaction(aliceKeyPair, transferTx);
  const transferTxPayloadJson = SymbolTransactionFactory.attachSignature(
    transferTx,
    transferTxSignature
  );
  const transferTxHash = facade.hashTransaction(transferTx);

  // 署名トランザクションを表示
  console.log(
    `signedTx          : ${Buffer.from(transferTx.serialize())
      .toString('hex')
      .toUpperCase()}`
  );
  // トランザクションハッシュを表示
  console.log(`txHash            : ${transferTxHash}`);

  // RESTにアナウンス
  const transactionsResponse = await fetch(
    new URL('/transactions', SymConst.REST_GATEWAY_URL),
    {
      method: 'PUT',
      headers: { 'Content-Type': 'application/json' },
      body: transferTxPayloadJson,
    }
  );

  // アナウンス結果を表示
  const transactionsResponseJson = await transactionsResponse.json();
  console.log(`restResponse      : ${transactionsResponseJson.message}`);
} catch (e) {
  console.error(e);
}
```


## メッセージを暗号化

メッセージを暗号化することも出来ます。暗号化用のクラスが用意されていて、アリスの秘密鍵とボブの公開鍵で暗号化。自動的にメッセージタイプ`0x01`が付与されます。ただ、v2の二重16進数化バグがあるので、バグが修正されたv3で暗号化した文章は現在のウォレットで復号できません。

```typescript
  const message = 'Hello Symbol!!';
  const aliceMsgEncoder = new MessageEncoder(aliceKeyPair);
  const messageData = aliceMsgEncoder.encode(
    bobPublicKey,
    new TextEncoder().encode(message)
  );
```


## メッセージにバイナリデータを格納

決まりはないのですが、メッセージタイプ:`0xFF`をバイナリデータとして扱います。メッセージタイプは特に決まりが無く、各アプリでどう扱うかに任せられます。

```typescript
const messageData = new Uint8Array([
  0xff,
  ...new TextEncoder().encode("Hello, Symbol!!"),
]);
```

![FireShot Capture 132 - Symbol Block Explorer - testnet.symbol.fyi.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3622558/6c943d9f-1a98-a3a5-3ea8-6e903cfb7b8d.jpeg)

![スクリーンショット 2024-01-14 152154.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3622558/3806cdd2-4988-cd69-2494-2220367ecd0a.png)

以下は、Zero埋めした1023バイトをメッセージに入れたトランザクションです。

https://testnet.symbol.fyi/transactions/3118C6B9E8BAD94CE875E420C8D603FB5A88E576562BF37CC1878F7DBB57F399