Blockchain Symbol でアグリゲートボンデッドトランザクションを発行する

前回のアグリゲートコンプリートトランザクションは、複数の相手に一括で送信することができるトランザクションでした。

https://qiita.com/ccHarvestasya/items/54eda260f76a4b44c394

今回は、アグリゲート**ボンデッドト**ランザクションです。
他アカウントから送金させるトランザクションを発行することが出来ます。もちろん勝手に送金させる事は出来ず、相手の署名が必要となります。

代金を請求するとか、複数のトランザクションを固めて送信するのに使えます。
便利な反面、過去これを悪用してアカウントからXYMを抜き取る問題が発生しました。

***署名すると、自アカウントからモザイクが動くということを認識しておいて下さい。***

***署名すると、自アカウントからモザイクが動くということを認識しておいて下さい。***

---

それでは、以下の２つのトランザクションを内包するアグリゲートトランザクションを作成してみましょう。

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

署名者はアリス。
ボブからキャロルへ転送はボブの署名が必要となります。

```mermaid
sequenceDiagram
    participant Alice
    participant Bob
    participant Carol
    Alice->>Bob: signature request
    Bob->>Alice: signature response
    par InnerTx1
        Alice->>Bob: 1XYM<br/>Hi Bob! I'm Alice!
    and InnerTx2
        Bob->>Carol: 2XYM<br/>Hi Carol! I'm Bob!
    end
```

## 必要な情報を設定

ボブからキャロルへの転送がありますが、ボブの秘密鍵は知らない状態とします。

```typescript
/** APIノードURL */
const NODE = "http://sym-test-01.opening-line.jp:3000";
/** ネットワーク識別 */
const NETWORK_IDENTIFIER = "testnet";
/** カレントモザイクID */
const CURRENCY_MOSAIC_ID = BigInt("0x72C0212E67A08BCE");

/** アリス秘密鍵 */
const ALICE_PRIVATE_KEY =
  "****************************************************************";
/** ボブ公開鍵 */
const BOB_PUBLIC_KEY =
  "94EC711522B4B32A1B6A6ED61D86D1E3EE11AFB9B912A17F8983EED3808819FD";
/** キャロル公開鍵 */
const CAROL_PUBLIC_KEY =
  "249B8ADE64EFF216D43BB655EF41DC1D7B8DDF96BD655749FFAF78BE3ACE7D77";
```

## アカウント生成

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

// 秘密鍵からアリスアカウント生成
const aliceKeyPair = new symbolSdk.symbol.KeyPair(
  new symbolSdk.PrivateKey(ALICE_PRIVATE_KEY)
);
const aliceAddress = facade.network.publicKeyToAddress(
  aliceKeyPair.publicKey
) as Address;
// 公開鍵からボブアドレス生成
const bobPublicKey = new symbolSdk.symbol.PublicKey(
  Uint8Array.from(Buffer.from(BOB_PUBLIC_KEY, "hex"))
);
const bobAddress: Address = facade.network.publicKeyToAddress(bobPublicKey);
// 公開鍵からキャロルアドレス生成
const carolPublicKey = new symbolSdk.symbol.PublicKey(
  Uint8Array.from(Buffer.from(CAROL_PUBLIC_KEY, "hex"))
);
const carolAddress: Address = facade.network.publicKeyToAddress(carolPublicKey);

console.log(`aliceAddress      : ${aliceAddress}`);
console.log(`alicePublicKey    : ${aliceKeyPair.publicKey}`);
console.log(`bobAddress        : ${bobAddress}`);
console.log(`carolAddress      : ${carolAddress}`);
```



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

以下の二つの内部トランザクションを生成します。

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

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

console.log(
  `innerTx1          : ${Buffer.from(innerTx1.serialize())
    .toString("hex")
    .toUpperCase()}`
);
console.log(
  `innerTx2          : ${Buffer.from(innerTx2.serialize())
    .toString("hex")
    .toUpperCase()}`
);
```

## アグリゲートボンデッドトランザクション生成
先ほど生成した二つの内部トランザクションを格納するトランザクションを生成します。
```typescript
// 内部トランザクションハッシュ取得
const innerTxs = [innerTx1, innerTx2];
const txHash = symbolSdk.facade.SymbolFacade.hashEmbeddedTransactions(innerTxs);
// deadline設定
const networkTimestamp = new symbolSdk.symbol.NetworkTimestamp(
  facade.network.fromDatetime(new Date())
);
const deadline = networkTimestamp.addHours(2).timestamp;
// アグリゲートトランザクション生成
const aggregateTx = facade.transactionFactory.create({
  type: "aggregate_bonded_transaction_v2",
  signerPublicKey: aliceKeyPair.publicKey,
  deadline,
  transactions: innerTxs,
  transactionsHash: txHash,
}) as AggregateBondedTransactionV2;
aggregateTx.fee = new symbolSdk.symbol.Amount(BigInt(aggregateTx.size * 100));

console.log(
  `aggregateTx       : ${Buffer.from(aggregateTx.serialize())
    .toString("hex")
    .toUpperCase()}`
);
```

