Skip to content

Commit 358dab0

Browse files
committedSep 4, 2024·
hum: address pr
Signed-off-by: bingyuyap <bingyu.yap.21@gmail.com>
1 parent 2fe3486 commit 358dab0

File tree

6 files changed

+218
-21
lines changed

6 files changed

+218
-21
lines changed
 

‎database/fast-transfer-schema.sql

+13
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ DROP TABLE IF EXISTS fast_transfer_settlements;
55
DROP TABLE IF EXISTS auction_logs;
66
DROP TABLE IF EXISTS auction_history_mapping;
77
DROP TABLE IF EXISTS redeem_swaps;
8+
DROP TABLE IF EXISTS chains;
9+
DROP TABLE IF EXISTS token_infos;
810

911
DROP TYPE IF EXISTS FastTransferStatus;
1012
DROP TYPE IF EXISTS FastTransferProtocol;
@@ -113,3 +115,14 @@ CREATE TABLE chains (
113115
id INTEGER PRIMARY KEY,
114116
name VARCHAR(255) NOT NULL UNIQUE
115117
)
118+
119+
-- Token Infos table to store information about different tokens
120+
-- A normalized table for us to reference token info for analytics purposes
121+
CREATE TABLE token_infos (
122+
name VARCHAR(255) NOT NULL,
123+
chain_id INTEGER NOT NULL,
124+
decimals INTEGER NOT NULL,
125+
symbol VARCHAR(255) NOT NULL,
126+
token_address VARCHAR(255) NOT NULL,
127+
PRIMARY KEY (token_address, chain_id)
128+
);

‎watcher/src/fastTransfer/swapLayer/parser.ts

+20-2
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,31 @@
11
import { ethers } from 'ethers';
22
import { TransferCompletion } from '../types';
33
import { parseVaa } from '@wormhole-foundation/wormhole-monitor-common';
4+
import { Knex } from 'knex';
5+
import { Chain, chainToChainId } from '@wormhole-foundation/sdk-base';
6+
import { TokenInfoManager } from '../../utils/TokenInfoManager';
47

