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

make useTransactionHistoryWHScan more robust #3289

Merged
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
10 changes: 5 additions & 5 deletions wormhole-connect/src/config/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -226,15 +226,15 @@ export interface Transaction {
sender?: string;
recipient: string;

amount: string;
amountUsd: number;
receiveAmount: string;
amount?: string;
amountUsd?: number;
receiveAmount?: string;

fromChain: Chain;
fromToken: Token;
fromToken?: Token;

toChain: Chain;
toToken: Token;
toToken?: Token;

// Timestamps
senderTimestamp: string;
Expand Down
10 changes: 9 additions & 1 deletion wormhole-connect/src/hooks/useFetchSupportedRoutes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,15 @@ const useFetchSupportedRoutes = (): HookReturn => {
return () => {
isActive = false;
};
}, [sourceToken, destToken, amount, fromChain, toChain, toNativeToken, receivingWallet]);
}, [
sourceToken,
destToken,
amount,
fromChain,
toChain,
toNativeToken,
receivingWallet,
]);

return {
supportedRoutes: routes,
Expand Down
289 changes: 165 additions & 124 deletions wormhole-connect/src/hooks/useTransactionHistoryWHScan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import { getGasToken } from 'utils';
import type { Chain, ChainId } from '@wormhole-foundation/sdk';
import type { Transaction } from 'config/types';
import { toFixedDecimals } from 'utils/balance';
import { useTokens } from 'contexts/TokensContext';
import { Token } from 'config/tokens';

interface WormholeScanTransaction {
id: string;
Expand Down Expand Up @@ -115,116 +117,135 @@ const useTransactionHistoryWHScan = (
const [error, setError] = useState('');
const [isFetching, setIsFetching] = useState(false);
const [hasMore, setHasMore] = useState(true);
const { getOrFetchToken } = useTokens();

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

// Common parsing logic for a single transaction from WHScan API.
// IMPORTANT: Anything specific to a route, please use that route's parser:
// parseTokenBridgeTx | parseNTTTx | parseCCTPTx | parsePorticoTx
const parseSingleTx = useCallback((tx: WormholeScanTransaction) => {
const { content, data, sourceChain, targetChain } = tx;
const { tokenAmount, usdAmount } = data || {};
const { standarizedProperties } = content || {};

const fromChainId = standarizedProperties.fromChain || sourceChain?.chainId;
const toChainId = standarizedProperties.toChain || targetChain?.chainId;
const tokenChainId = standarizedProperties.tokenChain;

const fromChain = chainIdToChain(fromChainId);

// Skip if we don't have the source chain
if (!fromChain) {
return;
}

const tokenChain = chainIdToChain(tokenChainId);

// Skip if we don't have the token chain
if (!tokenChain) {
return;
}

let token = config.tokens.get(
tokenChain,
standarizedProperties.tokenAddress,
);

if (!token) {
// IMPORTANT:
// If we don't have the token config from the token address,
// we can check if we can use the symbol to get it.
// So far this case is only for SUI and APT
const foundBySymbol =
data?.symbol && config.tokens.findBySymbol(tokenChain, data.symbol);
if (foundBySymbol) {
token = foundBySymbol;
const parseSingleTx = useCallback(
async (tx: WormholeScanTransaction) => {
const { content, data, sourceChain, targetChain } = tx;
const { standarizedProperties } = content || {};

const fromChainId =
standarizedProperties.fromChain || sourceChain?.chainId;
const toChainId = standarizedProperties.toChain || targetChain?.chainId;
const tokenChainId = standarizedProperties.tokenChain;

const fromChain = chainIdToChain(fromChainId);

// Skip if we don't have the source chain
if (!fromChain) {
return;
}

const tokenChain = tokenChainId
? chainIdToChain(tokenChainId)
: chainIdToChain(toChainId);

// Skip if we don't have the token chain
if (!tokenChain) {
return;
}

let token: Token | undefined;
try {
token = await getOrFetchToken(
Wormhole.tokenId(tokenChain, standarizedProperties.tokenAddress),
);
} catch (e) {
// This is ok
}
}

// If we've still failed to get the token, return early
if (!token) {
return;
}

const toChain = chainIdToChain(toChainId);

// data.tokenAmount holds the normalized token amount value.
// Otherwise we need to format standarizedProperties.amount using decimals
const sentAmountDisplay =
tokenAmount ??
sdkAmount.display(
{
amount: standarizedProperties.amount,
decimals: standarizedProperties.normalizedDecimals ?? DECIMALS,
},
0,
);

const receiveAmountValue =
BigInt(standarizedProperties.amount) - BigInt(standarizedProperties.fee);
// It's unlikely, but in case the above subtraction returns a non-positive number,
// we should not show that at all.
const receiveAmountDisplay =
receiveAmountValue > 0
? sdkAmount.display(
if (!token) {
// IMPORTANT:
// If we don't have the token config from the token address,
// we can check if we can use the symbol to get it.
// So far this case is only for SUI and APT
const foundBySymbol =
data?.symbol && config.tokens.findBySymbol(tokenChain, data.symbol);
if (foundBySymbol) {
token = foundBySymbol;
}
}

if (!token) {
console.warn("Can't find token", tx);
}

const toChain = chainIdToChain(toChainId);

let sentAmountDisplay: string | undefined = undefined;
let receiveAmountDisplay: string | undefined = undefined;
let usdAmount: number | undefined = undefined;

if (data && data.tokenAmount) {
sentAmountDisplay = data.tokenAmount;
} else if (standarizedProperties.amount) {
sentAmountDisplay = sdkAmount.display(
{
amount: standarizedProperties.amount,
decimals: standarizedProperties.normalizedDecimals ?? DECIMALS,
},
0,
);
}

if (standarizedProperties.amount && standarizedProperties.fee) {
const receiveAmountValue =
BigInt(standarizedProperties.amount) -
BigInt(standarizedProperties.fee);
// It's unlikely, but in case the above subtraction returns a non-positive number,
// we should not show that at all.
if (receiveAmountValue > 0) {
receiveAmountDisplay = sdkAmount.display(
{
amount: receiveAmountValue.toString(),
decimals: DECIMALS,
},
0,
)
: '';

const txHash = sourceChain.transaction?.txHash;

// Transaction is in-progress when the below are both true:
// 1- Source chain has confirmed
// 2- Target has either not received, or received but not completed
const inProgress =
sourceChain?.status?.toLowerCase() === 'confirmed' &&
targetChain?.status?.toLowerCase() !== 'completed';

const txData: Transaction = {
txHash,
sender: standarizedProperties.fromAddress || sourceChain.from,
recipient: standarizedProperties.toAddress,
amount: sentAmountDisplay,
amountUsd: usdAmount ? Number(usdAmount) : 0,
receiveAmount: receiveAmountDisplay,
fromChain,
fromToken: token,
toChain,
toToken: token,
senderTimestamp: sourceChain?.timestamp,
receiverTimestamp: targetChain?.timestamp,
explorerLink: `${WORMSCAN}tx/${txHash}${
config.isMainnet ? '' : '?network=TESTNET'
}`,
inProgress,
};
);
}
}

if (data && data.usdAmount) {
usdAmount = Number(data.usdAmount);
}

return txData;
}, []);
const txHash = sourceChain.transaction?.txHash;

// Transaction is in-progress when the below are both true:
// 1- Source chain has confirmed
// 2- Target has either not received, or received but not completed
const inProgress =
sourceChain?.status?.toLowerCase() === 'confirmed' &&
targetChain?.status?.toLowerCase() !== 'completed';

const txData: Transaction = {
txHash,
sender: standarizedProperties.fromAddress || sourceChain.from,
recipient: standarizedProperties.toAddress,
amount: sentAmountDisplay,
amountUsd: usdAmount,
receiveAmount: receiveAmountDisplay,
fromChain,
fromToken: token,
toChain,
toToken: token,
senderTimestamp: sourceChain?.timestamp,
receiverTimestamp: targetChain?.timestamp,
explorerLink: `${WORMSCAN}tx/${txHash}${
config.isMainnet ? '' : '?network=TESTNET'
}`,
inProgress,
};

return txData;
},
[getOrFetchToken],
);

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

// Parser for NTT transactions (appId === NATIVE_TOKEN_TRANSFER)
// IMPORTANT: This is where we can add any customizations specific to NTT data
// that we have retrieved from WHScan API
const parseGenericRelayer = useCallback(
(tx: WormholeScanTransaction) => {
return parseSingleTx(tx);
},
[parseSingleTx],
);

// Parser for NTT transactions (appId === NATIVE_TOKEN_TRANSFER)
// IMPORTANT: This is where we can add any customizations specific to NTT data
// that we have retrieved from WHScan API
Expand All @@ -260,8 +291,8 @@ const useTransactionHistoryWHScan = (
// IMPORTANT: This is where we can add any customizations specific to Portico data
// that we have retrieved from WHScan API
const parsePorticoTx = useCallback(
(tx: WormholeScanTransaction) => {
const txData = parseSingleTx(tx);
async (tx: WormholeScanTransaction) => {
const txData = await parseSingleTx(tx);
if (!txData) return;

const payload = tx.content.payload
Expand Down Expand Up @@ -331,47 +362,56 @@ const useTransactionHistoryWHScan = (
const PARSERS = useMemo(
() => ({
PORTAL_TOKEN_BRIDGE: parseTokenBridgeTx,
GENERIC_RELAYER: parseGenericRelayer,
NATIVE_TOKEN_TRANSFER: parseNTTTx,
CCTP_WORMHOLE_INTEGRATION: parseCCTPTx,
ETH_BRIDGE: parsePorticoTx,
USDT_BRIDGE: parsePorticoTx,
FAST_TRANSFERS: parseLLTx,
WORMHOLE_LIQUIDITY_LAYER: parseLLTx,
}),
[parseCCTPTx, parseNTTTx, parsePorticoTx, parseTokenBridgeTx, parseLLTx],
[
parseCCTPTx,
parseNTTTx,
parsePorticoTx,
parseTokenBridgeTx,
parseLLTx,
parseGenericRelayer,
],
);

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const parseTransactions = useCallback(
(allTxs: Array<WormholeScanTransaction>) => {
return allTxs
.map((tx) => {
// Locate the appIds
const appIds: Array<string> =
tx.content?.standarizedProperties?.appIds || [];

// TODO: SDKV2
// Some integrations may compose with multiple protocols and have multiple appIds
// Choose a more specific parser if available
if (appIds.includes('ETH_BRIDGE') || appIds.includes('USDT_BRIDGE')) {
return parsePorticoTx(tx);
}
async (allTxs: Array<WormholeScanTransaction>) => {
return (
await Promise.all(
allTxs.map(async (tx) => {
// Locate the appIds
const appIds: Array<string> =
tx.content?.standarizedProperties?.appIds || [];

// TODO: SDKV2
// Some integrations may compose with multiple protocols and have multiple appIds
// Choose a more specific parser if available
if (
appIds.includes('ETH_BRIDGE') ||
appIds.includes('USDT_BRIDGE')
) {
return parsePorticoTx(tx);
}

for (const appId of appIds) {
// Retrieve the parser for an appId
const parser = PARSERS[appId];
for (const appId of appIds) {
// Retrieve the parser for an appId
const parser = PARSERS[appId];

// If no parsers specified for the given appIds, we'll skip this transaction
if (parser) {
try {
// If no parsers specified for the given appIds, we'll skip this transaction
if (parser) {
return parser(tx);
} catch (e) {
console.error(`Error parsing transaction: ${e}`);
}
}
}
})
.filter((tx) => !!tx); // Filter out unsupported transactions
}),
)
).filter((tx) => !!tx); // Filter out unsupported transactions
},
[PARSERS, parsePorticoTx],
);
Expand Down Expand Up @@ -403,8 +443,9 @@ const useTransactionHistoryWHScan = (
if (!cancelled) {
const resData = resPayload?.operations;
if (resData) {
const parsedTxs = await parseTransactions(resData);

setTransactions((txs) => {
const parsedTxs = parseTransactions(resData);
if (txs && txs.length > 0) {
// We need to keep track of existing tx hashes to prevent duplicates in the final list
const existingTxs = new Set<string>();
Expand Down
Loading
Loading