From d7b6c52f1e1310bae4f9f1e35f7684fa9820db13 Mon Sep 17 00:00:00 2001 From: Denis Fadeev Date: Tue, 12 Sep 2023 08:44:21 +0400 Subject: [PATCH] feat(cctx): accept both inbound and cctx hash (#45) --- helpers/tx.ts | 153 +++++++++++++++++++++++++++++++++++++++----------- tasks/cctx.ts | 6 +- 2 files changed, 125 insertions(+), 34 deletions(-) diff --git a/helpers/tx.ts b/helpers/tx.ts index f76cfb9d..6c4dbe09 100644 --- a/helpers/tx.ts +++ b/helpers/tx.ts @@ -1,6 +1,7 @@ import { getEndpoints } from "@zetachain/networks"; import axios from "axios"; import clc from "cli-color"; +import { HardhatRuntimeEnvironment } from "hardhat/types"; import Spinnies from "spinnies"; import WebSocket from "ws"; @@ -12,24 +13,40 @@ const getEndpoint = (key: any): string => { return endpoint; }; -const fetchCCTX = async ( +const findByChainId = (config: any, targetChainId: Number): Object | null => { + for (const key in config) { + if (config.hasOwnProperty(key) && config[key].hasOwnProperty("chainId")) { + if (config[key].chainId === targetChainId) { + return config[key]; + } + } + } + return null; +}; + +const fetchCCTXByInbound = async ( hash: string, spinnies: any, API: string, - cctxList: any + cctxList: any, + json: Boolean ) => { try { const url = `${API}/zeta-chain/crosschain/inTxHashToCctx/${hash}`; const apiResponse = await axios.get(url); const res = apiResponse?.data?.inTxHashToCctx?.cctx_index; res.forEach((cctxHash: any) => { - if (cctxHash && !cctxList[cctxHash]) { + if ( + cctxHash && + !cctxList[cctxHash] && + !spinnies.spinners[`spinner-${cctxHash}`] + ) { cctxList[cctxHash] = []; - } - if (!spinnies.spinners[`spinner-${cctxHash}`]) { - spinnies.add(`spinner-${cctxHash}`, { - text: `${cctxHash}`, - }); + if (!json) { + spinnies.add(`spinner-${cctxHash}`, { + text: `${cctxHash}`, + }); + } } }); } catch (error) {} @@ -46,13 +63,27 @@ const fetchCCTXData = async ( cctxHash: string, spinnies: any, API: string, - cctxList: any + cctxList: any, + pendingNonces: any, + json: Boolean, + hre: HardhatRuntimeEnvironment ) => { - const url = `${API}/zeta-chain/crosschain/cctx/${cctxHash}`; - const apiResponse = await axios.get(url); - const cctx = apiResponse?.data?.CrossChainTx; + const { networks } = hre.config; + const cctx = await getCCTX(cctxHash, API); + const receiver_chainId = cctx.outbound_tx_params[0].receiver_chainId; + const outbound_tx_hash = cctx.outbound_tx_params[0].outbound_tx_hash; + let confirmed_on_destination = false; + if (outbound_tx_hash) { + const rpc = findByChainId(networks, parseInt(receiver_chainId))?.url; + const provider = new hre.ethers.providers.JsonRpcProvider(rpc); + const confirmed = await provider.getTransaction(outbound_tx_hash); + confirmed_on_destination = confirmed !== null; + } const tx = { - receiver_chainId: cctx.outbound_tx_params[0].receiver_chainId, + confirmed_on_destination, + outbound_tx_hash, + outbound_tx_tss_nonce: cctx.outbound_tx_params[0].outbound_tx_tss_nonce, + receiver_chainId, sender_chain_id: cctx.inbound_tx_params.sender_chain_id, status: cctx.cctx_status.status, status_message: cctx.cctx_status.status_message, @@ -63,6 +94,12 @@ const fetchCCTXData = async ( cctxList[cctxHash].push(tx); const sender = cctxList[cctxHash]?.[0].sender_chain_id; const receiver = cctxList[cctxHash]?.[0].receiver_chainId; + const pendingNonce = pendingNonces.find( + (n: any) => n.chain_id === tx.receiver_chainId + )?.nonce_low; + const txNonce = tx.outbound_tx_tss_nonce; + const queue = + txNonce > pendingNonce ? ` (${txNonce - pendingNonce} in queue)` : ""; const path = cctxList[cctxHash] .map( (x: any) => @@ -72,53 +109,104 @@ const fetchCCTXData = async ( ) .join(" → "); const text = { - text: `${cctxHash}: ${sender} → ${receiver}: ${path}`, + text: `${cctxHash}: ${sender} → ${receiver}${queue}: ${path}`, }; const id = `spinner-${cctxHash}`; - switch (tx.status) { - case "OutboundMined": - case "Reverted": + if (!json && spinnies.spinners[id]) { + const s = tx.status; + if (s == "OutboundMined" || s == "Reverted") { spinnies.succeed(id, text); - break; - case "Aborted": + } else if (s == "Aborted") { spinnies.fail(id, text); - break; - default: + } else { spinnies.update(id, text); - break; + } } } }; -export const trackCCTX = async (inboundTxHash: string): Promise => { +const getCCTX = async (hash: string, API: string) => { + try { + const url = `${API}/zeta-chain/crosschain/cctx/${hash}`; + const apiResponse = await axios.get(url); + return apiResponse?.data?.CrossChainTx; + } catch (e) {} +}; + +const fetchNonces = async (API: string, TSS: string) => { + try { + const url = `${API}/zeta-chain/crosschain/pendingNonces`; + const apiResponse = await axios.get(url); + const nonces = apiResponse?.data?.pending_nonces; + return nonces.filter((n: any) => n.tss === TSS); + } catch (e) {} +}; + +const fetchTSS = async (API: string) => { + try { + const url = `${API}/zeta-chain/crosschain/TSS`; + const apiResponse = await axios.get(url); + return apiResponse?.data?.TSS.tss_pubkey; + } catch (e) {} +}; + +export const trackCCTX = async ( + hash: string, + hre: HardhatRuntimeEnvironment, + json: Boolean +): Promise => { const spinnies = new Spinnies(); const API = getEndpoint("cosmos-http"); - const WSS = getEndpoint("tendermint-ws"); + const WSS = "wss://rpc-archive.athens.zetachain.com:26657/websocket"; + const TSS = await fetchTSS(API); return new Promise((resolve, reject) => { let cctxList: any = {}; + let pendingNonces: any = []; + const socket = new WebSocket(WSS); socket.on("open", () => socket.send(JSON.stringify(SUBSCRIBE_MESSAGE))); socket.on("message", async () => { + pendingNonces = await fetchNonces(API, TSS); if (Object.keys(cctxList).length === 0) { - spinnies.add(`search`, { - text: `Looking for cross-chain transactions (CCTXs) on ZetaChain...\n`, - }); - await fetchCCTX(inboundTxHash, spinnies, API, cctxList); + if (!json) { + spinnies.add(`search`, { + text: `Looking for cross-chain transactions (CCTXs) on ZetaChain...\n`, + }); + } + await fetchCCTXByInbound(hash, spinnies, API, cctxList, json); + } + if (Object.keys(cctxList).length === 0 && !cctxList[hash]) { + if ((await getCCTX(hash, API)) && !cctxList[hash]) { + cctxList[hash] = []; + if (!spinnies.spinners[`spinner-${hash}`] && !json) { + spinnies.add(`spinner-${hash}`, { + text: `${hash}`, + }); + } + } } for (const txHash in cctxList) { - await fetchCCTX(txHash, spinnies, API, cctxList); + await fetchCCTXByInbound(txHash, spinnies, API, cctxList, json); } if (Object.keys(cctxList).length > 0) { - if (spinnies.spinners["search"]) { + if (spinnies.spinners["search"] && !json) { spinnies.succeed(`search`, { text: `CCTXs on ZetaChain found.\n`, }); } for (const cctxHash in cctxList) { try { - fetchCCTXData(cctxHash, spinnies, API, cctxList); + fetchCCTXData( + cctxHash, + spinnies, + API, + cctxList, + pendingNonces, + json, + hre + ); } catch (error) {} } } @@ -132,13 +220,14 @@ export const trackCCTX = async (inboundTxHash: string): Promise => { .filter((s) => !["OutboundMined", "Aborted", "Reverted"].includes(s)) .length === 0 ) { + if (json) console.log(JSON.stringify(cctxList, null, 2)); socket.close(); } }); socket.on("error", (error: any) => { reject(error); }); - socket.on("close", () => { + socket.on("close", (code) => { resolve(); }); }); diff --git a/tasks/cctx.ts b/tasks/cctx.ts index b5b38380..fcc12387 100644 --- a/tasks/cctx.ts +++ b/tasks/cctx.ts @@ -6,11 +6,13 @@ import { trackCCTX } from "../helpers"; declare const hre: any; const main = async (args: any, hre: HardhatRuntimeEnvironment) => { - await trackCCTX(args.tx); + await trackCCTX(args.tx, hre, args.json); }; export const cctxTask = task( "cctx", "Track cross-chain transaction status", main -).addParam("tx", "Inbound transaction hash"); +) + .addPositionalParam("tx", "TX hash of an inbound transaction or a CCTX") + .addFlag("json", "Output as JSON");