58
class SwapLayerParser {
69
private provider: ethers.providers.JsonRpcProvider;
710
private swapLayerAddress: string;
811
private swapLayerInterface: ethers.utils.Interface;
9-
10-
constructor(provider: ethers.providers.JsonRpcProvider, swapLayerAddress: string) {
12+
private tokenInfoManager: TokenInfoManager | null = null;
13+
14+
constructor(
15+
provider: ethers.providers.JsonRpcProvider,
16+
swapLayerAddress: string,
17+
db: Knex | null,
18+
chain: Chain
19+
) {
1120
this.provider = provider;
1221
this.swapLayerAddress = swapLayerAddress;
1322
this.swapLayerInterface = new ethers.utils.Interface([
1423
'event Redeemed(address indexed recipient, address outputToken, uint256 outputAmount, uint256 relayingFee)',
1524
]);
25+
26+
if (db) {
27+
this.tokenInfoManager = new TokenInfoManager(db, chainToChainId(chain), provider);
28+
}
1629
}
1730

1831
async parseSwapLayerTransaction(
@@ -59,6 +72,11 @@ class SwapLayerParser {
5972

6073
if (!swapEvent) return null;
6174

75+
// if we have the tokenInfoManager inited, persist the token info
76+
// this ensures we have the token decimal and name for analytics purposes
77+
if (this.tokenInfoManager)
78+
await this.tokenInfoManager.saveTokenInfoIfNotExist(swapEvent.args.outputToken);
79+
6280
return {
6381
tx_hash: txHash,
6482
recipient: swapEvent.args.recipient,

‎watcher/src/fastTransfer/types.ts

+8
Original file line numberDiff line numberDiff line change
@@ -156,3 +156,11 @@ export type TransferCompletion = {
156156
// on Solana Swap Layer, this acts as a link between complete_{transfer, swap}_payload and release_inbound
157157
staged_inbound?: string;
158158
};
159+
160+
export type TokenInfo = {
161+
name: string;
162+
chain_id: number;
163+
decimals: number;
164+
symbol: string;
165+
token_address: string;
166+
};

‎watcher/src/utils/TokenInfoManager.ts

+160
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
import { Knex } from 'knex';
2+
import { ethers, providers } from 'ethers';
3+
import { ChainId, chainIdToChain } from '@wormhole-foundation/sdk-base';
4+
import { Connection, PublicKey } from '@solana/web3.js';
5+
import { TokenInfo } from 'src/fastTransfer/types';
6+
7+
const minABI = [
8+
{
9+
inputs: [],
10+
name: 'name',
11+
outputs: [{ internalType: 'string', name: '', type: 'string' }],
12+
stateMutability: 'view',
13+
type: 'function',
14+
},
15+
{
16+
inputs: [],
17+
name: 'symbol',
18+
outputs: [{ internalType: 'string', name: '', type: 'string' }],
19+
stateMutability: 'view',
20+
type: 'function',
21+
},
22+
{
23+
inputs: [],
24+
name: 'decimals',
25+
outputs: [{ internalType: 'uint8', name: '', type: 'uint8' }],
26+
stateMutability: 'view',
27+
type: 'function',
28+
},
29+
];
30+
// TokenInfoManager class for managing token information across different chains
31+
// This class is to ensure that token info (e.g. decimal, name) for tokens that we see on Swap Layer is persisted for analytics purposes
32+
export class TokenInfoManager {
33+
private tokenInfoMap: Map<string, TokenInfo>;
34+
private db: Knex;
35+
private chainId: ChainId;
36+
private provider: providers.JsonRpcProvider | Connection;
37+
38+
constructor(db: Knex, chainId: ChainId, provider: providers.JsonRpcProvider | Connection) {
39+
this.tokenInfoMap = new Map();
40+
this.db = db;
41+
this.chainId = chainId;
42+
this.provider = provider;
43+
}
44+
45+
// Retrieve token information from the database
46+
private async getTokenInfoFromDB(tokenAddress: string): Promise<TokenInfo | null> {
47+
return await this.db('token_infos')
48+
.select('token_address', 'name', 'symbol', 'decimals')
49+
.where('token_address', tokenAddress)
50+
.andWhere('chain_id', this.chainId)
51+
.first();
52+
}
53+
54+
private async saveTokenInfo(tokenAddress: string, tokenInfo: TokenInfo): Promise<void> {
55+
await this.db('token_infos')
56+
.insert({
57+
token_address: tokenAddress,
58+
name: tokenInfo.name,
59+
symbol: tokenInfo.symbol,
60+
decimals: tokenInfo.decimals,
61+
chain_id: this.chainId,
62+
})
63+
.onConflict(['token_address', 'chain_id'])
64+
.merge();
65+
}
66+
67+
// Save token information if it doesn't exist in the cache or database
68+
public async saveTokenInfoIfNotExist(tokenAddress: string): Promise<TokenInfo | null> {
69+
if (this.tokenInfoMap.has(tokenAddress)) {
70+
return this.tokenInfoMap.get(tokenAddress) || null;
71+
}
72+
// Check if token info is in the database
73+
const tokenInfo = await this.getTokenInfoFromDB(tokenAddress);
74+
if (tokenInfo) {
75+
this.tokenInfoMap.set(tokenAddress, tokenInfo);
76+
return tokenInfo;
77+
}
78+
// If not in database, fetch from RPC
79+
const fetchedTokenInfo = await this.fetchTokenInfoFromRPC(tokenAddress);
80+
if (fetchedTokenInfo) {
81+
await this.saveTokenInfo(tokenAddress, fetchedTokenInfo);
82+
this.tokenInfoMap.set(tokenAddress, fetchedTokenInfo);
83+
return fetchedTokenInfo;
84+
}
85+
return null;
86+
}
87+
88+
// Fetch token information from RPC based on the chain ID
89+
private async fetchTokenInfoFromRPC(tokenAddress: string): Promise<TokenInfo | null> {
90+
if (chainIdToChain(this.chainId) === 'Solana') {
91+
return this.fetchSolanaTokenInfo(tokenAddress);
92+
}
93+
return this.fetchEVMTokenInfo(tokenAddress);
94+
}
95+
96+
// Fetch Solana token information
97+
private async fetchSolanaTokenInfo(tokenAddress: string): Promise<TokenInfo | null> {
98+
try {
99+
const connection = this.provider as Connection;
100+
const tokenPublicKey = new PublicKey(tokenAddress);
101+
const accountInfo = await connection.getParsedAccountInfo(tokenPublicKey);
102+
103+
if (accountInfo.value && accountInfo.value.data && 'parsed' in accountInfo.value.data) {
104+
const parsedData = accountInfo.value.data.parsed;
105+
if (parsedData.type === 'mint' && 'info' in parsedData) {
106+
const { name, symbol, decimals } = parsedData.info;
107+
if (
108+
typeof name === 'string' &&
109+
typeof symbol === 'string' &&
110+
typeof decimals === 'number'
111+
) {
112+
return { name, symbol, decimals, chain_id: this.chainId, token_address: tokenAddress };
113+
}
114+
}
115+
}
116+
throw new Error('Invalid token account');
117+
} catch (error) {
118+
console.error('Error fetching Solana token info:', error);
119+
return null;
120+
}
121+
}
122+
123+
// Fetch EVM token information
124+
private async fetchEVMTokenInfo(tokenAddress: string): Promise<TokenInfo | null> {
125+
// If it's null address, it's Ether or Wrapped Ether
126+
if (tokenAddress.toLowerCase() === '0x0000000000000000000000000000000000000000') {
127+
const { name, symbol } = this.getEtherInfo();
128+
return {
129+
name,
130+
symbol,
131+
decimals: 18,
132+
chain_id: this.chainId,
133+
token_address: tokenAddress,
134+
};
135+
}
136+
137+
const provider = this.provider as providers.JsonRpcProvider;
138+
const tokenContract = new ethers.Contract(tokenAddress, minABI, provider);
139+
try {
140+
const name = await tokenContract.name();
141+
const symbol = await tokenContract.symbol();
142+
const decimals = await tokenContract.decimals();
143+
return { name, symbol, decimals, chain_id: this.chainId, token_address: tokenAddress };
144+
} catch (error) {
145+
console.error('Error fetching EVM token info:', error, tokenAddress);
146+
return null;
147+
}
148+
}
149+
150+
// Helper function to get Ether or Wrapped Ether info based on chain ID
151+
private getEtherInfo(): { name: string; symbol: string } {
152+
switch (this.chainId) {
153+
case 2:
154+
case 5:
155+
return { name: 'Ether', symbol: 'ETH' };
156+
default:
157+
return { name: 'Wrapped Ether', symbol: 'WETH' };
158+
}
159+
}
160+
}

‎watcher/src/watchers/FTEVMWatcher.ts

+16-18
Original file line numberDiff line numberDiff line change
@@ -42,27 +42,25 @@ export class FTEVMWatcher extends Watcher {
4242
this.provider = new ethers.providers.JsonRpcProvider(RPCS_BY_CHAIN[network][chain]);
4343
this.rpc = RPCS_BY_CHAIN[this.network][this.chain]!;
4444
this.tokenRouterParser = new TokenRouterParser(this.network, chain, this.provider);
45+
46+
// Initialize database connection before creating swap layer parser
47+
if (!isTest) {
48+
this.pg = knex({
49+
client: 'pg',
50+
connection: {
51+
user: assertEnvironmentVariable('PG_FT_USER'),
52+
password: assertEnvironmentVariable('PG_FT_PASSWORD'),
53+
database: assertEnvironmentVariable('PG_FT_DATABASE'),
54+
host: assertEnvironmentVariable('PG_FT_HOST'),
55+
port: Number(assertEnvironmentVariable('PG_FT_PORT')),
56+
},
57+
});
58+
}
59+
4560
this.swapLayerParser = this.swapLayerAddress
46-
? new SwapLayerParser(this.provider, this.swapLayerAddress)
61+
? new SwapLayerParser(this.provider, this.swapLayerAddress, this.pg, chain)
4762
: null;
4863
this.logger.debug('FTWatcher', network, chain, finalizedBlockTag);
49-
// hacky way to not connect to the db in tests
50-
// this is to allow ci to run without a db
51-
if (isTest) {
52-
// Components needed for testing is complete
53-
return;
54-
}
55-
56-
this.pg = knex({
57-
client: 'pg',
58-
connection: {
59-
user: assertEnvironmentVariable('PG_FT_USER'),
60-
password: assertEnvironmentVariable('PG_FT_PASSWORD'),
61-
database: assertEnvironmentVariable('PG_FT_DATABASE'),
62-
host: assertEnvironmentVariable('PG_FT_HOST'),
63-
port: Number(assertEnvironmentVariable('PG_FT_PORT')),
64-
},
65-
});
6664
}
6765

6866
async getBlock(blockNumberOrTag: number | BlockTag): Promise<Block> {

‎watcher/src/watchers/__tests__/FTEVMWatcher.test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ describe('SwapLayerParser', () => {
7474

7575
beforeEach(() => {
7676
mockProvider = new MockJsonRpcProvider();
77-
parser = new SwapLayerParser(mockProvider, swapLayerAddress);
77+
parser = new SwapLayerParser(mockProvider, swapLayerAddress, null, 'ArbitrumSepolia');
7878
});
7979

8080
it('should parse a swap layer transaction correctly', async () => {

0 commit comments

Comments
 (0)
Please sign in to comment.