Symbol SDK JS v3 でAggregateTransaction

# はじめに
SymbolのAggregateTransactionでは最大100個までのトランザクションを集約し、それらをまとめてアナウンスできます。本記事ではAggregateCompleteとAggregateBondedをSDK v3で構築する方法を解説します。Aggregateの詳しい解説は行いません。
公式ガイドを読んでください

https://docs.symbol.dev/ja/concepts/aggregate-transaction.html

- SDK v3のインストールはコチラ

https://qiita.com/Toshi_ma/items/d3f91aabc57d7752f339

- v3でのTransferTransactionの構築はこちら

https://qiita.com/Toshi_ma/private/db2a81e4973274370f0d

本記事でのAggregateはシンプルなTransferTransactionを使用します。前記事で解説もしているので一度目を通していただければ幸いです。

## AggregateCompleteTransaction

```js
import sdk from './sdk/javascript/src/index.js';
import fetch from 'node-fetch';

const netWork = new sdk.symbol.Network(
    'testnet',
	0x98,
	new Date(Date.UTC(2022, 9, 31, 21, 7, 47)),
	new sdk.Hash256('49D6E1CE276A85B70EAFE52349AACCA389302E7A9754BCF1221E79494FC665A4')
)

const facade = new sdk.facade.SymbolFacade(netWork);

const alicePrivateKey = new sdk.PrivateKey('ALICE_PRIVATE_KEY');
const aliceKeyPair = new sdk.symbol.KeyPair(alicePrivateKey);
const aliceAddress = facade.network.publicKeyToAddress(aliceKeyPair.publicKey);
const alicePlainAddress = new sdk.symbol.Address(aliceAddress).toString();

const bobPrivateKey = new sdk.PrivateKey('BOB_PRIVATE_KEY');
const bobKeyPair = new sdk.symbol.KeyPair(bobPrivateKey);
const bobAddress = facade.network.publicKeyToAddress(bobKeyPair.publicKey);
const bobPlainAddress = new sdk.symbol.Address(bobAddress).toString();

const deadline = new sdk.symbol.NetworkTimestamp(facade.network.fromDatetime(Date.now())).addHours(2).timestamp;

const aliceTransaction = facade.transactionFactory.createEmbedded({
	type: 'transfer_transaction_v1',
	signerPublicKey: aliceKeyPair.publicKey,
	recipientAddress: bobPlainAddress,
    mosaics: [
		{ mosaicId: 0x72C0212E67A08BCEn, amount: 1_000000n }
	],
    message: [0,...(new TextEncoder('utf-8')).encode('from alice to bob')],
});

const bobTransaction = facade.transactionFactory.createEmbedded({
	type: 'transfer_transaction_v1',
	signerPublicKey: bobKeyPair.publicKey,
	recipientAddress: alicePlainAddress,
    mosaics: [
		{ mosaicId: 0x72C0212E67A08BCEn, amount: 1_000000n }
	],
    message: [0,...(new TextEncoder('utf-8')).encode('from bob to alice')],
});

const embeddedTransactions = [aliceTransaction, bobTransaction]
const merkleHash = facade.constructor.hashEmbeddedTransactions(embeddedTransactions);

const aggreagteTransaction = facade.transactionFactory.create({
    type: 'aggregate_complete_transaction_v2',
    signerPublicKey: aliceKeyPair.publicKey,
    fee: 1_000000n,
    deadline,
    transactionsHash: merkleHash,
    transactions: embeddedTransactions
});

const aliceSignature = facade.signTransaction(aliceKeyPair, aggreagteTransaction);
facade.transactionFactory.constructor.attachSignature(aggreagteTransaction, aliceSignature);

const transactionHash = facade.hashTransaction(aggreagteTransaction).bytes;
const bobCosignature = new sdk.symbol.Cosignature();
bobCosignature.version = 0n;
bobCosignature.signature = new sdk.symbol.Signature(bobKeyPair.sign(transactionHash).bytes);
bobCosignature.signerPublicKey = new sdk.symbol.PublicKey(bobKeyPair.publicKey.bytes);

aggreagteTransaction.cosignatures.push(bobCosignature);
const jsonPayload = `{"payload": "${sdk.utils.uint8ToHex(aggreagteTransaction.serialize())}"}`;

(async()=> {
    const res = await fetch("NODE_URL", {
        method: 'put',
        body: jsonPayload  ,
        headers: {'Content-Type': 'application/json'}
    })
    console.log(await res.json());
})();
```

## 簡単な解説

アグリゲートトランザクションのインナートランザクションは`create()`ではなく`createEmbedded()`を使います。

```js
const aliceTransaction = facade.transactionFactory.createEmbedded({
	type: 'transfer_transaction_v1',
	signerPublicKey: aliceKeyPair.publicKey,
	recipientAddress: bobPlainAddress,
    mosaics: [
		{ mosaicId: 0x72C0212E67A08BCEn, amount: 1_000000n }
	],
    message: [0,...(new TextEncoder('utf-8')).encode('from alice to bob')],
});
```

すでにv2でアグリゲートトランザクションを使ったことがある方はさほど不要かと思いますが、v2との大きな違いはv3のほうが連署の追加が容易だと思われることです。

