Skip to content

Commit

Permalink
cloud_functions: Added updateTokenMetadata cloud function
Browse files Browse the repository at this point in the history
  • Loading branch information
kev1n-peters committed Oct 12, 2023
1 parent 10c26a9 commit affa1d9
Show file tree
Hide file tree
Showing 7 changed files with 269 additions and 135 deletions.
1 change: 1 addition & 0 deletions cloud_functions/scripts/deploy.sh
Original file line number Diff line number Diff line change
Expand Up @@ -156,3 +156,4 @@ gcloud functions deploy compute-tvl-tvm --entry-point computeTvlTvm --runtime no
gcloud functions deploy tvl-history --entry-point getTVLHistory --runtime nodejs16 --trigger-http --allow-unauthenticated --timeout 300 --memory 256MB --region europe-west3 --set-env-vars FIRESTORE_TVL_HISTORY_COLLECTION=$FIRESTORE_TVL_HISTORY_COLLECTION
gcloud functions deploy message-count-history --entry-point getMessageCountHistory --runtime nodejs16 --trigger-http --allow-unauthenticated --timeout 300 --memory 256MB --region europe-west3 --set-env-vars FIRESTORE_MESSAGE_COUNT_HISTORY_COLLECTION=$FIRESTORE_MESSAGE_COUNT_HISTORY_COLLECTION
gcloud functions deploy compute-message-count-history --entry-point computeMessageCountHistory --runtime nodejs16 --trigger-http --no-allow-unauthenticated --timeout 300 --memory 1GB --region europe-west3 --set-env-vars BIGTABLE_INSTANCE_ID=$BIGTABLE_INSTANCE_ID,BIGTABLE_SIGNED_VAAS_TABLE_ID=$BIGTABLE_SIGNED_VAAS_TABLE_ID,FIRESTORE_MESSAGE_COUNT_HISTORY_COLLECTION=$FIRESTORE_MESSAGE_COUNT_HISTORY_COLLECTION
gcloud functions deploy update-token-metadata --entry-point updateTokenMetadata --runtime nodejs16 --trigger-http --no-allow-unauthenticated --timeout 300 --memory 256MB --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
2 changes: 2 additions & 0 deletions cloud_functions/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export const { getTVLHistory } = require('./getTVLHistory');
export const { getMessageCountHistory } = require('./getMessageCountHistory');
export const { computeMessageCountHistory } = require('./computeMessageCountHistory');
export const { computeTvlTvm } = require('./computeTvlTvm');
export const { updateTokenMetadata } = require('./updateTokenMetadata');

// 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 All @@ -41,3 +42,4 @@ functions.http('getTVLHistory', getTVLHistory);
functions.http('getMessageCountHistory', getMessageCountHistory);
functions.http('computeMessageCountHistory', computeMessageCountHistory);
functions.http('computeTvlTvm', computeTvlTvm);
functions.http('updateTokenMetadata', updateTokenMetadata);
125 changes: 125 additions & 0 deletions cloud_functions/src/updateTokenMetadata.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import {
assertEnvironmentVariable,
chunkArray,
} from '@wormhole-foundation/wormhole-monitor-common';
import knex, { Knex } from 'knex';
import { ChainId, assertChain, toChainName } from '@certusone/wormhole-sdk';
import {
COINGECKO_PLATFORM_BY_CHAIN,
CoinGeckoCoin,
TokenMetadata,
fetchCoins,
getNativeAddress,
} from '@wormhole-foundation/wormhole-monitor-database';

const coinGeckoCoinIdCache = new Map<string, string>();

const findCoinGeckoCoinId = (
chainId: ChainId,
nativeAddress: string,
coinGeckoCoins: CoinGeckoCoin[]
): string | null => {
const key = `${chainId}/${nativeAddress}`;
const coinId = coinGeckoCoinIdCache.get(key);
if (coinId !== undefined) {
return coinId;
}
const chainName = toChainName(chainId);
const platform = COINGECKO_PLATFORM_BY_CHAIN[chainName];
if (platform === undefined) {
return null;
}
for (const coin of coinGeckoCoins) {
if (coin.platforms[platform] === nativeAddress) {
coinGeckoCoinIdCache.set(key, coin.id);
return coin.id;
}
}
return null;
};

