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: Added updateTokenMetadata cloud function #138

Merged
merged 1 commit into from
Oct 12, 2023
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
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