Skip to content

Commit

Permalink
Token registry (#87)
Browse files Browse the repository at this point in the history
  • Loading branch information
anondev2323 authored and barnjamin committed Nov 25, 2023
1 parent cb6c92c commit 4b2cf3f
Show file tree
Hide file tree
Showing 22 changed files with 2,768 additions and 57 deletions.
3 changes: 2 additions & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ jobs:
node-version: '18.x'
- run: npm ci
- run: npm run build --if-present
- run: npm test
- run: npm test
- run: cd core/tokenRegistry && npx ts-node src/scripts/checkForeignAssetsConfig.ts
11 changes: 3 additions & 8 deletions connect/src/wormhole.ts
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,7 @@ export class Wormhole {
*/
async getDecimals(
chain: ChainName,
token: TokenId | "native",
token: NativeAddress<PlatformName> | UniversalAddress | "native",
): Promise<bigint> {
const ctx = this.getChain(chain);
return await ctx.getDecimals(token);
Expand All @@ -275,7 +275,7 @@ export class Wormhole {
*/
async normalizeAmount(
chain: ChainName,
token: TokenId | "native",
token: UniversalAddress | NativeAddress<PlatformName> | "native",
amount: number | string,
): Promise<bigint> {
const ctx = this.getChain(chain);
Expand All @@ -293,15 +293,10 @@ export class Wormhole {
*/
async getBalance(
chain: ChainName,
token: string | TokenId | "native",
token: NativeAddress<PlatformName> | UniversalAddress | "native",
walletAddress: string,
): Promise<bigint | null> {
const ctx = this.getChain(chain);

if (typeof token === "string" && token !== "native") {
token = { chain: chain, address: toNative(chain, token) };
}

return ctx.getBalance(walletAddress, token);
}

Expand Down
8 changes: 5 additions & 3 deletions core/definitions/src/chain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@ import {
} from "./protocols/cctp";
import { supportsIbcBridge, IbcBridge } from "./protocols/ibc";
import { RpcConnection } from "./rpc";
import { SignedTx, TokenId } from "./types";
import { SignedTx } from "./types";
import { WormholeMessageId } from "./attestation";
import { UniversalAddress } from "./universalAddress";
import { NativeAddress } from "./address";

export abstract class ChainContext<P extends PlatformName> {
// Cached Protocol clients
Expand All @@ -37,14 +39,14 @@ export abstract class ChainContext<P extends PlatformName> {
}

// Get the number of decimals for a token
async getDecimals(token: TokenId | "native"): Promise<bigint> {
async getDecimals(token: NativeAddress<P> | UniversalAddress | "native"): Promise<bigint> {
return this.platform.getDecimals(this.chain, this.getRpc(), token);
}

// Get the balance of a token for a given address
async getBalance(
walletAddr: string,
token: TokenId | "native",
token: NativeAddress<P> | UniversalAddress | "native",
): Promise<bigint | null> {
return this.platform.getBalance(
this.chain,
Expand Down
6 changes: 4 additions & 2 deletions core/definitions/src/platform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import { RpcConnection } from "./rpc";
import { ChainsConfig, TokenId, TxHash } from "./types";
import { WormholeMessageId } from "./attestation";
import { SignedTx } from "./types";
import { NativeAddress } from "./address";
import { UniversalAddress } from "./universalAddress";

export interface PlatformUtils<P extends PlatformName> {
nativeTokenId(chain: ChainName): TokenId;
Expand All @@ -20,13 +22,13 @@ export interface PlatformUtils<P extends PlatformName> {
getDecimals(
chain: ChainName,
rpc: RpcConnection<P>,
token: TokenId | "native",
token: NativeAddress<P> | UniversalAddress | "native",
): Promise<bigint>;
getBalance(
chain: ChainName,
rpc: RpcConnection<P>,
walletAddr: string,
token: TokenId | "native",
token: NativeAddress<P> | UniversalAddress | "native",
): Promise<bigint | null>;
getCurrentBlock(rpc: RpcConnection<P>): Promise<number>;

Expand Down
5 changes: 3 additions & 2 deletions core/definitions/src/testing/mocks/platform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
toNative,
nativeIsRegistered,
NativeAddress,
UniversalAddress,
} from "../..";
import { MockRpc } from "./rpc";
import { MockChain } from "./chain";
Expand Down Expand Up @@ -70,15 +71,15 @@ export class MockPlatform<P extends PlatformName> implements Platform<P> {
getDecimals(
chain: ChainName,
rpc: RpcConnection<P>,
token: TokenId | "native",
token: NativeAddress<P> | UniversalAddress | "native",
): Promise<bigint> {
throw new Error("Method not implemented.");
}
getBalance(
chain: ChainName,
rpc: RpcConnection<P>,
walletAddr: string,
token: TokenId | "native",
token: NativeAddress<P> | UniversalAddress | "native",
): Promise<bigint | null> {
throw new Error("Method not implemented.");
}
Expand Down
Empty file.
3 changes: 3 additions & 0 deletions core/tokenRegistry/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Tokens Registry

Cache of token details and foreign asset addresses
39 changes: 39 additions & 0 deletions core/tokenRegistry/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
{
"name": "@wormhole-foundation/sdk-token-registry",
"version": "0.1.4",
"repository": {
"type": "git",
"url": "git+https://github.com/wormhole-foundation/connect-sdk.git"
},
"bugs": {
"url": "https://github.com/wormhole-foundation/connect-sdk/issues"
},
"homepage": "https://github.com/wormhole-foundation/connect-sdk#readme",
"directories": {
"test": "__tests__"
},
"main": "./dist/cjs/index.js",
"module": "./dist/esm/index.js",
"types": "./dist/esm/index.d.ts",
"files": [
"dist/**/*",
"src/**/*"
],
"scripts": {
"test": "jest --config ../../jest.config.ts __tests__/*.ts",
"build:cjs": "tsc -p ./tsconfig.cjs.json",
"build:esm": "tsc -p ./tsconfig.esm.json",
"build": "npm run build:cjs && npm run build:esm",
"rebuild": "npm run clean && npm run build:cjs && npm run build:esm",
"clean": "rm -rf ./dist && rm -f ./*.tsbuildinfo",
"lint": "npm run prettier && eslint --fix",
"prettier": "prettier --write ./src",
"updateForeignAssets": "npx ts-node ./src/scripts/updateForeignAssetConfig"
},
"dependencies": {
"@wormhole-foundation/connect-sdk": "*",
"@wormhole-foundation/connect-sdk-evm": "*",
"@wormhole-foundation/connect-sdk-solana": "*",
"@wormhole-foundation/connect-sdk-cosmwasm": "*"
}
}
132 changes: 132 additions & 0 deletions core/tokenRegistry/src/foreignAssets.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
// patch out annoying logs
const info = console.info;
console.info = function (x: any, ...rest: any) {
if (x !== 'secp256k1 unavailable, reverting to browser version') {
info(x, ...rest);
}
};
const warn = console.warn;
console.warn = function (x: any, ...rest: any) {
if (
!x
.toString()
.startsWith(
'Error: Error: RPC Validation Error: The response returned from RPC server does not match the TypeScript definition. This is likely because the SDK version is not compatible with the RPC server.',
)
) {
warn(x, ...rest);
}
};

import { ChainName, chains } from "@wormhole-foundation/sdk-base";
import { ForeignAssetsCache, TokensConfig } from "./types";
import { TokenId, Wormhole, toNative } from "@wormhole-foundation/connect-sdk";

// TODO: Question: How do we handle if a user tries to perform an action for a chain/platform which isn't installed??
// const supportedPlatforms: PlatformName[] = ['Evm', 'Solana'];
const supportedChains: ChainName[] = ['Ethereum', 'Polygon', 'Celo', 'Moonbeam', 'Fantom', 'Avalanche', 'Bsc', 'Optimism', 'Arbitrum', 'Solana']

export const isSupportedChain = (chain: ChainName) => {
return supportedChains.includes(chain);
}

export const createTokenId = (chain: ChainName, address: string) => {
if (!isSupportedChain(chain)) return;
return {
chain,
address: toNative(chain, address),
}
}

export const getForeignAddress = async (wh: Wormhole, chain: ChainName, tokenId: TokenId) => {
if (!isSupportedChain(chain)) return;
let foreignAddress: string | null = null;
try {
const foreignId = await wh.getWrappedAsset(chain, tokenId);
foreignAddress = foreignId.address.toString();
} catch (e: any) {
if (
e?.message === '3104 RPC not configured' ||
e?.message === 'wormchain RPC not configured'
) {
// do not throw on wormchain errors
} else if (e?.message.includes('is not a wrapped asset')) {
// do not throw if wrapped asset does not exist
} else {
// log error but keep going
console.error(e)
}
}
return foreignAddress;
}

export const getForeignAssetsData = async (wh: Wormhole, chain: ChainName, tokenId: TokenId | undefined, foreignAssetsCache: ForeignAssetsCache | undefined) => {
if (!tokenId) return;
let updates: ForeignAssetsCache = {};
for (const foreignChain of chains) {
const isSupported = isSupportedChain(foreignChain);
if (foreignChain !== tokenId.chain && isSupported) {
const configForeignAddress = foreignAssetsCache ? foreignAssetsCache[foreignChain] : undefined;
const foreignAddress = await getForeignAddress(wh, foreignChain, tokenId);
if (foreignAddress) {
const foreignDecimals = await wh.getDecimals(
foreignChain,
toNative(foreignChain, foreignAddress),
);
if (configForeignAddress) {
if (configForeignAddress.address !== foreignAddress) {
throw new Error(
`❌ Invalid foreign address detected! Env: ${wh.conf.network}, Existing Address: ${configForeignAddress.address}, Chain: ${chain}, Expected: ${foreignAddress}, Received: ${configForeignAddress.address}`,
);
} else if (configForeignAddress.decimals !== Number(foreignDecimals)) {
throw new Error(
`❌ Invalid foreign decimals detected! Env: ${wh.conf.network}, Existing Address: ${configForeignAddress.address}, Chain: ${chain}, Expected: ${foreignDecimals}, Received: ${configForeignAddress.decimals}`,
);
} else {
// console.log('✅ Config matches');
}
} else {
const update = {
[foreignChain]: {
address: foreignAddress,
decimals: Number(foreignDecimals)
}
}
updates = { ...updates, ...update }
}
}
}
}
return updates;
}

export const getSuggestedUpdates = async (wh: Wormhole, tokensConfig: TokensConfig) => {
let suggestedUpdates: TokensConfig = {};
let numUpdates = 0;

for (const [chain, chainTokensConfig] of Object.entries(tokensConfig)) {
for (const [token, config] of Object.entries(chainTokensConfig)) {
const tokenId = createTokenId(chain as ChainName, token);
const updates = await getForeignAssetsData(wh, chain as ChainName, tokenId, config.foreignAssets);
if (updates && Object.values(updates).length > 0) {
numUpdates += Object.values(updates).length;
suggestedUpdates = {
...suggestedUpdates,
[chain]: {
...(suggestedUpdates[chain as ChainName] || {}),
[token]: {
...(suggestedUpdates[chain as ChainName] ? suggestedUpdates[chain as ChainName]![token] || {} : {}),
foreignAssets: {
...(suggestedUpdates[chain as ChainName] ? suggestedUpdates[chain as ChainName]![token] ? suggestedUpdates[chain as ChainName]![token]!.foreignAssets : {} : {}),
...updates,
}
}
}
}
}
}
}
// console.log(`${numUpdates} updates available`);
// console.log(JSON.stringify(suggestedUpdates, null, 4));
return [numUpdates, suggestedUpdates];
}
57 changes: 57 additions & 0 deletions core/tokenRegistry/src/scripts/checkForeignAssetConfig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// patch out annoying logs
const info = console.info;
console.info = function (x: any, ...rest: any) {
if (x !== 'secp256k1 unavailable, reverting to browser version') {
info(x, ...rest);
}
};
const warn = console.warn;
console.warn = function (x: any, ...rest: any) {
if (
!x
.toString()
.startsWith(
'Error: Error: RPC Validation Error: The response returned from RPC server does not match the TypeScript definition. This is likely because the SDK version is not compatible with the RPC server.',
)
) {
warn(x, ...rest);
}
};

import * as fs from 'fs';
import { Network } from "@wormhole-foundation/sdk-base";
import { Wormhole } from "@wormhole-foundation/connect-sdk";
import { EvmPlatform } from "@wormhole-foundation/connect-sdk-evm";
import { SolanaPlatform } from "@wormhole-foundation/connect-sdk-solana";
import { getSuggestedUpdates } from '../foreignAssets';
import { TokensConfig } from "../types";

const testnetTokens = fs.readFileSync('src/tokens/testnetTokens.json', 'utf-8');
const TESTNET_TOKENS = JSON.parse(testnetTokens) as TokensConfig;
const mainnetTokens = fs.readFileSync('src/tokens/mainnetTokens.json', 'utf-8');
const MAINNET_TOKENS = JSON.parse(mainnetTokens) as TokensConfig;

// warning: be careful optimizing the RPC calls in this script, you may 429 yourself
// slow and steady, or something like that
const checkEnvConfig = async (
env: Network,
tokensConfig: TokensConfig,
) => {
const wh = new Wormhole(env, [EvmPlatform, SolanaPlatform]);

const [numUpdates, suggestedUpdates] = await getSuggestedUpdates(wh, tokensConfig);
if (numUpdates as number > 0) {
console.log(`
${numUpdates} updates available. To update, run:\n
npm run updateForeignAssets`
);
console.log(JSON.stringify(suggestedUpdates, null, 4));
} else {
console.log('Up to date')
}
}

(async () => {
await checkEnvConfig('Testnet', TESTNET_TOKENS);
await checkEnvConfig('Mainnet', MAINNET_TOKENS);
})();
Loading

0 comments on commit 4b2cf3f

Please sign in to comment.