アグリゲートコンプリートトランザクション - Blockchain Symbol-SDK v3.2

# アグリゲートコンプリートトランザクション

複数のトランザクションを1つにまとめて発行することが出来ます。署名者から複数の受信者へ送信するトランザクションを「アグリゲートコンプリートトランザクション」と言います。

## アカウント生成

+ アリスからボブへ
+ アリスからキャロルへ

上記の2つの転送を1つのトランザクションにまとめて発行します。署名者はアリスなので秘密鍵からアカウント生成します。ボブとキャロルは受信者なので公開鍵からのアドレスを生成します。

```mermaid
sequenceDiagram
    participant Alice
    participant Bob
    participant Carol
    Alice->>Bob: 1XYM<br/>Hello Bob!!
    activate Alice
    Alice->>Carol: 1.5XYM<br/>Hello Carol!!
    deactivate Alice
```

```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)
  );

  // 受信者：キャロル
  // 文字列公開鍵からPublicKey生成
  const carolPublicKey = new PublicKey(
    Uint8Array.from(Buffer.from(SymConst.CAROL_PUBLIC_KEY, 'hex'))
  );
  const carolAddress = new Address(
    facade.network.publicKeyToAddress(carolPublicKey)
  );
```


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

- アリスからボブへ1XYM転送するトランザクション
- アリスからキャロルへ1.5XYM転送するトランザクション

上記の二つの内部トランザクションを生成します。内部トランザクションは`createEmbedded`を使用して生成します。デッドラインと手数料はアグリゲートトランザクションの方に設定するため、内部トランザクションには不要です。

```typescript
  // 内部トランザクション生成
  //Alice -> Bob
  const innerTx1 = facade.transactionFactory.createEmbedded({
    type: 'transfer_transaction_v1',
    signerPublicKey: aliceKeyPair.publicKey,
    recipientAddress: bobAddress,
    mosaics: [{ mosaicId: SymConst.CURRENCY_MOSAIC_ID, amount: 1_000000n }],
    message: new Uint8Array([0x00, ...new TextEncoder().encode('Hello Bob!!')]),
  });
  //Alice -> Carol
  const innerTx2 = facade.transactionFactory.createEmbedded({
    type: 'transfer_transaction_v1',
    signerPublicKey: aliceKeyPair.publicKey,
    recipientAddress: carolAddress,
    mosaics: [{ mosaicId: SymConst.CURRENCY_MOSAIC_ID, amount: 1_500000n }],
    message: new Uint8Array([
      0x00,
      ...new TextEncoder().encode('Hello Carol!!'),
    ]),
  });
```


## アグリゲートコンプリートトランザクション生成

先ほど生成した2つの内部トランザクションを格納するトランザクションを生成します。アグリゲートトランザクションに設定するための内部トランザクションのハッシュ値を取得します。これで内部トランザクションのすり替えが不可能になります。

```typescript
  // 内部トランザクションハッシュ取得
  const innerTxs = [innerTx1, innerTx2];
  const innerTxsHash = SymbolFacade.hashEmbeddedTransactions(innerTxs);

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

  // アグリゲートトランザクション生成
  const aggregateTx = facade.transactionFactory.create({
    type: 'aggregate_complete_transaction_v2',
    signerPublicKey: aliceKeyPair.publicKey,
    deadline: deadlineTimestamp,
    transactions: innerTxs,
    transactionsHash: innerTxsHash,
  });
  aggregateTx.fee = new models.Amount(BigInt(aggregateTx.size * 100)); // 手数料
```


## 署名

アリスで署名します。アリスからの転送のみなので、アリスの署名だけです。

```typescript
  // アリス署名
  const aggregateTxSignature = facade.signTransaction(
    aliceKeyPair,
    aggregateTx
  );
  const aggregateTxPayloadJson = SymbolTransactionFactory.attachSignature(
    aggregateTx,
    aggregateTxSignature
  );
  const aggregateTxHash = facade.hashTransaction(aggregateTx);
```

## アナウンス

RESTにアナウンスします。

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

## コード例
```aggregateCompleteTx.ts
import {
  Address,
  KeyPair,
  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)
  );

  // 受信者：キャロル
  // 文字列公開鍵からPublicKey生成
  const carolPublicKey = new PublicKey(
    Uint8Array.from(Buffer.from(SymConst.CAROL_PUBLIC_KEY, 'hex'))
  );
  const carolAddress = new Address(
    facade.network.publicKeyToAddress(carolPublicKey)
  );

  // 内部トランザクション生成
  //Alice -> Bob
  const innerTx1 = facade.transactionFactory.createEmbedded({
    type: 'transfer_transaction_v1',
    signerPublicKey: aliceKeyPair.publicKey,
    recipientAddress: bobAddress,
    mosaics: [{ mosaicId: SymConst.CURRENCY_MOSAIC_ID, amount: 1_000000n }],
    message: new Uint8Array([0x00, ...new TextEncoder().encode('Hello Bob!!')]),
  });
  //Alice -> Carol
  const innerTx2 = facade.transactionFactory.createEmbedded({
    type: 'transfer_transaction_v1',
    signerPublicKey: aliceKeyPair.publicKey,
    recipientAddress: carolAddress,
    mosaics: [{ mosaicId: SymConst.CURRENCY_MOSAIC_ID, amount: 1_500000n }],
    message: new Uint8Array([
      0x00,
      ...new TextEncoder().encode('Hello Carol!!'),
    ]),
  });

  // 内部トランザクションハッシュ取得
  const innerTxs = [innerTx1, innerTx2];
  const innerTxsHash = SymbolFacade.hashEmbeddedTransactions(innerTxs);

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

  // アグリゲートトランザクション生成
  const aggregateTx = facade.transactionFactory.create({
    type: 'aggregate_complete_transaction_v2',
    signerPublicKey: aliceKeyPair.publicKey,
    deadline: deadlineTimestamp,
    transactions: innerTxs,
    transactionsHash: innerTxsHash,
  });
  aggregateTx.fee = new models.Amount(BigInt(aggregateTx.size * 100)); // 手数料

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

  // アリス署名
  const aggregateTxSignature = facade.signTransaction(
    aliceKeyPair,
    aggregateTx
  );
  const aggregateTxPayloadJson = SymbolTransactionFactory.attachSignature(
    aggregateTx,
    aggregateTxSignature
  );
  const aggregateTxHash = facade.hashTransaction(aggregateTx);

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

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

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

## 関連記事

https://qiita.com/ccHarvestasya/items/d6b87c47aade9d744bbe

https://qiita.com/ccHarvestasya/items/e29035ca16bb88bf1922