Skip to content

Commit 110008b

Browse files
panoelevan-gray
authored andcommitted
cloud_functions/dashboard: add getGuardianSetInfo cf
1 parent d015d56 commit 110008b

18 files changed

+2682
-4319
lines changed

cloud_functions/package.json

+7
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@
1111
"deploy": "bash scripts/deploy.sh",
1212
"gcp-build": "npm i ./dist/src/wormhole-foundation-wormhole-monitor-common-0.0.1.tgz ./dist/src/wormhole-foundation-wormhole-monitor-database-0.0.1.tgz"
1313
},
14+
"//": [
15+
"The @wormhole-foundation/sdk-base and @wormhole-foundation/sdk-definitions was required as the cloud_functions are currently deployed with packed",
16+
"versions of their siblings and does not include the workspace package lock. In this circumstance, npm was choosing an older sdk as the",
17+
"direct node_modules dependency when we wanted the version used by database and explicitly defined by the workspace."
18+
],
1419
"dependencies": {
1520
"@coral-xyz/anchor": "^0.29.0",
1621
"@cosmjs/cosmwasm-stargate": "^0.31.1",
@@ -19,6 +24,8 @@
1924
"@google-cloud/pubsub": "^3.4.1",
2025
"@google-cloud/storage": "^6.8.0",
2126
"@solana/web3.js": "^1.87.3",
27+
"@wormhole-foundation/sdk-base": "^0.8.0",
28+
"@wormhole-foundation/sdk-definitions": "^0.8.0",
2229
"@wormhole-foundation/sdk-evm-ntt": "^0.0.1-beta.4",
2330
"@wormhole-foundation/sdk-solana-ntt": "^0.0.1-beta.4",
2431
"borsh": "^1.0.0",

cloud_functions/scripts/deploy.sh

+7
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,11 @@ if [ -z "$FIRESTORE_LATEST_TVLTVM_COLLECTION" ]; then
9393
exit 1
9494
fi
9595

96+
if [ -z "$FIRESTORE_GUARDIAN_SET_INFO_COLLECTION" ]; then
97+
echo "FIRESTORE_GUARDIAN_SET_INFO_COLLECTION must be specified"
98+
exit 1
99+
fi
100+
96101
if [ -z "$MISSING_VAA_SLACK_CHANNEL_ID" ]; then
97102
echo "MISSING_VAA_SLACK_CHANNEL_ID must be specified"
98103
exit 1
@@ -221,9 +226,11 @@ if [ -z "$SOLANA_RPC" ]; then
221226
exit 1
222227
fi
223228

229+
gcloud functions --project "$GCP_PROJECT" deploy compute-guardian-set-info --entry-point computeGuardianSetInfo --gen2 --runtime nodejs18 --trigger-http --allow-unauthenticated --timeout 300 --memory 256MB --region europe-west3 --set-env-vars NETWORK=$NETWORK,FIRESTORE_GUARDIAN_SET_INFO_COLLECTION=$FIRESTORE_GUARDIAN_SET_INFO_COLLECTION
224230
gcloud functions --project "$GCP_PROJECT" deploy compute-tvl --entry-point computeTVL --gen2 --runtime nodejs18 --trigger-http --no-allow-unauthenticated --timeout 300 --memory 1GB --region europe-west3 --set-env-vars PG_USER=$PG_USER,PG_PASSWORD=$PG_PASSWORD,PG_DATABASE=$PG_DATABASE,PG_HOST=$PG_HOST,PG_ATTEST_MESSAGE_TABLE=$PG_ATTEST_MESSAGE_TABLE,PG_TOKEN_METADATA_TABLE=$PG_TOKEN_METADATA_TABLE,PG_TOKEN_TRANSFER_TABLE=$PG_TOKEN_TRANSFER_TABLE,FIRESTORE_TVL_COLLECTION=$FIRESTORE_TVL_COLLECTION,NETWORK=$NETWORK
225231
gcloud functions --project "$GCP_PROJECT" deploy compute-tvl-history --entry-point computeTVLHistory --gen2 --runtime nodejs18 --trigger-http --no-allow-unauthenticated --timeout 540 --memory 1GB --region europe-west3 --set-env-vars PG_USER=$PG_USER,PG_PASSWORD=$PG_PASSWORD,PG_DATABASE=$PG_DATABASE,PG_HOST=$PG_HOST,PG_ATTEST_MESSAGE_TABLE=$PG_ATTEST_MESSAGE_TABLE,PG_TOKEN_METADATA_TABLE=$PG_TOKEN_METADATA_TABLE,PG_TOKEN_TRANSFER_TABLE=$PG_TOKEN_TRANSFER_TABLE,FIRESTORE_TVL_HISTORY_COLLECTION=$FIRESTORE_TVL_HISTORY_COLLECTION,PG_TOKEN_PRICE_HISTORY_TABLE=$PG_TOKEN_PRICE_HISTORY_TABLE,NETWORK=$NETWORK
226232
gcloud functions --project "$GCP_PROJECT" deploy compute-tvl-tvm --entry-point computeTvlTvm --gen2 --runtime nodejs18 --trigger-http --no-allow-unauthenticated --timeout 540 --memory 1GB --region europe-west3 --set-env-vars PG_USER=$PG_USER,PG_PASSWORD=$PG_PASSWORD,PG_DATABASE=$PG_DATABASE,PG_HOST=$PG_HOST,PG_TOKEN_METADATA_TABLE=$PG_TOKEN_METADATA_TABLE,PG_TOKEN_PRICE_HISTORY_TABLE=$PG_TOKEN_PRICE_HISTORY_TABLE,FIRESTORE_LATEST_TVLTVM_COLLECTION=$FIRESTORE_LATEST_TVLTVM_COLLECTION,NETWORK=$NETWORK
233+
gcloud functions --project "$GCP_PROJECT" deploy get-guardian-set-info --entry-point getGuardianSetInfo --gen2 --runtime nodejs18 --trigger-http --allow-unauthenticated --timeout 300 --memory 512MB --region europe-west3 --set-env-vars NETWORK=$NETWORK,FIRESTORE_GUARDIAN_SET_INFO_COLLECTION=$FIRESTORE_GUARDIAN_SET_INFO_COLLECTION
227234
gcloud functions --project "$GCP_PROJECT" deploy get-solana-events --entry-point getSolanaEvents --gen2 --runtime nodejs18 --trigger-http --allow-unauthenticated --timeout 300 --memory 256MB --region europe-west3 --set-env-vars SOLANA_RPC=$SOLANA_RPC,NETWORK=$NETWORK
228235
gcloud functions --project "$GCP_PROJECT" deploy latest-tokendata --entry-point getLatestTokenData --gen2 --runtime nodejs18 --trigger-http --allow-unauthenticated --timeout 300 --memory 256MB --region europe-west3 --set-env-vars CLOUD_FUNCTIONS_REFRESH_TIME_INTERVAL=$CLOUD_FUNCTIONS_REFRESH_TIME_INTERVAL,PG_USER=$PG_USER,PG_PASSWORD=$PG_PASSWORD,PG_DATABASE=$PG_DATABASE,PG_HOST=$PG_HOST,PG_TOKEN_METADATA_TABLE=$PG_TOKEN_METADATA_TABLE,PG_TOKEN_PRICE_HISTORY_TABLE=$PG_TOKEN_PRICE_HISTORY_TABLE,NETWORK=$NETWORK
229236
gcloud functions --project "$GCP_PROJECT" deploy process-vaa --entry-point processVaa --gen2 --runtime nodejs18 --timeout 300 --memory 256MB --region europe-west3 --trigger-topic $PUBSUB_SIGNED_VAA_TOPIC --set-env-vars BIGTABLE_INSTANCE_ID=$BIGTABLE_INSTANCE_ID,BIGTABLE_SIGNED_VAAS_TABLE_ID=$BIGTABLE_SIGNED_VAAS_TABLE_ID,BIGTABLE_VAAS_BY_TX_HASH_TABLE_ID=$BIGTABLE_VAAS_BY_TX_HASH_TABLE_ID,PG_USER=$PG_USER,PG_PASSWORD=$PG_PASSWORD,PG_DATABASE=$PG_DATABASE,PG_HOST=$PG_HOST,PG_TOKEN_TRANSFER_TABLE=$PG_TOKEN_TRANSFER_TABLE,PG_ATTEST_MESSAGE_TABLE=$PG_ATTEST_MESSAGE_TABLE,PG_TOKEN_METADATA_TABLE=$PG_TOKEN_METADATA_TABLE,NETWORK=$NETWORK
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
import { Chain, chains, chainToPlatform, contracts, rpc } from '@wormhole-foundation/sdk-base';
2+
import {
3+
callContractMethod,
4+
getMethodId,
5+
GuardianSetInfo,
6+
GuardianSetInfoByChain,
7+
makeRpcCall,
8+
queryContractSmart,
9+
} from '@wormhole-foundation/wormhole-monitor-common';
10+
import { Firestore } from 'firebase-admin/firestore';
11+
import { assertEnvironmentVariable } from './utils';
12+
import { utils } from '@wormhole-foundation/sdk-solana-core';
13+
import axios from 'axios';
14+
15+
export async function computeGuardianSetInfo(req: any, res: any) {
16+
res.set('Access-Control-Allow-Origin', '*');
17+
if (req.method === 'OPTIONS') {
18+
// Send response to OPTIONS requests
19+
res.set('Access-Control-Allow-Methods', 'GET');
20+
res.set('Access-Control-Allow-Headers', 'Content-Type');
21+
res.set('Access-Control-Max-Age', '3600');
22+
res.status(204).send('');
23+
return;
24+
}
25+
const infosByChain: GuardianSetInfoByChain = await getGuardianSetInfoByChain();
26+
27+
await updateFirestore(infosByChain);
28+
res.status(200).send('successfully stored guardian set info');
29+
return;
30+
}
31+
32+
async function getGuardianSetInfoByChain(): Promise<GuardianSetInfoByChain> {
33+
let infosByChain: GuardianSetInfoByChain = {};
34+
for (const chain of chains) {
35+
const contract = contracts.coreBridge.get('Mainnet', chain); // Only support Mainnet for now
36+
if (!contract) {
37+
console.log(`No contract found for ${chain}`);
38+
continue;
39+
}
40+
const info: GuardianSetInfo = await fetchGuardianSetInfo(chain, contract);
41+
infosByChain[chain] = info;
42+
}
43+
console.log('Guardian set info:', infosByChain);
44+
return infosByChain;
45+
}
46+
47+
async function fetchGuardianSetInfo(chain: Chain, address: string): Promise<GuardianSetInfo> {
48+
const timestamp: string = new Date().toISOString();
49+
const mt: GuardianSetInfo = {
50+
timestamp,
51+
contract: '',
52+
guardianSetIndex: '0',
53+
guardianSet: '',
54+
};
55+
if (!address) throw new Error('Address not found');
56+
const rpcUrl =
57+
chain === 'Klaytn'
58+
? 'https://rpc.ankr.com/klaytn'
59+
: chain === 'Near'
60+
? 'https://rpc.mainnet.near.org'
61+
: chain === 'Pythnet'
62+
? 'http://pythnet.rpcpool.com'
63+
: rpc.rpcAddress('Mainnet', chain);
64+
if (!rpcUrl) {
65+
console.error(`Mainnet ${chain} rpc url not found`);
66+
return mt;
67+
}
68+
const platform = chainToPlatform(chain);
69+
try {
70+
if (platform === 'Evm') {
71+
const gsi = await callContractMethod(
72+
rpcUrl,
73+
address,
74+
getMethodId('getCurrentGuardianSetIndex()')
75+
);
76+
const gs = await callContractMethod(
77+
rpcUrl,
78+
address,
79+
getMethodId('getGuardianSet(uint32)'),
80+
gsi.substring(2) // strip 0x
81+
);
82+
return { timestamp, contract: address, guardianSetIndex: gsi, guardianSet: gs };
83+
} else if (platform === 'Cosmwasm') {
84+
const guardianSet = await queryContractSmart(rpcUrl, address, {
85+
guardian_set_info: {},
86+
});
87+
return {
88+
timestamp,
89+
contract: address,
90+
guardianSetIndex: guardianSet.guardian_set_index.toString(),
91+
guardianSet: guardianSet.addresses
92+
.map(
93+
(address: { bytes: string }) =>
94+
`0x${Buffer.from(address.bytes, 'base64').toString('hex')}`
95+
)
96+
.join(','),
97+
};
98+
} else if (platform === 'Solana') {
99+
let gsIdx = 0;
100+
let gsAddress = utils.deriveGuardianSetKey(address, gsIdx);
101+
// console.log(chain, gsIdx, gsAddress);
102+
let gsAccountInfo = await makeRpcCall(rpcUrl, 'getAccountInfo', [gsAddress], 'jsonParsed');
103+
let ret: GuardianSetInfo = {
104+
timestamp,
105+
contract: '',
106+
guardianSetIndex: '0',
107+
guardianSet: '',
108+
};
109+
while (
110+
gsAccountInfo &&
111+
gsAccountInfo.value &&
112+
gsAccountInfo.value.data &&
113+
gsAccountInfo.value.data[0] !== null
114+
) {
115+
const gs = utils.GuardianSetData.deserialize(
116+
Buffer.from(gsAccountInfo.value.data[0], 'base64')
117+
);
118+
ret = {
119+
timestamp,
120+
contract: address,
121+
guardianSetIndex: gsIdx.toString(),
122+
guardianSet: gs.keys.map((k) => `0x${k.toString('hex')}`).join(','),
123+
};
124+
gsIdx++;
125+
gsAddress = utils.deriveGuardianSetKey(address, gsIdx);
126+
// console.log(chain, gsIdx, gsAddress);
127+
gsAccountInfo = await makeRpcCall(rpcUrl, 'getAccountInfo', [gsAddress], 'jsonParsed');
128+
}
129+
return ret;
130+
} else if (platform === 'Algorand') {
131+
const response = await axios.get(`${rpcUrl}/v2/applications/${address}`);
132+
const currentGuardianSetIndexState = response.data.params['global-state'].find(
133+
(s: any) => Buffer.from(s.key, 'base64').toString('ascii') === 'currentGuardianSetIndex'
134+
);
135+
return {
136+
timestamp,
137+
contract: address,
138+
guardianSetIndex: currentGuardianSetIndexState.value.uint.toString(),
139+
guardianSet: '',
140+
};
141+
} else if (platform === 'Near') {
142+
const response = await axios.post(rpcUrl, {
143+
jsonrpc: '2.0',
144+
id: 'dontcare',
145+
method: 'query',
146+
params: {
147+
request_type: 'view_state',
148+
finality: 'final',
149+
account_id: address,
150+
prefix_base64: 'U1RBVEU=', // STATE
151+
},
152+
});
153+
const state = Buffer.from(
154+
response.data.result.values.find(
155+
(s: any) => Buffer.from(s.key, 'base64').toString('ascii') === 'STATE'
156+
).value,
157+
'base64'
158+
).toString('hex');
159+
// a tiny hack - instead of parsing the whole state, just find the expiry, which comes before the guardian set index
160+
// https://github.com/wormhole-foundation/wormhole/blob/main/near/contracts/wormhole/src/lib.rs#L109
161+
const expiry = `00004f91944e0000`; // = 24 * 60 * 60 * 1_000_000_000, // 24 hours in nanoseconds
162+
const expiryIndex = state.indexOf(expiry);
163+
const gsiIndex = expiryIndex + 16; // u64 len in hex
164+
const gsi = `0x${state
165+
.substring(gsiIndex, gsiIndex + 8)
166+
.match(/../g)
167+
?.reverse()
168+
.join('')}`;
169+
return { timestamp, contract: address, guardianSetIndex: gsi, guardianSet: '' };
170+
} else if (platform === 'Aptos') {
171+
const response = await axios.get(
172+
`${rpcUrl}/accounts/${address}/resource/${address}::state::WormholeState`
173+
);
174+
const gsi = response.data.data.guardian_set_index.number.toString();
175+
// const gsHandle = response.data.data.guardian_sets
176+
return { timestamp, contract: address, guardianSetIndex: gsi, guardianSet: '' };
177+
} else if (platform === 'Sui') {
178+
const response = await axios.post(rpcUrl, {
179+
jsonrpc: '2.0',
180+
id: 1,
181+
method: 'sui_getObject',
182+
params: [
183+
address,
184+
{
185+
showType: false,
186+
showOwner: false,
187+
showPreviousTransaction: false,
188+
showDisplay: false,
189+
showContent: true,
190+
showBcs: false,
191+
showStorageRebate: false,
192+
},
193+
],
194+
});
195+
const gsi = response.data.result.data.content.fields.guardian_set_index.toString();
196+
// const gsTable = response.data.result.data.content.fields.guardian_sets);
197+
return { timestamp, contract: address, guardianSetIndex: gsi, guardianSet: '' };
198+
}
199+
} catch (e) {
200+
console.error(`Failed to get guardian set for ${chain}:`, e);
201+
}
202+
return mt;
203+
}
204+
205+
async function updateFirestore(data: GuardianSetInfoByChain): Promise<void> {
206+
const firestore = new Firestore();
207+
const collection = firestore.collection(
208+
assertEnvironmentVariable('FIRESTORE_GUARDIAN_SET_INFO_COLLECTION')
209+
);
210+
try {
211+
for (const chain in data) {
212+
if (data.hasOwnProperty(chain)) {
213+
const chainData: GuardianSetInfo | undefined = data[chain as Chain];
214+
if (chainData) {
215+
const docRef = collection.doc(chain);
216+
await docRef.set(chainData);
217+
}
218+
}
219+
}
220+
} catch (e) {
221+
console.error('Error adding document: ', e);
222+
}
223+
}
+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { Chain } from '@wormhole-foundation/sdk-base';
2+
import {
3+
assertEnvironmentVariable,
4+
GuardianSetInfoByChain,
5+
} from '@wormhole-foundation/wormhole-monitor-common';
6+
import { Firestore } from 'firebase-admin/firestore';
7+
8+
export async function getGuardianSetInfo(req: any, res: any) {
9+
res.set('Access-Control-Allow-Origin', '*');
10+
if (req.method === 'OPTIONS') {
11+
// Send response to OPTIONS requests
12+
res.set('Access-Control-Allow-Methods', 'GET');
13+
res.set('Access-Control-Allow-Headers', 'Content-Type');
14+
res.set('Access-Control-Max-Age', '3600');
15+
res.status(204).send('');
16+
return;
17+
}
18+
19+
// This goes out to firestore to retrieve the guardian set info
20+
let info: GuardianSetInfoByChain = {};
21+
try {
22+
info = await getGuardianSetInfoByChain();
23+
res.status(200).send(JSON.stringify(info));
24+
} catch (e) {
25+
res.sendStatus(500);
26+
}
27+
}
28+
29+
async function getGuardianSetInfoByChain(): Promise<GuardianSetInfoByChain> {
30+
const firestoreCollection = assertEnvironmentVariable('FIRESTORE_GUARDIAN_SET_INFO_COLLECTION');
31+
let values: GuardianSetInfoByChain = {};
32+
const firestoreDb = new Firestore({});
33+
try {
34+
const collectionRef = firestoreDb.collection(firestoreCollection);
35+
const snapshot = await collectionRef.get();
36+
snapshot.docs.forEach((doc) => {
37+
values[doc.id as Chain] = {
38+
timestamp: doc.data().timestamp,
39+
contract: doc.data().contract,
40+
guardianSet: doc.data().guardianSet,
41+
guardianSetIndex: doc.data().guardianSetIndex,
42+
};
43+
});
44+
} catch (e) {
45+
console.error(e);
46+
}
47+
return values;
48+
}

cloud_functions/src/index.ts

+4
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ export const { getNTTRateLimits } = require('./getNTTRateLimits');
2929
export const { computeNTTRateLimits } = require('./computeNTTRateLimits');
3030
export const { getTotalSupplyAndLocked } = require('./getTotalSupplyAndLocked');
3131
export const { computeTotalSupplyAndLocked } = require('./computeTotalSupplyAndLocked');
32+
export const { computeGuardianSetInfo } = require('./computeGuardianSetInfo');
33+
export const { getGuardianSetInfo } = require('./getGuardianSetInfo');
3234

3335
// Register an HTTP function with the Functions Framework that will be executed
3436
// when you make an HTTP request to the deployed function's endpoint.
@@ -59,3 +61,5 @@ functions.http('getNTTRateLimits', getNTTRateLimits);
5961
functions.http('computeNTTRateLimits', computeNTTRateLimits);
6062
functions.http('getTotalSupplyAndLocked', getTotalSupplyAndLocked);
6163
functions.http('computeTotalSupplyAndLocked', computeTotalSupplyAndLocked);
64+
functions.http('computeGuardianSetInfo', computeGuardianSetInfo);
65+
functions.http('getGuardianSetInfo', getGuardianSetInfo);

common/src/consts.ts

+11
Original file line numberDiff line numberDiff line change
@@ -293,3 +293,14 @@ export const GUARDIAN_SET_4 = [
293293
name: 'Staking Facilities',
294294
},
295295
];
296+
297+
export type GuardianSetInfo = {
298+
timestamp: string;
299+
contract: string;
300+
guardianSetIndex: string;
301+
guardianSet: string;
302+
};
303+
304+
export type GuardianSetInfoByChain = {
305+
[chain in Chain]?: GuardianSetInfo;
306+
};

common/src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@ export * from './nttConsts';
88
export * from './evm';
99
export * from './types';
1010
export * from './wormhole';
11+
export * from './queryContractSmart';
File renamed without changes.

dashboard/src/components/Accountant.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ import useGetAccountantPendingTransfers, {
4141
} from '../hooks/useGetAccountantPendingTransfers';
4242
import { TokenDataByChainAddress, TokenDataEntry } from '../hooks/useTokenData';
4343
import { CHAIN_ICON_MAP, WORMCHAIN_URL } from '../utils/consts';
44-
import { queryContractSmart } from '../utils/queryContractSmart';
44+
import { queryContractSmart } from '@wormhole-foundation/wormhole-monitor-common/src/queryContractSmart';
4545
import CollapsibleSection from './CollapsibleSection';
4646
import { ExplorerTxHash } from './ExplorerTxHash';
4747
import Table from './Table';

0 commit comments

Comments
 (0)