13
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

nem / symbolAdvent Calendar 2022

Day 9

Symbol SDK JS v3 でAggregateTransaction

Last updated at Posted at 2022-12-08

はじめに

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

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

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

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

AggregateCompleteTransaction

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()を使います。

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のほうが連署の追加が容易だと思われることです。

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を作成し、アグリゲートに追加。

注意点としては

bobCosignature.signature = bobKeyPair.sign(transactionHash);
bobCosignature.signerPublicKey = bobKeyPair.publicKey.bytes;

では無い点です。

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

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

あとはアナウンス用のJsonを自作し、アナウンスするだけです。

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

AggregateBondedTransaction

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

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();
            }
        }
    };    
});

準備

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

AliceとBobの準備をしますがBobの秘密鍵は分かりません。そのため公開鍵からアドレスを生成します。Bobの連署を必要とするので公開鍵は必須です。

Transaction構築

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',とします。

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

署名後のアグリゲートトランザクションをハッシュ化しそのハッシュを持って、ハッシュロックトランザクションを構築します。

最後にハッシュロックのアナウンス、承認を待ってアグリゲートボンデッドをアナウンスします。

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について、以下の記事を参考にしました

13
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?