Skip to content

Commit aad378f

Browse files
committed
cloud_functions: getQuorumHeight
1 parent ebfb796 commit aad378f

File tree

2 files changed

+123
-0
lines changed

2 files changed

+123
-0
lines changed

cloud_functions/scripts/deploy.sh

+1
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ gcloud functions --project "$GCP_PROJECT" deploy compute-missing-vaas --entry-po
133133
gcloud functions --project "$GCP_PROJECT" deploy compute-ntt-rate-limits --entry-point computeNTTRateLimits --gen2 --runtime nodejs22 --trigger-http --allow-unauthenticated --timeout 300 --memory 512MB --region europe-west3 --set-env-vars NETWORK=$NETWORK,FUNCTION=computeNTTRateLimits
134134
gcloud functions --project "$GCP_PROJECT" deploy compute-total-supply-and-locked --entry-point computeTotalSupplyAndLocked --gen2 --runtime nodejs22 --trigger-http --allow-unauthenticated --timeout 300 --memory 512MB --region europe-west3 --set-env-vars NETWORK=$NETWORK,FUNCTION=computeTotalSupplyAndLocked
135135
gcloud functions --project "$GCP_PROJECT" deploy get-ntt-rate-limits --entry-point getNTTRateLimits --gen2 --runtime nodejs22 --trigger-http --allow-unauthenticated --timeout 300 --memory 512MB --region europe-west3 --set-env-vars NETWORK=$NETWORK,FUNCTION=getNTTRateLimits
136+
gcloud functions --project "$GCP_PROJECT" deploy get-quorum-height --entry-point getQuorumHeight --gen2 --runtime nodejs22 --trigger-http --allow-unauthenticated --timeout 300 --memory 512MB --region europe-west3 --set-env-vars NETWORK=$NETWORK,FUNCTION=getQuorumHeight
136137
gcloud functions --project "$GCP_PROJECT" deploy get-total-supply-and-locked --entry-point getTotalSupplyAndLocked --gen2 --runtime nodejs22 --trigger-http --allow-unauthenticated --timeout 300 --memory 512MB --region europe-west3 --set-env-vars NETWORK=$NETWORK,FUNCTION=getTotalSupplyAndLocked
137138
gcloud functions --project "$GCP_PROJECT" deploy latest-blocks --entry-point getLatestBlocks --gen2 --runtime nodejs22 --trigger-http --allow-unauthenticated --timeout 300 --memory 512MB --region europe-west3 --set-env-vars CLOUD_FUNCTIONS_REFRESH_TIME_INTERVAL=$CLOUD_FUNCTIONS_REFRESH_TIME_INTERVAL,FIRESTORE_LATEST_COLLECTION=$FIRESTORE_LATEST_COLLECTION,NETWORK=$NETWORK,FUNCTION=getLatestBlocks
138139
gcloud functions --project "$GCP_PROJECT" deploy latest-tvltvm --entry-point getLatestTvlTvm --gen2 --runtime nodejs22 --trigger-http --allow-unauthenticated --timeout 300 --memory 512MB --region europe-west3 --set-env-vars CLOUD_FUNCTIONS_REFRESH_TIME_INTERVAL=$CLOUD_FUNCTIONS_REFRESH_TIME_INTERVAL,FIRESTORE_LATEST_TVLTVM_COLLECTION=$FIRESTORE_LATEST_TVLTVM_COLLECTION,NETWORK=$NETWORK,FUNCTION=getLatestTvlTvm
+122
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import { STANDBY_GUARDIANS } from '@wormhole-foundation/wormhole-monitor-common';
2+
import { Firestore } from 'firebase-admin/firestore';
3+
4+
// TODO: pulled from getLastHeartbeats.ts, could be shared.
5+
interface Heartbeat {
6+
nodeName: string;
7+
counter: string;
8+
timestamp: string;
9+
networks: HeartbeatNetwork[];
10+
version: string;
11+
guardianAddr: string;
12+
bootTimestamp: string;
13+
features: string[];
14+
p2pNodeAddr?: string;
15+
}
16+
17+
interface HeartbeatNetwork {
18+
id: number;
19+
height: string;
20+
contractAddress: string;
21+
errorCount: string;
22+
safeHeight: string;
23+
finalizedHeight: string;
24+
}
25+
26+
const isTestnet = process.env.NETWORK === 'TESTNET';
27+
28+
export function getQuorumCount(numGuardians: number): number {
29+
return isTestnet ? 1 : Math.floor((numGuardians * 2) / 3 + 1);
30+
}
31+
32+
async function getHeartbeats_() {
33+
const heartbeats: Heartbeat[] = [];
34+
const firestoreDb = new Firestore();
35+
try {
36+
const collectionRef = firestoreDb.collection('heartbeats');
37+
const snapshot = await collectionRef.get();
38+
snapshot.docs.forEach((doc) => {
39+
heartbeats.push(doc.data() as Heartbeat);
40+
});
41+
} catch (e) {
42+
console.error(e);
43+
}
44+
return heartbeats;
45+
}
46+
47+
let cache = { heartbeats: [] as Heartbeat[], lastUpdated: Date.now() };
48+
// default refresh interval = 15 sec
49+
const REFRESH_TIME_INTERVAL =
50+
Number(process.env.CLOUD_FUNCTIONS_REFRESH_TIME_INTERVAL) || 1000 * 15;
51+
52+
export async function getQuorumHeight(req: any, res: any) {
53+
res.set('Access-Control-Allow-Origin', '*');
54+
if (req.method === 'OPTIONS') {
55+
// Send response to OPTIONS requests
56+
res.set('Access-Control-Allow-Methods', 'GET');
57+
res.set('Access-Control-Allow-Headers', 'Content-Type');
58+
res.set('Access-Control-Max-Age', '3600');
59+
res.status(204).send('');
60+
return;
61+
}
62+
const { chainId: chainIdStr } = req.query;
63+
const chainId = Number(chainIdStr);
64+
if (Number.isNaN(chainId)) {
65+
res.status(400).send(`Invalid chainId`);
66+
return;
67+
}
68+
let heartbeats: Heartbeat[] = [];
69+
try {
70+
if (cache.heartbeats.length === 0 || Date.now() - cache.lastUpdated > REFRESH_TIME_INTERVAL) {
71+
if (cache.heartbeats.length === 0) {
72+
console.log(`cache is empty, setting cache.heartbeats ${new Date()}`);
73+
} else {
74+
console.log(`cache is older than ${REFRESH_TIME_INTERVAL} ms, refreshing ${new Date()}`);
75+
}
76+
let prevDate = Date.now();
77+
heartbeats = await getHeartbeats_();
78+
let timeDiff = Date.now() - prevDate;
79+
console.log('After getHeartbeats_=', timeDiff);
80+
cache.heartbeats = heartbeats;
81+
cache.lastUpdated = Date.now();
82+
} else {
83+
console.log(`cache is still valid, not refreshing ${new Date()}`);
84+
heartbeats = cache.heartbeats;
85+
}
86+
if (heartbeats.length) {
87+
const latestHeights: bigint[] = [];
88+
const safeHeights: bigint[] = [];
89+
const finalizedHeights: bigint[] = [];
90+
for (const heartbeat of heartbeats) {
91+
// filter out standby guardians
92+
if (
93+
!STANDBY_GUARDIANS.find(
94+
(g) => g.pubkey.toLowerCase() === heartbeat.guardianAddr.toLowerCase()
95+
)
96+
) {
97+
const network = heartbeat.networks.find((n) => n.id === chainId);
98+
latestHeights.push(BigInt(network?.height || '0'));
99+
safeHeights.push(BigInt(network?.safeHeight || '0'));
100+
finalizedHeights.push(BigInt(network?.finalizedHeight || '0'));
101+
}
102+
}
103+
latestHeights.sort((a, b) => (a > b ? -1 : a < b ? 1 : 0));
104+
safeHeights.sort((a, b) => (a > b ? -1 : a < b ? 1 : 0));
105+
finalizedHeights.sort((a, b) => (a > b ? -1 : a < b ? 1 : 0));
106+
const quorumIdx = getQuorumCount(latestHeights.length) - 1;
107+
res.status(200).send(
108+
JSON.stringify({
109+
latest: latestHeights[quorumIdx].toString(),
110+
safe: safeHeights[quorumIdx].toString(),
111+
finalized: finalizedHeights[quorumIdx].toString(),
112+
})
113+
);
114+
} else {
115+
console.log('no heartbeats');
116+
res.sendStatus(500);
117+
}
118+
} catch (e) {
119+
console.log(e);
120+
res.sendStatus(500);
121+
}
122+
}

0 commit comments

Comments
 (0)