export async function updateTokenMetadata(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.sendStatus(204);
return;
}
let pg: Knex | undefined;
try {
pg = knex({
client: 'pg',
connection: {
host: assertEnvironmentVariable('PG_HOST'),
// port: 5432, // default
user: assertEnvironmentVariable('PG_USER'),
password: assertEnvironmentVariable('PG_PASSWORD'),
database: assertEnvironmentVariable('PG_DATABASE'),
},
});
const table = assertEnvironmentVariable('PG_TOKEN_METADATA_TABLE');
const result = await pg<TokenMetadata>(table)
.select()
.whereNull('native_address')
.orWhereNull('coin_gecko_coin_id');
const coinGeckoCoins = await fetchCoins();
const toUpdate: TokenMetadata[] = [];
for (let {
token_chain,
token_address,
native_address,
coin_gecko_coin_id,
name,
symbol,
decimals,
} of result) {
assertChain(token_chain);
let shouldUpdate = false;
if (native_address === null) {
native_address = await getNativeAddress(token_chain, token_address);
shouldUpdate ||= native_address !== null;
}
if (coin_gecko_coin_id === null && native_address !== null) {
coin_gecko_coin_id = findCoinGeckoCoinId(token_chain, native_address, coinGeckoCoins);
shouldUpdate ||= coin_gecko_coin_id !== null;
}
if (shouldUpdate) {
const tokenMetadata: TokenMetadata = {
token_chain,
token_address,
native_address: native_address?.replace('\x00', '') || null, // postgres complains about invalid utf8 byte sequence
coin_gecko_coin_id,
name,
symbol,
decimals,
};
toUpdate.push(tokenMetadata);
console.log('will update', tokenMetadata);
}
}
if (toUpdate.length > 0) {
const chunks = chunkArray(toUpdate, 100);
let numUpdated = 0;
for (const chunk of chunks) {
const result: any = await pg<TokenMetadata>(table)
.insert(chunk)
.onConflict(['token_chain', 'token_address'])
.merge(['native_address', 'coin_gecko_coin_id']);
numUpdated += result.rowCount;
}
console.log(`updated ${numUpdated} rows`);
} else {
console.log(`nothing to update`);
}
res.sendStatus('200');
} catch (e) {
console.error(e);
res.sendStatus(500);
}
if (pg) {
await pg.destroy();
}
}
141 changes: 7 additions & 134 deletions database/scripts/updateTokenMetadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,149 +4,22 @@ import {
assertEnvironmentVariable,
chunkArray,
} from '@wormhole-foundation/wormhole-monitor-common';
import { ChainId, assertChain, toChainName } from '@certusone/wormhole-sdk';
import {
CHAIN_ID_ALGORAND,
CHAIN_ID_APTOS,
CHAIN_ID_INJECTIVE,
CHAIN_ID_NEAR,
CHAIN_ID_SOLANA,
CHAIN_ID_SUI,
CHAIN_ID_TERRA,
CHAIN_ID_TERRA2,
CHAIN_ID_XPLA,
CONTRACTS,
ChainId,
ChainName,
assertChain,
getTypeFromExternalAddress,
hexToUint8Array,
isEVMChain,
queryExternalId,
queryExternalIdInjective,
toChainName,
tryHexToNativeAssetString,
tryHexToNativeStringNear,
} from '@certusone/wormhole-sdk';
import { CoinGeckoCoin, TokenMetadata, fetchCoins } from '../src';
COINGECKO_PLATFORM_BY_CHAIN,
CoinGeckoCoin,
TokenMetadata,
fetchCoins,
getNativeAddress,
} from '../src';
import knex from 'knex';
import { ChainGrpcWasmApi } from '@injectivelabs/sdk-ts';
import { Network, getNetworkInfo } from '@injectivelabs/networks';
import { LCDClient } from '@xpla/xpla.js';
import { connect } from 'near-api-js';
import { AptosClient } from 'aptos';
import { Connection, JsonRpcProvider } from '@mysten/sui.js';
import { getTokenCoinType } from '@certusone/wormhole-sdk/lib/cjs/sui';

const PG_USER = assertEnvironmentVariable('PG_USER');
const PG_PASSWORD = assertEnvironmentVariable('PG_PASSWORD');
const PG_DATABASE = assertEnvironmentVariable('PG_DATABASE');
const PG_HOST = assertEnvironmentVariable('PG_HOST');
const TOKEN_METADATA_TABLE = assertEnvironmentVariable('PG_TOKEN_METADATA_TABLE');

