Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

cloud_functions/dashboard: add getGuardianSetInfo cf #336

Merged
merged 1 commit into from
Jul 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions cloud_functions/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@
"deploy": "bash scripts/deploy.sh",
"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"
},
"//": [
"The @wormhole-foundation/sdk-base and @wormhole-foundation/sdk-definitions was required as the cloud_functions are currently deployed with packed",
"versions of their siblings and does not include the workspace package lock. In this circumstance, npm was choosing an older sdk as the",
"direct node_modules dependency when we wanted the version used by database and explicitly defined by the workspace."
],
"dependencies": {
"@coral-xyz/anchor": "^0.29.0",
"@cosmjs/cosmwasm-stargate": "^0.31.1",
Expand All @@ -19,6 +24,8 @@
"@google-cloud/pubsub": "^3.4.1",
"@google-cloud/storage": "^6.8.0",
"@solana/web3.js": "^1.87.3",
"@wormhole-foundation/sdk-base": "^0.8.0",
evan-gray marked this conversation as resolved.
Show resolved Hide resolved
"@wormhole-foundation/sdk-definitions": "^0.8.0",
"@wormhole-foundation/sdk-evm-ntt": "^0.0.1-beta.4",
"@wormhole-foundation/sdk-solana-ntt": "^0.0.1-beta.4",
"borsh": "^1.0.0",
Expand Down
7 changes: 7 additions & 0 deletions cloud_functions/scripts/deploy.sh
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,11 @@ if [ -z "$FIRESTORE_LATEST_TVLTVM_COLLECTION" ]; then
exit 1
fi

if [ -z "$FIRESTORE_GUARDIAN_SET_INFO_COLLECTION" ]; then
echo "FIRESTORE_GUARDIAN_SET_INFO_COLLECTION must be specified"
exit 1
fi

if [ -z "$MISSING_VAA_SLACK_CHANNEL_ID" ]; then
echo "MISSING_VAA_SLACK_CHANNEL_ID must be specified"
exit 1
Expand Down Expand Up @@ -221,9 +226,11 @@ if [ -z "$SOLANA_RPC" ]; then
exit 1
fi

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
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
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
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
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
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
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
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
Expand Down
223 changes: 223 additions & 0 deletions cloud_functions/src/computeGuardianSetInfo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
import { Chain, chains, chainToPlatform, contracts, rpc } from '@wormhole-foundation/sdk-base';
import {
callContractMethod,
getMethodId,
GuardianSetInfo,
GuardianSetInfoByChain,
makeRpcCall,
queryContractSmart,
} from '@wormhole-foundation/wormhole-monitor-common';
import { Firestore } from 'firebase-admin/firestore';
import { assertEnvironmentVariable } from './utils';
import { utils } from '@wormhole-foundation/sdk-solana-core';
import axios from 'axios';

export async function computeGuardianSetInfo(req: any, res: any) {
res.set('Access-Control-Allow-Origin', '*');
if (req.method === 'OPTIONS') {
// Send response to OPTIONS requests
res.set('Access-Control-Allow-Methods', 'GET');
res.set('Access-Control-Allow-Headers', 'Content-Type');
res.set('Access-Control-Max-Age', '3600');
res.status(204).send('');
return;
}
const infosByChain: GuardianSetInfoByChain = await getGuardianSetInfoByChain();

await updateFirestore(infosByChain);
res.status(200).send('successfully stored guardian set info');
return;
}

async function getGuardianSetInfoByChain(): Promise<GuardianSetInfoByChain> {
let infosByChain: GuardianSetInfoByChain = {};
for (const chain of chains) {
const contract = contracts.coreBridge.get('Mainnet', chain); // Only support Mainnet for now
if (!contract) {
console.log(`No contract found for ${chain}`);
continue;
}
const info: GuardianSetInfo = await fetchGuardianSetInfo(chain, contract);
infosByChain[chain] = info;
}
console.log('Guardian set info:', infosByChain);
return infosByChain;
}

async function fetchGuardianSetInfo(chain: Chain, address: string): Promise<GuardianSetInfo> {
const timestamp: string = new Date().toISOString();
const mt: GuardianSetInfo = {
timestamp,
contract: '',
guardianSetIndex: '0',
guardianSet: '',
};
if (!address) throw new Error('Address not found');
const rpcUrl =
chain === 'Klaytn'
? 'https://rpc.ankr.com/klaytn'
: chain === 'Near'
? 'https://rpc.mainnet.near.org'
: chain === 'Pythnet'
? 'http://pythnet.rpcpool.com'
: rpc.rpcAddress('Mainnet', chain);
if (!rpcUrl) {
console.error(`Mainnet ${chain} rpc url not found`);
return mt;
}
const platform = chainToPlatform(chain);
try {
if (platform === 'Evm') {
const gsi = await callContractMethod(
rpcUrl,
address,
getMethodId('getCurrentGuardianSetIndex()')
);
const gs = await callContractMethod(
rpcUrl,
address,
getMethodId('getGuardianSet(uint32)'),
gsi.substring(2) // strip 0x
);
return { timestamp, contract: address, guardianSetIndex: gsi, guardianSet: gs };
} else if (platform === 'Cosmwasm') {
const guardianSet = await queryContractSmart(rpcUrl, address, {
guardian_set_info: {},
});
return {
timestamp,
contract: address,
guardianSetIndex: guardianSet.guardian_set_index.toString(),
guardianSet: guardianSet.addresses
.map(
(address: { bytes: string }) =>
`0x${Buffer.from(address.bytes, 'base64').toString('hex')}`
)
.join(','),
};
} else if (platform === 'Solana') {
let gsIdx = 0;
let gsAddress = utils.deriveGuardianSetKey(address, gsIdx);
// console.log(chain, gsIdx, gsAddress);
let gsAccountInfo = await makeRpcCall(rpcUrl, 'getAccountInfo', [gsAddress], 'jsonParsed');
let ret: GuardianSetInfo = {
timestamp,
contract: '',
guardianSetIndex: '0',
guardianSet: '',
};
while (
gsAccountInfo &&
gsAccountInfo.value &&
gsAccountInfo.value.data &&
gsAccountInfo.value.data[0] !== null
) {
const gs = utils.GuardianSetData.deserialize(
Buffer.from(gsAccountInfo.value.data[0], 'base64')
);
ret = {
timestamp,
contract: address,
guardianSetIndex: gsIdx.toString(),
guardianSet: gs.keys.map((k) => `0x${k.toString('hex')}`).join(','),
};
gsIdx++;
gsAddress = utils.deriveGuardianSetKey(address, gsIdx);
// console.log(chain, gsIdx, gsAddress);
gsAccountInfo = await makeRpcCall(rpcUrl, 'getAccountInfo', [gsAddress], 'jsonParsed');
}
return ret;
} else if (platform === 'Algorand') {
const response = await axios.get(`${rpcUrl}/v2/applications/${address}`);
const currentGuardianSetIndexState = response.data.params['global-state'].find(
(s: any) => Buffer.from(s.key, 'base64').toString('ascii') === 'currentGuardianSetIndex'
);
return {
timestamp,
contract: address,
guardianSetIndex: currentGuardianSetIndexState.value.uint.toString(),
guardianSet: '',
};
} else if (platform === 'Near') {
const response = await axios.post(rpcUrl, {
jsonrpc: '2.0',
id: 'dontcare',
method: 'query',
params: {
request_type: 'view_state',
finality: 'final',
account_id: address,
prefix_base64: 'U1RBVEU=', // STATE
},
});
const state = Buffer.from(
response.data.result.values.find(
(s: any) => Buffer.from(s.key, 'base64').toString('ascii') === 'STATE'
).value,
'base64'
).toString('hex');
// a tiny hack - instead of parsing the whole state, just find the expiry, which comes before the guardian set index
// https://github.com/wormhole-foundation/wormhole/blob/main/near/contracts/wormhole/src/lib.rs#L109
const expiry = `00004f91944e0000`; // = 24 * 60 * 60 * 1_000_000_000, // 24 hours in nanoseconds
const expiryIndex = state.indexOf(expiry);
const gsiIndex = expiryIndex + 16; // u64 len in hex
const gsi = `0x${state
.substring(gsiIndex, gsiIndex + 8)
.match(/../g)
?.reverse()
.join('')}`;
return { timestamp, contract: address, guardianSetIndex: gsi, guardianSet: '' };
} else if (platform === 'Aptos') {
const response = await axios.get(
`${rpcUrl}/accounts/${address}/resource/${address}::state::WormholeState`
);
const gsi = response.data.data.guardian_set_index.number.toString();
// const gsHandle = response.data.data.guardian_sets
return { timestamp, contract: address, guardianSetIndex: gsi, guardianSet: '' };
} else if (platform === 'Sui') {
const response = await axios.post(rpcUrl, {
jsonrpc: '2.0',
id: 1,
method: 'sui_getObject',
params: [
address,
{
showType: false,
showOwner: false,
showPreviousTransaction: false,
showDisplay: false,
showContent: true,
showBcs: false,
showStorageRebate: false,
},
],
});
const gsi = response.data.result.data.content.fields.guardian_set_index.toString();
// const gsTable = response.data.result.data.content.fields.guardian_sets);
return { timestamp, contract: address, guardianSetIndex: gsi, guardianSet: '' };
}
} catch (e) {
console.error(`Failed to get guardian set for ${chain}:`, e);
}
return mt;
}

async function updateFirestore(data: GuardianSetInfoByChain): Promise<void> {
const firestore = new Firestore();
const collection = firestore.collection(
assertEnvironmentVariable('FIRESTORE_GUARDIAN_SET_INFO_COLLECTION')
);
try {
for (const chain in data) {
if (data.hasOwnProperty(chain)) {
const chainData: GuardianSetInfo | undefined = data[chain as Chain];
if (chainData) {
const docRef = collection.doc(chain);
await docRef.set(chainData);
}
}
}
} catch (e) {
console.error('Error adding document: ', e);
}
}
48 changes: 48 additions & 0 deletions cloud_functions/src/getGuardianSetInfo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { Chain } from '@wormhole-foundation/sdk-base';
import {
assertEnvironmentVariable,
GuardianSetInfoByChain,
} from '@wormhole-foundation/wormhole-monitor-common';
import { Firestore } from 'firebase-admin/firestore';

export async function getGuardianSetInfo(req: any, res: any) {
res.set('Access-Control-Allow-Origin', '*');
if (req.method === 'OPTIONS') {
// Send response to OPTIONS requests
res.set('Access-Control-Allow-Methods', 'GET');
res.set('Access-Control-Allow-Headers', 'Content-Type');
res.set('Access-Control-Max-Age', '3600');
res.status(204).send('');
return;
}

// This goes out to firestore to retrieve the guardian set info
let info: GuardianSetInfoByChain = {};
try {
info = await getGuardianSetInfoByChain();
res.status(200).send(JSON.stringify(info));
} catch (e) {
res.sendStatus(500);
}
}

async function getGuardianSetInfoByChain(): Promise<GuardianSetInfoByChain> {
const firestoreCollection = assertEnvironmentVariable('FIRESTORE_GUARDIAN_SET_INFO_COLLECTION');
let values: GuardianSetInfoByChain = {};
const firestoreDb = new Firestore({});
try {
const collectionRef = firestoreDb.collection(firestoreCollection);
const snapshot = await collectionRef.get();
snapshot.docs.forEach((doc) => {
values[doc.id as Chain] = {
timestamp: doc.data().timestamp,
contract: doc.data().contract,
guardianSet: doc.data().guardianSet,
guardianSetIndex: doc.data().guardianSetIndex,
};
});
} catch (e) {
console.error(e);
}
return values;
}
4 changes: 4 additions & 0 deletions cloud_functions/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ export const { getNTTRateLimits } = require('./getNTTRateLimits');
export const { computeNTTRateLimits } = require('./computeNTTRateLimits');
export const { getTotalSupplyAndLocked } = require('./getTotalSupplyAndLocked');
export const { computeTotalSupplyAndLocked } = require('./computeTotalSupplyAndLocked');
export const { computeGuardianSetInfo } = require('./computeGuardianSetInfo');
export const { getGuardianSetInfo } = require('./getGuardianSetInfo');

// Register an HTTP function with the Functions Framework that will be executed
// when you make an HTTP request to the deployed function's endpoint.
Expand Down Expand Up @@ -59,3 +61,5 @@ functions.http('getNTTRateLimits', getNTTRateLimits);
functions.http('computeNTTRateLimits', computeNTTRateLimits);
functions.http('getTotalSupplyAndLocked', getTotalSupplyAndLocked);
functions.http('computeTotalSupplyAndLocked', computeTotalSupplyAndLocked);
functions.http('computeGuardianSetInfo', computeGuardianSetInfo);
functions.http('getGuardianSetInfo', getGuardianSetInfo);
11 changes: 11 additions & 0 deletions common/src/consts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -293,3 +293,14 @@ export const GUARDIAN_SET_4 = [
name: 'Staking Facilities',
},
];

export type GuardianSetInfo = {
timestamp: string;
contract: string;
guardianSetIndex: string;
guardianSet: string;
};

export type GuardianSetInfoByChain = {
[chain in Chain]?: GuardianSetInfo;
};
1 change: 1 addition & 0 deletions common/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ export * from './nttConsts';
export * from './evm';
export * from './types';
export * from './wormhole';
export * from './queryContractSmart';
2 changes: 1 addition & 1 deletion dashboard/src/components/Accountant.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ import useGetAccountantPendingTransfers, {
} from '../hooks/useGetAccountantPendingTransfers';
import { TokenDataByChainAddress, TokenDataEntry } from '../hooks/useTokenData';
import { CHAIN_ICON_MAP, WORMCHAIN_URL } from '../utils/consts';
import { queryContractSmart } from '../utils/queryContractSmart';
import { queryContractSmart } from '@wormhole-foundation/wormhole-monitor-common/src/queryContractSmart';
import CollapsibleSection from './CollapsibleSection';
import { ExplorerTxHash } from './ExplorerTxHash';
import Table from './Table';
Expand Down
Loading
Loading