Qucik Learning Symbol Section 10
Chapter 10 Monitoring
Symbol nodes can monitor blockchain state changes via WebSocket communication.
10.1 Listener configuration
PHP is a server-side language, and monitoring blockchains using WebSockets with PHP is a fairly rare pattern, so here we will show you how to implement it on the front end using javascript. We will introduce how to implement it on the front end using javascript. The following is based on SymbolV3 for quick learning. The listener in v2 is a function dependent on rxjs, so v3 does not have a listener function. Therefore, implementers need to program WebSocket clients.
// チャンネル名
ListenerChannelName = {
block: "block",
confirmedAdded: "confirmedAdded",
unconfirmedAdded: "unconfirmedAdded",
unconfirmedRemoved: "unconfirmedRemoved",
partialAdded: "partialAdded",
partialRemoved: "partialRemoved",
cosignature: "cosignature",
modifyMultisigAccount: "modifyMultisigAccount",
status: "status",
finalizedBlock: "finalizedBlock",
};
// 各種設定
wsEndpoint = NODE.replace("http", "ws") + "/ws"; // WebSocketエンドポイント設定
uid = "";
funcs = {};
// チャンネルへのコールバック追加
addCallback = (channel, callback) => {
if (!funcs.hasOwnProperty(channel)) {
funcs[channel] = [];
}
funcs[channel].push(callback);
};
// WebSocket初期化
listener = new WebSocket(wsEndpoint);
// メッセージ受信時処理
listener.onmessage = function (e) {
// 受信データをJSON変換
data = JSON.parse(e.data);
// WebSocket初期化後、ノードから uid を渡されるため保持しておく
if (data.uid != undefined) {
uid = data.uid;
return;
}
// subscribe しているチャンネルであればコールバックを実行する
if (funcs.hasOwnProperty(data.topic)) {
funcs[data.topic].forEach((f) => {
f(data.data);
});
}
};
// エラー時処理
listener.onerror = function (error) {
console.error(error.data);
};
// クローズ時処理
listener.onclose = function (closeEvent) {
uid = "";
funcs = {};
console.log(closeEvent);
};
10.2 Receiving transactions
Detects transactions received by the account.
// 承認トランザクション検知時の処理
channelName =
ListenerChannelName.confirmedAdded + "/" + aliceAddress.toString();
addCallback(channelName, (tx) => {
console.log(tx);
});
// 承認トランザクション検知設定
listener.send(
JSON.stringify({
uid: uid,
subscribe: channelName,
}),
);
// 未承認トランザクション検知時の処理
channelName =
ListenerChannelName.unconfirmedAdded + "/" + aliceAddress.toString();
addCallback(channelName, (tx) => {
console.log(tx);
});
// 未承認トランザクション検知設定
listener.send(
JSON.stringify({
uid: uid,
subscribe: channelName,
}),
);
Sample output
> {transaction: {…}, meta: {…}}
> meta:
hash: "A95F2E59D22EDA24D1E82515D452F106B93D7C869B601CCA63A46D0EB2CFB182"
height: "0"
merkleComponentHash: "A95F2E59D22EDA24D1E82515D452F106B93D7C869B601CCA63A46D0EB2CFB182"
> transaction:
deadline: "22961573427"
maxFee: "25168"
> mosaics: Array(1)
0: {id: '72C0212E67A08BCE', amount: '1000000'}
length: 1
network: 152
recipientAddress: "98223AF34A98119217DC2427C6DE7F577A33D8242A2F54C3"
signature: "926C1474D285D9C3022ED250A0E3B43096BF94D70036D8FB68CEED56A05B5DFFD75250FE57B1390A4BAFBE9517126F6E109AF156CB5B1E9FC23F433E6FC11E0F"
signerPublicKey: "69A31A837EB7DE323F08CA52495A57BA0A95B52D1BB54CEA9A94C12A87B1CADB"
type: 16724
version: 1
Unconfirmed transactions are received with transactionInfo.height=0.
■ notePlease note that the sender may be sending using a namespace when detecting receipt by destination address or mosaic ID. For example, the mosaic ID of XYM on the mainnet is 6BED913FA20223F8, but if a user sends with a namespace ID (symbol.xym), the transaction records the ID E74B99BA41F4AFEE.
10.3 Block monitoring
Detects newly generated blocks.
// ブロック生成検知時の処理
addCallback(ListenerChannelName.block, (block) => {
console.log(block);
});
// ブロック生成検知設定
listener.send(
JSON.stringify({
uid: uid,
subscribe: ListenerChannelName.block,
}),
);
Sample output
> {block: {…}, meta: {…}}
> block:
beneficiaryAddress: "98BE9AC4CD3E833736762A12A63065FF42E476744E6FC597"
difficulty: "11527429947328"
feeMultiplier: 0
height: "663306"
network: 152
previousBlockHash: "511D2E9940130E3F58875D9FAF0ACABAEA85094B2CF2BE4FE8785B81294BEC5D"
proofGamma: "A6FE7FE258D37448C33357EFE2A66E93895FAA38C8AF219CA7DECE6186D44754"
proofScalar: "BC71B55808169D3A245CC1818C47B5ECDAB29ED02BF98916144CA52BDC06C403"
proofVerificationHash: "C027383E6ED8937242FCE3CC439B52D4"
receiptsHash: "69CB5A2E56E51812065187CC61AEAB6B5E413CFB34B0CBB9ADBDB588986A1624"
signature: "21C31EF3019A4D888A34FE1EE3864B66FBA818EFB53CA72CEB50001393527D766FD2897C428867C3421F472FA9546B57E6856076FD522AF3CE9D3CD618C2170E"
signerPublicKey: "87EEE5E3D69BAA60C093FC2080BA5D36E623C5C0BCDC529B8712A9B6212420D7"
stateHash: "DF47AA56BBB3D74088342A9DFFB6DB164F5699BB9D607789B7016A55DE5D15C9"
timestamp: "22953836986"
transactionsHash: "0000000000000000000000000000000000000000000000000000000000000000"
type: 33091
version: 1
> meta:
generationHash: "B76DE01D89CC6672F30AC183BCEA601DE019AD7D37C84CAE723814A59AED253F"
hash: "88277C8A9B45D075BF554DA5DAA24667DAE844DE1C583DFB4A5891822BE9A0DB"
10.4 Signature request
Detects when a transaction requiring a signature occurs.
// 署名が必要なアグリゲートボンデッドトランザクション発生検知時の処理
channelName = ListenerChannelName.partialAdded + "/" + aliceAddress.toString();
addCallback(channelName, (tx) => {
console.log(tx);
});
// 署名が必要なアグリゲートボンデッドトランザクション発生検知設定
listener.send(
JSON.stringify({
uid: uid,
subscribe: channelName,
}),
);
Sample output
> {transaction: {…}, meta: {…}}
> meta:
hash: "D7DD269A6D6EDC6A1F95045609BBF645B5FD908ED540C312489261C2913955DB"
height: "0"
merkleComponentHash: "0000000000000000000000000000000000000000000000000000000000000000"
> transaction:
deadline: "23127942600"
maxFee: "47200"
network: 152
signature: "05FBC99BA33EA0DA79AB04D555645552DDDC66ABCFDF0FBAEEC0E84531F3CF52E3D8B76F45692B009421185C79993B3B41574E4B149D2931D08632318B7C610E"
signerPublicKey: "69A31A837EB7DE323F08CA52495A57BA0A95B52D1BB54CEA9A94C12A87B1CADB"
> transactions: Array(1)
> 0:
> transaction:
> mosaics: Array(1)
0: {id: '72C0212E67A08BCE', amount: '1000000'}
length: 1
network: 152
recipientAddress: "98223AF34A98119217DC2427C6DE7F577A33D8242A2F54C3"
signerPublicKey: "99687A9A5C5DA3EC97D0568781FE5AB5C4BB9D18F4BA9343AE5BBD1D2C0CA788"
type: 16724
version: 1
length: 1
transactionsHash: "5B59E92F56E78AB14E751177413807CEA7A4C8426F1A756DA43B74EEE1F32679"
type: 16961
version: 2
All Aggregate Transactions involving the specified address are detected. Whether a cosignature is required is determined by a separate filter.
10.5 Tips for use
10.5.1 Continuous connection
Select randomly from the node list and try to connect.
Connection to node
//ノード一覧
NODES = ["https://node.com:3001",...];
function connectNode(nodes) {
const node = nodes[Math.floor(Math.random() * nodes.length)] ;
console.log("try:" + node);
return new Promise((resolve, reject) => {
let req = new XMLHttpRequest();
req.timeout = 2000; //タイムアウト値:2秒(=2000ms)
req.open('GET', node + "/node/health", true);
req.onload = function() {
if (req.status === 200) {
const status = JSON.parse(req.responseText).status;
if(status.apiNode == "up" && status.db == "up"){
return resolve(node);
}else{
console.log("fail node status:" + status);
return connectNode(nodes).then(node => resolve(node));
}
} else {
console.log("fail request status:" + req.status)
return connectNode(nodes).then(node => resolve(node));
}
};
req.onerror = function(e) {
console.log("onerror:" + e)
return connectNode(nodes).then(node => resolve(node));
};
req.ontimeout = function (e) {
console.log("ontimeout")
return connectNode(nodes).then(node => resolve(node));
};
req.send();
});
}
Set a timeout value and re-select a node if connected node response is slow. Check the endpoint /node/health and reselect the node if the status is not normal.
■Creation of repositoriesSince there are no repositories in v3, the URLs of nodes that can be connected are returned.
function searchUrl(nodes) {
return connectNode(nodes).then(async function onFulfilled(node) {
try {
epochAdjustment = await fetch(new URL("/network/properties", node), {
method: "GET",
headers: { "Content-Type": "application/json" },
})
.then((res) => res.json())
.then((json) => {
identifier = json.network.identifier; // v3 only
facade = new symbolSdk.facade.SymbolFacade(identifier); // v3 only
e = json.network.epochAdjustment;
return Number(e.substring(0, e.length - 1));
});
} catch (error) {
console.error("fail searchUrl", error);
return await searchUrl(nodes);
}
return node;
});
}
■Continuous connection listeners
async function listenerKeepOpening(nodes) {
const url = await searchUrl(nodes);
let wsEndpoint = url.replace("http", "ws") + "/ws";
// WebSocket初期化
const lner = new WebSocket(wsEndpoint);
try {
// メッセージ受信時処理
lner.onmessage = function (e) {
// 受信データをJSON変換
data = JSON.parse(e.data);
// WebSocket初期化後、ノードから uid を渡されるため保持しておく
if (data.uid != undefined) {
uid = data.uid;
return;
}
// subscribe しているチャンネルであればコールバックを実行する
if (funcs.hasOwnProperty(data.topic)) {
funcs[data.topic].forEach((f) => {
f(data.data);
});
}
};
// エラー時処理
lner.onerror = function (error) {
console.error(error.data);
};
// クローズ時処理
lner.onclose = async function (closeEvent) {
console.log("listener onclose");
uid = "";
funcs = {};
listener = await listenerKeepOpening(nodes);
};
// WebSocket がオープンされるまで待機
await new Promise((resolve, reject) => {
interval = setInterval(() => {
if (lner.readyState > 0) {
clearInterval(interval);
resolve();
}
}, 1000);
});
// ブロック生成検知時の処理
addCallback(ListenerChannelName.block, (block) => {
console.log(block);
});
// ブロック生成検知設定
lner.send(
JSON.stringify({
uid: uid,
subscribe: ListenerChannelName.block,
}),
);
} catch (e) {
console.error("fail websocket", e);
return await listenerKeepOpening(nodes);
}
console.log("listener connected : ", lner.url);
return lner;
}
If the listener closes, it reconnects.
■Start of listener
listener = await listenerKeepOpening(NODES);
10.5.2 Unsigned transaction auto-signature
Detect unsigned transactions, then sign and announce to the network. Two patterns of detection are required: reception during initial screen display and during screen viewing.
// 選択中アカウントの完了トランザクション検知リスナー
const statusChanged = function (address, hash) {
// 承認トランザクション検知時の処理
const confirmedChannelName =
ListenerChannelName.confirmedAdded + "/" + address.toString();
addCallback(confirmedChannelName, (tx) => {
if (tx.meta.hash === hash.toString()) {
console.log(tx);
}
});
// 承認トランザクション検知設定
listener.send(
JSON.stringify({
uid: uid,
subscribe: confirmedChannelName,
}),
);
// ステータス変更検知時の処理
const statusChannelName =
ListenerChannelName.status + "/" + address.toString();
addCallback(statusChannelName, (status) => {
if (status.hash === hash.toString()) {
console.error(status);
}
});
// ステータス変更検知設定
listener.send(
JSON.stringify({
uid: uid,
subscribe: statusChannelName,
}),
);
};
// 連署実行
async function exeAggregateBondedCosignature(aggTx) {
// インナートランザクションの署名者に自分が指定されている場合
if (
aggTx.transaction.transactions.find(
(inTx) =>
inTx.transaction.signerPublicKey === bobKey.publicKey.toString(),
) !== undefined
) {
// Aliceのトランザクションで署名
cosignature = new symbolSdk.symbol.DetachedCosignature();
signTxHash = new symbolSdk.symbol.Hash256(
symbolSdk.utils.hexToUint8(aggTx.meta.hash),
);
cosignature.parentHash = signTxHash;
cosignature.version = 0n;
cosignature.signerPublicKey = bobKey.publicKey;
cosignature.signature = new symbolSdk.symbol.Signature(
bobKey.sign(signTxHash.bytes).bytes,
);
// アナウンス
body = {
parentHash: cosignature.parentHash.toString(),
signature: cosignature.signature.toString(),
signerPublicKey: cosignature.signerPublicKey.toString(),
version: cosignature.version.toString(),
};
await fetch(new URL("/transactions/cosignature", NODE), {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
})
.then((res) => res.json())
.then((json) => {
return json;
});
statusChanged(bobAddress, signTxHash);
}
}
bondedSubscribe = async function (tx) {
// すでに署名済みか確認するため、トランザクション情報を取得
body = {
transactionIds: [tx.meta.hash],
};
partialTx = await fetch(new URL("/transactions/partial", NODE), {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
})
.then((res) => res.json())
.then((json) => {
if (json.length <= 0) {
return undefined;
}
return json[0];
});
if (partialTx === undefined) {
cosole.error("get tx info failed.");
return;
}
// 署名済みか確認
if (!partialTx.transaction.hasOwnProperty("cosignatures")) {
cosole.log("not aggregate tx.");
return;
}
bobCosignature = partialTx.transaction.cosignatures.find((c) => {
return c.signerPublicKey === bobKey.publicKey.toString();
});
if (
bobCosignature !== undefined &&
partialTx.transaction.signerPublicKey === bobKey.publicKey.toString()
) {
cosole.log("already signed.");
return;
}
console.log(partialTx);
exeAggregateBondedCosignature(partialTx);
};
// 署名が必要なアグリゲートボンデッドトランザクション発生検知時の処理
channelName = ListenerChannelName.partialAdded + "/" + bobAddress.toString();
addCallback(channelName, async (tx) => {
bondedSubscribe(tx);
});
// 署名が必要なアグリゲートボンデッドトランザクション発生検知設定
listener.send(
JSON.stringify({
uid: uid,
subscribe: channelName,
}),
);
// 指定アドレスのパーシャルトランザクションを検索する
async function searchPartialTxes(address, page = 1) {
query = new URLSearchParams({
address: address.toString(),
pageSize: 100,
pageNumber: page,
});
bondedTxes = await fetch(
new URL("/transactions/partial?" + query.toString(), NODE),
{
method: "GET",
headers: { "Content-Type": "application/json" },
},
)
.then((res) => res.json())
.then((json) => {
return json;
});
if (bondedTxes.data.length === 0) {
return [];
}
return bondedTxes.data.concat(await searchUnsignedBonded(address, page + 1));
}
// 指定アドレスの全てのパーシャルトランザクションを取得する
async function getAllPartialTxes(address) {
return await searchPartialTxes(address);
}
// 初期表示時
(await getAllPartialTxes(bobAddress)).forEach((partialTx) => {
bondedSubscribe(partialTx);
});
■Note
To avoid auto-signing scam transactions, make sure that you ensure a checking procedure is carried out, e.g. by checking the sender's account.