symbol-sdk@3.2.1でオフラインでアグリゲートコンプリートを完成させる

# はじめに

今回は、オフラインで、自分と他者の署名が必要なアグリゲートコンプリートトランザクションを作成してみようと思います。

登場人物はこんな感じです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172833/95bee689-05a2-7e17-0ddd-519e8d8e88f1.png)

この場合、アリスとボブの署名が要ります。

今回のシナリオでは、アリスがトランザクションを作成してボブに渡し、ボブが連署してネットワークに送信するような感じでやっていきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172833/c40d92bc-61cb-effa-2008-4d84f4161c30.png)

アリスからボブへ渡すデータの形式は、16進数文字列のトランザクションデータにします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172833/c3be5d59-9457-0e18-b897-234ba1a5d2cb.png)

# アリス側

## 全部

### アリスのターン

```javascript
import {
    PrivateKey,
    PublicKey,
} from 'symbol-sdk';
import {
    Address,
    KeyPair,
    Network,
    SymbolFacade,
} from 'symbol-sdk/symbol'

const ALICE_PRIVATE_KEY = "<alice-private-key>"

const currencyMosaicId = 0x72C0212E67A08BCEn;

const network = Network.TESTNET
const facade = new SymbolFacade(network.name);
const deadline = facade.now().addHours(2).timestamp;

const alicePrivateKey = new PrivateKey(ALICE_PRIVATE_KEY);
const aliceKeyPair = new KeyPair(alicePrivateKey);

const bobPublicKey = new PublicKey('39538DCA630979A96F7003D47E300C3CE06BCDAF3D353F12479FEFD8698C8A9C');

const carolAddress = new Address('TB577ISBQFM6K6I5UM362TTQ573TOL5HWPCDI2A')

const innerTransaction1 = facade.transactionFactory.createEmbedded({
    type: 'transfer_transaction_v1',
    signerPublicKey: aliceKeyPair.publicKey,
    recipientAddress: carolAddress,
    mosaics: [
        { mosaicId: currencyMosaicId, amount: 1000000n },
    ],
});

const innerTransaction2 = facade.transactionFactory.createEmbedded({
    type: 'transfer_transaction_v1',
    signerPublicKey: bobPublicKey,
    recipientAddress: carolAddress,
    mosaics: [
        { mosaicId: currencyMosaicId, amount: 1000000n },
    ],
});

const innerTransactions = [ innerTransaction1, innerTransaction2 ];
const transactionsHash = SymbolFacade.hashEmbeddedTransactions(innerTransactions)

const transaction = facade.transactionFactory.create({
    type: 'aggregate_complete_transaction_v2',
    signerPublicKey: aliceKeyPair.publicKey,
    fee: 1000000n,
    deadline,
    transactions: innerTransactions,
    transactionsHash,
});

const signature = facade.signTransaction(aliceKeyPair, transaction);
const jsonPayloadWithoutCosig = facade.transactionFactory.static.attachSignature(transaction, signature);
const hash = facade.hashTransaction(transaction).toString();

const { payload } = JSON.parse(jsonPayloadWithoutCosig);

console.log("payload", payload)
console.log("hash", hash)
```

### ボブのターン

```javascript
import {
    PrivateKey,
    utils,
} from 'symbol-sdk';
import {
    KeyPair,
    Network,
    SymbolFacade,
    models,
} from 'symbol-sdk/symbol'

const NODE_URL = "<your-node-url>"
const BOB_PRIVATE_KEY = "<bob-private-key>"

const network = Network.TESTNET
const facade = new SymbolFacade(network.name);

const bobPrivateKey = new PrivateKey(BOB_PRIVATE_KEY);
const bobKeyPair = new KeyPair(bobPrivateKey);

const payload = "680100000000000024140FB446B5C15203BFDF53BA3A3E5895421A3B4603604C33EEBF84AD034968C05FB48FFE1F7CABB768CE31ED0642BC5DC99B851A0A3830E787DEBFC2A7C20497F0F997BB59790239CEF1B284A158EFCD87BF7BBA1E0AFD0B54B26C05D15123000000000298414140420F0000000000A0D1F9EE0A000000FE5806FDC22B3F2650234AD8E83D3CF7FFEC4040FE30003B74C0736BB3603622C000000000000000600000000000000097F0F997BB59790239CEF1B284A158EFCD87BF7BBA1E0AFD0B54B26C05D15123000000000198544198B0BA037DAF763812FFB675B9B5511F81AC0E7284D0A13C0000010000000000CE8BA0672E21C07240420F0000000000600000000000000039538DCA630979A96F7003D47E300C3CE06BCDAF3D353F12479FEFD8698C8A9C0000000001985441987BFFA2418159E5791DA337ED4E70EFF7372FA7B3C434680000010000000000CE8BA0672E21C07280841E0000000000"

const aggregateComplete = models.TransactionFactory.deserialize(utils.hexToUint8(payload));
const bobCosignature = facade.cosignTransaction(bobKeyPair, aggregateComplete);
aggregateComplete.cosignatures.push(bobCosignature);
const jsonPayload = `{"payload":"${utils.uint8ToHex(aggregateComplete.serialize())}"}`;
const hash = facade.hashTransaction(aggregateComplete).toString();

console.log(jsonPayload);
console.log(hash);

const sendRes = await fetch(
    new URL('/transactions', NODE_URL),
    { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: jsonPayload }
)
    .then((res) => res.json());
console.log(sendRes);

await new Promise((resolve) => setTimeout(resolve, 1000));

const statusRes = await fetch(new URL("/transactionStatus/" + hash, NODE_URL))
    .then((res) => res.json());
console.log(statusRes);
```