```js
const transactionHash = facade.hashTransaction(aggreagteTransaction).bytes;
const bobCosignature = new sdk.symbol.Cosignature();
bobCosignature.version = 0n;
bobCosignature.signature = new sdk.symbol.Signature(bobKeyPair.sign(transactionHash).bytes);
bobCosignature.signerPublicKey = new sdk.symbol.PublicKey(bobKeyPair.publicKey.bytes);

aggreagteTransaction.cosignatures.push(bobCosignature);
```
アグリゲートトランザクションのAlice署名後のハッシュを算出し、Bobが署名しています。
その署名を持ってCosignatureを作成し、アグリゲートに追加。

注意点としては
```js
bobCosignature.signature = bobKeyPair.sign(transactionHash);
bobCosignature.signerPublicKey = bobKeyPair.publicKey.bytes;
```
では無い点です。

Cosignature.signature と bobKeyPair.sign()の戻り値は同じSignatureというクラス名ですが実態は違います。なのでbytesを取り出して再構築する必要があります。（公開鍵も同じ）


```js
const jsonPayload = `{"payload": "${sdk.utils.uint8ToHex(aggreagteTransaction.serialize())}"}`;
```
あとはアナウンス用のJsonを自作し、アナウンスするだけです。

たいした解説ではありませんが、基本的にv2でやってたことをv3でも使いたいというニーズへの記事のためこれぐらいで終わります。

## AggregateBondedTransaction

ボンデッドとはBobの秘密鍵は分からず、Bobの連署を待つ方法です。例のごとく全文。


```js
import sdk from './sdk/javascript/src/index.js';
import fetch from 'node-fetch';
import WebSocket from 'ws';

const netWork = new sdk.symbol.Network(
    'testnet',
	0x98,
	new Date(Date.UTC(2022, 9, 31, 21, 7, 47)),
	new sdk.Hash256('49D6E1CE276A85B70EAFE52349AACCA389302E7A9754BCF1221E79494FC665A4')
)

const NODE = "NODE_URL";
const WS_NODE = NODE.replace("https://", "wss://") + "/ws";
const ws = new WebSocket(WS_NODE);
const facade = new sdk.facade.SymbolFacade(netWork);

const alicePrivateKey = new sdk.PrivateKey('ALICE_PRIVATE_KEY');
const aliceKeyPair = new sdk.symbol.KeyPair(alicePrivateKey);
const aliceAddress = facade.network.publicKeyToAddress(aliceKeyPair.publicKey);
const alicePlainAddress = new sdk.symbol.Address(aliceAddress).toString();

const bobPublicKey = new sdk.PublicKey(sdk.utils.hexToUint8('BOB_PUBLIC_KEY'));
const bobAddress = facade.network.publicKeyToAddress(bobPublicKey);

const deadline = new sdk.symbol.NetworkTimestamp(facade.network.fromDatetime(Date.now())).addHours(2).timestamp;

const aliceTransaction = facade.transactionFactory.createEmbedded({
	type: 'transfer_transaction_v1',
	signerPublicKey: aliceKeyPair.publicKey,
	recipientAddress: bobAddress,
    mosaics: [
		{ mosaicId: 0x72C0212E67A08BCEn, amount: 1_000000n }
	],
    message: [0,...(new TextEncoder('utf-8')).encode('from alice to bob')],
});

const bobTransaction = facade.transactionFactory.createEmbedded({
	type: 'transfer_transaction_v1',
	signerPublicKey: bobPublicKey,
	recipientAddress: aliceAddress,
    mosaics: [
		{ mosaicId: 0x72C0212E67A08BCEn, amount: 1_000000n }
	],
    message: [0,...(new TextEncoder('utf-8')).encode('from bob to alice')],
});

const embeddedTransactions = [aliceTransaction, bobTransaction]
const merkleHash = facade.constructor.hashEmbeddedTransactions(embeddedTransactions);

const aggreagteTransaction = facade.transactionFactory.create({
    type: 'aggregate_bonded_transaction_v2',
    signerPublicKey: aliceKeyPair.publicKey,
    deadline,
    transactionsHash: merkleHash,
    transactions: embeddedTransactions
});
aggreagteTransaction.fee = new sdk.symbol.Amount(BigInt(aggreagteTransaction.size * 100));

const aliceSignature = facade.signTransaction(aliceKeyPair, aggreagteTransaction);
const aggregateTransactionPayload = facade.transactionFactory.constructor.attachSignature(aggreagteTransaction, aliceSignature);

const transactionHash = facade.hashTransaction(aggreagteTransaction).bytes;
const hashLockTransaction = facade.transactionFactory.create({
	type: 'hash_lock_transaction_v1',
    mosaic: { mosaicId: 0x72C0212E67A08BCEn, amount: 10_000000n },
	duration: 5760n,
	hash: transactionHash,
	signerPublicKey: aliceKeyPair.publicKey,
    deadline,
});
hashLockTransaction.fee = new sdk.symbol.Amount(BigInt(hashLockTransaction.size * 100));

const aliceHashLockSignature = facade.signTransaction(aliceKeyPair, hashLockTransaction);
const hasuLockTransactionPayload = facade.transactionFactory.constructor.attachSignature(hashLockTransaction, aliceHashLockSignature);

const announce = async (url, payload)=>{
    const res = await fetch("NODE_URL", {
        method: 'put',
        body: jsonPayload  ,
        headers: {'Content-Type': 'application/json'}
    })
    console.log(await res.json());
}

announce(NODE + "/transactions", hasuLockTransactionPayload).then(()=>{
    ws.onopen = function (e) {
        console.log("connect");
    };
    
    ws.onmessage=function(event){
        const response=JSON.parse(event.data);    
        if('uid' in response){ 
            const uid=response.uid;
            const body = '{"uid":"' + uid +'","subscribe":"block"}';
            const transaction= `{"uid":"${uid}","subscribe":"confirmedAdded/${alicePlainAddress}"}`
            ws.send(body);
            ws.send(transaction);
        }
    
        if(response.topic ==`confirmedAdded/${alicePlainAddress}`){
            if(sdk.utils.uint8ToHex(transactionHash) == response.data.transaction.hash) {
                announce(NODE + "/transactions/partial", aggregateTransactionPayload);
                ws.close();
            }
        }
    };    
});
```

