Skip to content

Instantly share code, notes, and snippets.

@YasunoriMATSUOKA
Last active August 29, 2022 03:07
Show Gist options
  • Save YasunoriMATSUOKA/2469864e4349529c692317371e3e261b to your computer and use it in GitHub Desktop.
Save YasunoriMATSUOKA/2469864e4349529c692317371e3e261b to your computer and use it in GitHub Desktop.
Sample code to send aggregate complete transaction to send transactions from multi accounts with @nemtus/symbol-sdk-openapi-generator-typescript-axios and @nemtus/symbol-sdk-typescript
import { SymbolFacade } from "@nemtus/symbol-sdk-typescript/esm/facade/SymbolFacade";
import {
PrivateKey,
PublicKey,
} from "@nemtus/symbol-sdk-typescript/esm/CryptoTypes";
import { PublicKey as PublicKeyModel } from "@nemtus/symbol-sdk-typescript/esm/symbol/models";
import { KeyPair } from "@nemtus/symbol-sdk-typescript/esm/symbol/KeyPair";
import { Signature } from "@nemtus/symbol-sdk-typescript/esm/symbol/models";
import { hexToUint8 } from "@nemtus/symbol-sdk-typescript/esm/utils/converter";
import {
Configuration,
NetworkRoutesApi,
TransactionRoutesApi,
} from "@nemtus/symbol-sdk-openapi-generator-typescript-axios";
import WebSocket from "ws";
import { Cosignature } from "@nemtus/symbol-sdk-typescript/esm/symbol/models";
const NODE_DOMAIN = "symbol-test.next-web-technology.com";
(async () => {
// epochAdjustment, networkCurrencyMosaicIdの取得のためNetworkRoutesApi.getNetworkPropertiesを呼び出す
const configurationParameters = {
basePath: `http://${NODE_DOMAIN}:3000`,
};
const configuration = new Configuration(configurationParameters);
const networkRoutesApi = new NetworkRoutesApi(configuration);
const networkPropertiesDTO = (await networkRoutesApi.getNetworkProperties())
.data;
// epochAdjustmentのレスポンス値は文字列でsが末尾に含まれるため除去してnumberに変換する
const epochAdjustmentOriginal = networkPropertiesDTO.network.epochAdjustment;
if (!epochAdjustmentOriginal) {
throw Error("epochAdjustment is not found");
}
const epochAdjustment = parseInt(epochAdjustmentOriginal.replace(/s/g, ""));
// networkCurrencyMosaicIdのレスポンス値はhex文字列で途中に'が含まれるため除去してBigIntに変換する
const networkCurrencyMosaicIdOriginal =
networkPropertiesDTO.chain.currencyMosaicId;
if (!networkCurrencyMosaicIdOriginal) {
throw Error("networkCurrencyMosaicId is not found");
}
const networkCurrencyMosaicId = BigInt(
networkCurrencyMosaicIdOriginal.replace(/'/g, "")
);
// facadeの中に指定するtestnet等のネットワーク名を取得するためNetworkRoutesApi.getNetworkTypeを呼び出す
const networkTypeDTO = (await networkRoutesApi.getNetworkType()).data;
if (!networkTypeDTO) {
throw Error("networkType is not found");
}
const networkName = networkTypeDTO.name;
// ネットワーク名を指定してSDKを初期化
const facade = new SymbolFacade(networkName);
// トランザクションを送信するアカウント関連データを作成
const signer1PrivateKey = new PrivateKey(
process.env.SIGNER_1_PRIVATE_KEY ? process.env.SIGNER_1_PRIVATE_KEY : ""
);
const signer1KeyPair = new KeyPair(signer1PrivateKey);
const signer1PublicKey = signer1KeyPair.publicKey;
const signer1PublicKeyString = signer1PublicKey.toString();
const signer1Address = facade.network.publicKeyToAddress(signer1PublicKey);
const signer1AddressString = signer1Address.toString();
console.log("signer1AddressString", signer1AddressString);
const signer2PrivateKey = new PrivateKey(
process.env.SIGNER_2_PRIVATE_KEY ? process.env.SIGNER_2_PRIVATE_KEY : ""
);
const signer2KeyPair = new KeyPair(signer2PrivateKey);
const signer2PublicKey = signer2KeyPair.publicKey;
const signer2PublicKeyString = signer2PublicKey.toString();
const signer2Address = facade.network.publicKeyToAddress(signer2PublicKey);
const signer2AddressString = signer2Address.toString();
console.log("signer2AddressString", signer2AddressString);
const signer3PrivateKey = new PrivateKey(
process.env.SIGNER_3_PRIVATE_KEY ? process.env.SIGNER_3_PRIVATE_KEY : ""
);
const signer3KeyPair = new KeyPair(signer3PrivateKey);
const signer3PublicKey = signer3KeyPair.publicKey;
const signer3PublicKeyString = signer3PublicKey.toString();
const signer3Address = facade.network.publicKeyToAddress(signer3PublicKey);
const signer3AddressString = signer3Address.toString();
console.log("signer3AddressString", signer3AddressString);
// deadlineの計算(2時間で設定しているが変更可能、ただし遠すぎるとエラーになる。5~6時間くらいにテストネットでは閾値がある?)
const now = Date.now();
const deadline = BigInt(now - epochAdjustment * 1000 + 2 * 60 * 60 * 1000);
// 送信先アドレス
const recipientAddressString = "TBK7XV2NHC466HZ63XC7RPESLNXFEGCSJ3ZZ2FY";
console.log("recipientAddressString", recipientAddressString);
// アグリゲートトランザクションに埋め込まれる内部トランザクションのデータ生成
const embeddedTransaction1 = facade.transactionFactory.createEmbedded({
type: "transfer_transaction",
signerPublicKey: signer1PublicKeyString,
recipientAddress: recipientAddressString,
mosaics: [{ mosaicId: networkCurrencyMosaicId, amount: 1000000n }],
});
const embeddedTransaction2 = facade.transactionFactory.createEmbedded({
type: "transfer_transaction",
signerPublicKey: signer2PublicKeyString,
recipientAddress: recipientAddressString,
mosaics: [{ mosaicId: networkCurrencyMosaicId, amount: 1000000n }],
});
const embeddedTransaction3 = facade.transactionFactory.createEmbedded({
type: "transfer_transaction",
signerPublicKey: signer3PublicKeyString,
recipientAddress: recipientAddressString,
mosaics: [{ mosaicId: networkCurrencyMosaicId, amount: 1000000n }],
});
const embeddedTransactions = [
embeddedTransaction1,
embeddedTransaction2,
embeddedTransaction3,
];
// 内部トランザクションのハッシュ値を計算
const embeddedTransactionsHash = (
facade.constructor as any
).hashEmbeddedTransactions(embeddedTransactions);
// アグリゲートコンプリートトランザクションのデータ生成
const aggregateCompleteTransaction = facade.transactionFactory.create({
type: "aggregate_complete_transaction",
signerPublicKey: signer1PublicKeyString,
deadline,
transactionsHash: embeddedTransactionsHash.toString(),
transactions: embeddedTransactions,
});
// 手数料設定 ... 送信先ノードの設定によるが100なら基本的に足りないことはないと思う
const feeMultiplier = 100;
const signerNumber = 3;
console.log((aggregateCompleteTransaction as any).size);
(aggregateCompleteTransaction as any).fee.value = BigInt(
((aggregateCompleteTransaction as any).size +
Signature.SIZE * signerNumber) *
feeMultiplier
);
// アグリゲートコンプリートトランザクションに署名 ... ネットワークにアナウンスするsigner1による署名
const aggregateCompleteTransactionSignature = facade.signTransaction(
signer1KeyPair,
aggregateCompleteTransaction
);
(aggregateCompleteTransaction as any).signature = new Signature(
aggregateCompleteTransactionSignature.bytes
);
// アグリゲートコンプリートトランザクションに各ネットワーク固有のgenerationHashSeedを設定
(aggregateCompleteTransaction as any).network.generationHashSeed =
facade.network;
// アグリゲートコンプリートトランザクションのハッシュを計算 ... トランザクションの承認状態を後でWebSocketで確認したり、事前にオフライン署名を集める際に必要
const aggregateCompleteTransactionHash = facade.hashTransaction(
aggregateCompleteTransaction
);
console.log(
"aggregateCompleteTransaction",
aggregateCompleteTransactionHash.toString()
);
console.log(
`https://testnet.symbol.fyi/transactions/${aggregateCompleteTransactionHash.toString()}`
);
// signer2による連署作成 ... アグリゲートコンプリートトランザクションのハッシュのバイナリに対して署名する
const signatureBySigner2 = new Signature(
signer2KeyPair.sign(
hexToUint8(aggregateCompleteTransactionHash.toString())
).bytes
);
const cosignatureBySigner2 = new Cosignature();
cosignatureBySigner2.signerPublicKey = new PublicKeyModel(
signer2PublicKey.bytes
);
cosignatureBySigner2.signature = signatureBySigner2;
// signer3による連署作成 ... アグリゲートコンプリートトランザクションのハッシュのバイナリに対して署名する
const signatureBySigner3 = new Signature(
signer3KeyPair.sign(
hexToUint8(aggregateCompleteTransactionHash.toString())
).bytes
);
const cosignatureBySigner3 = new Cosignature();
cosignatureBySigner3.signerPublicKey = new PublicKeyModel(
signer3PublicKey.bytes
);
cosignatureBySigner3.signature = signatureBySigner3;
// 連署データを事前にオフラインで配列に集約してトランザクションにセット
const cosignatures = [cosignatureBySigner2, cosignatureBySigner3];
(aggregateCompleteTransaction as any).cosignatures = cosignatures;
// トランザクション送信時にはこのデータを使う必要あり
const aggregateCompleteTransactionPayload = (
facade.transactionFactory.constructor as any
).attachSignature(
aggregateCompleteTransaction,
aggregateCompleteTransactionSignature
);
// 1 confirmation以外の場合の設定
const confirmationHeight = 6; // 6confで確認と見なす場合
let aggregateCompleteTransactionHeight = 0;
let blockHeight = 0;
let finalizedBlockHeight = 0;
// WebSocketでトランザクション送信時の各種イベントに応じた処理を事前定義しておく必要がある
const ws = new WebSocket(`wss://${NODE_DOMAIN}:3001/ws`);
ws.on("open", () => {
console.log("connection open");
});
ws.on("close", () => {
console.log("connection closed");
});
ws.on("message", async (msg: any) => {
const res = JSON.parse(msg);
if ("uid" in res) {
console.log(`uid : ${res.uid}`);
// ターゲットアドレスの部分トランザクションが追加されるのを監視
const partialBody = `{"uid": "${res.uid}", "subscribe": "partialAdded/${signer1AddressString}"}`;
console.log(partialBody);
ws.send(partialBody);
// ターゲットアドレスのトランザクションが未承認状態になったのを監視
const unconfirmedBody = `{"uid": "${res.uid}", "subscribe": "unconfirmedAdded/${signer1AddressString}"}`;
console.log(unconfirmedBody);
ws.send(unconfirmedBody);
// ターゲットアドレスのトランザクションが承認されるの監視
const confirmedBody = `{"uid": "${res.uid}", "subscribe": "confirmedAdded/${signer1AddressString}"}`;
console.log(confirmedBody);
ws.send(confirmedBody);
// ターゲットアドレスのトランザクションがエラーになったのを監視
const statusBody = `{"uid": "${res.uid}", "subscribe": "status/${signer1AddressString}"}`;
console.log(statusBody);
ws.send(statusBody);
// 新しいブロックを監視
const blockBody = `{"uid": "${res.uid}", "subscribe": "block"}`;
console.log(blockBody);
ws.send(blockBody);
// ファイナライズされたブロックを監視
const finalizedBlockBody = `{"uid": "${res.uid}", "subscribe": "finalizedBlock"}`;
console.log(finalizedBlockBody);
ws.send(finalizedBlockBody);
}
// アグリゲートコンプリートトランザクションが未承認になったときに発火
if (
res.topic === `unconfirmedAdded/${signer1AddressString}` &&
res.data.meta.hash === aggregateCompleteTransactionHash.toString()
) {
console.log("aggregateCompleteTransaction unconfirmed");
}
// アグリゲートコンプリートトランザクションが承認されたときに発火
if (
res.topic === `confirmedAdded/${signer1AddressString}` &&
res.data.meta.hash === aggregateCompleteTransactionHash.toString()
) {
console.log("hashLockTransaction confirmed");
aggregateCompleteTransactionHeight = parseInt(res.data.meta.height);
}
// ブロック生成時に発火
if (res.topic === `block`) {
console.log("block");
blockHeight = parseInt(res.data.block.height);
}
// ブロックのファイナライズ時に発火
if (res.topic === `finalizedBlock`) {
console.log("finalizedBlock");
console.log(res);
finalizedBlockHeight = parseInt(res.data.height);
}
// トランザクションがエラーになったときに発火
if (
res.topic === `status/${signer1AddressString}` &&
res.data.hash === aggregateCompleteTransactionHash.toString()
) {
console.log(res);
console.log(res.data.code);
ws.close();
} else {
console.log(res);
}
// confirmationHeightブロック後に監視終了
if (
aggregateCompleteTransactionHeight !== 0 &&
aggregateCompleteTransactionHeight + confirmationHeight - 1 <= blockHeight
) {
console.log(
`${confirmationHeight} blocks confirmed. transactionHeight is ${aggregateCompleteTransactionHeight} blockHeight is ${blockHeight}.`
);
ws.close();
} else {
console.log(
`wait for ${confirmationHeight} blocks. transactionHeight is ${aggregateCompleteTransactionHeight} blockHeight is ${blockHeight}.`
);
}
// finalizedBlockHeightが対象ブロックを追い越した後に監視終了
if (
aggregateCompleteTransactionHeight !== 0 &&
aggregateCompleteTransactionHeight <= finalizedBlockHeight
) {
console.log(
`${finalizedBlockHeight} block finalized. transactionHeight is ${aggregateCompleteTransactionHeight} blockHeight is ${blockHeight}.`
);
ws.close();
} else {
console.log(
`wait for finalized block. transactionHeight is ${aggregateCompleteTransactionHeight} blockHeight is ${blockHeight}.`
);
}
});
// トランザクションのアナウンス実行
try {
const transactionRoutesApi = new TransactionRoutesApi(configuration);
console.log(aggregateCompleteTransactionPayload);
const response = await transactionRoutesApi.announceTransaction({
transactionPayload: aggregateCompleteTransactionPayload,
});
console.log(response.data);
} catch (err) {
console.error(err);
}
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment