Skip to content

Commit fab83cc

Browse files
authored
make useTransactionHistoryWHScan more robust (#3289)
* make useTransactionHistoryWHScan more robust * lol * linty
1 parent 91e6ebe commit fab83cc

File tree

4 files changed

+194
-147
lines changed

4 files changed

+194
-147
lines changed

wormhole-connect/src/config/types.ts

+5-5
Original file line numberDiff line numberDiff line change
@@ -226,15 +226,15 @@ export interface Transaction {
226226
sender?: string;
227227
recipient: string;
228228

229-
amount: string;
230-
amountUsd: number;
231-
receiveAmount: string;
229+
amount?: string;
230+
amountUsd?: number;
231+
receiveAmount?: string;
232232

233233
fromChain: Chain;
234-
fromToken: Token;
234+
fromToken?: Token;
235235

236236
toChain: Chain;
237-
toToken: Token;
237+
toToken?: Token;
238238

239239
// Timestamps
240240
senderTimestamp: string;

wormhole-connect/src/hooks/useFetchSupportedRoutes.ts

+9-1
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,15 @@ const useFetchSupportedRoutes = (): HookReturn => {
9999
return () => {
100100
isActive = false;
101101
};
102-
}, [sourceToken, destToken, amount, fromChain, toChain, toNativeToken, receivingWallet]);
102+
}, [
103+
sourceToken,
104+
destToken,
105+
amount,
106+
fromChain,
107+
toChain,
108+
toNativeToken,
109+
receivingWallet,
110+
]);
103111

