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

Token registry #87

Merged
merged 10 commits into from
Oct 13, 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
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);
}
};
barnjamin marked this conversation as resolved.
Show resolved Hide resolved
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??
barnjamin marked this conversation as resolved.
Show resolved Hide resolved
// 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);
}
};
barnjamin marked this conversation as resolved.
Show resolved Hide resolved
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
Loading