Created
August 26, 2022 08:51
-
-
Save YasunoriMATSUOKA/de73a1478efbb94cd6a0aa3bd8356da5 to your computer and use it in GitHub Desktop.
Sample code to send aggregate complete transaction to send from a single account to multi accounts with @nemtus/symbol-sdk-openapi-generator-typescript-axios and @nemtus/symbol-sdk-typescript
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { SymbolFacade } from "@nemtus/symbol-sdk-typescript/esm/facade/SymbolFacade"; | |
import { PrivateKey } from "@nemtus/symbol-sdk-typescript/esm/CryptoTypes"; | |
import { KeyPair } from "@nemtus/symbol-sdk-typescript/esm/symbol/KeyPair"; | |
import { Signature } from "@nemtus/symbol-sdk-typescript/esm/symbol/models"; | |
import { | |
Configuration, | |
NetworkRoutesApi, | |
TransactionRoutesApi, | |
} from "@nemtus/symbol-sdk-openapi-generator-typescript-axios"; | |
import WebSocket from "ws"; | |
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: signer1PublicKeyString, | |
recipientAddress: signer2AddressString, | |
mosaics: [{ mosaicId: networkCurrencyMosaicId, amount: 1000000n }], | |
}); | |
const embeddedTransaction3 = facade.transactionFactory.createEmbedded({ | |
type: "transfer_transaction", | |
signerPublicKey: signer1PublicKeyString, | |
recipientAddress: signer3AddressString, | |
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 | |
); | |
// アグリゲートコンプリートトランザクションに署名 | |
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()}` | |
); | |
// トランザクション送信時にはこのデータを使う必要あり | |
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