### 準備
```js
const NODE = "NODE_URL";
const WS_NODE = NODE.replace("https://", "wss://") + "/ws";
const ws = new WebSocket(WS_NODE);
const facade = new sdk.facade.SymbolFacade(netWork);
```
アグリゲートボンデッドではハッシュロックトランザクションの承認を待ってアナウンスするためウェブソケットを使います。

```js
const alicePrivateKey = new sdk.PrivateKey('ALICE_PRIVATE_KEY');
const aliceKeyPair = new sdk.symbol.KeyPair(alicePrivateKey);
const aliceAddress = facade.network.publicKeyToAddress(aliceKeyPair.publicKey);
const alicePlainAddress = new sdk.symbol.Address(aliceAddress).toString();

const bobPublicKey = new sdk.PublicKey(sdk.utils.hexToUint8('BOB_PUBLIC_KEY'));
const bobAddress = facade.network.publicKeyToAddress(bobPublicKey);
```
AliceとBobの準備をしますがBobの秘密鍵は分かりません。そのため公開鍵からアドレスを生成します。Bobの連署を必要とするので公開鍵は必須です。

### Transaction構築

```js
const aggreagteTransaction = facade.transactionFactory.create({
    type: 'aggregate_bonded_transaction_v2',
    signerPublicKey: aliceKeyPair.publicKey,
    deadline,
    transactionsHash: merkleHash,
    transactions: embeddedTransactions
});
aggreagteTransaction.fee = new sdk.symbol.Amount(BigInt(aggreagteTransaction.size * 100));
```
インナートランザクションはコンプリートと同じですがアグリゲートボンデッドの構築時は
`type: 'aggregate_bonded_transaction_v2',`とします。

```js
const transactionHash = facade.hashTransaction(aggreagteTransaction).bytes;
const hashLockTransaction = facade.transactionFactory.create({
	type: 'hash_lock_transaction_v1',
    mosaic: { mosaicId: 0x72C0212E67A08BCEn, amount: 10_000000n },
	duration: 5760n,
	hash: transactionHash,
	signerPublicKey: aliceKeyPair.publicKey,
    deadline,
});
hashLockTransaction.fee = new sdk.symbol.Amount(BigInt(hashLockTransaction.size * 100));

const aliceHashLockSignature = facade.signTransaction(aliceKeyPair, hashLockTransaction);
const hasuLockTransactionPayload = facade.transactionFactory.constructor.attachSignature(hashLockTransaction, aliceHashLockSignature);
```
署名後のアグリゲートトランザクションをハッシュ化しそのハッシュを持って、ハッシュロックトランザクションを構築します。

最後にハッシュロックのアナウンス、承認を待ってアグリゲートボンデッドをアナウンスします。
```js
announce(NODE + "/transactions", hasuLockTransactionPayload).then(()=>{
    ws.onopen = function (e) {
        console.log("connect");
    };
    
    ws.onmessage=function(event){
        const response=JSON.parse(event.data);    
        if('uid' in response){ 
            const uid=response.uid;
            const body = '{"uid":"' + uid +'","subscribe":"block"}';
            const transaction= `{"uid":"${uid}","subscribe":"confirmedAdded/${alicePlainAddress}"}`
            ws.send(body);
            ws.send(transaction);
        }
    
        if(response.topic ==`confirmedAdded/${alicePlainAddress}`){
            if(sdk.utils.uint8ToHex(transactionHash) == response.data.transaction.hash) {
                announce(NODE + "/transactions/partial", aggregateTransactionPayload);
                ws.close();
            }
        }
    };    
});
```
注意点はアナウンス先のエンドポイントは`/transactions/partial`です。

これでBobの連署待ちになります。

以上で、v3でのアグリゲートの記事を終えます。

webcoketについて、以下の記事を参考にしました

https://qiita.com/bootarouapp/items/dfe55168986e63c28321