const getNativeAddress = async (
tokenChain: ChainId,
tokenAddress: string
): Promise<string | null> => {
try {
if (
isEVMChain(tokenChain) ||
tokenChain === CHAIN_ID_SOLANA ||
tokenChain === CHAIN_ID_ALGORAND ||
tokenChain === CHAIN_ID_TERRA
) {
return tryHexToNativeAssetString(tokenAddress, tokenChain);
} else if (tokenChain === CHAIN_ID_XPLA) {
const client = new LCDClient({
URL: 'https://dimension-lcd.xpla.dev',
chainID: 'dimension_37-1',
});
return (
(await queryExternalId(client, CONTRACTS.MAINNET.xpla.token_bridge, tokenAddress)) || null
);
} else if (tokenChain === CHAIN_ID_TERRA2) {
const client = new LCDClient({
URL: 'https://phoenix-lcd.terra.dev',
chainID: 'phoenix-1',
});
return (
(await queryExternalId(client, CONTRACTS.MAINNET.terra2.token_bridge, tokenAddress)) || null
);
} else if (tokenChain === CHAIN_ID_INJECTIVE) {
const client = new ChainGrpcWasmApi(getNetworkInfo(Network.MainnetK8s).grpc);
return await queryExternalIdInjective(
client,
CONTRACTS.MAINNET.injective.token_bridge,
tokenAddress
);
} else if (tokenChain === CHAIN_ID_APTOS) {
const client = new AptosClient('https://fullnode.mainnet.aptoslabs.com');
return await getTypeFromExternalAddress(
client,
CONTRACTS.MAINNET.aptos.token_bridge,
tokenAddress
);
} else if (tokenChain === CHAIN_ID_NEAR) {
const NATIVE_NEAR_WH_ADDRESS =
'0000000000000000000000000000000000000000000000000000000000000000';
const NATIVE_NEAR_PLACEHOLDER = 'near';
if (tokenAddress === NATIVE_NEAR_WH_ADDRESS) {
return NATIVE_NEAR_PLACEHOLDER;
} else {
const connection = await connect({
nodeUrl: 'https://rpc.mainnet.near.org',
networkId: 'mainnet',
});
return await tryHexToNativeStringNear(
connection.connection.provider,
CONTRACTS.MAINNET.near.token_bridge,
tokenAddress
);
}
} else if (tokenChain === CHAIN_ID_SUI) {
const provider = new JsonRpcProvider(
new Connection({ fullnode: 'https://fullnode.mainnet.sui.io' })
);
return await getTokenCoinType(
provider,
CONTRACTS.MAINNET.sui.token_bridge,
hexToUint8Array(tokenAddress),
CHAIN_ID_SUI
);
}
} catch (e) {
console.error(e);
}
return null;
};

// https://api.coingecko.com/api/v3/asset_platforms
const COINGECKO_PLATFORM_BY_CHAIN: { [key in ChainName]?: string } = {
solana: 'solana',
ethereum: 'ethereum',
terra: 'terra',
terra2: 'terra-2',
bsc: 'binance-smart-chain',
polygon: 'polygon-pos',
avalanche: 'avalanche',
oasis: 'oasis',
algorand: 'algorand',
aptos: 'aptos',
aurora: 'aurora',
fantom: 'fantom',
karura: 'karura',
acala: 'acala',
klaytn: 'klay-token',
celo: 'celo',
near: 'near-protocol',
moonbeam: 'moonbeam',
arbitrum: 'arbitrum-one',
optimism: 'optimistic-ethereum',
xpla: undefined,
injective: undefined,
sui: 'sui',
base: 'base',
};

const coinGeckoCoinIdCache = new Map<string, string>();

const findCoinGeckoCoinId = (
Expand Down
31 changes: 30 additions & 1 deletion database/src/coingecko.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,39 @@
import { ChainName } from '@certusone/wormhole-sdk';
import { chunkArray, sleep } from '@wormhole-foundation/wormhole-monitor-common';
import axios, { AxiosError, isAxiosError } from 'axios';
import axios, { isAxiosError } from 'axios';

const COIN_GECKO_API_BASE_URL = 'https://api.coingecko.com/api/v3';
const COIN_GECKO_PRO_API_BASE_URL = 'https://pro-api.coingecko.com/api/v3';
const COIN_GECKO_API_SLEEP_MS = 200;

// https://api.coingecko.com/api/v3/asset_platforms
export const COINGECKO_PLATFORM_BY_CHAIN: { [key in ChainName]?: string } = {
solana: 'solana',
ethereum: 'ethereum',
terra: 'terra',
terra2: 'terra-2',
bsc: 'binance-smart-chain',
polygon: 'polygon-pos',
avalanche: 'avalanche',
oasis: 'oasis',
algorand: 'algorand',
aptos: 'aptos',
aurora: 'aurora',
fantom: 'fantom',
karura: 'karura',
acala: 'acala',
klaytn: 'klay-token',
celo: 'celo',
near: 'near-protocol',
moonbeam: 'moonbeam',
arbitrum: 'arbitrum-one',
optimism: 'optimistic-ethereum',
xpla: undefined,
injective: 'injective',
sui: 'sui',
base: 'base',
};

export interface CoinGeckoPrices {
[coinId: string]: { usd: number | null };
}
Expand Down
Loading

0 comments on commit affa1d9

Please sign in to comment.