はじめに
今回は、オフラインで、自分と他者の署名が必要なアグリゲートコンプリートトランザクションを作成してみようと思います。
登場人物はこんな感じです。
この場合、アリスとボブの署名が要ります。
今回のシナリオでは、アリスがトランザクションを作成してボブに渡し、ボブが連署してネットワークに送信するような感じでやっていきます。
アリスからボブへ渡すデータの形式は、16進数文字列のトランザクションデータにします。
アリス側
全部
アリスのターン
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)
ボブのターン
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);
詳細
アリスのターン
アリスの手元では、自分の秘密鍵を利用できます。
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,
});
アリスはそれに署名して、payloadをボブに渡し、ターンエンドです。
const signature = facade.signTransaction(aliceKeyPair, transaction);
const payloadString = facade.transactionFactory.static.attachSignature(transaction, signature);
const { payload } = JSON.parse(payloadString);
ボブのターン
アリスからpayloadを受け取ります。
const payload = "680100000000000024140FB446B5C15203BFDF53BA3A3E5895421A3B4603604C33EEBF84AD034968C05FB48FFE1F7CABB768CE31ED0642BC5DC99B851A0A3830E787DEBFC2A7C20497F0F997BB59790239CEF1B284A158EFCD87BF7BBA1E0AFD0B54B26C05D15123000000000298414140420F0000000000A0D1F9EE0A000000FE5806FDC22B3F2650234AD8E83D3CF7FFEC4040FE30003B74C0736BB3603622C000000000000000600000000000000097F0F997BB59790239CEF1B284A158EFCD87BF7BBA1E0AFD0B54B26C05D15123000000000198544198B0BA037DAF763812FFB675B9B5511F81AC0E7284D0A13C0000010000000000CE8BA0672E21C07240420F0000000000600000000000000039538DCA630979A96F7003D47E300C3CE06BCDAF3D353F12479FEFD8698C8A9C0000000001985441987BFFA2418159E5791DA337ED4E70EFF7372FA7B3C434680000010000000000CE8BA0672E21C07280841E0000000000"
これをdeserializeします。
const aggregateComplete = models.AggregateCompleteTransactionV2.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);
まとめ
今回大切なところはここだけです。
const aggregateComplete = models.TransactionFactory.deserialize(utils.hexToUint8(payload));
16進数文字列のトランザクションを、トランザクションのインスタンスに変換しているところです。