## 署名
アリスで署名します。
```typescript
// 署名
const aggregateTxSignature = facade.signTransaction(aliceKeyPair, aggregateTx);
const aggregatePayloadJson =
  symbolSdk.symbol.SymbolTransactionFactory.attachSignature(
    aggregateTx,
    aggregateTxSignature
  );
const aggregateTxHash = facade.hashTransaction(aggregateTx);

console.log(
  `signedTx          : ${Buffer.from(aggregateTx.serialize())
    .toString("hex")
    .toUpperCase()}`
);
console.log(`txHash            : ${aggregateTxHash}`);
```

## ハッシュロックトランザクション生成
ボブの署名を貰うまでトランザクションが承認されないよう、ロックするためのトランザクションを生成します。
```typescript
// ハッシュロックトランザクション生成
const blockDuration = new symbolSdk.symbol.BlockDuration(6n); // 6ブロックで期限切れ
const hashLockTx = facade.transactionFactory.create({
  type: "hash_lock_transaction_v1",
  signerPublicKey: aliceKeyPair.publicKey,
  deadline,
  mosaic: { mosaicId: CURRENCY_MOSAIC_ID, amount: 10000000n }, // 10XYMロック
  duration: blockDuration,
  hash: aggregateTxHash,
}) as HashLockTransactionV1;
hashLockTx.fee = new symbolSdk.symbol.Amount(BigInt(hashLockTx.size * 100));

console.log(
  `hashLockTx        : ${Buffer.from(hashLockTx.serialize())
    .toString("hex")
    .toUpperCase()}`
);
```

## ハッシュトランザクションをアナウンス
先にハッシュロックをアナウンスして、承認を待ちます。
```typescript
// ハッシュロックトランザクションをRESTにアナウンス
const hashLockTxResponse = await fetch(new URL("/transactions", NODE), {
  method: "PUT",
  headers: { "Content-Type": "application/json" },
  body: hashLockPayloadJson,
});

// ハッシュロックトランザクション承認待ち
for (let i = 0; i < 100; i++) {
  await new Promise((resolve) => setTimeout(resolve, 1000));
  const hashLockStatus = await fetch(
    new URL(`/transactionStatus/${hashLockTxHash}`, NODE),
    {
      method: "GET",
      headers: { "Content-Type": "application/json" },
    }
  );
  if ((await hashLockStatus.json()).group === "confirmed") {
    break;
  }
}
```

## アグリゲートボンデッドトランザクションをアナウンス
```typescript
// アグリゲートボンデッドトランザクションをRESTにアナウンス
const aggregateTxResponse = await fetch(
  new URL("/transactions/partial", NODE),
  {
    method: "PUT",
    headers: { "Content-Type": "application/json" },
    body: aggregatePayloadJson,
  }
);
const responseJson = await aggregateTxResponse.json();

console.log(`restResponse      : ${responseJson.message}`);
```

## ボブの署名
ボブの署名はウォレットで。コードでも出来ますが、その話はまた今度。。。

## 全文