104112
return {
105113
supportedRoutes: routes,

wormhole-connect/src/hooks/useTransactionHistoryWHScan.ts

+165-124
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import { getGasToken } from 'utils';
1313
import type { Chain, ChainId } from '@wormhole-foundation/sdk';
1414
import type { Transaction } from 'config/types';
1515
import { toFixedDecimals } from 'utils/balance';
16+
import { useTokens } from 'contexts/TokensContext';
17+
import { Token } from 'config/tokens';
1618

1719
interface WormholeScanTransaction {
1820
id: string;
@@ -115,116 +117,135 @@ const useTransactionHistoryWHScan = (
115117
const [error, setError] = useState('');
116118
const [isFetching, setIsFetching] = useState(false);
117119
const [hasMore, setHasMore] = useState(true);
120+
const { getOrFetchToken } = useTokens();
118121

119122
const { address, page = 0, pageSize = 30 } = props;
120123

121124
// Common parsing logic for a single transaction from WHScan API.
122125
// IMPORTANT: Anything specific to a route, please use that route's parser:
123126
// parseTokenBridgeTx | parseNTTTx | parseCCTPTx | parsePorticoTx
124-
const parseSingleTx = useCallback((tx: WormholeScanTransaction) => {
125-
const { content, data, sourceChain, targetChain } = tx;
126-
const { tokenAmount, usdAmount } = data || {};
127-
const { standarizedProperties } = content || {};
128-
129-
const fromChainId = standarizedProperties.fromChain || sourceChain?.chainId;
130-
const toChainId = standarizedProperties.toChain || targetChain?.chainId;
131-
const tokenChainId = standarizedProperties.tokenChain;
132-
133-
const fromChain = chainIdToChain(fromChainId);
134-
135-
// Skip if we don't have the source chain
136-
if (!fromChain) {
137-
return;
138-
}
139-
140-
const tokenChain = chainIdToChain(tokenChainId);
141-
142-
// Skip if we don't have the token chain
143-
if (!tokenChain) {
144-
return;
145-
}
146-
147-
let token = config.tokens.get(
148-
tokenChain,
149-
standarizedProperties.tokenAddress,
150-
);
151-
152-
if (!token) {
153-
// IMPORTANT:
154-
// If we don't have the token config from the token address,
155-
// we can check if we can use the symbol to get it.
156-
// So far this case is only for SUI and APT
157-
const foundBySymbol =
158-
data?.symbol && config.tokens.findBySymbol(tokenChain, data.symbol);
159-
if (foundBySymbol) {
160-
token = foundBySymbol;
127+
const parseSingleTx = useCallback(
128+
async (tx: WormholeScanTransaction) => {
129+
const { content, data, sourceChain, targetChain } = tx;
130+
const { standarizedProperties } = content || {};
131+
132+
const fromChainId =
133+
standarizedProperties.fromChain || sourceChain?.chainId;
134+
const toChainId = standarizedProperties.toChain || targetChain?.chainId;
135+
const tokenChainId = standarizedProperties.tokenChain;
136+
137+
const fromChain = chainIdToChain(fromChainId);
138+
139+
// Skip if we don't have the source chain
140+
if (!fromChain) {
141+
return;
142+
}
143+
144+
const tokenChain = tokenChainId
145+
? chainIdToChain(tokenChainId)
146+
: chainIdToChain(toChainId);
147+
148+
// Skip if we don't have the token chain
149+
if (!tokenChain) {
150+
return;
151+
}
152+
153+
let token: Token | undefined;
154+
try {
155+
token = await getOrFetchToken(
156+
Wormhole.tokenId(tokenChain, standarizedProperties.tokenAddress),
157+
);
158+
} catch (e) {
159+
// This is ok
161160
}
162-
}
163-
164-
// If we've still failed to get the token, return early
165-
if (!token) {
166-
return;
167-
}
168-
169-
const toChain = chainIdToChain(toChainId);
170-
171-
// data.tokenAmount holds the normalized token amount value.
172-
// Otherwise we need to format standarizedProperties.amount using decimals
173-
const sentAmountDisplay =
174-
tokenAmount ??
175-
sdkAmount.display(
176-
{
177-
amount: standarizedProperties.amount,
178-
decimals: standarizedProperties.normalizedDecimals ?? DECIMALS,
179-
},
180-
0,
181-
);
182161

183-
const receiveAmountValue =
184-
BigInt(standarizedProperties.amount) - BigInt(standarizedProperties.fee);
185-
// It's unlikely, but in case the above subtraction returns a non-positive number,
186-
// we should not show that at all.
187-
const receiveAmountDisplay =
188-
receiveAmountValue > 0
189-
? sdkAmount.display(
162+
if (!token) {
163+
// IMPORTANT:
164+
// If we don't have the token config from the token address,
165+
// we can check if we can use the symbol to get it.
166+
// So far this case is only for SUI and APT
167+
const foundBySymbol =
168+
data?.symbol && config.tokens.findBySymbol(tokenChain, data.symbol);
169+
if (foundBySymbol) {
170+
token = foundBySymbol;
171+
}
172+
}
173+
174+
if (!token) {
175+
console.warn("Can't find token", tx);
176+
}
177+
178+
const toChain = chainIdToChain(toChainId);
179+
180+
let sentAmountDisplay: string | undefined = undefined;
181+
let receiveAmountDisplay: string | undefined = undefined;
182+
let usdAmount: number | undefined = undefined;
183+
184+
if (data && data.tokenAmount) {
185+
sentAmountDisplay = data.tokenAmount;
186+
} else if (standarizedProperties.amount) {
187+
sentAmountDisplay = sdkAmount.display(
188+
{
189+
amount: standarizedProperties.amount,
190+
decimals: standarizedProperties.normalizedDecimals ?? DECIMALS,
191+
},
192+
0,
193+
);
194+
}
195+
196+
if (standarizedProperties.amount && standarizedProperties.fee) {
197+
const receiveAmountValue =
198+
BigInt(standarizedProperties.amount) -
199+
BigInt(standarizedProperties.fee);
200+
// It's unlikely, but in case the above subtraction returns a non-positive number,
201+
// we should not show that at all.
202+
if (receiveAmountValue > 0) {
203+
receiveAmountDisplay = sdkAmount.display(
190204
{
191205
amount: receiveAmountValue.toString(),
192206
decimals: DECIMALS,
193207
},
194208
0,
195-
)
196-
: '';
197-
198-
const txHash = sourceChain.transaction?.txHash;
199-
200-
// Transaction is in-progress when the below are both true:
201-
// 1- Source chain has confirmed
202-
// 2- Target has either not received, or received but not completed
203-
const inProgress =
204-
sourceChain?.status?.toLowerCase() === 'confirmed' &&
205-
targetChain?.status?.toLowerCase() !== 'completed';
206-
207-
const txData: Transaction = {
208-
txHash,
209-
sender: standarizedProperties.fromAddress || sourceChain.from,
210-
recipient: standarizedProperties.toAddress,
211-
amount: sentAmountDisplay,
212-
amountUsd: usdAmount ? Number(usdAmount) : 0,
213-
receiveAmount: receiveAmountDisplay,
214-
fromChain,
215-
fromToken: token,
216-
toChain,
217-
toToken: token,
218-
senderTimestamp: sourceChain?.timestamp,
219-
receiverTimestamp: targetChain?.timestamp,
220-
explorerLink: `${WORMSCAN}tx/${txHash}${
221-
config.isMainnet ? '' : '?network=TESTNET'
222-
}`,
223-
inProgress,
224-
};
209+
);
210+
}
211+
}
212+
213+
if (data && data.usdAmount) {
214+
usdAmount = Number(data.usdAmount);
215+
}
225216

226-
return txData;
227-
}, []);
217+
const txHash = sourceChain.transaction?.txHash;
218+
219+
// Transaction is in-progress when the below are both true:
220+
// 1- Source chain has confirmed
221+
// 2- Target has either not received, or received but not completed
222+
const inProgress =
223+
sourceChain?.status?.toLowerCase() === 'confirmed' &&
224+
targetChain?.status?.toLowerCase() !== 'completed';
225+
226+
const txData: Transaction = {
227+
txHash,
228+
sender: standarizedProperties.fromAddress || sourceChain.from,
229+
recipient: standarizedProperties.toAddress,
230+
amount: sentAmountDisplay,
231+
amountUsd: usdAmount,
232+
receiveAmount: receiveAmountDisplay,
233+
fromChain,
234+
fromToken: token,
235+
toChain,
236+
toToken: token,
237+
senderTimestamp: sourceChain?.timestamp,
238+
receiverTimestamp: targetChain?.timestamp,
239+
explorerLink: `${WORMSCAN}tx/${txHash}${
240+
config.isMainnet ? '' : '?network=TESTNET'
241+
}`,
242+
inProgress,
243+
};
244+
245+
return txData;
246+
},
247+
[getOrFetchToken],
248+
);
228249

229250
// Parser for Portal Token Bridge transactions (appId === PORTAL_TOKEN_BRIDGE)
230251
// IMPORTANT: This is where we can add any customizations specific to Token Bridge data
@@ -236,6 +257,16 @@ const useTransactionHistoryWHScan = (
236257
[parseSingleTx],
237258
);
238259

260+
// Parser for NTT transactions (appId === NATIVE_TOKEN_TRANSFER)
261+
// IMPORTANT: This is where we can add any customizations specific to NTT data
262+
// that we have retrieved from WHScan API
263+
const parseGenericRelayer = useCallback(
264+
(tx: WormholeScanTransaction) => {
265+
return parseSingleTx(tx);
266+
},
267+
[parseSingleTx],
268+
);
269+
239270
// Parser for NTT transactions (appId === NATIVE_TOKEN_TRANSFER)
240271
// IMPORTANT: This is where we can add any customizations specific to NTT data
241272
// that we have retrieved from WHScan API
@@ -260,8 +291,8 @@ const useTransactionHistoryWHScan = (
260291
// IMPORTANT: This is where we can add any customizations specific to Portico data
261292
// that we have retrieved from WHScan API
262293
const parsePorticoTx = useCallback(
263-
(tx: WormholeScanTransaction) => {
264-
const txData = parseSingleTx(tx);
294+
async (tx: WormholeScanTransaction) => {
295+
const txData = await parseSingleTx(tx);
265296
if (!txData) return;
266297

267298
const payload = tx.content.payload
@@ -331,47 +362,56 @@ const useTransactionHistoryWHScan = (
331362
const PARSERS = useMemo(
332363
() => ({
333364
PORTAL_TOKEN_BRIDGE: parseTokenBridgeTx,
365+
GENERIC_RELAYER: parseGenericRelayer,
334366
NATIVE_TOKEN_TRANSFER: parseNTTTx,
335367
CCTP_WORMHOLE_INTEGRATION: parseCCTPTx,
336368
ETH_BRIDGE: parsePorticoTx,
337369
USDT_BRIDGE: parsePorticoTx,
338370
FAST_TRANSFERS: parseLLTx,
339371
WORMHOLE_LIQUIDITY_LAYER: parseLLTx,
340372
}),
341-
[parseCCTPTx, parseNTTTx, parsePorticoTx, parseTokenBridgeTx, parseLLTx],
373+
[
374+
parseCCTPTx,
375+
parseNTTTx,
376+
parsePorticoTx,
377+
parseTokenBridgeTx,
378+
parseLLTx,
379+
parseGenericRelayer,
380+
],
342381
);
343382

344383
// eslint-disable-next-line @typescript-eslint/no-explicit-any
345384
const parseTransactions = useCallback(
346-
(allTxs: Array<WormholeScanTransaction>) => {
347-
return allTxs
348-
.map((tx) => {
349-
// Locate the appIds
350-
const appIds: Array<string> =
351-
tx.content?.standarizedProperties?.appIds || [];
352-
353-
// TODO: SDKV2
354-
// Some integrations may compose with multiple protocols and have multiple appIds
355-
// Choose a more specific parser if available
356-
if (appIds.includes('ETH_BRIDGE') || appIds.includes('USDT_BRIDGE')) {
357-
return parsePorticoTx(tx);
358-
}
385+
async (allTxs: Array<WormholeScanTransaction>) => {
386+
return (
387+
await Promise.all(
388+
allTxs.map(async (tx) => {
389+
// Locate the appIds
390+
const appIds: Array<string> =
391+
tx.content?.standarizedProperties?.appIds || [];
392+
393+
// TODO: SDKV2
394+
// Some integrations may compose with multiple protocols and have multiple appIds
395+
// Choose a more specific parser if available
396+
if (
397+
appIds.includes('ETH_BRIDGE') ||
398+
appIds.includes('USDT_BRIDGE')
399+
) {
400+
return parsePorticoTx(tx);
401+
}
359402

360-
for (const appId of appIds) {
361-
// Retrieve the parser for an appId
362-
const parser = PARSERS[appId];
403+
for (const appId of appIds) {
404+
// Retrieve the parser for an appId
405+
const parser = PARSERS[appId];
363406

364-
// If no parsers specified for the given appIds, we'll skip this transaction
365-
if (parser) {
366-
try {
407+
// If no parsers specified for the given appIds, we'll skip this transaction
408+
if (parser) {
367409
return parser(tx);
368-
} catch (e) {
369-
console.error(`Error parsing transaction: ${e}`);
370410
}
371411
}
372-
}
373-
})
374-
.filter((tx) => !!tx); // Filter out unsupported transactions
412+
}),
413+
)
414+
).filter((tx) => !!tx); // Filter out unsupported transactions
375415
},
376416
[PARSERS, parsePorticoTx],
377417
);
@@ -403,8 +443,9 @@ const useTransactionHistoryWHScan = (
403443
if (!cancelled) {
404444
const resData = resPayload?.operations;
405445
if (resData) {
446+
const parsedTxs = await parseTransactions(resData);
447+
406448
setTransactions((txs) => {
407-
const parsedTxs = parseTransactions(resData);
408449
if (txs && txs.length > 0) {
409450
// We need to keep track of existing tx hashes to prevent duplicates in the final list
410451
const existingTxs = new Set<string>();

0 commit comments

Comments
 (0)