Created
August 25, 2022 07:24
-
-
Save YasunoriMATSUOKA/6b5c0f6391c39c1eafad1ee128b575e1 to your computer and use it in GitHub Desktop.
Sample code to send aggregate bonded transaction( = transfer transaction from 2 of 3 multisig account ) 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, | |
PublicKey, | |
} 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); | |
// 送信元となるマルチシグアカウントの公開鍵を指定 | |
const fromPublicKeyString = | |
"653CEB2649A4E86D818E2FACD2F67D76268E2BDBF9DB277627B236BFCE4C2835"; | |
const fromPublicKey = new PublicKey(fromPublicKeyString); | |
const fromAddress = facade.network.publicKeyToAddress(fromPublicKey); | |
const fromAddressString = fromAddress.toString(); | |
console.log("fromAddressString", fromAddressString); | |
// deadlineの計算(2時間で設定しているが変更可能、ただし遠すぎるとエラーになる。5~6時間くらいにテストネットでは閾値がある?) | |
const now = Date.now(); | |
const deadline = BigInt(now - epochAdjustment * 1000 + 2 * 60 * 60 * 1000); | |
// 送信先アドレス | |
const recipientAddressString = "TBFVGBN5XKVFWF3PKWRQRPH6SHSOTXJMXKYSTEQ"; | |
console.log("recipientAddressString", recipientAddressString); | |
// アグリゲートトランザクションに埋め込まれる内部トランザクションのデータ生成 | |
const embeddedTransaction1 = facade.transactionFactory.createEmbedded({ | |
type: "transfer_transaction", | |
signerPublicKey: fromPublicKeyString, | |
recipientAddress: recipientAddressString, | |
mosaics: [{ mosaicId: networkCurrencyMosaicId, amount: 1000000n }], | |
}); | |
const embeddedTransactions = [embeddedTransaction1]; | |
// 内部トランザクションのハッシュ値を計算 | |
const embeddedTransactionsHash = ( | |
facade.constructor as any | |
).hashEmbeddedTransactions(embeddedTransactions); | |
// アグリゲートボンデッドトランザクションのデータ生成 | |
const aggregateBondedTransaction = facade.transactionFactory.create({ | |
type: "aggregate_bonded_transaction", | |
signerPublicKey: signer1PublicKeyString, | |
deadline, | |
transactionsHash: embeddedTransactionsHash.toString(), | |
transactions: embeddedTransactions, | |
}); | |
// 手数料設定 ... 送信先ノードの設定によるが100なら基本的に足りないことはないと思う | |
const feeMultiplier = 100; | |
const signerNumber = 2; | |
console.log((aggregateBondedTransaction as any).size); | |
(aggregateBondedTransaction as any).fee.value = BigInt( | |
((aggregateBondedTransaction as any).size + Signature.SIZE * signerNumber) * | |
feeMultiplier | |
); | |
// アグリゲートボンデッドトランザクションに署名 | |
const aggregateBondedTransactionSignature = facade.signTransaction( | |
signer1KeyPair, | |
aggregateBondedTransaction | |
); | |
(aggregateBondedTransaction as any).signature = new Signature( | |
aggregateBondedTransactionSignature.bytes | |
); | |
// アグリゲートボンデッドトランザクションに各ネットワーク固有のgenerationHashSeedを設定 | |
(aggregateBondedTransaction as any).network.generationHashSeed = | |
facade.network; | |
// アグリゲートボンデッドトランザクションのハッシュを計算 ... トランザクションの承認状態を後でWebSocketで確認する時などに必要 | |
const aggregateBondedTransactionHash = facade.hashTransaction( | |
aggregateBondedTransaction | |
); | |
console.log( | |
"aggregateBondedTransaction", | |
aggregateBondedTransactionHash.toString() | |
); | |
console.log( | |
`https://testnet.symbol.fyi/transactions/${aggregateBondedTransactionHash.toString()}` | |
); | |
// トランザクション送信時にはこのデータを使う必要あり | |
const aggregateBondedTransactionPayload = ( | |
facade.transactionFactory.constructor as any | |
).attachSignature( | |
aggregateBondedTransaction, | |
aggregateBondedTransactionSignature | |
); | |
// ハッシュロックトランザクションの生成 | |
const hashLockTransaction = facade.transactionFactory.create({ | |
type: "hash_lock_transaction", | |
signerPublicKey: signer1PublicKeyString, | |
deadline, | |
hash: aggregateBondedTransactionHash.toString(), | |
mosaic: { mosaicId: networkCurrencyMosaicId, amount: 10000000n }, | |
duration: BigInt(((24 * 60 * 60) / 30) * 2), // 2日間の想定ブロック数 | |
}); | |
// ハッシュロックトランザクションの手数料を設定 | |
(hashLockTransaction as any).fee.value = BigInt( | |
(hashLockTransaction as any).size * feeMultiplier | |
); | |
// ハッシュロックトランザクションの署名 | |
const hashLockTransactionSignature = facade.signTransaction( | |
signer1KeyPair, | |
hashLockTransaction | |
); | |
(hashLockTransaction as any).signature = new Signature( | |
hashLockTransactionSignature.bytes | |
); | |
(hashLockTransaction as any).network.generationHashSeed = facade.network; | |
const hashLockTransactionHash = facade.hashTransaction(hashLockTransaction); | |
// ハッシュロックトランザクションのペイロードを生成 | |
const hashLockTransactionPayload = ( | |
facade.transactionFactory.constructor as any | |
).attachSignature(hashLockTransaction, hashLockTransactionSignature); | |
console.log(hashLockTransactionHash.toString()); | |
console.log( | |
`https://testnet.symbol.fyi/transactions/${hashLockTransactionHash.toString()}` | |
); | |
// 1 confirmation以外の場合の設定 | |
const confirmationHeight = 6; // 6confで確認と見なす場合 | |
let hashLockTransactionHeight = 0; | |
let aggregateBondedTransactionHeight = 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 === hashLockTransactionHash.toString() | |
) { | |
console.log("hashLockTransaction unconfirmed"); | |
} | |
// ハッシュロックトランザクションが承認されたときに発火 | |
if ( | |
res.topic === `confirmedAdded/${signer1AddressString}` && | |
res.data.meta.hash === hashLockTransactionHash.toString() | |
) { | |
console.log("hashLockTransaction confirmed"); | |
hashLockTransactionHeight = parseInt(res.data.meta.height); | |
// ハッシュロックトランザクションが承認されたら、アグリゲートボンデッドトランザクションをアナウンス | |
console.log("announce aggregateBondedTransaction"); | |
try { | |
const transactionRoutesApi = new TransactionRoutesApi(configuration); | |
console.log(aggregateBondedTransactionPayload); | |
const response = await transactionRoutesApi.announcePartialTransaction({ | |
transactionPayload: aggregateBondedTransactionPayload, | |
}); | |
console.log(response.data); | |
} catch (error) { | |
console.error(error); | |
} | |
} | |
// アグリゲートボンデッドトランザクションが未承認になったときに発火 | |
if ( | |
res.topic === `unconfirmedAdded/${signer1AddressString}` && | |
res.data.meta.hash === aggregateBondedTransactionHash.toString() | |
) { | |
console.log("aggregateBondedTransaction unconfirmed"); | |
} | |
// アグリゲートボンデッドトランザクションが承認されたときに発火 | |
if ( | |
res.topic === `confirmedAdded/${signer1AddressString}` && | |
res.data.meta.hash === aggregateBondedTransactionHash.toString() | |
) { | |
console.log("aggregateBondedTransaction confirmed"); | |
aggregateBondedTransactionHeight = 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 === aggregateBondedTransactionHash.toString() | |
) { | |
console.log(res.data.code); | |
ws.close(); | |
} else { | |
console.log(res); | |
} | |
// confirmationHeightブロック後に監視終了 | |
if ( | |
aggregateBondedTransactionHeight !== 0 && | |
aggregateBondedTransactionHeight + confirmationHeight - 1 <= blockHeight | |
) { | |
console.log( | |
`${confirmationHeight} blocks confirmed. transactionHeight is ${aggregateBondedTransactionHeight} blockHeight is ${blockHeight}.` | |
); | |
try { | |
const transactionRoutesApi = new TransactionRoutesApi(configuration); | |
const confirmedTransactionResponse = | |
await transactionRoutesApi.getConfirmedTransaction({ | |
transactionId: aggregateBondedTransactionHash.toString(), | |
}); | |
console.log(confirmedTransactionResponse.data); | |
if ( | |
confirmedTransactionResponse.data.meta.hash === | |
aggregateBondedTransactionHash.toString() | |
) { | |
console.log("transaction has confirmed with confirmation height."); | |
} else { | |
console.error("something unexpected error occurred."); | |
} | |
} catch (error) { | |
console.error( | |
"target transaction is not confirmed. It may be rollback." | |
); | |
console.error(error); | |
} | |
ws.close(); | |
} else { | |
console.log( | |
`wait for ${confirmationHeight} blocks. transactionHeight is ${aggregateBondedTransactionHeight} blockHeight is ${blockHeight}.` | |
); | |
} | |
// finalizedBlockHeightが対象ブロックを追い越した後に監視終了 | |
if ( | |
aggregateBondedTransactionHeight !== 0 && | |
aggregateBondedTransactionHeight <= finalizedBlockHeight | |
) { | |
console.log( | |
`${finalizedBlockHeight} block finalized. transactionHeight is ${aggregateBondedTransactionHeight} blockHeight is ${blockHeight}.` | |
); | |
try { | |
const transactionRoutesApi = new TransactionRoutesApi(configuration); | |
const confirmedTransactionResponse = | |
await transactionRoutesApi.getConfirmedTransaction({ | |
transactionId: aggregateBondedTransactionHash.toString(), | |
}); | |
console.log(confirmedTransactionResponse.data); | |
if ( | |
confirmedTransactionResponse.data.meta.hash === | |
aggregateBondedTransactionHash.toString() | |
) { | |
console.log("transaction has confirmed with confirmation height."); | |
} else { | |
console.error("something unexpected error occurred."); | |
} | |
} catch (error) { | |
console.error( | |
"target transaction is not confirmed. It may be rollback." | |
); | |
console.error(error); | |
} | |
ws.close(); | |
} else { | |
console.log( | |
`wait for finalized block. transactionHeight is ${aggregateBondedTransactionHeight} blockHeight is ${blockHeight}.` | |
); | |
} | |
}); | |
// トランザクションのアナウンス実行 | |
try { | |
const transactionRoutesApi = new TransactionRoutesApi(configuration); | |
console.log(hashLockTransactionPayload); | |
const response = await transactionRoutesApi.announceTransaction({ | |
transactionPayload: hashLockTransactionPayload, | |
}); | |
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