## 詳細

### アリスのターン

アリスの手元では、自分の秘密鍵を利用できます。

```javascript
const alicePrivateKey = new PrivateKey(ALICE_PRIVATE_KEY);
const aliceKeyPair = new KeyPair(alicePrivateKey);
```

ボブの公開鍵を教えてもらう必要があります。

```javascript
const bobPublicKey = new PublicKey('39538DCA630979A96F7003D47E300C3CE06BCDAF3D353F12479FEFD8698C8A9C');
```

キャロルのアドレスを教えてもらう必要があります。

```javascript
const carolAddress = new Address('TB577ISBQFM6K6I5UM362TTQ573TOL5HWPCDI2A')
```

というわけで、アリスが知っている情報はこんな感じです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172833/c86dcde8-281f-3a27-ca15-7d7b148c5044.png)


アリスからキャロルへのトランザクション

```javascript
const innerTransaction1 = facade.transactionFactory.createEmbedded({
    type: 'transfer_transaction_v1',
    signerPublicKey: aliceKeyPair.publicKey,
    recipientAddress: carolAddress,
    mosaics: [
        { mosaicId: currencyMosaicId, amount: 1000000n },
    ],
});
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172833/014c302c-4451-7bae-2f8d-753981f6a5a9.png)

ボブからキャロルへのトランザクション

```javascript
const innerTransaction2 = facade.transactionFactory.createEmbedded({
    type: 'transfer_transaction_v1',
    signerPublicKey: bobPublicKey,
    recipientAddress: carolAddress,
    mosaics: [
        { mosaicId: currencyMosaicId, amount: 1000000n },
    ],
});
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172833/edabb606-aa09-1661-21cd-ad63830f5118.png)

それらをアグリゲートコンプリートトランザクションに入れ込みます。

```javascript
const innerTransactions = [ innerTransaction1, innerTransaction2 ];
const transactionsHash = SymbolFacade.hashEmbeddedTransactions(innerTransactions)

const transaction = facade.transactionFactory.create({
    type: 'aggregate_complete_transaction_v2',
    signerPublicKey: aliceKeyPair.publicKey,
    fee: 1000000n,
    deadline,
    transactions: innerTransactions,
    transactionsHash,
});
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172833/c7c88d9f-dc5d-f4e1-19ff-728d7d2bfd6e.png)

アリスはそれに署名して、payloadをボブに渡し、ターンエンドです。

```javascript
const signature = facade.signTransaction(aliceKeyPair, transaction);
const payloadString = facade.transactionFactory.static.attachSignature(transaction, signature);
const { payload } = JSON.parse(payloadString);
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172833/0fedae1b-5085-45d2-7c1b-85770383b5a4.png)

### ボブのターン

アリスからpayloadを受け取ります。

```javascript
const payload = "680100000000000024140FB446B5C15203BFDF53BA3A3E5895421A3B4603604C33EEBF84AD034968C05FB48FFE1F7CABB768CE31ED0642BC5DC99B851A0A3830E787DEBFC2A7C20497F0F997BB59790239CEF1B284A158EFCD87BF7BBA1E0AFD0B54B26C05D15123000000000298414140420F0000000000A0D1F9EE0A000000FE5806FDC22B3F2650234AD8E83D3CF7FFEC4040FE30003B74C0736BB3603622C000000000000000600000000000000097F0F997BB59790239CEF1B284A158EFCD87BF7BBA1E0AFD0B54B26C05D15123000000000198544198B0BA037DAF763812FFB675B9B5511F81AC0E7284D0A13C0000010000000000CE8BA0672E21C07240420F0000000000600000000000000039538DCA630979A96F7003D47E300C3CE06BCDAF3D353F12479FEFD8698C8A9C0000000001985441987BFFA2418159E5791DA337ED4E70EFF7372FA7B3C434680000010000000000CE8BA0672E21C07280841E0000000000"
```

これをdeserializeします。

```javascript
const aggregateComplete = models.AggregateCompleteTransactionV2.deserialize(utils.hexToUint8(payload));
```

これに連署します。

```javascript
const bobCosignature = facade.cosignTransaction(bobKeyPair, aggregateComplete);
aggregateComplete.cosignatures.push(bobCosignature);
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172833/b2189b5e-1547-6377-2aac-f8da6f99d2a3.png)

これにて、必要な署名が全部集まりました。

ノードにトランザクションを送信するためのデータを作ります。

```javascript
const jsonPayload = `{"payload":"${utils.uint8ToHex(aggregateComplete.serialize())}"}`;
```

後で問い合わせするので、トランザクションハッシュを計算しときます。

```javascript
const hash = facade.hashTransaction(aggregateComplete).toString();
```

送信します。

```javascript
console.log(jsonPayload);
console.log(hash);

const sendRes = await fetch(
    new URL('/transactions', NODE_URL),
    { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: jsonPayload }
)
    .then((res) => res.json());
console.log(sendRes);

await new Promise((resolve) => setTimeout(resolve, 1000));

const statusRes = await fetch(new URL("/transactionStatus/" + hash, NODE_URL))
    .then((res) => res.json());
console.log(statusRes);
```

# まとめ

今回大切なところはここだけです。

```javascript
const aggregateComplete = models.TransactionFactory.deserialize(utils.hexToUint8(payload));
```

16進数文字列のトランザクションを、トランザクションのインスタンスに変換しているところです。