#概要
仮想通貨ではトークン同士の交換がよく行われます.しかし,交換する相手が必ずしも信頼できるとは限りません.通常の送金トランザクションだけで交換しようとした場合,こちらがトークンを送っても相手が持ち逃げしてしまう可能性があります.
これを防ぐためには,通常の送金トランザクション以外でトークンを交換する必要があります.Symbolではモザイク(トークン)同士を安全に交換したいに利用できるトランザクションとしては次のようなものがあります.
トランザクション | 連署の最大猶予期間 | 必要な署名の回数 | 不成立時のペナルティ |
---|---|---|---|
アグリゲートコンプリート | 6時間(アナウンス前) | 2(作成者),1(連署者) | 無(TX手数料も0) |
アグリゲートボンデッド | 約2日間 | 1 | 10XYM |
シークレットロック,プルーフ | 約365日 | 2(連署者は1も可) | 有効期限満了までモザイクロック |
モザイクの交換を行いたい場合はこのようなトランザクションを生成していれば良いのですが,各トランザクションによって有効期間も連署に必要な署名の数も変わってきます.これらをまとめてみました.
今回はAが7XYMを,Bが5Xembook.tomatoを交換したいと想定してこの記事を書いていきます.
#アグリゲートコンプリート
アグリゲートコンプリートは事前にオフライン環境でトランザクションのペイロードを共有できる場合に利用できます.
トランザクションのペイロード(内容)のみメールやQRコードなどで転送して連署を行うことで成立させることができます.Symbolのネットワークを利用しないため,トランザクションの署名に失敗してもTX手数料などの手数料が一切かからないところが非常に強いです.
##手順
###1.Aがアグリゲートコンプリートを生成する
const xym = require('symbol-sdk');
const netwotkType = xym.NetworkType.MAIN_NET;//Xembook.tomatoを送金したいのでMAIN_NET
const epochAdjustment = '';//epchAdjustmentを入れる,http://(ノードのFQDN):3000/network/propertiesで取得可能
const currencyMosaicId = '';//http://(ノードのFQDN):3000/network/propertiesで取得可能
const networkGenerationHash = '';//http://(ノードのFQDN):3000/network/propertiesで取得可能
const feemultiplier = 100;//早く送金したいなら100,遅くても良い場合は10がおすすめ(2021/12/02現在)
const a_Account = xym.Account.createFromPrivateKey(
'*********************************',
networkType,
);
const a_publicAccount = xym.PublicAccount.createFromPublicKey(
'*****************',
networkType
);
const b_publicAccount = xym.PublicAccount.createFromPublicKey(
'****************',
networkType
);
const xymMosaic = new xym.Mosaic(
currencyMosaicId,
xym.UInt64.fromUint(7*1000000)//送金量*可分性(xymは6)
);
const tomatoMosaic = new xym.Mosaic(
'310378C18A140D1B',//モザイクID310378C18A140D1B
xym.UInt64.fromUint(5*1)//送金量*可分性
);
const atobTx = xym.TransferTransaction.create(
xym.Deadline.create(epochAdjustment),
b_publicAccount.address,//受取人アドレス
[xymMosaic],//モザイク
xym.PlainMessage.create('AtoB'),//メッセージ
netwotktype,//ネットワークタイプ
)
const btoaTX = xym.TransferTransaction.create(
xym.Deadline.create(epochAdjustment),
a_publicAccount.address,//受取人アドレス
[tomatoMosaic],//モザイク
xym.PlainMessage.create('BtoA'),//メッセージ
netwotktype,//ネットワークタイプ
);
const aggregateTx = xym.AggregateTransaction.createComplete(
xym.Deadline.create(epochAdjustment,6),//有効期限,アグリゲートコンプリートは6時間後のものまで受け入れられる
[
atobTx.toAggregate(a_publicAccount),
btoaTX.toAggregate(b_publicAccount),
],
netwotktype,
[],
).setMaxFeeForAggregate(feemultiplier,1);
const signedTx = a_Account.sign(
aggregateTx ,
networkGenerationHash,
);
console.log(signedTx.payload);//このペイロードが必要
###2.Bに生成したアグリゲートコンプリートのペイロードを伝え,連署させる
const b_Account = xym.Account.createFromPrivateKey(
'*********************************',
networkType,
);
const payload = '';//Aから伝えられたpayload
const cosignature = xym.CosignatureTransaction.signTransactionPayload(
b_Account ,
payload,
networkGenerationHash,
);
console.log(JSON.stringfy(cosignature));//連署が生成される,これをAに伝える
###3.AはBの連署を使ってアグリゲートコンプリートを完成させ,ネットワークにアナウンスする
cpnst node = '';//ノードのURLを入れる,例 http://api-peer.xym-node.com:3000
const repo = new xym.RepositoryFactoryHttp(node);
const transactionHttp = repo.createTransactionRepository();
const cosigJson = JSON.parse("*************");//連署のJSONをparseする
const congnature = new xym.CosignatureSignedTransaction(//CosignatureSignedTransactionに戻す
cosigJson.parentHash,
cosigJson.signature,
cosigJson.signerPublicKey,
new xym.UInt64([cosigJson.version.lower,cosigJson.version.higher]),
);
const complete = a_account.signTransactionGivenSignatures(//連署を使って完成させる
aggregateTx,[congnature],
networkGenerationHash
);
transactionHttp.announce(complete ).subscribe(//ネットワークにアナウンスする
(x) => console.log(x),
(err) => console.error(err),
);
console.log(node+'/transactionStatus/'+complete.hash);//ステータス確認ができる
###全てを一括実行するサンプル
const xym = require('symbol-sdk');
var networkGenerationHash ='';
var epochAdjustment = '';
var repo;
var transactionHttp;
var currencyMosaicId;
var node;
var networkType = xym.NetworkType.MAIN_NET;
var feemultiplier;
console.log("***************************************************************");
console.log(Date());
console.log("***************************************************************");
//送信元アカウント
const a_account = xym.Account.createFromPrivateKey(
'****************************',
networkType,
);
const b_account = xym.Account.createFromPrivateKey(
'****************************',
networkType,
);
(async()=>{
node = 'http://api-peer.xym-node.com:3000';
repo = new xym.RepositoryFactoryHttp(node);
transactionHttp = repo.createTransactionRepository();
await getInfo();
const data = await aggregateCompleteOffline();
const cosigStr = JSON.stringify(data.cosignature);
const cosigJson = JSON.parse(cosigStr);
const co = new xym.CosignatureSignedTransaction(
cosigJson.parentHash,
cosigJson.signature,
cosigJson.signerPublicKey,
new xym.UInt64([cosigJson.version.lower,cosigJson.version.higher]),
);
const complete = a_account.signTransactionGivenSignatures(data.transaction, [co],networkGenerationHash);
announceTX(complete);
})();
async function aggregateCompleteOffline(){
const a_publicAccount = xym.PublicAccount.createFromPublicKey(
'D77668C77A26F6979ED2D0C5EF99E0A1FFA7A988601E40E82B9A25861A6B2D12',
networkType
);
const b_publicAccount = xym.PublicAccount.createFromPublicKey(
'B9198633C7EC0BED5D79319C5B07748D42A2206ACEB75A485A9070CFA0993F35',
networkType
);
const xymMosaic = new xym.Mosaic(
currencyMosaicId,
xym.UInt64.fromUint(7*1000000)//送金量*10^(可分性)(xymは6)
);
const tomatoMosaic = new xym.Mosaic(
new xym.MosaicId('310378C18A140D1B'),//モザイクID,310378C18A140D1B
xym.UInt64.fromUint(5 * 1)//送金量*送金量*10^(可分性)(310378C18A140D1Bは6)
);
const atobTx = xym.TransferTransaction.create(
xym.Deadline.create(epochAdjustment),
b_publicAccount.address,//受取人アドレス
[xymMosaic],//モザイク
xym.PlainMessage.create('AtoB'),//メッセージ
networkType,//ネットワークタイプ
)
const btoaTX = xym.TransferTransaction.create(
xym.Deadline.create(epochAdjustment),
a_publicAccount.address,//受取人アドレス
[tomatoMosaic],//モザイク
xym.PlainMessage.create('BtoA'),//メッセージ
networkType,//ネットワークタイプ
);
const aggregateTx = xym.AggregateTransaction.createComplete(
xym.Deadline.create(epochAdjustment,6),//有効期限,アグリゲートコンプリートは6時間後のものまで受け入れられる
[
atobTx.toAggregate(a_publicAccount),
btoaTX.toAggregate(b_publicAccount),
],
networkType,
[],
).setMaxFeeForAggregate(feemultiplier,1);
const signedTx = a_account.sign(aggregateTx,networkGenerationHash);//payloadを作成する
console.log(signedTx);
//payloadからトランザクションを作る
const newTx = xym.TransactionMapping.createFromPayload(signedTx.payload);
console.log(newTx);
const coAgg = xym.CosignatureTransaction.signTransactionPayload(//作成したpayloadに連署しておく
b_account,
signedTx.payload,
networkGenerationHash);
console.log(coAgg);
return {transaction:newTx,cosignature:coAgg}
}
function announceTX(signed){
console.log("送信トランザクションハッシュ: "+signed.hash);
console.log(node+'/transactionStatus/'+signed.hash);
transactionHttp.announce(signed).subscribe(
(x) => console.log(x),
(err) => console.error(err),
);
}
async function getInfo(){
feemultiplier = 10;
epochAdjustment = await repo.getEpochAdjustment().toPromise();
networkGenerationHash = await repo.getGenerationHash().toPromise();
currencyMosaicId = new xym.MosaicId((await repo.createNetworkRepository().getNetworkProperties().toPromise()).chain.currencyMosaicId.replace(/0x/,"").replace(/'/g,""));
}
アグリゲートボンデッド
アグリゲートコンプリートでは連署を手動で集めましたが,アグリゲートボンデッドでは連署をSymbolのネットワークを介して集めることができます.この方法はSymbolのネットワークで連署要求ができる他,公式ウォレットにも直接送金要求を送ることができ,アグリゲートコンプリートよりも連署が行いやすくなっています.
また,有効期限も最大2日に設定できるためアグリゲートコンプリートよりも連署までの有効時間が長くなります.
さらに,必要な連署を全て集めると自動でトランザクションが実行されるため,アグリゲートコンプリートのように集めた連署を使ってトランザクションを完成させる手間を省くことができます.連署を集める機能に関してはアグリゲートボンデッドの方が便利です.
アグリゲートボンデッドの利用の際はアナウンスを行ったアカウントから10XYMが最大2日間ロックされます.成功すると返還されますが,失敗した場合はネットワークのハーベスト報酬に回され帰ってきません.10XYMのロックは結構痛いので,アグリゲートボンデッドはトランザクションが確実に連署される場合に利用した方が良いでしょう.
##手順
###1.Aがアグリゲートボンデッドを生成する
const xym = require('symbol-sdk');
const netwotkType = xym.NetworkType.MAIN_NET;//Xembook.tomatoを送金したいのでMAIN_NET
const epochAdjustment = '';//epchAdjustmentを入れる,http://(ノードのFQDN):3000/network/propertiesで取得可能
const currencyMosaicId = '';//http://(ノードのFQDN):3000/network/propertiesで取得可能
const networkGenerationHash = '';//http://(ノードのFQDN):3000/network/propertiesで取得可能
const feemultiplier = 100;//早く送金したいなら100,遅くても良い場合は10がおすすめ(2021/12/02現在)
function createAggregateBonded(){
const netwotktype = xym.NetworkType.TEST_NET;
const a_publicAccount = xym.PublicAccount.createFromPublicKey(
'54B24D5FCA1EDB2CA9B73424752CDD6BECE9FFCD14F83125599B51A03E2E6942',
networkType
);
const b_publicAccount = xym.PublicAccount.createFromPublicKey(
'9C4BE4A67607DA47992DD3D881B67E2553955121854D2947CE51A5159CC32154',
networkType
);
const xymMosaic = new xym.Mosaic(
currencyMosaicId,
xym.UInt64.fromUint(7*1000000)//送金量*可分性(xymは6)
);
const tomatoMosaic = new xym.Mosaic(
new xym.MosaicId('310378C18A140D1B'),
xym.UInt64.fromUint(5*1)//送金量*可分性
);
const atobTx = xym.TransferTransaction.create(
xym.Deadline.create(epochAdjustment),
b_publicAccount.address,//受取人アドレス
[xymMosaic],//モザイク
xym.PlainMessage.create('AtoB'),//メッセージ
netwotktype,//ネットワークタイプ
)
const btoaTX = xym.TransferTransaction.create(
xym.Deadline.create(epochAdjustment),
a_publicAccount.address,//受取人アドレス
[tomatoMosaic],//モザイク
//xym.PlainMessage.create(""),
xym.PlainMessage.create('BtoA'),//メッセージ
netwotktype,//ネットワークタイプ
);
const aggregateTx = xym.AggregateTransaction.createBonded(
xym.Deadline.create(epochAdjustment,48),//有効期限,アグリゲートボンデッドは48時間後まで受け入れられる
[
atobTx.toAggregate(a_publicAccount),
btoaTX.toAggregate(b_publicAccount),
],
netwotktype,
[],
).setMaxFeeForAggregate(100,1);
const signedTx = senderAccount.sign(aggregateTx,networkGenerationHash);
return signedTx;
}
###2.アグリゲートボンデッドのハッシュを使ってハッシュロックをアナウンスする
function createHashLock(signedTx){
const lockXym = new xym.Mosaic(
currencyMosaicId,
xym.UInt64.fromUint(10 * 1000000)//10XYMロック
);
const hashLockTx = xym.HashLockTransaction.create(
xym.Deadline.create(epochAdjustment),
lockXym,
xym.UInt64.fromUint(2 * 2880),//最大2日(2*2880ブロック)留め置きできる
signedTx,
networkType,
).setMaxFee(feemultiplier);
const signed = senderAccount.sign(hashLockTx,networkGenerationHash);
return signed;
}
###3.ハッシュロックが承認されるまで待機し,アグリゲートボンデッドをアナウンスする
(async()=>{
const node = 'http://api-peer.xym-node.com:3000';
const wsEndpoint = node.replace('http', 'ws') + "/ws";
const nsHttp = new xym.NamespaceHttp(node);
const listener = new xym.Listener(wsEndpoint,nsHttp,WebSocket);
const repo = new xym.RepositoryFactoryHttp(node);
const transactionHttp = repo.createTransactionRepository();
const receiptRepo = repo.createReceiptRepository();
const transactionService = new xym.TransactionService(transactionHttp,receiptRepo);
websocketConnection();
const aggTx =createAggregateBonded();
const hashLockTx = createHashLock(aggTx);
//ハッシュロックが承認されてからアグリゲートボンデッドを投げる
const res = await transactionService.announceHashLockAggregateBonded(hashLockTx,aggTx,listener).toPromise();
})();
function websocketConnection(){
listener.open().then(() => {
listener.newBlock()
.subscribe(block =>{
console.log("ブロック高: "+block.height);
},err=>{
console.log("失敗しました");
});
});
}
###4.Bがネットワークのパーシャルキャッシュから情報を取得し連署,アナウンスする
const networkType = xym.MAIN_NET;
const b_account = xym.Account.createFromPrivateKey(
'************************',
networkType,
);
var node;
var wsEndpoint;
var nsHttp;
var listener;
var repo;
var transactionHttp;
(async()=>{
node = 'http://api-peer.xym-node.com:3000';
wsEndpoint = node.replace('http', 'ws') + "/ws";
nsHttp = new xym.NamespaceHttp(node);
listener = new xym.Listener(wsEndpoint,nsHttp,WebSocket);
repo = new xym.RepositoryFactoryHttp(node);
transactionHttp = repo.createTransactionRepository();
websocketConnection();
function websocketConnection(){
listener.open().then(() => {
listener.newBlock()
.subscribe(block =>{
console.log("ブロック高: "+block.height);
signAllPartialTransaction(b_account);
},err=>{
console.log("失敗しました");
});
});
}
async function signAllPartialTransaction(account){
const transactions = await transactionHttp.search({ address: account.address, group: xym.TransactionGroup.Partial }).toPromise();
for (var data of transactions.data) {
const cosignatureTx = xym.CosignatureTransaction.create(data);
const signedTx = account.signCosignatureTransaction(cosignatureTx);
transactionHttp.announceAggregateBondedCosignature(signedTx).subscribe(
(x) => console.log(x),
(er) => console.error(er),
);
}
}
###全てを一括実行するサンプル
const xym = require('symbol-sdk');
const WebSocket = require('ws');
var networkGenerationHash ='';
var epochAdjustment = '';
var repo;
var transactionHttp;
var receiptRepo;
var currencyMosaicId;
var node;
var networkType = xym.NetworkType.MAIN_NET;
var feemultiplier;
var nsHttp;
var wsEndpoint;
var listener;
var transactionService;
const a_publicAccount = xym.PublicAccount.createFromPublicKey(
'************',
networkType
);
const b_publicAccount = xym.PublicAccount.createFromPublicKey(
'*************',
networkType
);
console.log("***************************************************************");
console.log(Date());
console.log("***************************************************************");
//送信元アカウント
const a_account = xym.Account.createFromPrivateKey(
'******',
networkType,
);
const b_account = xym.Account.createFromPrivateKey(
'******',
networkType,
);
(async()=>{
node = 'http://api-peer.xym-node.com:3000';
nsHttp = new xym.NamespaceHttp(node);
wsEndpoint = node.replace('http', 'ws') + "/ws";
listener = new xym.Listener(wsEndpoint,nsHttp,WebSocket);
repo = new xym.RepositoryFactoryHttp(node);
transactionHttp = repo.createTransactionRepository();
receiptRepo = repo.createReceiptRepository();
transactionStatusRepo = repo.createTransactionStatusRepository();
transactionService = new xym.TransactionService(transactionHttp,receiptRepo);
websocketConnection();
await getInfo();
const aggTx = await createAggregateBonded();
const hashLockTx = createHashLock(aggTx);
const res = await transactionService.announceHashLockAggregateBonded(hashLockTx,aggTx,listener).toPromise();
console.log(res);
})();
async function signAllPartialTransaction(account){//partial状態であれば全て署名するので注意
const transactions = await transactionHttp.search({ address: account.address, group: xym.TransactionGroup.Partial }).toPromise();
for (var data of transactions.data) {
const cosignatureTx = xym.CosignatureTransaction.create(data);
const signedTx = account.signCosignatureTransaction(cosignatureTx);
transactionHttp.announceAggregateBondedCosignature(signedTx).subscribe(
(x) => console.log(x),
(er) => console.error(er),
);
}
}
function websocketConnection(){
listener.open().then(() => {
listener.newBlock()
.subscribe(block =>{
console.log("ブロック高: "+block.height);
signAllPartialTransaction(b_account);
},err=>{
console.log("失敗しました");
});
});
}
async function createAggregateBonded(){
const xymMosaic = new xym.Mosaic(
currencyMosaicId,
xym.UInt64.fromUint(7 * 1000000)//送金量*10^(可分性)(xymは6)
);
const tomatoMosaic = new xym.Mosaic(
new xym.MosaicId('310378C18A140D1B'),//モザイクID,310378C18A140D1B
xym.UInt64.fromUint(5 * 1)//送金量*送金量*10^(可分性)(310378C18A140D1Bは6)
);
const atobTx = xym.TransferTransaction.create(
xym.Deadline.create(epochAdjustment),
b_publicAccount.address,//受取人アドレス
[xymMosaic],//モザイク
xym.PlainMessage.create('AtoB'),//メッセージ
networkType,//ネットワークタイプ
)
const btoaTX = xym.TransferTransaction.create(
xym.Deadline.create(epochAdjustment),
a_publicAccount.address,//受取人アドレス
[tomatoMosaic],//モザイク
xym.PlainMessage.create('BtoA'),//メッセージ
networkType,//ネットワークタイプ
);
const aggregateTx = xym.AggregateTransaction.createBonded(
xym.Deadline.create(epochAdjustment,48),//有効期限,アグリゲートボンデッドは2日後まで受け入れられる
[
atobTx.toAggregate(a_publicAccount),
btoaTX.toAggregate(b_publicAccount),
],
networkType,
[],
).setMaxFeeForAggregate(100,1);
const signedTx = a_account.sign(aggregateTx,networkGenerationHash);//payloadを作成する
return signedTx;
}
function createHashLock(signedTx){
const lockXym = new xym.Mosaic(
currencyMosaicId,
xym.UInt64.fromUint(10 * 1000000)//10XYMロック
);
const hashLockTx = xym.HashLockTransaction.create(
xym.Deadline.create(epochAdjustment),
lockXym,
xym.UInt64.fromUint(2 * 2880),//最大2日(2*2880ブロック)留め置きできる
signedTx,
networkType,
).setMaxFee(feemultiplier);
const signed = a_account.sign(hashLockTx,networkGenerationHash);
return signed;
}
async function getInfo(){
feemultiplier = 100;
epochAdjustment = await repo.getEpochAdjustment().toPromise();
networkGenerationHash = await repo.getGenerationHash().toPromise();
currencyMosaicId = new xym.MosaicId((await repo.createNetworkRepository().getNetworkProperties().toPromise()).chain.currencyMosaicId.replace(/0x/,"").replace(/'/g,""));
}
シークレットロック,プルーフ
交換したい当事者同士が同じシークレットを使用してシークレットロックを行い,プルーフをアナウンスすることで交換が成立します.交換が不成立の場合(プルーフトランザクションがアナウンスされない場合)ロックした分のモザイクが有効期限が切れるまで変換されません.
シークレットプルーフを使った場合の例(アグリゲートコンプリートで一括実行の場合)
なお,この方法ではアトミックスワップと同じ仕組みを利用しているため他のチェーンが対応していれば,他のチェーンの通貨との交換(アトミックスワップ)が可能です.交換が失敗した(他方がシークレットロックを行わなかった,片方のシークレットロックの期限が切れてしまった)場合,ロックした期間だけモザイクが変換されないデメリットがあります.
##手順
###1.Aがシークレット,プルーフのペアを生成する
const random = crypto.randomBytes(20);
const proof = random.toString('hex');//Bが正しくシークレットロックを行うまで絶対に伝えてはならない
console.log('Proof:', proof);
const hash = sha3_256.create();
const secret = hash.update(random).hex().toUpperCase();//Bに伝える
console.log('Secret:', secret);
###2.Aが生成されたシークレットを使ってシークレットロックトランザクションを生成し,アナウンスする
const networkType = xym.MAIN_NET;
const a_account = xym.Account.createFromPrivateKey(
'************************',
networkType,
);
const b_address = xym.Address.createFromRawAddress(
'************************',
networkType,
);
const xymMosaic = new xym.Mosaic(
currencyMosaicId,
xym.UInt64.fromUint(7*1000000)//送金量*可分性(xymは6)
);
const atob= xym.SecretLockTransaction.create(
xym.Deadline.create(epochAdjustment),
xymMosaic,
xym.UInt64.fromUint((10 * 24 * 3600) / 30), // 10日間(28800ブロック)ロックする
xym.LockHashAlgorithm.Op_Sha3_256,
secret,
b_address ,
networkType,
);
const atobSigned= a_account.sign(
atob,
networkGenerationHash,
);
transactionHttp.announce(atobSigned).subscribe(
(x) => console.log(x),
(err) => console.error(err),
);
}
###3.BがAと同じシークレットを使ってシークレットロックトランザクションを生成する
ここでは,ロックがAが生成したハッシュロックよりも早く期限切れになるように設定する必用があります.Aが期限切れになったあとにもハッシュロックがされていると,AはBのモザイクを取り出せるのにBはAのモザイクを取り出せないといった状態になってしまいます.
今回はAがシークレットロックを行ってから1日以内にこのトランザクションを生成すると仮定し,ロック期間を1日短い9日で設定します
const secret = '';//Aが生成したものと同じものを使う
const networkType = xym.MAIN_NET;
const b_account = xym.Account.createFromPrivateKey(
'************************',
networkType,
);
const a_address = xym.Address.createFromRawAddress(
'************************',
networkType,
);
const tomatoMosaic = new xym.Mosaic(
new xym.MosaicId('310378C18A140D1B'),//モザイクID310378C18A140D1B
xym.UInt64.fromUint(5*1)//送金量*可分性
);
const btoa = xym.SecretLockTransaction.create(
xym.Deadline.create(epochAdjustment),
tomatoMosaic ,
xym.UInt64.fromUint((9 * 24 * 3600) / 30), // 9日間(28800ブロック)ロックする
xym.LockHashAlgorithm.Op_Sha3_256,
secret,
a_address,
networkType,
);
const btoaSigned= b_account.sign(
btoa,
networkGenerationHash,
);
transactionHttp.announce(btoaSigned).subscribe(
(x) => console.log(x),
(err) => console.error(err),
);
}
###4.AはBが同じシークレットを用いてシークレットロックトランザクションを発行したことを確認し,自分宛のシークレットプルーフトランザクションをアナウンスする.
Bの行ったトランザクションがファイナライズされてから行うようにしてください.
const btoaPloof = xym.SecretProofTransaction.create(
xym.Deadline.create(epochAdjustment),
xym.LockHashAlgorithm.Op_Sha3_256,
secret,
a_account.address,
proof,
networkType,
).setMaxFee(feemultiplier);
const btoaPloofSigned = a_account.sign(btoaPloof, networkGenerationHash);
transactionHttp.announce(btoaPloofSigned ).subscribe(
(x) => console.log(x),
(err) => console.error(err),
);
###5.BはAがアナウンスしたシークレットプルーフトランザクションからproofを取得し,自分宛のシークレットプルーフトランザクションをアナウンスする.(署名をAが行えば代行することが出来ます)
const atobPloof = xym.SecretProofTransaction.create(
xym.Deadline.create(epochAdjustment),
xym.LockHashAlgorithm.Op_Sha3_256,
secret,
b_account.address,
proof,
networkType,
).setMaxFee(feemultiplier);
const atobPloofSigned = b_account.sign(atobPloof , networkGenerationHash);
transactionHttp.announce(atobPloofSigned ).subscribe(
(x) => console.log(x),
(err) => console.error(err),
);
Aがシークレットプルーフトランザクションを一括実行してあげる場合(手数料は現状あまり変わらないので,こちらの方が良いと思います)
const btoaPloof = xym.SecretProofTransaction.create(
xym.Deadline.create(epochAdjustment),
xym.LockHashAlgorithm.Op_Sha3_256,
secret,
a_account.address,
proof,
networkType,
);
const atobPloof = xym.SecretProofTransaction.create(
xym.Deadline.create(epochAdjustment),
xym.LockHashAlgorithm.Op_Sha3_256,
secret,
b_address,
proof,
networkType,
);
const aggregateTx = xym.AggregateTransaction.createComplete(
xym.Deadline.create(epochAdjustment,6),
[
btoaPloof.toAggregate(a_account.publicAccount),
atobPloof.toAggregate(a_account.publicAccount),
],
netwotktype,
[],
).setMaxFeeForAggregate(feemultiplier,1);
const aggSined= a_account.sign(aggregateTx , networkGenerationHash);
transactionHttp.announce(aggSined).subscribe(
(x) => console.log(x),
(err) => console.error(err),
);
###全てを一括実行するサンプル(ファイナライズ等の考慮は全くしていません,ご注意ください)
const xym = require('symbol-sdk');
const crypto = require('crypto');
const sha3_256 = require('js-sha3').sha3_256;
const _sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
var networkGenerationHash ='';
var epochAdjustment = '';
var node = "";
var repo;
var transactionHttp;
var currencyMosaicId;
var networkType;
var feemultiplier;
console.log("***************************************************************");
console.log(Date());
console.log("***************************************************************");
var networkType = xym.NetworkType.MAIN_NET;
const a_account = xym.Account.createFromPrivateKey(
'***************',
networkType,
);
const b_account = xym.Account.createFromPrivateKey(
'***************',
networkType,
);
const a_publicAccount = xym.PublicAccount.createFromPublicKey(
'D77668C77A26F6979ED2D0C5EF99E0A1FFA7A988601E40E82B9A25861A6B2D12',
networkType
);
const b_publicAccount = xym.PublicAccount.createFromPublicKey(
'B9198633C7EC0BED5D79319C5B07748D42A2206ACEB75A485A9070CFA0993F35',
networkType
);
(async()=>{
node = 'http://api-peer.xym-node.com:3000';
repo = new xym.RepositoryFactoryHttp(node);
transactionHttp = repo.createTransactionRepository();
await getInfo();
const data = await makeSecretLock();
await makeSecretLockb(data.secret);
await _sleep(40000);
createSecretProofTransaction(data.secret,data.proof);
})();
async function makeSecretLock(){
//atob生成
const xymMosaic = new xym.Mosaic(
currencyMosaicId,
xym.UInt64.fromUint(7 * 1000000)//送金量*可分性(xymは6)
);
const random = crypto.randomBytes(20);
const proof = random.toString('hex');
console.log('Proof:', proof);
const hash = sha3_256.create();
const secret = hash.update(random).hex().toUpperCase();
console.log('Secret:', secret);
const atob = xym.SecretLockTransaction.create(
xym.Deadline.create(epochAdjustment),
xymMosaic,
xym.UInt64.fromUint(10 * (24 * 3600) / 30),
xym.LockHashAlgorithm.Op_Sha3_256,
secret,
b_publicAccount.address,
networkType,
).setMaxFee(feemultiplier);
const atobSigned = a_account.sign(
atob,
networkGenerationHash,
);
await announceTX(atobSigned);
return {"secret":secret,"proof":proof};
}
async function makeSecretLockb(secret) {//ノード接続後の処理
//atob生成
const tomatoMosaic = new xym.Mosaic(
new xym.MosaicId('310378C18A140D1B'),//モザイクID,310378C18A140D1B
xym.UInt64.fromUint(5 * 1)//送金量*送金量*10^(可分性)(310378C18A140D1Bは6)
);
const btoa = xym.SecretLockTransaction.create(
xym.Deadline.create(epochAdjustment),
tomatoMosaic,
xym.UInt64.fromUint(9* (24 * 3600) / 30),
xym.LockHashAlgorithm.Op_Sha3_256,
secret,
a_publicAccount.address,
networkType,
).setMaxFee(feemultiplier);
const btoaSigned = b_account.sign(
btoa,
networkGenerationHash,
);
await announceTX(btoaSigned);
}
async function getInfo(){
feemultiplier = 10;
epochAdjustment = await repo.getEpochAdjustment().toPromise();
networkGenerationHash = await repo.getGenerationHash().toPromise();
currencyMosaicId = new xym.MosaicId((await repo.createNetworkRepository().getNetworkProperties().toPromise()).chain.currencyMosaicId.replace(/0x/,"").replace(/'/g,""));
}
function createSecretProofTransaction(secret, proof) {
const btoaProof = xym.SecretProofTransaction.create(
xym.Deadline.create(epochAdjustment),
xym.LockHashAlgorithm.Op_Sha3_256,
secret,
a_account.address,
proof,
networkType,
);
const atobProof = xym.SecretProofTransaction.create(
xym.Deadline.create(epochAdjustment),
xym.LockHashAlgorithm.Op_Sha3_256,
secret,
b_publicAccount.address,
proof,
networkType,
);
const aggregateTx = xym.AggregateTransaction.createComplete(
xym.Deadline.create(epochAdjustment, 6),
[
btoaProof.toAggregate(a_account.publicAccount),
atobProof.toAggregate(a_account.publicAccount),
],
networkType,
[],
).setMaxFeeForAggregate(100, 1);
const aggSined = a_account.sign(aggregateTx, networkGenerationHash);
announceTX(aggSined);
}
async function announceTX(signed) {
console.log("送信トランザクションハッシュ: " + signed.hash);
console.log(node + '/transactionStatus/' + signed.hash);
transactionHttp.announce(signed).subscribe(
(x) => console.log(x),
(err) => console.error(err),
);
}
#番外編 有効期限を延長する
これまで有効期限について書いてきましたが,これらは未来のトランザクション(未来のdeadLine)のトランザクションを生成することで期限を延長することができます.アグリゲートボンデッド以外のトランザクションであれば有効期限が6時間になっているため,6時間毎の未来のトランザクションを生成することで有効期限を無限に伸ばしてあげることができます.アナウンスの際はdeadLineにあったTXに連署し,実行する必要があります.
この方法ではトランザクションを複数回投げてしまうことができるので対策が必要になります.具体的には,トランザクションを一度きりしか成立しないように組んであげる必要があります.
##例1 使い捨てアドレスにモザイクを入れておく
この方法では使い捨てアドレス(カッコ良く言えばコントラクトアドレスと言えるのでしょうか...)にモザイクを入れ,口座の残高の範囲で取引を制限します.一度取引を成立させれば資産が移動するため以降のトランザクションは成立させることができなくなります.
const a_account = xym.Account.createFromPrivateKey(
******,
networkType,
);
const b_account = xym.Account.createFromPrivateKey(
'******',
networkType,
);
const a_publicAccount = xym.PublicAccount.createFromPublicKey(
'8E9765F3471D5E7AEA39620F0998C25C96CEFA24F78980F31B55604FAF7C8868',
networkType,
);
const b_publicAccount = xym.PublicAccount.createFromPublicKey(
'CCECC77ABE531E7038A7D4E416BB99739642B56014034F1C45B1BD157803C105',
networkType,
);
const Txs = deadLineTest(7);//6時間*7回=42時間の有効期限のトランザクションを生成
var signedTxs = [];
for(tx of Txs){
const singed = a_account.sign(tx,networkGenerationHash);
const onetimeCosign = xym.CosignatureTransaction.signTransactionPayload(//onetimeアドレスの連署
onetimeAccount,
singed.payload,
networkGenerationHash
);
signedTxs.push({"signed":singed,"cosign":onetimeCosign});
}
function futureDeadlineTx(num){
const now = Date.now();
var Txs = [];
for(i=6;i<num*6;i+=6){
//現在のUNIXミリ秒-Symbolが誕生したUNIX秒*1000 + 6の倍数
const deadline = new xym.Deadline(now - epochAdjustment * 1000 + 60*60*i*1000);
console.log(deadline);
Txs.push(createFutureTransaction(deadline));
}
return Txs;
}
function createFutureTransaction(futureDeadline){
//使い捨てアドレスからb
const onetimetob = xym.TransferTransaction.create(
futureDeadline,
b_publicAccount.address,
[new xym.Mosaic(
currencyMosaicId,
xym.UInt64.fromUint(7*1000000)
)],
xym.PlainMessage.create('atob'),
networkType,
);
//bからa
const btoa = xym.TransferTransaction.create(
futureDeadline,
a_publicAccount.address,
[new xym.Mosaic(
new xym.MosaicId('310378C18A140D1B'),
xym.UInt64.fromUint(5)
)],
xym.PlainMessage.create('btoa'),
networkType,
);
//手数料をAに負担させるために空トランザクションを作る
const feeTx = xym.TransferTransaction.create(
futureDeadline,
a_publicAccount.address,
[new xym.Mosaic(
currencyMosaicId,
xym.UInt64.fromUint(0)
)],
xym.PlainMessage.create(''),
networkType,
);
const aggregateTx = xym.AggregateTransaction.createComplete(
futureDeadline,
[
onetimetob.toAggregate(onetime_publicAccount),
btoa.toAggregate(b_publicAccount),
feeTx.toAggregate(a_publicAccount),
],
networkType,
[],
).setMaxFeeForAggregate(feemultiplier,2);
return aggregateTx;
}
Bは通常のアグリゲートコンプリート同様,連署してトランザクションを返却します.Aが再署名する余裕を持たせるため,状況に応じて複数のトランザクションに連署して返した方が良いと思います.
この方法はワンタイムアドレスのモザイクを一度きりしか行えない量に制限することで取引が複数回行われないようになっています.ただし,モザイクを再入金することで再度行える状態にすることも可能です.第三者にモザイクを入金され,トランザクションを複数回実行されないようにしたい場合にはアドレス制限をかけて入金されないようにするなどの対策を取る必要があります.
ワンタイムアドレスに送金したモザイクをトランザクションが実行する前に自分のアドレスに戻すことで,取引の中止を行うことも可能です.
##例2 ランダムに生成したアドレスに対してアドレス制限を行う
この方法ではアドレス制限を混ぜることで複数回実行しようとすると'Failure_RestrictionAccount_Invalid_Modification'が発生し無効になります.仕組み上,ワンタイムアドレスを利用する必要はありません.ただし,アドレス制限を手動で解除した場合は再実行が可能です.また,同じアドレスにアドレス制限を加えていくことになるためアドレス制限の枠を使い切ると使用できなくなります.ワンタイムアドレスがアドレス制限を行えば枠の問題は無いのですが,この場合はワンタイムアドレスの秘密鍵を確実かつ安全に破棄することが必要になります.
const randomAccount = xym.Account.generateNewAccount(networkType);
const a_account = xym.Account.createFromPrivateKey(
******,
networkType,
);
const b_account = xym.Account.createFromPrivateKey(
'******',
networkType,
);
const a_publicAccount = xym.PublicAccount.createFromPublicKey(
'8E9765F3471D5E7AEA39620F0998C25C96CEFA24F78980F31B55604FAF7C8868',
networkType,
);
const b_publicAccount = xym.PublicAccount.createFromPublicKey(
'CCECC77ABE531E7038A7D4E416BB99739642B56014034F1C45B1BD157803C105',
networkType,
);
const Txs = deadLineTest(7);//6時間*7回=42時間の有効期限のトランザクションを生成
var signedTxs = [];
for(tx of Txs){
const singed = a_account.sign(tx,networkGenerationHash);
const onetimeCosign = xym.CosignatureTransaction.signTransactionPayload(//onetimeアドレスの連署
onetimeAccount,
singed.payload,
networkGenerationHash
);
signedTxs.push({"signed":singed,"cosign":onetimeCosign});
}
function futureDeadlineTx(num){
const now = Date.now();
var Txs = [];
for(i=6;i<num*6;i+=6){
//現在のUNIXミリ秒-Symbolが誕生したUNIX秒*1000 + 6の倍数
const deadline = new xym.Deadline(now - epochAdjustment * 1000 + 60*60*i*1000);
console.log(deadline);
Txs.push(createFutureTransaction(deadline));
}
return Txs;
}
function createFutureTransaction(futureDeadline){
//使い捨てアドレスからb
const onetimetob = xym.TransferTransaction.create(
futureDeadline,
b_publicAccount.address,
[new xym.Mosaic(
currencyMosaicId,
xym.UInt64.fromUint(7*1000000)
)],
xym.PlainMessage.create('atob'),
networkType,
);
//bからa
const btoa = xym.TransferTransaction.create(
futureDeadline,
a_publicAccount.address,
[new xym.Mosaic(
new xym.MosaicId('310378C18A140D1B'),
xym.UInt64.fromUint(5)
)],
xym.PlainMessage.create('btoa'),
networkType,
);
//アドレス制限を行う
const addressRestrictionTx = xym.AccountAddressRestrictionTransaction.create(
futureDeadline,
xym.AddressRestrictionFlag.BlockOutgoingAddress,
[randomAccount.address],
[],
networkType,
);
const aggregateTx = xym.AggregateTransaction.createComplete(
futureDeadline,
[
atob.toAggregate(a_publicAccount),
btoa.toAggregate(b_publicAccount),
addressRestrictionTx.toAggregate(a_publicAccount),
],
networkType,
[],
).setMaxFeeForAggregate(feemultiplier,1);
return aggregateTx;
}
その他にもアドレスに事前にリンクさせたマルチシグを解除したり,ランダム生成したワンタイムアカウントのアドレス制限を混ぜるなど(プログラム実行後に秘密鍵を破棄)色々なやり方があると思います.しかし,どの方法でも条件をクリアさえしてしまえばトランザクションの再実行ができてしまいます.個人的には,ワンタイムアドレスの秘密鍵が漏れても被害が少なくなるように作るのが良いと思います.
この記事を書いてから思ったのですがアグリゲートボンデッドでもおそらく同じことが可能です.アグリゲートボンデッドで作ればAが署名を完成させる手間を省くことができますし,有効期間も2日なので必要な未来のトランザクションの量が1/8に減少します.署名したいときにBがハッシュロックを行い,ボンデッドをBがアナウンスして連署すればおそらく可能とおもいます.アグリゲートコンプリートを使うよりも断然便利ですし,アグリゲートボンデッドを使用する上で最大のネックとなっていた10XYMのロック問題も解決します.もし可能であればアグリゲートボンデッドを使うべきだと思います(今回は記事が長くなったので未検証です)
###未来のトランザクションをノードにアナウンスした際の挙動について
未来のトランザクションをノードは受け入れない仕様になっているため(deadlineが6時間後までのトランザクション)未来のトランザクションをノードに送り付けると,’Failure_Core_Future_Deadline’となり失敗します.そして,その未来のトランザクションを送ったノードでは有効なdeadlineになっても受け入れなくなります.このため,未来のトランザクションは有効になる前にノードに送らないように注意する必要があります.有効になる前に送ってしまった場合は他のノードにアナウンスしてあげることで正常にアナウンスすることが可能です.
#番外編2 署名済みトランザクション自体をチェーンに書き込む
連署のためにはトランザクションを相手にメールなどの様々な手段で送ってあげる必要がありますが,署名済みトランザクション自体は公開しても害のあるものではないのでチェーンにそのまま書き込んでしまうことで署名を伝送する手間を省くことができます.
const signed = JSON.stringify(signedTransaction);//署名済みトランザクションを文字列化
const transferTransaction = xym.TransferTransaction.create(
xym.Deadline.crate(epochAdjustment),
targetAddress,
[new xym.Mosaic(
currencyMosaicId,
xym.UInt64.fromUint(0)
)],
xym.PlainMessage.create(signed),
networkType,
).setMaxFee(feemultiplier);
const payloadTransferTransactionSigned= a_account.sign(
transferTransaction,
networkGenerationHash,
);
トランザクションによってはpayloadが長くなり,transferTransactionでは納めきれなくなると思います.その場合はアグリゲートトランザクションを使って文字数を増やすと良いと思います.
この方法は平文で署名済トランザクションを公開しているため誰でもトランザクションを見ることができます.このため,いたずらで署名済みトランザクションをノードに投げられルト,アナウンスが出来なくなる可能性があります.簡単な暗号化をおこなうか,外部にrestを公開しないapiノードを建ててそのノードからアナウンスすることで対応は可能だと思います.