```typescript
import symbolSdk from 'symbol-sdk';
import {
  Address,
  AggregateBondedTransactionV2,
  HashLockTransactionV1,
} from 'symbol-sdk/ts/src/symbol/models';

/** APIノードURL */
const NODE = 'http://sym-test-01.opening-line.jp:3000';
/** ネットワーク識別 */
const NETWORK_IDENTIFIER = 'testnet';
/** カレントモザイクID */
const CURRENCY_MOSAIC_ID = BigInt('0x72C0212E67A08BCE');

/** アリス秘密鍵 */
const ALICE_PRIVATE_KEY = "****************************************************************";
/** ボブ公開鍵 */
const BOB_PUBLIC_KEY = '94EC711522B4B32A1B6A6ED61D86D1E3EE11AFB9B912A17F8983EED3808819FD';
/** キャロル公開鍵 */
const CAROL_PUBLIC_KEY = '249B8ADE64EFF216D43BB655EF41DC1D7B8DDF96BD655749FFAF78BE3ACE7D77';

// faced生成
const facade = new symbolSdk.facade.SymbolFacade(NETWORK_IDENTIFIER);

// 秘密鍵からアリスアカウント生成
const aliceKeyPair = new symbolSdk.symbol.KeyPair(new symbolSdk.PrivateKey(ALICE_PRIVATE_KEY));
const aliceAddress = facade.network.publicKeyToAddress(aliceKeyPair.publicKey) as Address;
// 公開鍵からボブアドレス生成
const bobPublicKey = new symbolSdk.symbol.PublicKey(
  Uint8Array.from(Buffer.from(BOB_PUBLIC_KEY, 'hex'))
);
const bobAddress: Address = facade.network.publicKeyToAddress(bobPublicKey);
// 公開鍵からキャロルアドレス生成
const carolPublicKey = new symbolSdk.symbol.PublicKey(
  Uint8Array.from(Buffer.from(CAROL_PUBLIC_KEY, 'hex'))
);
const carolAddress: Address = facade.network.publicKeyToAddress(carolPublicKey);

console.log(`aliceAddress      : ${aliceAddress}`);
console.log(`alicePublicKey    : ${aliceKeyPair.publicKey}`);
console.log(`bobAddress        : ${bobAddress}`);
console.log(`carolAddress      : ${carolAddress}`);

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

console.log(
  `innerTx1          : ${Buffer.from(innerTx1.serialize()).toString('hex').toUpperCase()}`
);
console.log(
  `innerTx2          : ${Buffer.from(innerTx2.serialize()).toString('hex').toUpperCase()}`
);

// 内部トランザクションハッシュ取得
const innerTxs = [innerTx1, innerTx2];
const txHash = symbolSdk.facade.SymbolFacade.hashEmbeddedTransactions(innerTxs);
// deadline設定(2時間)
const networkTimestamp = new symbolSdk.symbol.NetworkTimestamp(
  facade.network.fromDatetime(new Date())
);
const deadline = networkTimestamp.addHours(2).timestamp;
// アグリゲートトランザクション生成
const aggregateTx = facade.transactionFactory.create({
  type: 'aggregate_bonded_transaction_v2',
  signerPublicKey: aliceKeyPair.publicKey,
  deadline,
  transactions: innerTxs,
  transactionsHash: txHash,
}) as AggregateBondedTransactionV2;
aggregateTx.fee = new symbolSdk.symbol.Amount(BigInt(aggregateTx.size * 100));

console.log(
  `aggregateTx       : ${Buffer.from(aggregateTx.serialize()).toString('hex').toUpperCase()}`
);

// 署名
const aggregateTxSignature = facade.signTransaction(aliceKeyPair, aggregateTx);
const aggregatePayloadJson = symbolSdk.symbol.SymbolTransactionFactory.attachSignature(
  aggregateTx,
  aggregateTxSignature
);
const aggregateTxHash = facade.hashTransaction(aggregateTx);

console.log(
  `signedTx          : ${Buffer.from(aggregateTx.serialize()).toString('hex').toUpperCase()}`
);
console.log(`txHash            : ${aggregateTxHash}`);

// ハッシュロックトランザクション生成
const blockDuration = new symbolSdk.symbol.BlockDuration(2880n); // 2880ブロックで期限切れ
const hashLockTx = facade.transactionFactory.create({
  type: 'hash_lock_transaction_v1',
  signerPublicKey: aliceKeyPair.publicKey,
  deadline,
  mosaic: { mosaicId: CURRENCY_MOSAIC_ID, amount: 10000000n }, // 10XYMロック
  duration: blockDuration,
  hash: aggregateTxHash,
}) as HashLockTransactionV1;
hashLockTx.fee = new symbolSdk.symbol.Amount(BigInt(hashLockTx.size * 100));

console.log(
  `hashLockTx        : ${Buffer.from(hashLockTx.serialize()).toString('hex').toUpperCase()}`
);

// ハッシュロックトランザクション署名
const hashLockTxSignature = facade.signTransaction(aliceKeyPair, hashLockTx);
const hashLockPayloadJson = symbolSdk.symbol.SymbolTransactionFactory.attachSignature(
  hashLockTx,
  hashLockTxSignature
);
const hashLockTxHash = facade.hashTransaction(hashLockTx);

console.log(
  `hashLockTx        : ${Buffer.from(hashLockTx.serialize()).toString('hex').toUpperCase()}`
);
console.log(`hashLockTxHash    : ${hashLockTxHash}`);

// ハッシュロックトランザクションをRESTにアナウンス
const hashLockTxResponse = await fetch(new URL('/transactions', NODE), {
  method: 'PUT',
  headers: { 'Content-Type': 'application/json' },
  body: hashLockPayloadJson,
});
const hashLockTxResponseJson = await hashLockTxResponse.json();

console.log(`restResponse      : ${hashLockTxResponseJson.message}`);

// ハッシュロックトランザクション承認待ち
for (let i = 0; i < 100; i++) {
  await new Promise((resolve) => setTimeout(resolve, 1000));
  const hashLockStatus = await fetch(new URL(`/transactionStatus/${hashLockTxHash}`, NODE), {
    method: 'GET',
    headers: { 'Content-Type': 'application/json' },
  });
  if ((await hashLockStatus.json()).group === 'confirmed') {
    break;
  }
}

// アグリゲートボンデッドトランザクションをRESTにアナウンス
const aggregateTxResponse = await fetch(new URL('/transactions/partial', NODE), {
  method: 'PUT',
  headers: { 'Content-Type': 'application/json' },
  body: aggregatePayloadJson,
});
const responseJson = await aggregateTxResponse.json();

console.log(`restResponse      : ${responseJson.message}`);
```