From 0294a617f6c8d76850cea74bf68edc919187cd8a Mon Sep 17 00:00:00 2001 From: J M Rossy Date: Thu, 10 Aug 2023 11:42:17 -0400 Subject: [PATCH 01/58] Make name, symbol, decimals optional in config Check collateral contract balance before transfer Add option to hide disabled tokens in modal Tweak sidebar styles --- src/consts/config.ts | 2 + src/consts/tokens.ts | 60 ++++++------ src/features/tokens/TokenListModal.tsx | 8 +- .../tokens/adapters/EvmTokenAdapter.ts | 25 +++-- src/features/tokens/adapters/ITokenAdapter.ts | 2 +- .../tokens/adapters/SealevelTokenAdapter.ts | 12 +-- src/features/tokens/metadata.ts | 97 +++++++++++++++---- src/features/tokens/routes/hooks.ts | 53 ++-------- src/features/tokens/types.ts | 11 ++- src/features/transfer/useTokenTransfer.ts | 33 +++++-- src/features/wallet/SideBarMenu.tsx | 6 +- src/features/wallet/hooks.tsx | 5 +- 12 files changed, 181 insertions(+), 133 deletions(-) diff --git a/src/consts/config.ts b/src/consts/config.ts index a4ed2a76..d7d37d64 100644 --- a/src/consts/config.ts +++ b/src/consts/config.ts @@ -8,6 +8,7 @@ interface Config { version: string | null; // Matches version number in package.json explorerApiKeys: Record; // Optional map of API keys for block explorer showTipBox: boolean; // Show/Hide the blue tip box above the main form + showDisabledTokens: boolean; // Show/Hide invalid token options in the selection modal walletConnectProjectId: string; } @@ -16,5 +17,6 @@ export const config: Config = Object.freeze({ version, explorerApiKeys, showTipBox: true, + showDisabledTokens: true, walletConnectProjectId, }); diff --git a/src/consts/tokens.ts b/src/consts/tokens.ts index 75dc7804..cd2317a4 100644 --- a/src/consts/tokens.ts +++ b/src/consts/tokens.ts @@ -14,27 +14,27 @@ export const tokenList: WarpTokenConfig = [ }, // Example NFT (ERC721) token for an EVM chain - { - chainId: 5, - name: 'Test721', - symbol: 'TEST721', - decimals: 0, - type: 'collateral', - address: '0x77566D540d1E207dFf8DA205ed78750F9a1e7c55', - hypCollateralAddress: '0xDcbc0faAA269Cf649AC8950838664BB7B355BD6B', - isNft: true, - }, + // { + // chainId: 5, + // name: 'Test721', + // symbol: 'TEST721', + // decimals: 0, + // type: 'collateral', + // address: '0x77566D540d1E207dFf8DA205ed78750F9a1e7c55', + // hypCollateralAddress: '0xDcbc0faAA269Cf649AC8950838664BB7B355BD6B', + // isNft: true, + // }, // Example native token for an EVM chain - { - type: 'native', - chainId: 11155111, - name: 'Ether', - symbol: 'ETH', - decimals: 18, - hypNativeAddress: '0xEa44A29da87B5464774978e6A4F4072A4c048949', - logoURI: '/logos/weth.png', - }, + // { + // type: 'native', + // chainId: 11155111, + // name: 'Ether', + // symbol: 'ETH', + // decimals: 18, + // hypNativeAddress: '0xEa44A29da87B5464774978e6A4F4072A4c048949', + // logoURI: '/logos/weth.png', + // }, // Example native token for a Sealevel (Solana) chain { @@ -49,15 +49,15 @@ export const tokenList: WarpTokenConfig = [ }, // Example collateral token for a Sealevel (Solana) chain - { - type: 'collateral', - protocol: 'sealevel', - chainId: 1399811151, - address: 'Gh9ZwEmdLJ8DscKNTkTqPbNwLNNBjuSzaG9Vp2KGtKJr', - hypCollateralAddress: 'Hsb2PdnUvd7VvZJ1svS8TrVLfsRDdDTWoHK5r2RwGZBS', - name: 'dUSDC', - symbol: 'dUSDC', - decimals: 6, - isSpl2022: false, - }, + // { + // type: 'collateral', + // protocol: 'sealevel', + // chainId: 1399811151, + // address: 'Gh9ZwEmdLJ8DscKNTkTqPbNwLNNBjuSzaG9Vp2KGtKJr', + // hypCollateralAddress: 'Hsb2PdnUvd7VvZJ1svS8TrVLfsRDdDTWoHK5r2RwGZBS', + // name: 'dUSDC', + // symbol: 'dUSDC', + // decimals: 6, + // isSpl2022: false, + // }, ]; diff --git a/src/features/tokens/TokenListModal.tsx b/src/features/tokens/TokenListModal.tsx index 923cd67a..d21c07c2 100644 --- a/src/features/tokens/TokenListModal.tsx +++ b/src/features/tokens/TokenListModal.tsx @@ -4,11 +4,12 @@ import { useMemo, useState } from 'react'; import { TokenIcon } from '../../components/icons/TokenIcon'; import { TextInput } from '../../components/input/TextField'; import { Modal } from '../../components/layout/Modal'; +import { config } from '../../consts/config'; import InfoIcon from '../../images/icons/info-circle.svg'; import { getAssetNamespace, getTokenAddress, isNativeToken } from '../caip/tokens'; import { getChainDisplayName } from '../chains/utils'; -import { getAllTokens } from './metadata'; +import { getTokens } from './metadata'; import { RoutesMap } from './routes/types'; import { hasTokenRoute } from './routes/utils'; import { TokenMetadata } from './types'; @@ -81,7 +82,7 @@ export function TokenList({ }) { const tokens = useMemo(() => { const q = searchQuery?.trim().toLowerCase(); - return getAllTokens() + return getTokens() .map((t) => { const hasRoute = hasTokenRoute(originCaip2Id, destinationCaip2Id, t.caip19Id, tokenRoutes); return { ...t, disabled: !hasRoute }; @@ -98,7 +99,8 @@ export function TokenList({ t.symbol.toLowerCase().includes(q) || t.caip19Id.toLowerCase().includes(q) ); - }); + }) + .filter((t) => (config.showDisabledTokens ? true : !t.disabled)); }, [searchQuery, originCaip2Id, destinationCaip2Id, tokenRoutes]); return ( diff --git a/src/features/tokens/adapters/EvmTokenAdapter.ts b/src/features/tokens/adapters/EvmTokenAdapter.ts index 25e39bfb..9b9cd46e 100644 --- a/src/features/tokens/adapters/EvmTokenAdapter.ts +++ b/src/features/tokens/adapters/EvmTokenAdapter.ts @@ -46,10 +46,10 @@ export class EvmNativeTokenAdapter implements ITokenAdapter { } async populateTransferTx({ - amountOrId, + weiAmountOrId, recipient, }: TransferParams): Promise { - const value = BigNumber.from(amountOrId); + const value = BigNumber.from(weiAmountOrId); return { value, to: recipient }; } @@ -96,17 +96,17 @@ export class EvmTokenAdapter } override populateApproveTx({ - amountOrId, + weiAmountOrId, recipient, }: TransferParams): Promise { - return this.contract.populateTransaction.approve(recipient, amountOrId); + return this.contract.populateTransaction.approve(recipient, weiAmountOrId); } override populateTransferTx({ - amountOrId, + weiAmountOrId, recipient, }: TransferParams): Promise { - return this.contract.populateTransaction.transfer(recipient, amountOrId); + return this.contract.populateTransaction.transfer(recipient, weiAmountOrId); } } @@ -144,15 +144,20 @@ export class EvmHypSyntheticAdapter } populateTransferRemoteTx({ - amountOrId, + weiAmountOrId, destination, recipient, txValue, }: TransferRemoteParams): Promise { const recipBytes32 = utils.addressToBytes32(addressToByteHexString(recipient)); - return this.contract.populateTransaction.transferRemote(destination, recipBytes32, amountOrId, { - value: txValue, - }); + return this.contract.populateTransaction.transferRemote( + destination, + recipBytes32, + weiAmountOrId, + { + value: txValue, + }, + ); } } diff --git a/src/features/tokens/adapters/ITokenAdapter.ts b/src/features/tokens/adapters/ITokenAdapter.ts index 2a61a444..4df0e6b1 100644 --- a/src/features/tokens/adapters/ITokenAdapter.ts +++ b/src/features/tokens/adapters/ITokenAdapter.ts @@ -1,7 +1,7 @@ import { MinimalTokenMetadata } from '../types'; export interface TransferParams { - amountOrId: string | number; + weiAmountOrId: string | number; recipient: Address; // Solana-specific params diff --git a/src/features/tokens/adapters/SealevelTokenAdapter.ts b/src/features/tokens/adapters/SealevelTokenAdapter.ts index c52c5501..c3ed2ddf 100644 --- a/src/features/tokens/adapters/SealevelTokenAdapter.ts +++ b/src/features/tokens/adapters/SealevelTokenAdapter.ts @@ -54,13 +54,13 @@ export class SealevelNativeTokenAdapter implements ITokenAdapter { throw new Error('Approve not required for native tokens'); } - populateTransferTx({ amountOrId, recipient, fromAccountOwner }: TransferParams): Transaction { + populateTransferTx({ weiAmountOrId, recipient, fromAccountOwner }: TransferParams): Transaction { const fromPubkey = resolveAddress(fromAccountOwner, this.signerAddress); return new Transaction().add( SystemProgram.transfer({ fromPubkey, toPubkey: new PublicKey(recipient), - lamports: new BigNumber(amountOrId).toNumber(), + lamports: new BigNumber(weiAmountOrId).toNumber(), }), ); } @@ -95,7 +95,7 @@ export class SealevelTokenAdapter implements ITokenAdapter { } populateTransferTx({ - amountOrId, + weiAmountOrId, recipient, fromAccountOwner, fromTokenAccount, @@ -107,7 +107,7 @@ export class SealevelTokenAdapter implements ITokenAdapter { new PublicKey(fromTokenAccount), new PublicKey(recipient), fromWalletPubKey, - new BigNumber(amountOrId).toNumber(), + new BigNumber(weiAmountOrId).toNumber(), ), ); } @@ -181,7 +181,7 @@ export abstract class SealevelHypTokenAdapter } async populateTransferRemoteTx({ - amountOrId, + weiAmountOrId, destination, recipient, fromAccountOwner, @@ -202,7 +202,7 @@ export abstract class SealevelHypTokenAdapter data: new TransferRemoteInstruction({ destination_domain: destination, recipient: addressToBytes(recipient), - amount_or_id: new BigNumber(amountOrId).toNumber(), + amount_or_id: new BigNumber(weiAmountOrId).toNumber(), }), }); const serializedData = serialize(TransferRemoteSchema, value); diff --git a/src/features/tokens/metadata.ts b/src/features/tokens/metadata.ts index 9cd27057..87f8c11f 100644 --- a/src/features/tokens/metadata.ts +++ b/src/features/tokens/metadata.ts @@ -5,23 +5,38 @@ import { tokenList } from '../../consts/tokens'; import { logger } from '../../utils/logger'; import { getCaip2Id } from '../caip/chains'; import { getCaip19Id, getNativeTokenAddress, resolveAssetNamespace } from '../caip/tokens'; +import { getMultiProvider } from '../multiProvider'; -import { TokenMetadata, WarpTokenConfig, WarpTokenConfigSchema } from './types'; +import { EvmTokenAdapter } from './adapters/EvmTokenAdapter'; +import { ITokenAdapter } from './adapters/ITokenAdapter'; +import { getHypErc20CollateralContract } from './contracts/evmContracts'; +import { + MinimalTokenMetadata, + TokenMetadata, + WarpTokenConfig, + WarpTokenConfigSchema, +} from './types'; let tokens: TokenMetadata[]; -export function getAllTokens() { - if (!tokens) { - tokens = parseTokenConfigs(tokenList); - } - return tokens; +export function getTokens() { + return tokens || []; } export function getToken(caip19Id: Caip19Id) { - return getAllTokens().find((t) => t.caip19Id === caip19Id); + return getTokens().find((t) => t.caip19Id === caip19Id); +} + +export async function parseTokens() { + if (!tokens) { + tokens = await parseTokenConfigs(tokenList); + } + return tokens; } -function parseTokenConfigs(configList: WarpTokenConfig): TokenMetadata[] { +// Converts the more user-friendly config format into a validated, extended format +// that's easier for the UI to work with +async function parseTokenConfigs(configList: WarpTokenConfig): Promise { const result = WarpTokenConfigSchema.safeParse(configList); if (!result.success) { logger.error('Invalid token config', result.error); @@ -30,19 +45,29 @@ function parseTokenConfigs(configList: WarpTokenConfig): TokenMetadata[] { const parsedConfig = result.data; const tokenMetadata: TokenMetadata[] = []; - for (const token of parsedConfig) { - const { type, chainId, name, symbol, decimals, logoURI } = token; - const protocol = token.protocol || ProtocolType.Ethereum; + for (const config of parsedConfig) { + const { type, chainId, logoURI } = config; + + const protocol = config.protocol || ProtocolType.Ethereum; const caip2Id = getCaip2Id(protocol, chainId); - const isNft = type === TokenType.collateral && token.isNft; - const isSpl2022 = type === TokenType.collateral && token.isSpl2022; - const address = type === TokenType.collateral ? token.address : getNativeTokenAddress(protocol); + const isNative = type == TokenType.native; + const isNft = type === TokenType.collateral && config.isNft; + const isSpl2022 = type === TokenType.collateral && config.isSpl2022; + const address = + type === TokenType.collateral ? config.address : getNativeTokenAddress(protocol); const routerAddress = - type === TokenType.collateral ? token.hypCollateralAddress : token.hypNativeAddress; - const namespace = resolveAssetNamespace(protocol, type == TokenType.native, isNft, isSpl2022); + type === TokenType.collateral ? config.hypCollateralAddress : config.hypNativeAddress; + const namespace = resolveAssetNamespace(protocol, isNative, isNft, isSpl2022); const caip19Id = getCaip19Id(caip2Id, namespace, address); + + const { name, symbol, decimals } = await fetchNameAndDecimals( + config, + protocol, + routerAddress, + isNft, + ); + tokenMetadata.push({ - chainId, name, symbol, decimals, @@ -54,3 +79,41 @@ function parseTokenConfigs(configList: WarpTokenConfig): TokenMetadata[] { } return tokenMetadata; } + +async function fetchNameAndDecimals( + tokenConfig: WarpTokenConfig[number], + protocol: ProtocolType, + routerAddress: Address, + isNft?: boolean, +): Promise { + const { type, chainId, name, symbol, decimals } = tokenConfig; + if (name && symbol && decimals) { + // Already provided in the config + return { name, symbol, decimals }; + } + + const multiProvider = getMultiProvider(); + if (type === TokenType.native) { + // Use the native token config that may be in the chain metadata + const metadata = multiProvider.getChainMetadata(chainId).nativeToken; + if (!metadata) throw new Error('Name, symbol, or decimals is missing for native token'); + return metadata; + } + + if (type === TokenType.collateral) { + // Fetch the data from the contract + let tokenAdapter: ITokenAdapter; + if (protocol === ProtocolType.Ethereum) { + const provider = multiProvider.getProvider(chainId); + const collateralContract = getHypErc20CollateralContract(routerAddress, provider); + const wrappedTokenAddr = await collateralContract.wrappedToken(); + tokenAdapter = new EvmTokenAdapter(provider, wrappedTokenAddr); + } else { + // TODO solana support when hyp tokens have metadata + throw new Error('Name, symbol, and decimals is required for non-EVM token configs'); + } + return tokenAdapter.getMetadata(isNft); + } + + throw new Error(`Unsupported token type ${type}`); +} diff --git a/src/features/tokens/routes/hooks.ts b/src/features/tokens/routes/hooks.ts index 8d08f744..a5da27f3 100644 --- a/src/features/tokens/routes/hooks.ts +++ b/src/features/tokens/routes/hooks.ts @@ -1,11 +1,10 @@ import { useQuery } from '@tanstack/react-query'; import { useMemo } from 'react'; -import { TokenType } from '@hyperlane-xyz/hyperlane-token'; import { ProtocolType } from '@hyperlane-xyz/sdk'; import { logger } from '../../../utils/logger'; -import { getCaip2Id, getProtocolType } from '../../caip/chains'; +import { getCaip2Id } from '../../caip/chains'; import { getCaip2FromToken, getCaip19Id, @@ -13,12 +12,9 @@ import { parseCaip19Id, resolveAssetNamespace, } from '../../caip/tokens'; -import { getMultiProvider, getProvider } from '../../multiProvider'; +import { getMultiProvider } from '../../multiProvider'; import { AdapterFactory } from '../adapters/AdapterFactory'; -import { EvmTokenAdapter } from '../adapters/EvmTokenAdapter'; -import { ITokenAdapter } from '../adapters/ITokenAdapter'; -import { getHypErc20CollateralContract } from '../contracts/evmContracts'; -import { getAllTokens } from '../metadata'; +import { getTokens, parseTokens } from '../metadata'; import { TokenMetadata, TokenMetadataWithHypTokens } from '../types'; import { RouteType, RoutesMap } from './types'; @@ -32,10 +28,10 @@ export function useTokenRoutes() { ['token-routes'], async () => { logger.info('Searching for token routes'); + const parsedTokens = await parseTokens(); const tokens: TokenMetadataWithHypTokens[] = []; - for (const token of getAllTokens()) { + for (const token of parsedTokens) { // Consider parallelizing here but concerned about RPC rate limits - await validateTokenMetadata(token); const tokenWithHypTokens = await fetchRemoteHypTokens(token); tokens.push(tokenWithHypTokens); } @@ -47,43 +43,6 @@ export function useTokenRoutes() { return { isLoading, error, tokenRoutes }; } -async function validateTokenMetadata(token: TokenMetadata) { - const { type, caip19Id, symbol, decimals, routerAddress } = token; - const caip2Id = getCaip2FromToken(caip19Id); - const isNft = isNonFungibleToken(caip19Id); - - // Native tokens cannot be queried for metadata - if (type !== TokenType.collateral) return; - - logger.info(`Validating token ${symbol} on ${caip2Id}`); - - const protocol = getProtocolType(caip2Id); - let tokenAdapter: ITokenAdapter; - if (protocol === ProtocolType.Ethereum) { - const provider = getProvider(caip2Id); - const collateralContract = getHypErc20CollateralContract(routerAddress, provider); - const wrappedTokenAddr = await collateralContract.wrappedToken(); - tokenAdapter = new EvmTokenAdapter(provider, wrappedTokenAddr); - } else if (protocol === ProtocolType.Sealevel) { - // TODO solana support when hyp tokens have metadata - return; - } - - const { decimals: decimalsOnChain, symbol: symbolOnChain } = await tokenAdapter!.getMetadata( - isNft, - ); - if (decimals !== decimalsOnChain) { - throw new Error( - `Token config decimals ${decimals} does not match contract decimals ${decimalsOnChain}`, - ); - } - if (symbol !== symbolOnChain) { - throw new Error( - `Token config symbol ${symbol} does not match contract decimals ${symbolOnChain}`, - ); - } -} - async function fetchRemoteHypTokens( originToken: TokenMetadata, ): Promise { @@ -186,7 +145,7 @@ function getChainsFromTokens(tokens: TokenMetadataWithHypTokens[]): Caip2Id[] { export function useRouteChains(tokenRoutes: RoutesMap): Caip2Id[] { return useMemo(() => { const allCaip2Ids = Object.keys(tokenRoutes) as Caip2Id[]; - const collateralCaip2Ids = getAllTokens().map((t) => getCaip2FromToken(t.caip19Id)); + const collateralCaip2Ids = getTokens().map((t) => getCaip2FromToken(t.caip19Id)); return allCaip2Ids.sort((c1, c2) => { // Surface collateral chains first if (collateralCaip2Ids.includes(c1) && !collateralCaip2Ids.includes(c2)) return -1; diff --git a/src/features/tokens/types.ts b/src/features/tokens/types.ts index 53040a31..c4a08284 100644 --- a/src/features/tokens/types.ts +++ b/src/features/tokens/types.ts @@ -6,11 +6,11 @@ import { ProtocolType } from '@hyperlane-xyz/sdk'; export type MinimalTokenMetadata = Omit; const commonTokenFields = z.object({ - chainId: z.number().positive().or(z.string().nonempty()), + chainId: z.number().positive().or(z.string()), protocol: z.nativeEnum(ProtocolType).optional(), - name: z.string().nonempty(), - symbol: z.string().nonempty(), - decimals: z.number().nonnegative(), // decimals == 0 for NFTs + name: z.string().optional(), + symbol: z.string().optional(), + decimals: z.number().nonnegative().optional(), // decimals == 0 for NFTs logoURI: z.string().optional(), }); type CommonTokenFields = z.infer; @@ -68,10 +68,11 @@ export type WarpTokenConfig = Array; * * See src/features/tokens/metadata.ts */ -interface BaseTokenMetadata extends CommonTokenFields { +interface BaseTokenMetadata extends MinimalTokenMetadata { type: TokenType; caip19Id: Caip19Id; routerAddress: Address; // Shared name for hypCollateralAddr or hypNativeAddr + logoURI?: string; } interface CollateralTokenMetadata extends BaseTokenMetadata { diff --git a/src/features/transfer/useTokenTransfer.ts b/src/features/transfer/useTokenTransfer.ts index 19b3b7e6..94cca4ba 100644 --- a/src/features/transfer/useTokenTransfer.ts +++ b/src/features/transfer/useTokenTransfer.ts @@ -115,7 +115,7 @@ async function executeTransfer({ if (!tokenRoute) throw new Error('No token route found between chains'); const isNft = isNonFungibleToken(tokenCaip19Id); - const amountOrId = isNft ? amount : toWei(amount, tokenRoute.decimals).toString(); + const weiAmountOrId = isNft ? amount : toWei(amount, tokenRoute.decimals).toString(); const activeAccountAddress = activeAccounts.accounts[originProtocol]?.address || ''; addTransfer({ @@ -126,10 +126,12 @@ async function executeTransfer({ params: values, }); + await ensureSufficientCollateral(tokenRoute, weiAmountOrId, isNft); + const hypTokenAdapter = AdapterFactory.HypTokenAdapterFromRouteOrigin(tokenRoute); const triggerParams: ExecuteTransferParams = { - amountOrId, + weiAmountOrId, destinationDomainId, recipientAddress, tokenRoute, @@ -177,8 +179,21 @@ async function executeTransfer({ if (onDone) onDone(); } +// In certain cases, like when a synthetic token has >1 collateral tokens +// it's possible that the collateral contract balance is insufficient to +// cover the remote transfer. This ensures the balance is sufficient or throws. +async function ensureSufficientCollateral(route: Route, weiAmount: string, isNft?: boolean) { + if (route.type !== RouteType.SyntheticToBase || isNft) return; + const adapter = AdapterFactory.TokenAdapterFromAddress(route.baseCaip19Id); + logger.debug('Checking collateral balance for token', route.baseCaip19Id); + const balance = await adapter.getBalance(route.baseRouterAddress); + if (BigNumber.from(balance).lt(weiAmount)) { + throw new Error('Collateral contract has insufficient balance'); + } +} + interface ExecuteTransferParams { - amountOrId: string; + weiAmountOrId: string; destinationDomainId: DomainId; recipientAddress: Address; tokenRoute: Route; @@ -191,7 +206,7 @@ interface ExecuteTransferParams { } async function executeEvmTransfer({ - amountOrId, + weiAmountOrId, destinationDomainId, recipientAddress, tokenRoute, @@ -206,7 +221,7 @@ async function executeEvmTransfer({ updateStatus(TransferStatus.CreatingApprove); const tokenAdapter = AdapterFactory.TokenAdapterFromAddress(baseCaip19Id); const approveTxRequest = (await tokenAdapter.populateApproveTx({ - amountOrId, + weiAmountOrId, recipient: baseRouterAddress, })) as EvmTransaction; @@ -230,10 +245,10 @@ async function executeEvmTransfer({ // If sending native tokens (e.g. Eth), the gasPayment must be added to the tx value and sent together const txValue = routeType === RouteType.BaseToSynthetic && isNativeToken(baseCaip19Id) - ? BigNumber.from(gasPayment).add(amountOrId) + ? BigNumber.from(gasPayment).add(weiAmountOrId) : gasPayment; const transferTxRequest = (await hypTokenAdapter.populateTransferRemoteTx({ - amountOrId, + weiAmountOrId, recipient: recipientAddress, destination: destinationDomainId, txValue: txValue.toString(), @@ -254,7 +269,7 @@ async function executeEvmTransfer({ } async function executeSealevelTransfer({ - amountOrId, + weiAmountOrId, destinationDomainId, recipientAddress, tokenRoute, @@ -274,7 +289,7 @@ async function executeSealevelTransfer({ // logger.debug('Quoted gas payment', gasPayment); const transferTxRequest = (await hypTokenAdapter.populateTransferRemoteTx({ - amountOrId, + weiAmountOrId, destination: destinationDomainId, recipient: recipientAddress, fromAccountOwner: activeAccount.address, diff --git a/src/features/wallet/SideBarMenu.tsx b/src/features/wallet/SideBarMenu.tsx index f3f7d505..80025ee8 100644 --- a/src/features/wallet/SideBarMenu.tsx +++ b/src/features/wallet/SideBarMenu.tsx @@ -111,10 +111,10 @@ export function SideBarMenu({ )}
-
+
Connected Wallets
-
+
{readyAccounts.map((a) => (
-
+
Transfer History
diff --git a/src/features/wallet/hooks.tsx b/src/features/wallet/hooks.tsx index e4ad5c2e..5fb3fdab 100644 --- a/src/features/wallet/hooks.tsx +++ b/src/features/wallet/hooks.tsx @@ -242,12 +242,12 @@ export function useTransactionFns(): Record< }) => { if (activeCap2Id && activeCap2Id !== caip2Id) await onSwitchEvmNetwork(caip2Id); const chainId = getEthereumChainId(caip2Id); - const result = await sendEvmTransaction({ + logger.debug(`Sending tx on chain ${caip2Id}`); + const { hash, wait } = await sendEvmTransaction({ chainId, request: tx as providers.TransactionRequest, mode: 'recklesslyUnprepared', }); - const { hash, wait } = result; return { hash, confirm: () => wait(1) }; }, [onSwitchEvmNetwork], @@ -278,6 +278,7 @@ export function useTransactionFns(): Record< value: { blockhash, lastValidBlockHeight }, } = await connection.getLatestBlockhashAndContext(); + logger.debug(`Sending tx on chain ${caip2Id}`); const signature = await sendSolTransaction(tx, connection, { minContextSlot }); const confirm = () => From eaf37930f144cc81fe743a3936031babe91cc847 Mon Sep 17 00:00:00 2001 From: J M Rossy Date: Thu, 10 Aug 2023 11:44:35 -0400 Subject: [PATCH 02/58] Revert token config changes --- src/consts/tokens.ts | 60 ++++++++++++++++++++++---------------------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/src/consts/tokens.ts b/src/consts/tokens.ts index cd2317a4..75dc7804 100644 --- a/src/consts/tokens.ts +++ b/src/consts/tokens.ts @@ -14,27 +14,27 @@ export const tokenList: WarpTokenConfig = [ }, // Example NFT (ERC721) token for an EVM chain - // { - // chainId: 5, - // name: 'Test721', - // symbol: 'TEST721', - // decimals: 0, - // type: 'collateral', - // address: '0x77566D540d1E207dFf8DA205ed78750F9a1e7c55', - // hypCollateralAddress: '0xDcbc0faAA269Cf649AC8950838664BB7B355BD6B', - // isNft: true, - // }, + { + chainId: 5, + name: 'Test721', + symbol: 'TEST721', + decimals: 0, + type: 'collateral', + address: '0x77566D540d1E207dFf8DA205ed78750F9a1e7c55', + hypCollateralAddress: '0xDcbc0faAA269Cf649AC8950838664BB7B355BD6B', + isNft: true, + }, // Example native token for an EVM chain - // { - // type: 'native', - // chainId: 11155111, - // name: 'Ether', - // symbol: 'ETH', - // decimals: 18, - // hypNativeAddress: '0xEa44A29da87B5464774978e6A4F4072A4c048949', - // logoURI: '/logos/weth.png', - // }, + { + type: 'native', + chainId: 11155111, + name: 'Ether', + symbol: 'ETH', + decimals: 18, + hypNativeAddress: '0xEa44A29da87B5464774978e6A4F4072A4c048949', + logoURI: '/logos/weth.png', + }, // Example native token for a Sealevel (Solana) chain { @@ -49,15 +49,15 @@ export const tokenList: WarpTokenConfig = [ }, // Example collateral token for a Sealevel (Solana) chain - // { - // type: 'collateral', - // protocol: 'sealevel', - // chainId: 1399811151, - // address: 'Gh9ZwEmdLJ8DscKNTkTqPbNwLNNBjuSzaG9Vp2KGtKJr', - // hypCollateralAddress: 'Hsb2PdnUvd7VvZJ1svS8TrVLfsRDdDTWoHK5r2RwGZBS', - // name: 'dUSDC', - // symbol: 'dUSDC', - // decimals: 6, - // isSpl2022: false, - // }, + { + type: 'collateral', + protocol: 'sealevel', + chainId: 1399811151, + address: 'Gh9ZwEmdLJ8DscKNTkTqPbNwLNNBjuSzaG9Vp2KGtKJr', + hypCollateralAddress: 'Hsb2PdnUvd7VvZJ1svS8TrVLfsRDdDTWoHK5r2RwGZBS', + name: 'dUSDC', + symbol: 'dUSDC', + decimals: 6, + isSpl2022: false, + }, ]; From efe3bec63562a33c92e5013315f694f8e15b3b71 Mon Sep 17 00:00:00 2001 From: J M Rossy Date: Sun, 13 Aug 2023 09:42:12 -0400 Subject: [PATCH 03/58] Use chain metadata for token protocol --- src/features/tokens/metadata.ts | 3 ++- src/features/tokens/types.ts | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/features/tokens/metadata.ts b/src/features/tokens/metadata.ts index 87f8c11f..4cc772be 100644 --- a/src/features/tokens/metadata.ts +++ b/src/features/tokens/metadata.ts @@ -42,13 +42,14 @@ async function parseTokenConfigs(configList: WarpTokenConfig): Promise; const commonTokenFields = z.object({ chainId: z.number().positive().or(z.string()), - protocol: z.nativeEnum(ProtocolType).optional(), name: z.string().optional(), symbol: z.string().optional(), decimals: z.number().nonnegative().optional(), // decimals == 0 for NFTs From 940a21103dcf8324e4f6645cd5819033748250a4 Mon Sep 17 00:00:00 2001 From: J M Rossy Date: Sun, 13 Aug 2023 10:44:10 -0400 Subject: [PATCH 04/58] Update tokendata schema Trim decimals for large numbers --- .../tokens/contracts/sealevelSerialization.ts | 26 +++++++++++++++++++ src/utils/amount.ts | 3 ++- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/src/features/tokens/contracts/sealevelSerialization.ts b/src/features/tokens/contracts/sealevelSerialization.ts index 6f672b23..af376c40 100644 --- a/src/features/tokens/contracts/sealevelSerialization.ts +++ b/src/features/tokens/contracts/sealevelSerialization.ts @@ -34,6 +34,14 @@ export class HyperlaneTokenData { /// The interchain security module. interchain_security_module?: Uint8Array; interchain_security_module_pubkey?: PublicKey; + // The interchain gas paymaster + interchain_gas_paymaster?: { + address: Uint8Array; + type: number; + }; + interchain_gas_paymaster_pubkey?: PublicKey; + // Gas amounts by destination + destination_gas?: Map; /// Remote routers. remote_routers?: Map; remote_router_pubkeys: Map; @@ -45,6 +53,9 @@ export class HyperlaneTokenData { this.interchain_security_module_pubkey = this.interchain_security_module ? new PublicKey(this.interchain_security_module) : undefined; + this.interchain_gas_paymaster_pubkey = this.interchain_gas_paymaster?.address + ? new PublicKey(this.interchain_gas_paymaster.address) + : undefined; this.remote_router_pubkeys = new Map(); if (this.remote_routers) { for (const [k, v] of this.remote_routers.entries()) { @@ -78,6 +89,21 @@ export const HyperlaneTokenDataSchema = new Map([ ['remote_decimals', 'u8'], ['owner', { kind: 'option', type: [32] }], ['interchain_security_module', { kind: 'option', type: [32] }], + [ + 'interchain_gas_paymaster', + { + kind: 'option', + type: { + kind: 'struct', + fields: [ + ['address', [32]], + ['type', 'u8'], + ], + }, + }, + ], + // ['interchain_gas_paymaster_type', { kind: 'option', type: 'u8' }], + ['destination_gas', { kind: 'map', key: 'u32', value: 'u64' }], ['remote_routers', { kind: 'map', key: 'u32', value: [32] }], ], }, diff --git a/src/utils/amount.ts b/src/utils/amount.ts index b0ecff69..38320057 100644 --- a/src/utils/amount.ts +++ b/src/utils/amount.ts @@ -35,7 +35,8 @@ export function fromWeiRounded( else return MIN_ROUNDED_VALUE.toString(); } - return amount.toFixed(DISPLAY_DECIMALS).toString(); + const displayDecimals = amount.gte(10000) ? 0 : DISPLAY_DECIMALS; + return amount.toFixed(displayDecimals).toString(); } export function toWei( From fc1596b9b0f3837c29e74eb9d2190bb16da1a751 Mon Sep 17 00:00:00 2001 From: J M Rossy Date: Sun, 13 Aug 2023 10:45:02 -0400 Subject: [PATCH 05/58] Setup Eclipse tokens and chains --- src/consts/chains.ts | 35 +++++++++++++++++---------- src/consts/tokens.ts | 56 +++++++------------------------------------- 2 files changed, 31 insertions(+), 60 deletions(-) diff --git a/src/consts/chains.ts b/src/consts/chains.ts index 9f5c9b6b..b6140afd 100644 --- a/src/consts/chains.ts +++ b/src/consts/chains.ts @@ -1,10 +1,5 @@ -import { ChainMap, ChainMetadataWithArtifacts } from '@hyperlane-xyz/sdk'; -import { - solana, - solanadevnet, - solanatestnet, - zbctestnet, -} from '@hyperlane-xyz/sdk/dist/consts/chainMetadata'; +import { ChainMap, ChainMetadataWithArtifacts, ProtocolType } from '@hyperlane-xyz/sdk'; +import { solana, solanadevnet, solanatestnet } from '@hyperlane-xyz/sdk/dist/consts/chainMetadata'; // A map of chain names to ChainMetadata export const chains: ChainMap = { @@ -53,11 +48,25 @@ export const chains: ChainMap = { interchainGasPaymaster: '', validatorAnnounce: '', }, - zbctestnet: { - ...zbctestnet, - mailbox: '4hW22NXtJ2AXrEVbeAmxjhvxWPSNvfTfAphKXdRBZUco', - interchainGasPaymaster: '', - validatorAnnounce: '', - logoURI: '/logos/zebec.png', + proteustestnet: { + chainId: 88002, + domainId: 88002, + name: 'proteustestnet', + protocol: ProtocolType.Ethereum, + displayName: 'Proteus Testnet', + displayNameShort: 'Proteus', + nativeToken: { + name: 'Zebec', + symbol: 'ZBC', + decimals: 18, + }, + rpcUrls: [ + { + http: 'https://api.proteus.nautchain.xyz/solana', + }, + ], + mailbox: '0x918D3924Fad8F71551D9081172e9Bb169745461e', + interchainGasPaymaster: '0x06b62A9F5AEcc1E601D0E02732b4E1D0705DE7Db', + validatorAnnounce: '0xEEea93d0d0287c71e47B3f62AFB0a92b9E8429a1', }, }; diff --git a/src/consts/tokens.ts b/src/consts/tokens.ts index 75dc7804..43606628 100644 --- a/src/consts/tokens.ts +++ b/src/consts/tokens.ts @@ -1,63 +1,25 @@ import { WarpTokenConfig } from '../features/tokens/types'; export const tokenList: WarpTokenConfig = [ - // Example collateral token for an EVM chain + // bsctestnet { type: 'collateral', - chainId: 5, - address: '0xb4fbf271143f4fbf7b91a5ded31805e42b2208d6', - hypCollateralAddress: '0x145de8760021c4ac6676376691b78038d3DE9097', - name: 'Weth', - symbol: 'WETH', - decimals: 18, - logoURI: '/logos/weth.png', // See public/logos/ + chainId: 97, + address: '0x64544969ed7ebf5f083679233325356ebe738930', + hypCollateralAddress: '0x31b5234A896FbC4b3e2F7237592D054716762131', }, - // Example NFT (ERC721) token for an EVM chain - { - chainId: 5, - name: 'Test721', - symbol: 'TEST721', - decimals: 0, - type: 'collateral', - address: '0x77566D540d1E207dFf8DA205ed78750F9a1e7c55', - hypCollateralAddress: '0xDcbc0faAA269Cf649AC8950838664BB7B355BD6B', - isNft: true, - }, - - // Example native token for an EVM chain + // proteustestnet { type: 'native', - chainId: 11155111, - name: 'Ether', - symbol: 'ETH', - decimals: 18, - hypNativeAddress: '0xEa44A29da87B5464774978e6A4F4072A4c048949', - logoURI: '/logos/weth.png', + chainId: 88002, + hypNativeAddress: '0x34A9af13c5555BAD0783C220911b9ef59CfDBCEf', }, - // Example native token for a Sealevel (Solana) chain + // solanadevnet { type: 'native', - protocol: 'sealevel', - chainId: 1399811151, - hypNativeAddress: '3s6afZYk3EmjsZQ33N9yPTdSk4cY5CKeQ5wtoBcWjFUn', - name: 'Sol', - symbol: 'SOL', - decimals: 9, - logoURI: '/logos/solana.svg', - }, - - // Example collateral token for a Sealevel (Solana) chain - { - type: 'collateral', - protocol: 'sealevel', chainId: 1399811151, - address: 'Gh9ZwEmdLJ8DscKNTkTqPbNwLNNBjuSzaG9Vp2KGtKJr', - hypCollateralAddress: 'Hsb2PdnUvd7VvZJ1svS8TrVLfsRDdDTWoHK5r2RwGZBS', - name: 'dUSDC', - symbol: 'dUSDC', - decimals: 6, - isSpl2022: false, + hypNativeAddress: 'PJH5QAbxAqrrnSXfH3GHR8icua8CDFZmo97z91xmpvx', }, ]; From 898bcd6292f5b888dfbd16ebc386f094f435c2f0 Mon Sep 17 00:00:00 2001 From: J M Rossy Date: Sun, 13 Aug 2023 11:47:24 -0400 Subject: [PATCH 06/58] Fix solanadevnet token config --- src/consts/tokens.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/consts/tokens.ts b/src/consts/tokens.ts index 43606628..e2014db4 100644 --- a/src/consts/tokens.ts +++ b/src/consts/tokens.ts @@ -18,8 +18,13 @@ export const tokenList: WarpTokenConfig = [ // solanadevnet { - type: 'native', + type: 'collateral', chainId: 1399811151, - hypNativeAddress: 'PJH5QAbxAqrrnSXfH3GHR8icua8CDFZmo97z91xmpvx', + address: 'Gh9ZwEmdLJ8DscKNTkTqPbNwLNNBjuSzaG9Vp2KGtKJr', + hypCollateralAddress: 'PJH5QAbxAqrrnSXfH3GHR8icua8CDFZmo97z91xmpvx', + name: 'dUSDC', + symbol: 'dUSDC', + decimals: 6, + isSpl2022: false, }, ]; From eb0b2a116f0d6ce59ce270308e595bd626f1d911 Mon Sep 17 00:00:00 2001 From: J M Rossy Date: Sun, 13 Aug 2023 15:52:52 -0400 Subject: [PATCH 07/58] Fetch remote router decimal values during init Use origin/remote decimals when checking balances Fix bug with address decoding for evm->sol routers --- .../tokens/adapters/AdapterFactory.ts | 23 ++++-- .../tokens/adapters/EvmTokenAdapter.ts | 20 ++--- src/features/tokens/adapters/ITokenAdapter.ts | 4 +- .../tokens/adapters/SealevelTokenAdapter.ts | 36 ++++++--- src/features/tokens/balances.tsx | 4 +- src/features/tokens/routes/hooks.ts | 76 +++++++++++++------ src/features/tokens/routes/types.ts | 3 +- src/features/tokens/types.ts | 2 +- src/features/transfer/TransferTokenForm.tsx | 4 +- src/features/transfer/useTokenTransfer.ts | 2 +- src/utils/addresses.ts | 10 +++ 11 files changed, 125 insertions(+), 59 deletions(-) diff --git a/src/features/tokens/adapters/AdapterFactory.ts b/src/features/tokens/adapters/AdapterFactory.ts index e27fdf75..d1c530b2 100644 --- a/src/features/tokens/adapters/AdapterFactory.ts +++ b/src/features/tokens/adapters/AdapterFactory.ts @@ -44,14 +44,27 @@ export class AdapterFactory { } } - static HypCollateralAdapterFromAddress(caip19Id: Caip19Id, routerAddress: Address) { - const caip2Id = getCaip2FromToken(caip19Id); + static HypCollateralAdapterFromAddress(baseCaip19Id: Caip19Id, routerAddress: Address) { return AdapterFactory.selectHypAdapter( - caip2Id, + getCaip2FromToken(baseCaip19Id), routerAddress, - caip19Id, + baseCaip19Id, EvmHypCollateralAdapter, - isNativeToken(caip19Id) ? SealevelHypNativeAdapter : SealevelHypCollateralAdapter, + isNativeToken(baseCaip19Id) ? SealevelHypNativeAdapter : SealevelHypCollateralAdapter, + ); + } + + static HypSyntheticTokenAdapterFromAddress( + baseCaip19Id: Caip19Id, + caip2Id: Caip2Id, + routerAddress: Address, + ) { + return AdapterFactory.selectHypAdapter( + caip2Id, + routerAddress, + baseCaip19Id, + EvmHypSyntheticAdapter, + SealevelHypSyntheticAdapter, ); } diff --git a/src/features/tokens/adapters/EvmTokenAdapter.ts b/src/features/tokens/adapters/EvmTokenAdapter.ts index 9b9cd46e..43182162 100644 --- a/src/features/tokens/adapters/EvmTokenAdapter.ts +++ b/src/features/tokens/adapters/EvmTokenAdapter.ts @@ -4,11 +4,7 @@ import { BigNumber, PopulatedTransaction, Signer, providers } from 'ethers'; import type { ERC20Upgradeable, HypERC20 } from '@hyperlane-xyz/hyperlane-token'; import { utils } from '@hyperlane-xyz/utils'; -import { - addressToByteHexString, - isValidEvmAddress, - normalizeEvmAddress, -} from '../../../utils/addresses'; +import { addressToByteHexString, isValidEvmAddress, trimLeading0x } from '../../../utils/addresses'; import { getErc20Contract, getHypErc20CollateralContract, @@ -127,14 +123,20 @@ export class EvmHypSyntheticAdapter return this.contract.domains(); } - async getRouterAddress(domain: DomainId): Promise
{ + async getRouterAddress(domain: DomainId): Promise { const routerAddressesAsBytes32 = await this.contract.routers(domain); - return normalizeEvmAddress(utils.bytes32ToAddress(routerAddressesAsBytes32)); + // Evm addresses will be padded with 12 bytes + if (routerAddressesAsBytes32.startsWith('0x000000000000000000000000')) { + return Buffer.from(trimLeading0x(utils.bytes32ToAddress(routerAddressesAsBytes32)), 'hex'); + // Otherwise leave the address unchanged + } else { + return Buffer.from(trimLeading0x(routerAddressesAsBytes32), 'hex'); + } } - async getAllRouters(): Promise> { + async getAllRouters(): Promise> { const domains = await this.getDomains(); - const routers: Address[] = await Promise.all(domains.map((d) => this.getRouterAddress(d))); + const routers: Buffer[] = await Promise.all(domains.map((d) => this.getRouterAddress(d))); return domains.map((d, i) => ({ domain: d, address: routers[i] })); } diff --git a/src/features/tokens/adapters/ITokenAdapter.ts b/src/features/tokens/adapters/ITokenAdapter.ts index 4df0e6b1..db2e8c56 100644 --- a/src/features/tokens/adapters/ITokenAdapter.ts +++ b/src/features/tokens/adapters/ITokenAdapter.ts @@ -25,8 +25,8 @@ export interface ITokenAdapter { export interface IHypTokenAdapter extends ITokenAdapter { getDomains(): Promise; - getRouterAddress(domain: DomainId): Promise
; - getAllRouters(): Promise>; + getRouterAddress(domain: DomainId): Promise; + getAllRouters(): Promise>; quoteGasPayment(destination: DomainId): Promise; populateTransferRemoteTx(TransferParams: TransferRemoteParams): unknown | Promise; } diff --git a/src/features/tokens/adapters/SealevelTokenAdapter.ts b/src/features/tokens/adapters/SealevelTokenAdapter.ts index c3ed2ddf..ba5cb79d 100644 --- a/src/features/tokens/adapters/SealevelTokenAdapter.ts +++ b/src/features/tokens/adapters/SealevelTokenAdapter.ts @@ -22,6 +22,7 @@ import { addressToBytes, isZeroishAddress } from '../../../utils/addresses'; import { AccountDataWrapper, HypTokenInstruction, + HyperlaneTokenData, HyperlaneTokenDataSchema, TransferRemoteInstruction, TransferRemoteSchema, @@ -147,31 +148,42 @@ export abstract class SealevelHypTokenAdapter this.warpProgramPubKey = new PublicKey(warpRouteProgramId); } + async getTokenAccountData(): Promise { + const tokenPda = this.deriveHypTokenAccount(); + const accountInfo = await this.connection.getAccountInfo(tokenPda); + if (!accountInfo) throw new Error(`No account info found for ${tokenPda}`); + const wrappedData = deserializeUnchecked( + HyperlaneTokenDataSchema, + AccountDataWrapper, + accountInfo.data, + ); + return wrappedData.data; + } + + override async getMetadata(): Promise { + const tokenData = await this.getTokenAccountData(); + // TODO full token metadata support + return { decimals: tokenData.decimals, symbol: 'HYP', name: 'Unknown Hyp Token' }; + } + async getDomains(): Promise { const routers = await this.getAllRouters(); return routers.map((router) => router.domain); } - async getRouterAddress(domain: DomainId): Promise
{ + async getRouterAddress(domain: DomainId): Promise { const routers = await this.getAllRouters(); const addr = routers.find((router) => router.domain === domain)?.address; if (!addr) throw new Error(`No router found for ${domain}`); return addr; } - async getAllRouters(): Promise> { - const tokenPda = this.deriveHypTokenAccount(); - const accountInfo = await this.connection.getAccountInfo(tokenPda); - if (!accountInfo) throw new Error(`No account info found for ${tokenPda}}`); - const tokenData = deserializeUnchecked( - HyperlaneTokenDataSchema, - AccountDataWrapper, - accountInfo.data, - ); - const domainToPubKey = tokenData.data.remote_router_pubkeys; + async getAllRouters(): Promise> { + const tokenData = await this.getTokenAccountData(); + const domainToPubKey = tokenData.remote_router_pubkeys; return Array.from(domainToPubKey.entries()).map(([domain, pubKey]) => ({ domain, - address: pubKey.toBase58(), + address: pubKey.toBuffer(), })); } diff --git a/src/features/tokens/balances.tsx b/src/features/tokens/balances.tsx index 27f54740..8604eb75 100644 --- a/src/features/tokens/balances.tsx +++ b/src/features/tokens/balances.tsx @@ -41,7 +41,7 @@ export function useOriginBalance( if (!route || !address || !isValidAddress(address, protocol)) return null; const adapter = AdapterFactory.HypTokenAdapterFromRouteOrigin(route); const balance = await adapter.getBalance(address); - return { balance, decimals: route.decimals }; + return { balance, decimals: route.originDecimals }; }, refetchInterval: 5000, }); @@ -76,7 +76,7 @@ export function useDestinationBalance( if (!route || !recipientAddress || !isValidAddress(recipientAddress, protocol)) return null; const adapter = AdapterFactory.HypTokenAdapterFromRouteDest(route); const balance = await adapter.getBalance(recipientAddress); - return { balance, decimals: route.decimals }; + return { balance, decimals: route.destDecimals }; }, refetchInterval: 5000, }); diff --git a/src/features/tokens/routes/hooks.ts b/src/features/tokens/routes/hooks.ts index a5da27f3..a1adc4e3 100644 --- a/src/features/tokens/routes/hooks.ts +++ b/src/features/tokens/routes/hooks.ts @@ -3,6 +3,7 @@ import { useMemo } from 'react'; import { ProtocolType } from '@hyperlane-xyz/sdk'; +import { areAddressesEqual, bytesToProtocolAddress } from '../../../utils/addresses'; import { logger } from '../../../utils/logger'; import { getCaip2Id } from '../../caip/chains'; import { @@ -32,7 +33,7 @@ export function useTokenRoutes() { const tokens: TokenMetadataWithHypTokens[] = []; for (const token of parsedTokens) { // Consider parallelizing here but concerned about RPC rate limits - const tokenWithHypTokens = await fetchRemoteHypTokens(token); + const tokenWithHypTokens = await fetchRemoteHypTokens(token, parsedTokens); tokens.push(tokenWithHypTokens); } return computeTokenRoutes(tokens); @@ -44,26 +45,43 @@ export function useTokenRoutes() { } async function fetchRemoteHypTokens( - originToken: TokenMetadata, + baseToken: TokenMetadata, + allTokens: TokenMetadata[], ): Promise { - const { symbol, caip19Id, routerAddress } = originToken; - const isNft = isNonFungibleToken(caip19Id); - logger.info(`Fetching remote tokens for symbol ${symbol} (${caip19Id})`); + const { symbol: baseSymbol, caip19Id: baseCaip19Id, routerAddress: baseRouter } = baseToken; + const isNft = isNonFungibleToken(baseCaip19Id); + logger.info(`Fetching remote tokens for symbol ${baseSymbol} (${baseCaip19Id})`); - const hypTokenAdapter = AdapterFactory.HypCollateralAdapterFromAddress(caip19Id, routerAddress); + const baseAdapter = AdapterFactory.HypCollateralAdapterFromAddress(baseCaip19Id, baseRouter); - const remoteRouters = await hypTokenAdapter.getAllRouters(); + const remoteRouters = await baseAdapter.getAllRouters(); logger.info(`Router addresses found:`, remoteRouters); const multiProvider = getMultiProvider(); - const hypTokens = remoteRouters.map((router) => { - const destMetadata = multiProvider.getChainMetadata(router.domain); - const protocol = destMetadata.protocol || ProtocolType.Ethereum; - const caip2Id = getCaip2Id(protocol, multiProvider.getChainId(router.domain)); - const namespace = resolveAssetNamespace(protocol, false, isNft, true); - return getCaip19Id(caip2Id, namespace, router.address); - }); - return { ...originToken, hypTokens }; + const hypTokens = await Promise.all( + remoteRouters.map(async (router) => { + const destMetadata = multiProvider.getChainMetadata(router.domain); + const protocol = destMetadata.protocol || ProtocolType.Ethereum; + const caip2Id = getCaip2Id(protocol, multiProvider.getChainId(router.domain)); + const namespace = resolveAssetNamespace(protocol, false, isNft, true); + const formattedAddress = bytesToProtocolAddress(router.address, protocol); + const caip19Id = getCaip19Id(caip2Id, namespace, formattedAddress); + // Attempt to find the decimals from the token list + const routerMetadata = allTokens.find((token) => + areAddressesEqual(formattedAddress, token.routerAddress), + ); + if (routerMetadata) return { caip19Id, decimals: routerMetadata.decimals }; + // Otherwise try to query the contract + const remoteAdapter = AdapterFactory.HypSyntheticTokenAdapterFromAddress( + baseCaip19Id, + caip2Id, + formattedAddress, + ); + const metadata = await remoteAdapter.getMetadata(); + return { caip19Id, decimals: metadata.decimals }; + }), + ); + return { ...baseToken, hypTokens }; } // Process token list to populates routesCache with all possible token routes (e.g. router pairs) @@ -83,10 +101,16 @@ function computeTokenRoutes(tokens: TokenMetadataWithHypTokens[]) { // Compute all possible routes, in both directions for (const token of tokens) { for (const hypToken of token.hypTokens) { - const { caip19Id: baseCaip19Id, routerAddress: baseRouterAddress, decimals } = token; + const { + caip19Id: baseCaip19Id, + routerAddress: baseRouterAddress, + decimals: baseDecimals, + } = token; const baseCaip2Id = getCaip2FromToken(baseCaip19Id); - const { caip2Id: syntheticCaip2Id, address: syntheticRouterAddress } = - parseCaip19Id(hypToken); + const { caip2Id: syntheticCaip2Id, address: syntheticRouterAddress } = parseCaip19Id( + hypToken.caip19Id, + ); + const syntheticDecimals = hypToken.decimals; const commonRouteProps = { baseCaip19Id, @@ -97,33 +121,37 @@ function computeTokenRoutes(tokens: TokenMetadataWithHypTokens[]) { ...commonRouteProps, originCaip2Id: baseCaip2Id, originRouterAddress: baseRouterAddress, + originDecimals: baseDecimals, destCaip2Id: syntheticCaip2Id, destRouterAddress: syntheticRouterAddress, - decimals, + destDecimals: syntheticDecimals, }); tokenRoutes[syntheticCaip2Id][baseCaip2Id]?.push({ type: RouteType.SyntheticToBase, ...commonRouteProps, originCaip2Id: syntheticCaip2Id, originRouterAddress: syntheticRouterAddress, + originDecimals: syntheticDecimals, destCaip2Id: baseCaip2Id, destRouterAddress: baseRouterAddress, - decimals, + destDecimals: baseDecimals, }); for (const otherHypToken of token.hypTokens) { // Skip if it's same hypToken as parent loop (no route to self) if (otherHypToken === hypToken) continue; - const { caip2Id: otherSynCaip2Id, address: otherHypTokenAddress } = - parseCaip19Id(otherHypToken); + const { caip2Id: otherSynCaip2Id, address: otherHypTokenAddress } = parseCaip19Id( + otherHypToken.caip19Id, + ); tokenRoutes[syntheticCaip2Id][otherSynCaip2Id]?.push({ type: RouteType.SyntheticToSynthetic, ...commonRouteProps, originCaip2Id: syntheticCaip2Id, originRouterAddress: syntheticRouterAddress, + originDecimals: syntheticDecimals, destCaip2Id: otherSynCaip2Id, destRouterAddress: otherHypTokenAddress, - decimals, + destDecimals: otherHypToken.decimals, }); } } @@ -136,7 +164,7 @@ function getChainsFromTokens(tokens: TokenMetadataWithHypTokens[]): Caip2Id[] { for (const token of tokens) { chains.add(getCaip2FromToken(token.caip19Id)); for (const hypToken of token.hypTokens) { - chains.add(getCaip2FromToken(hypToken)); + chains.add(getCaip2FromToken(hypToken.caip19Id)); } } return Array.from(chains); diff --git a/src/features/tokens/routes/types.ts b/src/features/tokens/routes/types.ts index 8a22191e..e1d66a64 100644 --- a/src/features/tokens/routes/types.ts +++ b/src/features/tokens/routes/types.ts @@ -10,9 +10,10 @@ export interface Route { baseRouterAddress: Address; originCaip2Id: Caip2Id; originRouterAddress: Address; + originDecimals: number; destCaip2Id: Caip2Id; destRouterAddress: Address; - decimals: number; + destDecimals: number; } export type RoutesMap = Record>; diff --git a/src/features/tokens/types.ts b/src/features/tokens/types.ts index ac3e319e..2bdd907b 100644 --- a/src/features/tokens/types.ts +++ b/src/features/tokens/types.ts @@ -88,7 +88,7 @@ export type TokenMetadata = CollateralTokenMetadata | NativeTokenMetadata; * Extended types including synthetic hyp token addresses */ interface HypTokens { - hypTokens: Array; + hypTokens: Array<{ caip19Id: Caip19Id; decimals: number }>; } type NativeTokenMetadataWithHypTokens = NativeTokenMetadata & HypTokens; diff --git a/src/features/transfer/TransferTokenForm.tsx b/src/features/transfer/TransferTokenForm.tsx index 4c73ca88..08ea52af 100644 --- a/src/features/transfer/TransferTokenForm.tsx +++ b/src/features/transfer/TransferTokenForm.tsx @@ -354,7 +354,7 @@ function ReviewDetails({ visible, tokenRoutes }: { visible: boolean; tokenRoutes const route = getTokenRoute(originCaip2Id, destinationCaip2Id, token, tokenRoutes); const isNft = token && isNonFungibleToken(token); - const sendValue = isNft ? amount.toString() : toWei(amount, route?.decimals).toString(); + const sendValue = isNft ? amount.toString() : toWei(amount, route?.originDecimals).toString(); const isApproveRequired = route && isTransferApproveRequired(route, token); const originProtocol = getProtocolType(originCaip2Id); const originUnitName = ProtocolSmallestUnit[originProtocol]; @@ -416,7 +416,7 @@ function validateFormValues( const parsedAmount = tryParseAmount(amount); if (!parsedAmount || parsedAmount.lte(0)) return { amount: isNft ? 'Invalid Token Id' : 'Invalid amount' }; - const sendValue = isNft ? parsedAmount : toWei(parsedAmount, route?.decimals); + const sendValue = isNft ? parsedAmount : toWei(parsedAmount, route?.originDecimals); if (!isNft) { // Validate balances for ERC20-like tokens diff --git a/src/features/transfer/useTokenTransfer.ts b/src/features/transfer/useTokenTransfer.ts index 94cca4ba..0bcda366 100644 --- a/src/features/transfer/useTokenTransfer.ts +++ b/src/features/transfer/useTokenTransfer.ts @@ -115,7 +115,7 @@ async function executeTransfer({ if (!tokenRoute) throw new Error('No token route found between chains'); const isNft = isNonFungibleToken(tokenCaip19Id); - const weiAmountOrId = isNft ? amount : toWei(amount, tokenRoute.decimals).toString(); + const weiAmountOrId = isNft ? amount : toWei(amount, tokenRoute.originDecimals).toString(); const activeAccountAddress = activeAccounts.accounts[originProtocol]?.address || ''; addTransfer({ diff --git a/src/utils/addresses.ts b/src/utils/addresses.ts index 8dd6d4c6..7748cc95 100644 --- a/src/utils/addresses.ts +++ b/src/utils/addresses.ts @@ -192,6 +192,16 @@ export function convertToProtocolAddress(address: string, protocol: ProtocolType } } +export function bytesToProtocolAddress(bytes: Buffer, toProtocol: ProtocolType) { + if (toProtocol === ProtocolType.Sealevel) { + return new PublicKey(bytes).toBase58(); + } else if (toProtocol === ProtocolType.Ethereum) { + return utils.bytes32ToAddress(bytes.toString('hex')); + } else { + throw new Error(`Unsupported protocol for address ${toProtocol}`); + } +} + export function trimLeading0x(input: string) { return input.startsWith('0x') ? input.substring(2) : input; } From a36f1c92ad776b68d3fce784c33633101d122f37 Mon Sep 17 00:00:00 2001 From: Trevor Porter Date: Mon, 14 Aug 2023 15:36:41 +0100 Subject: [PATCH 08/58] ZBC symbol, hide routes we don't want to see, fix Solana remote balance checking --- src/consts/config.ts | 4 ++-- src/consts/tokens.ts | 10 ++++++++-- src/features/tokens/balances.tsx | 21 ++++++++++++++++++--- src/features/tokens/routes/utils.ts | 5 ++++- src/features/transfer/useTokenTransfer.ts | 1 + 5 files changed, 33 insertions(+), 8 deletions(-) diff --git a/src/consts/config.ts b/src/consts/config.ts index d7d37d64..029b92a9 100644 --- a/src/consts/config.ts +++ b/src/consts/config.ts @@ -16,7 +16,7 @@ export const config: Config = Object.freeze({ debug: isDevMode, version, explorerApiKeys, - showTipBox: true, - showDisabledTokens: true, + showTipBox: false, + showDisabledTokens: false, walletConnectProjectId, }); diff --git a/src/consts/tokens.ts b/src/consts/tokens.ts index e2014db4..4fa734f6 100644 --- a/src/consts/tokens.ts +++ b/src/consts/tokens.ts @@ -7,6 +7,9 @@ export const tokenList: WarpTokenConfig = [ chainId: 97, address: '0x64544969ed7ebf5f083679233325356ebe738930', hypCollateralAddress: '0x31b5234A896FbC4b3e2F7237592D054716762131', + symbol: 'ZBC', + name: 'Zebec', + decimals: 18, }, // proteustestnet @@ -14,6 +17,9 @@ export const tokenList: WarpTokenConfig = [ type: 'native', chainId: 88002, hypNativeAddress: '0x34A9af13c5555BAD0783C220911b9ef59CfDBCEf', + symbol: 'ZBC', + name: 'Zebec', + decimals: 18, }, // solanadevnet @@ -22,8 +28,8 @@ export const tokenList: WarpTokenConfig = [ chainId: 1399811151, address: 'Gh9ZwEmdLJ8DscKNTkTqPbNwLNNBjuSzaG9Vp2KGtKJr', hypCollateralAddress: 'PJH5QAbxAqrrnSXfH3GHR8icua8CDFZmo97z91xmpvx', - name: 'dUSDC', - symbol: 'dUSDC', + name: 'Zebec', + symbol: 'ZBC', decimals: 6, isSpl2022: false, }, diff --git a/src/features/tokens/balances.tsx b/src/features/tokens/balances.tsx index 8604eb75..b5b09027 100644 --- a/src/features/tokens/balances.tsx +++ b/src/features/tokens/balances.tsx @@ -71,12 +71,27 @@ export function useDestinationBalance( tokenRoutes, ], queryFn: async () => { - const route = getTokenRoute(originCaip2Id, destinationCaip2Id, tokenCaip19Id, tokenRoutes); + // NOTE: this is a hack to accommodate destination balances, specifically the case + // when the destination is a Sealevel chain and is a non-synthetic warp route. + // This only really works with the specific setup of tokens.ts. + + // This searches for the route where the origin chain is destinationCaip2Id + // and the destination chain is originCaip2Id and where the origin is a base token. + const targetBaseCaip19Id = tokenRoutes[destinationCaip2Id][originCaip2Id].find((r) => + r.baseCaip19Id.startsWith(destinationCaip2Id), + )!.baseCaip19Id; + const route = getTokenRoute( + destinationCaip2Id, + originCaip2Id, + targetBaseCaip19Id, + tokenRoutes, + ); const protocol = getProtocolType(destinationCaip2Id); if (!route || !recipientAddress || !isValidAddress(recipientAddress, protocol)) return null; - const adapter = AdapterFactory.HypTokenAdapterFromRouteDest(route); + + const adapter = AdapterFactory.HypTokenAdapterFromRouteOrigin(route); const balance = await adapter.getBalance(recipientAddress); - return { balance, decimals: route.destDecimals }; + return { balance, decimals: route.originDecimals }; }, refetchInterval: 5000, }); diff --git a/src/features/tokens/routes/utils.ts b/src/features/tokens/routes/utils.ts index 31dc50e4..ee0773a2 100644 --- a/src/features/tokens/routes/utils.ts +++ b/src/features/tokens/routes/utils.ts @@ -26,5 +26,8 @@ export function hasTokenRoute( caip19Id: Caip19Id, tokenRoutes: RoutesMap, ): boolean { - return !!getTokenRoute(originCaip2Id, destinationCaip2Id, caip19Id, tokenRoutes); + const tokenRoute = getTokenRoute(originCaip2Id, destinationCaip2Id, caip19Id, tokenRoutes); + // This will break things if there are other warp routes configured! + // This only looks for routes in which the origin is the base token. + return !!tokenRoute && caip19Id.startsWith(originCaip2Id); } diff --git a/src/features/transfer/useTokenTransfer.ts b/src/features/transfer/useTokenTransfer.ts index 0bcda366..7ff2c303 100644 --- a/src/features/transfer/useTokenTransfer.ts +++ b/src/features/transfer/useTokenTransfer.ts @@ -126,6 +126,7 @@ async function executeTransfer({ params: values, }); + // Come back here await ensureSufficientCollateral(tokenRoute, weiAmountOrId, isNft); const hypTokenAdapter = AdapterFactory.HypTokenAdapterFromRouteOrigin(tokenRoute); From fb7da5a9280d5526d7062d5e7902311081278bdb Mon Sep 17 00:00:00 2001 From: Trevor Porter Date: Mon, 14 Aug 2023 19:12:21 +0100 Subject: [PATCH 09/58] Fix check if there's sufficient collateral at the dest --- src/features/transfer/useTokenTransfer.ts | 50 +++++++++++++++++++---- src/utils/amount.ts | 15 +++++++ 2 files changed, 56 insertions(+), 9 deletions(-) diff --git a/src/features/transfer/useTokenTransfer.ts b/src/features/transfer/useTokenTransfer.ts index 7ff2c303..4406bf9b 100644 --- a/src/features/transfer/useTokenTransfer.ts +++ b/src/features/transfer/useTokenTransfer.ts @@ -6,7 +6,7 @@ import { toast } from 'react-toastify'; import { HyperlaneCore, ProtocolType } from '@hyperlane-xyz/sdk'; import { toastTxSuccess } from '../../components/toast/TxSuccessToast'; -import { toWei } from '../../utils/amount'; +import { convertDecimals, toWei } from '../../utils/amount'; import { logger } from '../../utils/logger'; import { getProtocolType, parseCaip2Id } from '../caip/chains'; import { isNativeToken, isNonFungibleToken } from '../caip/tokens'; @@ -27,6 +27,8 @@ import { import { TransferContext, TransferFormValues, TransferStatus } from './types'; +const COLLATERAL_CONTRACT_BALANCE_INSUFFICIENT_ERROR = 'Collateral contract balance insufficient'; + export function useTokenTransfer(onDone?: () => void) { const { transfers, addTransfer, updateTransferStatus } = useStore((s) => ({ transfers: s.transfers, @@ -127,7 +129,7 @@ async function executeTransfer({ }); // Come back here - await ensureSufficientCollateral(tokenRoute, weiAmountOrId, isNft); + await ensureSufficientCollateral(tokenRoutes, tokenRoute, weiAmountOrId, isNft); const hypTokenAdapter = AdapterFactory.HypTokenAdapterFromRouteOrigin(tokenRoute); @@ -183,13 +185,43 @@ async function executeTransfer({ // In certain cases, like when a synthetic token has >1 collateral tokens // it's possible that the collateral contract balance is insufficient to // cover the remote transfer. This ensures the balance is sufficient or throws. -async function ensureSufficientCollateral(route: Route, weiAmount: string, isNft?: boolean) { - if (route.type !== RouteType.SyntheticToBase || isNft) return; - const adapter = AdapterFactory.TokenAdapterFromAddress(route.baseCaip19Id); - logger.debug('Checking collateral balance for token', route.baseCaip19Id); - const balance = await adapter.getBalance(route.baseRouterAddress); - if (BigNumber.from(balance).lt(weiAmount)) { - throw new Error('Collateral contract has insufficient balance'); +async function ensureSufficientCollateral( + tokenRoutes: RoutesMap, + route: Route, + weiAmount: string, + isNft?: boolean, +) { + if (isNft) return; + + // NOTE: this is a hack to accommodate destination balances, specifically the case + // when the destination is a Sealevel chain and is a non-synthetic warp route. + // This only really works with the specific setup of tokens.ts. + + // This searches for the route where the origin chain is destinationCaip2Id + // and the destination chain is originCaip2Id and where the origin is a base token. + const targetBaseCaip19Id = tokenRoutes[route.destCaip2Id][route.originCaip2Id].find((r) => + r.baseCaip19Id.startsWith(route.destCaip2Id), + )!.baseCaip19Id; + const targetRoute = getTokenRoute( + route.destCaip2Id, + route.originCaip2Id, + targetBaseCaip19Id, + tokenRoutes, + ); + if (!targetRoute) return; + + const adapter = AdapterFactory.HypTokenAdapterFromRouteOrigin(targetRoute); + const destinationBalance = await adapter.getBalance(targetRoute.baseRouterAddress); + + const destinationBalanceInOriginDecimals = convertDecimals( + route.destDecimals, + route.originDecimals, + destinationBalance, + ); + + if (destinationBalanceInOriginDecimals.lt(weiAmount)) { + toast.error(COLLATERAL_CONTRACT_BALANCE_INSUFFICIENT_ERROR); + throw new Error(COLLATERAL_CONTRACT_BALANCE_INSUFFICIENT_ERROR); } } diff --git a/src/utils/amount.ts b/src/utils/amount.ts index 38320057..3e825975 100644 --- a/src/utils/amount.ts +++ b/src/utils/amount.ts @@ -75,3 +75,18 @@ export function areAmountsNearlyEqual(amountInWei1: BigNumber, amountInWei2: Num // Is difference btwn amount and balance less than min amount shown for token return amountInWei1.minus(amountInWei2).abs().lt(minValueWei); } + +export function convertDecimals(fromDecimals: number, toDecimals: number, value: NumberT) { + const amount = new BigNumber(value); + + if (fromDecimals === toDecimals) return amount; + else if (fromDecimals > toDecimals) { + const difference = fromDecimals - toDecimals; + return amount.div(new BigNumber(10).pow(difference)).integerValue(BigNumber.ROUND_FLOOR); + } + // fromDecimals < toDecimals + else { + const difference = toDecimals - fromDecimals; + return amount.times(new BigNumber(10).pow(difference)); + } +} From 07531fd83b81634fb4f8362aa69c487f49b6fabb Mon Sep 17 00:00:00 2001 From: J M Rossy Date: Mon, 14 Aug 2023 15:49:25 -0400 Subject: [PATCH 10/58] Remove Hyperlane explorer link Fix block explorer links Show toast on recipient balance increase Show toast from self button if wallet not connected Improve sidebar menu scrolling --- src/consts/chains.ts | 23 ++- src/features/multiProvider.ts | 24 ++++ src/features/transfer/TransferTokenForm.tsx | 32 ++++- .../transfer/TransfersDetailsModal.tsx | 23 ++- src/features/wallet/SideBarMenu.tsx | 135 +++++++++--------- src/utils/links.ts | 3 + 6 files changed, 163 insertions(+), 77 deletions(-) diff --git a/src/consts/chains.ts b/src/consts/chains.ts index b6140afd..77e91bcb 100644 --- a/src/consts/chains.ts +++ b/src/consts/chains.ts @@ -1,4 +1,9 @@ -import { ChainMap, ChainMetadataWithArtifacts, ProtocolType } from '@hyperlane-xyz/sdk'; +import { + ChainMap, + ChainMetadataWithArtifacts, + ExplorerFamily, + ProtocolType, +} from '@hyperlane-xyz/sdk'; import { solana, solanadevnet, solanatestnet } from '@hyperlane-xyz/sdk/dist/consts/chainMetadata'; // A map of chain names to ChainMetadata @@ -44,6 +49,14 @@ export const chains: ChainMap = { }, solanadevnet: { ...solanadevnet, + blockExplorers: [ + { + name: 'Solana Explorer', + url: 'https://explorer.solana.com', + apiUrl: 'https://explorer.solana.com', + family: ExplorerFamily.Other, + }, + ], mailbox: '4v25Dz9RccqUrTzmfHzJMsjd1iVoNrWzeJ4o6GYuJrVn', interchainGasPaymaster: '', validatorAnnounce: '', @@ -65,6 +78,14 @@ export const chains: ChainMap = { http: 'https://api.proteus.nautchain.xyz/solana', }, ], + blockExplorers: [ + { + name: 'Proteus Explorer', + url: 'https://proteus.nautscan.com/proteus', + apiUrl: 'https://proteus.nautscan.com/proteus', + family: ExplorerFamily.Other, + }, + ], mailbox: '0x918D3924Fad8F71551D9081172e9Bb169745461e', interchainGasPaymaster: '0x06b62A9F5AEcc1E601D0E02732b4E1D0705DE7Db', validatorAnnounce: '0xEEea93d0d0287c71e47B3f62AFB0a92b9E8429a1', diff --git a/src/features/multiProvider.ts b/src/features/multiProvider.ts index 893db8d6..7d5e7886 100644 --- a/src/features/multiProvider.ts +++ b/src/features/multiProvider.ts @@ -44,6 +44,30 @@ class MultiProtocolProvider extends MultiProvider { if (metadata?.protocol && metadata.protocol !== ProtocolType.Ethereum) return null; return super.tryGetSigner(chainNameOrId); } + + override async tryGetExplorerAddressUrl( + chainNameOrId: ChainName | number, + address?: string, + ): Promise { + const url = await super.tryGetExplorerAddressUrl(chainNameOrId, address); + // TODO hacking fix for solana explorer url here + if (this.getChainName(chainNameOrId) === 'solanadevnet') { + return `${url}?cluster=devnet`; + } + return url; + } + + override tryGetExplorerTxUrl( + chainNameOrId: ChainName | number, + response: { hash: string }, + ): string | null { + const url = super.tryGetExplorerTxUrl(chainNameOrId, response); + // TODO hacking fix for solana explorer url here + if (this.getChainName(chainNameOrId) === 'solanadevnet') { + return `${url}?cluster=devnet`; + } + return url; + } } let multiProvider: MultiProtocolProvider; diff --git a/src/features/transfer/TransferTokenForm.tsx b/src/features/transfer/TransferTokenForm.tsx index 08ea52af..804a54ca 100644 --- a/src/features/transfer/TransferTokenForm.tsx +++ b/src/features/transfer/TransferTokenForm.tsx @@ -1,5 +1,7 @@ +import BigNumber from 'bignumber.js'; import { Form, Formik, useFormikContext } from 'formik'; -import { useMemo, useState } from 'react'; +import { useEffect, useMemo, useRef, useState } from 'react'; +import { toast } from 'react-toastify'; import { ProtocolSmallestUnit } from '@hyperlane-xyz/sdk'; import { WideChevron } from '@hyperlane-xyz/widgets'; @@ -208,6 +210,29 @@ function RecipientSection({ const { values } = useFormikContext(); const { balance, decimals } = useDestinationBalance(values, tokenRoutes); + // TODO hacking in a crude way to detect transfer completions by triggering + // toast on recipientAddress balance increase. This is not ideal because it + // could confuse unrelated balance changes for message delivery and it + // doesn't update the store state yet + const recipientAddress = values.recipientAddress; + const prevRecipientBalance = useRef<{ balance?: string; recipientAddress?: string }>({ + balance: '', + recipientAddress: '', + }); + useEffect(() => { + if ( + recipientAddress && + balance && + prevRecipientBalance.current.balance && + prevRecipientBalance.current.recipientAddress === recipientAddress && + new BigNumber(balance).gt(prevRecipientBalance.current.balance) + ) { + toast.success('Recipient has received funds, transfer complete!'); + } else { + prevRecipientBalance.current = { balance, recipientAddress }; + } + }, [balance, recipientAddress, prevRecipientBalance]); + return (
@@ -332,7 +357,10 @@ function SelfButton({ disabled }: { disabled?: boolean }) { const { values, setFieldValue } = useFormikContext(); const address = useAccountForChain(values.destinationCaip2Id)?.address; const onClick = () => { - if (address && !disabled) setFieldValue('recipientAddress', address); + if (disabled) return; + if (address) setFieldValue('recipientAddress', address); + else + toast.warn(`No wallet connected for chain ${getChainDisplayName(values.destinationCaip2Id)}`); }; return ( { try { if (originTxHash) { - const originTx = multiProvider.tryGetExplorerTxUrl(chain, { hash: originTxHash }); + const originTx = multiProvider.tryGetExplorerTxUrl(originChain, { hash: originTxHash }); if (originTx) setOriginTxUrl(originTx); } const [fromUrl, toUrl, tokenUrl] = await Promise.all([ - multiProvider.tryGetExplorerAddressUrl(chain, activeAccountAddress), - multiProvider.tryGetExplorerAddressUrl(chain, recipientAddress), - multiProvider.tryGetExplorerAddressUrl(chain, tokenAddress), + multiProvider.tryGetExplorerAddressUrl(originChain, activeAccountAddress), + multiProvider.tryGetExplorerAddressUrl(destChain, recipientAddress), + multiProvider.tryGetExplorerAddressUrl(originChain, tokenAddress), ]); if (fromUrl) setFromUrl(fromUrl); if (toUrl) setToUrl(toUrl); @@ -63,7 +64,15 @@ export function TransfersDetailsModal({ } catch (error) { logger.error('Error fetching URLs:', error); } - }, [activeAccountAddress, originTxHash, multiProvider, recipientAddress, chain, tokenAddress]); + }, [ + activeAccountAddress, + originTxHash, + multiProvider, + recipientAddress, + originChain, + destChain, + tokenAddress, + ]); useEffect(() => { if (!transfer) return; diff --git a/src/features/wallet/SideBarMenu.tsx b/src/features/wallet/SideBarMenu.tsx index 80025ee8..d2def77e 100644 --- a/src/features/wallet/SideBarMenu.tsx +++ b/src/features/wallet/SideBarMenu.tsx @@ -110,11 +110,11 @@ export function SideBarMenu({ )} -
-
+
+
Connected Wallets
-
+
{readyAccounts.map((a) => (
-
+
Transfer History
-
- {sortedTransfers?.length > 0 && - sortedTransfers.map((t) => ( - - ))} +
+ {STATUSES_WITH_ICON.includes(t.status) ? ( + + ) : ( + + )} +
+ + ))} +
+ {sortedTransfers?.length > 0 && ( + + )}
-
{selectedTransfer && ( diff --git a/src/utils/links.ts b/src/utils/links.ts index bb01485a..c84343b2 100644 --- a/src/utils/links.ts +++ b/src/utils/links.ts @@ -7,6 +7,9 @@ import { toBase64 } from './base64'; // TODO test with solana chain config, or disallow it export function getHypExplorerLink(originCaip2Id: Caip2Id, msgId?: string) { + // TODO Disabling this for eclipse for now + if ('true') return null; + if (!originCaip2Id || !msgId) return null; const baseLink = `${links.explorer}/message/${msgId}`; if (isPermissionlessChain(originCaip2Id)) { From 35c72462015d73dc452c8e3558687f892ebe6b4c Mon Sep 17 00:00:00 2001 From: Nam Chu Hoai Date: Mon, 14 Aug 2023 16:58:56 -0400 Subject: [PATCH 11/58] Catch destination balance checking --- src/features/transfer/useTokenTransfer.ts | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/src/features/transfer/useTokenTransfer.ts b/src/features/transfer/useTokenTransfer.ts index 4406bf9b..bddbab34 100644 --- a/src/features/transfer/useTokenTransfer.ts +++ b/src/features/transfer/useTokenTransfer.ts @@ -211,18 +211,20 @@ async function ensureSufficientCollateral( if (!targetRoute) return; const adapter = AdapterFactory.HypTokenAdapterFromRouteOrigin(targetRoute); - const destinationBalance = await adapter.getBalance(targetRoute.baseRouterAddress); + try { + const destinationBalance = await adapter.getBalance(targetRoute.baseRouterAddress); - const destinationBalanceInOriginDecimals = convertDecimals( - route.destDecimals, - route.originDecimals, - destinationBalance, - ); + const destinationBalanceInOriginDecimals = convertDecimals( + route.destDecimals, + route.originDecimals, + destinationBalance, + ); - if (destinationBalanceInOriginDecimals.lt(weiAmount)) { - toast.error(COLLATERAL_CONTRACT_BALANCE_INSUFFICIENT_ERROR); - throw new Error(COLLATERAL_CONTRACT_BALANCE_INSUFFICIENT_ERROR); - } + if (destinationBalanceInOriginDecimals.lt(weiAmount)) { + toast.error(COLLATERAL_CONTRACT_BALANCE_INSUFFICIENT_ERROR); + throw new Error(COLLATERAL_CONTRACT_BALANCE_INSUFFICIENT_ERROR); + } + } catch (error) {} } interface ExecuteTransferParams { From 3f9b6a5958057b89d9bbd1413ae5722d6c51f5bd Mon Sep 17 00:00:00 2001 From: Nam Chu Hoai Date: Mon, 14 Aug 2023 17:02:44 -0400 Subject: [PATCH 12/58] Fix lint --- src/features/transfer/useTokenTransfer.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/features/transfer/useTokenTransfer.ts b/src/features/transfer/useTokenTransfer.ts index bddbab34..b8768b1b 100644 --- a/src/features/transfer/useTokenTransfer.ts +++ b/src/features/transfer/useTokenTransfer.ts @@ -224,6 +224,7 @@ async function ensureSufficientCollateral( toast.error(COLLATERAL_CONTRACT_BALANCE_INSUFFICIENT_ERROR); throw new Error(COLLATERAL_CONTRACT_BALANCE_INSUFFICIENT_ERROR); } + // eslint-disable-next-line no-empty } catch (error) {} } From 8cf18087ed07df9c1c745702130e44b9d4ad236c Mon Sep 17 00:00:00 2001 From: Nam Chu Hoai Date: Mon, 14 Aug 2023 17:21:06 -0400 Subject: [PATCH 13/58] Brand Nautilus --- src/components/nav/Footer.tsx | 4 ++-- src/components/nav/Header.tsx | 4 ---- src/images/logos/app-logo.svg | 32 +++++++++++++++++++++++++++++++- src/images/logos/app-title.svg | 27 ++++++++++++++++++++++++++- src/pages/_document.tsx | 6 +++--- 5 files changed, 62 insertions(+), 11 deletions(-) diff --git a/src/components/nav/Footer.tsx b/src/components/nav/Footer.tsx index 7af62209..41fb214b 100644 --- a/src/components/nav/Footer.tsx +++ b/src/components/nav/Footer.tsx @@ -18,9 +18,9 @@ export function Footer() {

- Go interchain + Bridge ZBC with the Nautilus Chain Bridge
- with Hyperlane + Build with Hyperlane

diff --git a/src/components/nav/Header.tsx b/src/components/nav/Header.tsx index 96eae461..e22f343e 100644 --- a/src/components/nav/Header.tsx +++ b/src/components/nav/Header.tsx @@ -2,8 +2,6 @@ import Image from 'next/image'; import Link from 'next/link'; import { WalletControlBar } from '../../features/wallet/WalletControlBar'; -import Logo from '../../images/logos/app-logo.svg'; -import Name from '../../images/logos/app-name.svg'; import Title from '../../images/logos/app-title.svg'; export function Header() { @@ -11,8 +9,6 @@ export function Header() {
- -
diff --git a/src/images/logos/app-logo.svg b/src/images/logos/app-logo.svg index 39b16cad..6f04b152 100644 --- a/src/images/logos/app-logo.svg +++ b/src/images/logos/app-logo.svg @@ -1 +1,31 @@ - \ No newline at end of file + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/images/logos/app-title.svg b/src/images/logos/app-title.svg index 073c8aae..5721172e 100644 --- a/src/images/logos/app-title.svg +++ b/src/images/logos/app-title.svg @@ -1 +1,26 @@ - \ No newline at end of file + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/pages/_document.tsx b/src/pages/_document.tsx index fe391a3f..f8144e8c 100644 --- a/src/pages/_document.tsx +++ b/src/pages/_document.tsx @@ -15,7 +15,7 @@ export default function Document() { - + - + - + Date: Mon, 14 Aug 2023 19:21:33 -0400 Subject: [PATCH 14/58] Support deposit-only mode --- src/components/tip/TipCard.tsx | 6 +++--- src/consts/config.ts | 5 ++++- src/features/transfer/TransferTokenForm.tsx | 7 +++++++ 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/components/tip/TipCard.tsx b/src/components/tip/TipCard.tsx index 0b1c69c0..d4d779ff 100644 --- a/src/components/tip/TipCard.tsx +++ b/src/components/tip/TipCard.tsx @@ -12,11 +12,11 @@ export function TipCard() { if (!show) return null; return (
-

Bridge Tokens with Hyperlane Warp Routes!

+

⚠️ Nautilus Bridge is in deposit-only mode.

- Warp Routes make it easy to permissionlessly take your tokens interchain. Fork this - template to get started! + Currently, you can bridge from BSC and Solana to Nautilus. Transfers originating Nautilus + are expected to go live September 1st.

Date: Mon, 14 Aug 2023 21:57:10 -0400 Subject: [PATCH 15/58] Reverse direction --- src/features/transfer/TransferTokenForm.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/features/transfer/TransferTokenForm.tsx b/src/features/transfer/TransferTokenForm.tsx index 804a54ca..73c6b587 100644 --- a/src/features/transfer/TransferTokenForm.tsx +++ b/src/features/transfer/TransferTokenForm.tsx @@ -467,8 +467,8 @@ function useFormInitialValues(caip2Ids: Caip2Id[], tokenRoutes: RoutesMap): Tran (routes) => routes.length, )[0][0]; return { - originCaip2Id: firstRoute.originCaip2Id, - destinationCaip2Id: firstRoute.destCaip2Id, + originCaip2Id: firstRoute.destCaip2Id, + destinationCaip2Id: firstRoute.originCaip2Id, amount: '', tokenCaip19Id: '' as Caip19Id, recipientAddress: '', From af7ddaf276b772869bceb36c7ac1db23ea19395f Mon Sep 17 00:00:00 2001 From: Trevor Porter Date: Tue, 15 Aug 2023 09:30:34 +0100 Subject: [PATCH 16/58] Fix collateral checking to Solana; fix nautilus tx explorer url --- src/features/multiProvider.ts | 8 ++++++- .../tokens/adapters/SealevelTokenAdapter.ts | 14 +++++++++++ src/features/transfer/useTokenTransfer.ts | 24 +++++++++---------- 3 files changed, 32 insertions(+), 14 deletions(-) diff --git a/src/features/multiProvider.ts b/src/features/multiProvider.ts index 7d5e7886..5e3a6d05 100644 --- a/src/features/multiProvider.ts +++ b/src/features/multiProvider.ts @@ -62,9 +62,15 @@ class MultiProtocolProvider extends MultiProvider { response: { hash: string }, ): string | null { const url = super.tryGetExplorerTxUrl(chainNameOrId, response); + if (!url) return null; + + const chainName = this.getChainName(chainNameOrId); // TODO hacking fix for solana explorer url here - if (this.getChainName(chainNameOrId) === 'solanadevnet') { + if (chainName === 'solanadevnet') { return `${url}?cluster=devnet`; + } else if (chainName === 'nautilus' || chainName === 'proteustestnet') { + // TODO hacking fix for nautilus explorer url here + return url.replaceAll('/tx/', '/transaction/'); } return url; } diff --git a/src/features/tokens/adapters/SealevelTokenAdapter.ts b/src/features/tokens/adapters/SealevelTokenAdapter.ts index ba5cb79d..3adabb61 100644 --- a/src/features/tokens/adapters/SealevelTokenAdapter.ts +++ b/src/features/tokens/adapters/SealevelTokenAdapter.ts @@ -364,6 +364,20 @@ export class SealevelHypNativeAdapter extends SealevelHypTokenAdapter { // Interacts with Hyp Collateral token programs export class SealevelHypCollateralAdapter extends SealevelHypTokenAdapter { + async getBalance(owner: Address): Promise { + // Special case where the owner is the warp route program ID. + // This is because collateral warp routes don't hold escrowed collateral + // tokens in their associated token account - instead, they hold them in + // the escrow account. + if (owner === this.warpRouteProgramId) { + const collateralAccount = this.deriveEscrowAccount(); + const response = await this.connection.getTokenAccountBalance(collateralAccount); + return response.value.amount; + } + + return super.getBalance(owner); + } + override getTransferInstructionKeyList( sender: PublicKey, mailbox: PublicKey, diff --git a/src/features/transfer/useTokenTransfer.ts b/src/features/transfer/useTokenTransfer.ts index b8768b1b..5fa4f39e 100644 --- a/src/features/transfer/useTokenTransfer.ts +++ b/src/features/transfer/useTokenTransfer.ts @@ -211,21 +211,19 @@ async function ensureSufficientCollateral( if (!targetRoute) return; const adapter = AdapterFactory.HypTokenAdapterFromRouteOrigin(targetRoute); - try { - const destinationBalance = await adapter.getBalance(targetRoute.baseRouterAddress); - const destinationBalanceInOriginDecimals = convertDecimals( - route.destDecimals, - route.originDecimals, - destinationBalance, - ); + const destinationBalance = await adapter.getBalance(targetRoute.baseRouterAddress); - if (destinationBalanceInOriginDecimals.lt(weiAmount)) { - toast.error(COLLATERAL_CONTRACT_BALANCE_INSUFFICIENT_ERROR); - throw new Error(COLLATERAL_CONTRACT_BALANCE_INSUFFICIENT_ERROR); - } - // eslint-disable-next-line no-empty - } catch (error) {} + const destinationBalanceInOriginDecimals = convertDecimals( + route.destDecimals, + route.originDecimals, + destinationBalance, + ); + + if (destinationBalanceInOriginDecimals.lt(weiAmount)) { + toast.error(COLLATERAL_CONTRACT_BALANCE_INSUFFICIENT_ERROR); + throw new Error(COLLATERAL_CONTRACT_BALANCE_INSUFFICIENT_ERROR); + } } interface ExecuteTransferParams { From 322b406d310bd024c5d0ebc0874d28e306066c75 Mon Sep 17 00:00:00 2001 From: Trevor Porter Date: Tue, 15 Aug 2023 14:51:32 +0100 Subject: [PATCH 17/58] mainnet --- src/consts/chains.ts | 32 +++++++++++++++++++++++++++++++- src/consts/tokens.ts | 26 +++++++++++++------------- src/utils/amount.ts | 2 +- 3 files changed, 45 insertions(+), 15 deletions(-) diff --git a/src/consts/chains.ts b/src/consts/chains.ts index 77e91bcb..15b1554d 100644 --- a/src/consts/chains.ts +++ b/src/consts/chains.ts @@ -37,7 +37,12 @@ export const chains: ChainMap = { // Including configs for some Solana chains by default solana: { ...solana, - mailbox: 'TODO', + rpcUrls: [ + { + http: process.env.NEXT_PUBLIC_SOLANA_RPC_URL ?? 'https://api.mainnet-beta.solana.com', + }, + ], + mailbox: 'Ge9atjAc3Ltu91VTbNpJDCjZ9CFxFyck4h3YBcTF9XPq', interchainGasPaymaster: '', validatorAnnounce: '', }, @@ -90,4 +95,29 @@ export const chains: ChainMap = { interchainGasPaymaster: '0x06b62A9F5AEcc1E601D0E02732b4E1D0705DE7Db', validatorAnnounce: '0xEEea93d0d0287c71e47B3f62AFB0a92b9E8429a1', }, + nautilus: { + chainId: 22222, + domainId: 22222, + name: 'nautilus', + protocol: ProtocolType.Ethereum, + displayName: 'Nautilus', + nativeToken: { + name: 'Zebec', + symbol: 'ZBC', + decimals: 18, + }, + rpcUrls: [ + { + http: 'https://api.nautilus.nautchain.xyz', + }, + ], + blocks: { + confirmations: 1, + reorgPeriod: 1, + estimateBlockTime: 1, + }, + mailbox: '0xF59557dfacDc5a1cb8A36Af43aA4819a6A891e88', + interchainGasPaymaster: '0x3a464f746D23Ab22155710f44dB16dcA53e0775E', + validatorAnnounce: '0x23ce76645EC601148fa451e751eeB75785b97A00', + }, }; diff --git a/src/consts/tokens.ts b/src/consts/tokens.ts index 4fa734f6..18480b74 100644 --- a/src/consts/tokens.ts +++ b/src/consts/tokens.ts @@ -1,36 +1,36 @@ import { WarpTokenConfig } from '../features/tokens/types'; export const tokenList: WarpTokenConfig = [ - // bsctestnet + // bsc { type: 'collateral', - chainId: 97, - address: '0x64544969ed7ebf5f083679233325356ebe738930', - hypCollateralAddress: '0x31b5234A896FbC4b3e2F7237592D054716762131', + chainId: 56, + address: '0x37a56cdcD83Dce2868f721De58cB3830C44C6303', + hypCollateralAddress: '0x69B42169AbD9D2C073f12768a1B358bcD79be1e8', symbol: 'ZBC', name: 'Zebec', - decimals: 18, + decimals: 9, }, - // proteustestnet + // nautilus { type: 'native', - chainId: 88002, - hypNativeAddress: '0x34A9af13c5555BAD0783C220911b9ef59CfDBCEf', + chainId: 22222, + hypNativeAddress: '0x09edd60B833685FDd6dda49f160F43ED9E49C321', symbol: 'ZBC', name: 'Zebec', decimals: 18, }, - // solanadevnet + // solana { type: 'collateral', - chainId: 1399811151, - address: 'Gh9ZwEmdLJ8DscKNTkTqPbNwLNNBjuSzaG9Vp2KGtKJr', - hypCollateralAddress: 'PJH5QAbxAqrrnSXfH3GHR8icua8CDFZmo97z91xmpvx', + chainId: 1399811149, + address: 'zebeczgi5fSEtbpfQKVZKCJ3WgYXxjkMUkNNx7fLKAF', + hypCollateralAddress: 'FTbMPUDfbejo8MRze4Mom4hM7HC2yb7KaHFzRTdLCoG7', name: 'Zebec', symbol: 'ZBC', - decimals: 6, + decimals: 9, isSpl2022: false, }, ]; diff --git a/src/utils/amount.ts b/src/utils/amount.ts index 3e825975..8b3b4615 100644 --- a/src/utils/amount.ts +++ b/src/utils/amount.ts @@ -35,7 +35,7 @@ export function fromWeiRounded( else return MIN_ROUNDED_VALUE.toString(); } - const displayDecimals = amount.gte(10000) ? 0 : DISPLAY_DECIMALS; + const displayDecimals = amount.gte(10000) ? 3 : DISPLAY_DECIMALS; return amount.toFixed(displayDecimals).toString(); } From 30f7a02c545b76fae8c05eb7e50a12ec01998e28 Mon Sep 17 00:00:00 2001 From: J M Rossy Date: Tue, 15 Aug 2023 10:42:13 -0400 Subject: [PATCH 18/58] Set Rainbowkit initialChain to Proteus --- src/features/wallet/EvmWalletContext.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/features/wallet/EvmWalletContext.tsx b/src/features/wallet/EvmWalletContext.tsx index e8fe5241..18f618aa 100644 --- a/src/features/wallet/EvmWalletContext.tsx +++ b/src/features/wallet/EvmWalletContext.tsx @@ -65,6 +65,7 @@ export function EvmWalletContext({ children }: PropsWithChildren) { borderRadius: 'small', fontStack: 'system', })} + initialChain={88002} > {children} From db8273c04277399076665748b4ddcba3bca2d19c Mon Sep 17 00:00:00 2001 From: Trevor Porter Date: Tue, 15 Aug 2023 17:02:01 +0100 Subject: [PATCH 19/58] new warp route deploy --- src/consts/tokens.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/consts/tokens.ts b/src/consts/tokens.ts index 18480b74..34625cb9 100644 --- a/src/consts/tokens.ts +++ b/src/consts/tokens.ts @@ -6,7 +6,7 @@ export const tokenList: WarpTokenConfig = [ type: 'collateral', chainId: 56, address: '0x37a56cdcD83Dce2868f721De58cB3830C44C6303', - hypCollateralAddress: '0x69B42169AbD9D2C073f12768a1B358bcD79be1e8', + hypCollateralAddress: '0xC27980812E2E66491FD457D488509b7E04144b98', symbol: 'ZBC', name: 'Zebec', decimals: 9, @@ -16,7 +16,7 @@ export const tokenList: WarpTokenConfig = [ { type: 'native', chainId: 22222, - hypNativeAddress: '0x09edd60B833685FDd6dda49f160F43ED9E49C321', + hypNativeAddress: '0x4501bBE6e731A4bC5c60C03A77435b2f6d5e9Fe7', symbol: 'ZBC', name: 'Zebec', decimals: 18, @@ -26,8 +26,8 @@ export const tokenList: WarpTokenConfig = [ { type: 'collateral', chainId: 1399811149, - address: 'zebeczgi5fSEtbpfQKVZKCJ3WgYXxjkMUkNNx7fLKAF', - hypCollateralAddress: 'FTbMPUDfbejo8MRze4Mom4hM7HC2yb7KaHFzRTdLCoG7', + address: 'wzbcJyhGhQDLTV1S99apZiiBdE4jmYfbw99saMMdP59', + hypCollateralAddress: 'EJqwFjvVJSAxH8Ur2PYuMfdvoJeutjmH6GkoEFQ4MdSa', name: 'Zebec', symbol: 'ZBC', decimals: 9, From 1afa6a46436c43d77ccc06a46d85f269f3d9bd18 Mon Sep 17 00:00:00 2001 From: Nam Chu Hoai Date: Tue, 15 Aug 2023 13:38:28 -0400 Subject: [PATCH 20/58] Default to Nautilus --- src/features/wallet/EvmWalletContext.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/features/wallet/EvmWalletContext.tsx b/src/features/wallet/EvmWalletContext.tsx index 18f618aa..dd5a5c23 100644 --- a/src/features/wallet/EvmWalletContext.tsx +++ b/src/features/wallet/EvmWalletContext.tsx @@ -65,7 +65,7 @@ export function EvmWalletContext({ children }: PropsWithChildren) { borderRadius: 'small', fontStack: 'system', })} - initialChain={88002} + initialChain={22222} > {children} From 5f930805747d358c40b9709f074e1700f4bf4885 Mon Sep 17 00:00:00 2001 From: Nam Chu Hoai Date: Tue, 15 Aug 2023 13:49:32 -0400 Subject: [PATCH 21/58] Branding --- public/android-chrome-192x192.png | Bin 5877 -> 5533 bytes public/android-chrome-512x512.png | Bin 17377 -> 13953 bytes public/apple-touch-icon.png | Bin 4377 -> 5160 bytes public/favicon-16x16.png | Bin 818 -> 404 bytes public/favicon-32x32.png | Bin 1203 -> 717 bytes public/favicon.ico | Bin 15086 -> 15406 bytes public/icon.png | Bin 17377 -> 34930 bytes src/components/layout/AppLayout.tsx | 2 +- 8 files changed, 1 insertion(+), 1 deletion(-) diff --git a/public/android-chrome-192x192.png b/public/android-chrome-192x192.png index a6e60be27f840840cf5d24015192f9b99563c333..cc5e1bfbbe6c60d0f306b586cc4e9d9cc6c0aa86 100644 GIT binary patch literal 5533 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 JTzTWC{|D$0cUu4e literal 17377 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 diff --git a/public/apple-touch-icon.png b/public/apple-touch-icon.png index e34a352909cd1d8adc17f329b70c207575bfccd4..4ae4eefe78072d6d51d07f95c0675b1a3f97fe53 100644 GIT binary patch literal 5160 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 literal 4377 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 PTEXDy>gTe~DWM4f=Tl$A diff --git a/public/favicon-32x32.png b/public/favicon-32x32.png index d2c62c009c2640bb1a910f27b7855386ea15ed90..6d7b10d437d4c29bd3254fa6227040158a0938c8 100644 GIT binary patch delta 704 zcmV;x0zdt;3C#tN8Gi-<0047(dh`GQ00DDSM?wIu&K&6g00M_eL_t(oN9~qDNK{c2 zhQIUPm{-F@c8dg#jF6(vxb{yDoAma|?Rz**UC1(F3pz4e6hTQ8qpBJKV10doBs)=U?P_U3Kf{128mx z65*wLcaPA0Rx|2v%E?*)m1YSAd8~FF<(WEa(%! zb_GycDfM6eSF^n;_%NA=G1q|xAlYP%gB-x|jU)ufJ-tvXYMK)5LM@5T-<-Y!5&%60 zQUmNsail-S@z<@_CZk@fA074b({_E3Yz!kam?ej@qJQcC0iYPErOem85nSdSu%v|& z-?c=?lwkn9g%h~y`i4yL0RrppBar%p8stt3Afh=(vfjpjLjXv z4nRg62!DSX1nVPWUqP1+Vd*5lvPZ~?1yS|`#boWi!Qzj49RFRy6WuI$E|MKBfUG?1 zm?16}PTJKTjUPmp`msDUnAkHc&nd)w5r)UxK?^ZFN2D6n z&sYqB7NaYVj1pq{29KwWxHe$(2<1LPB_aWoFgZ#Q^8Esi6IlV+rFCGvLt~XFa|m;& mR3OreF-NfJ!f<@K5B&zN08w}2l@D+L0000a#gb6UJBx%RXbIKK=lW|Qdyz?pVEqm9e7JpI0_ckgE$?r|^%OHpRR2c8*R+ z)%5blB7aLil9|;wwzDU3&TJ#)bxZd5YuIy$+r^qe6`FS{e1C)fXvaj=E(x~h*1qY6 zRln+9EFCm`@b)WjYj}aL9#{^)S=2MRGPNXJ6>cAX{AwF&QI_I(`Pz4>JaO6@ zRX3yHEfm4L2tVUMvWzAkGDtGsH5`C5gnt%RdLf4ag>n>I2OGJsNYqgcxEBA=ONdFf z#aTOH+3C;xoI8k$N$m1uR_)t*?bM2S9LIlpzU|i-Jzmx9%)Dq!H&)<}jT3(>-0E*` z>{owsjUjHJ-w4gOqjsJ`r00BEM_%e(x1DdbO1=H&iZRWOAeWbE8($fu)$4Egu+}Br zq`i;;3oq;f~xUD~zg(*wY&NOgl!ktpua$N$;tXvzrzQ~)t9{4GbxpGW?c(na! zVKi|@)}oS`V#!hycIDh423ukW1L;;Vk2V=6iX;PG_fqxf0*~Ly6 z&`=*^rf*>8WMFD;h&9KY)5BoQF&K*n)Q!4-5y2te0lxSCpKw4E5D?%T?c_pn^t_8E zhlThC1o@yT_sBlz05XLHKve#MP!SP&UWkgIyY3a_l%f&F(B%Q8`KxUNp{zZU9@G%ed-`9tnQ~Xs|># diff --git a/public/favicon.ico b/public/favicon.ico index bac1b9413f7e8d378f67082860883afe25e83bcb..619c4dbbe685a573d27b16fa5f37d6f5b3c48ee2 100644 GIT binary patch literal 15406 zcmeHO4Qv!e6n^|j0D)Tq6$AcMB}ybhjS3p0LWn;BdaeA4h+2UlDk26&F#?StpjfaN zAsV4oF`$JAm}syh0{)PQmA2g8U60zk-d%qviXb5Em7nu_cb#@Q?`L;!uPtVqyv*#( zy!XC&GyCSv+bdBQ>PC`8DAQ?PccM{5l%5`b9@d|z0Bso=E&VqW9Y`Y@hB06PN4OnD z)IYDCcKKzRgK`>Zrr%DFcx9U6v(q9xzgR8P{GuZ2hPF(vgBI61=rOO8p02jj6QD&= zg?4abNeNw1W2ZdeU*NaV2(55D9_oqt2U|&|gMKGHpjOU*_#q2eF8XA8Q;m#h{H<8* z^h%_R(^LDTHD^jWf*PkuvoQWkP_2Sb%%>T1xlJjfIkVs0jaEyrjruilKiiQpL&im= z9PE*8YWT$%@V=|$M?LUI1@#sEvGj|6=Pxt>Jm-`$Fo!Xs|BSW&h^8z{sUXbbVDoq> zLrK0eDhBRSL73OWfXVkN9JN3zeV{mo?ohV(0Y%ZMtfw+ahKCA=qvUfrW^6T>t%d@kfI#JHV)2h9d=A^g-tv~%A&&>`Npbq=~7^{LfPx~KjS zrSW@>@pC|{k5^JZ@P~XFeBxK!4_q1WCF80qXe{`~iTSa9;8_Z?qJ29sX4lDd9rz}@ zZ8QjS=7mbk3v(PV_{`F@Kq3nSgVeR&L+N!TG%(TFRP^Kgk+6Tm=Y>qCQU3|F!e^s9 zQ=u)T195E+W>Pl&o&%*Qq~<^X*SQa}r?)WLc&(TBZH5 zs43g>9iMfW(=3#mQJ!HNtE7TN`kOMPg^}@<^7*XQa#AUy#ua%Vv2`51cJ>?6~Af+t$3S3k?*`&dH#cB>=Kf3F`=|H4 z?aX|fC*jLN4k&J(#7;-tGL1z3!tJ)xeemNuaNhom@+0`#4T#59Bj!KsaZ)T1;YBaRQ{CTnc;B;+iww$TKXMHo98{aX>= z&cyRJ&??SPhS4J*6pj(|pywu+iv}Y1^E%F;)v(=c&}$gK0Q{e#{G4^6??F7T#T~mh z+MK}j4CrgdkM`-nT*WqNaMK_+az>o5tgoarj;m^IbZrHmG46TL7e~wKO2q6vq32c3 z3o$0xdky3+=X{mFjE49RP%mEv4fd3h#CE9zMuDA6)&<=&pm!~B6yT1VhqZfwd8$gt z!hBrk_&$`6M~)0*rw3)~0luMr7Y%7p5&J83nsw6xrUgt3bT$jX7j|Kv%Dy?M@bR4u zXDmGUe$gN2!YAUIhV}w0Pnd-IUV(^7C)~M=Fp7w_yO_XTvrEuFNn00 z7W`tLA%^(}_1#rvgkLap?4KU|Vjd`uBZi&i_fxNqnnjZM`Th7C{%fiD*4p0lFaST# zf%j8hdt+yP2H_9+DZJb7wD(>z5tOpK?&NP+d`2nltESGe z_^eo1qqQ52U+BQM$jP|hoYFCeAGO5Wslwlq`^yLZW=(UD>D&nodni@-c^we{-wNzL zP561w*=U|v$3$f+@w0Bc%kOK(@1QM-aEty_;}5NYL&fg`6z@9bT*kRufiYg0YW%tQ zEfwr9Yg~gWJ|E-Z7JaG4&w3DVY|?~ZCO>@FaDg#i*&g_TC07%E{I(n4XKs&&Tl6*I zdw*j#zUQkX{EqtBKqh`4pn*;->-`qav>dhM!T%9NeghGBlP%9q955tNj1<`)uv%6o ztJf%g6TeZG)F;QZzdpMe#bw&xC`;;-W7=Pz-HhTg?QfJN^~t&1?XPdvCfEPL0{;Sl COsF>i literal 15086 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 literal 17377 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 diff --git a/src/components/layout/AppLayout.tsx b/src/components/layout/AppLayout.tsx index ad7db5f9..6d5703f0 100644 --- a/src/components/layout/AppLayout.tsx +++ b/src/components/layout/AppLayout.tsx @@ -10,7 +10,7 @@ export function AppLayout({ children }: PropsWithChildren) { {/* https://nextjs.org/docs/messages/no-document-viewport-meta */} - Hyperlane Warp Route Template UI + Nautilus Bridge
From e61eddd76c712a0e978821863dfbed45bf07b2c8 Mon Sep 17 00:00:00 2001 From: Nam Chu Hoai Date: Tue, 15 Aug 2023 13:54:03 -0400 Subject: [PATCH 22/58] Small change --- src/components/nav/Footer.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/nav/Footer.tsx b/src/components/nav/Footer.tsx index 41fb214b..7b672750 100644 --- a/src/components/nav/Footer.tsx +++ b/src/components/nav/Footer.tsx @@ -18,7 +18,7 @@ export function Footer() {

- Bridge ZBC with the Nautilus Chain Bridge + Bridge ZBC with the Nautilus Chain Bridge.
Build with Hyperlane

From b6f083449ed9043d3fdeea2f5aa7d4e05fcfaf44 Mon Sep 17 00:00:00 2001 From: Nam Chu Hoai Date: Tue, 15 Aug 2023 13:56:07 -0400 Subject: [PATCH 23/58] Undo --- src/components/nav/Footer.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/nav/Footer.tsx b/src/components/nav/Footer.tsx index 7b672750..41fb214b 100644 --- a/src/components/nav/Footer.tsx +++ b/src/components/nav/Footer.tsx @@ -18,7 +18,7 @@ export function Footer() {

- Bridge ZBC with the Nautilus Chain Bridge. + Bridge ZBC with the Nautilus Chain Bridge
Build with Hyperlane

From ca305b1f3ac55aa6b89052317790385e5d006e15 Mon Sep 17 00:00:00 2001 From: Nam Chu Hoai Date: Tue, 15 Aug 2023 15:29:44 -0400 Subject: [PATCH 24/58] Update meta --- src/pages/_document.tsx | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/src/pages/_document.tsx b/src/pages/_document.tsx index f8144e8c..72893bc7 100644 --- a/src/pages/_document.tsx +++ b/src/pages/_document.tsx @@ -16,14 +16,8 @@ export default function Document() { - - + + @@ -33,10 +27,7 @@ export default function Document() { - +
From 13e42c61aa36abf6969d67ec6e039111988eff7e Mon Sep 17 00:00:00 2001 From: Nam Chu Hoai Date: Mon, 21 Aug 2023 08:42:04 -0400 Subject: [PATCH 25/58] Add Nautilus chain logo --- src/consts/chains.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/consts/chains.ts b/src/consts/chains.ts index 15b1554d..3e7f57a1 100644 --- a/src/consts/chains.ts +++ b/src/consts/chains.ts @@ -101,6 +101,7 @@ export const chains: ChainMap = { name: 'nautilus', protocol: ProtocolType.Ethereum, displayName: 'Nautilus', + logoURI: 'https://www.nautchain.xyz/media/nuatchain_media_kit/naut_sq.png', nativeToken: { name: 'Zebec', symbol: 'ZBC', From 49cda5bd90aaddc763556bf50267a5e141cd8fb0 Mon Sep 17 00:00:00 2001 From: J M Rossy Date: Mon, 21 Aug 2023 16:53:23 -0400 Subject: [PATCH 26/58] Fix build error in balances --- src/features/tokens/balances.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/features/tokens/balances.tsx b/src/features/tokens/balances.tsx index 5066d2fa..d756160d 100644 --- a/src/features/tokens/balances.tsx +++ b/src/features/tokens/balances.tsx @@ -4,7 +4,7 @@ import { useEffect } from 'react'; import { areAddressesEqual, isValidAddress } from '../../utils/addresses'; import { logger } from '../../utils/logger'; import { getProtocolType } from '../caip/chains'; -import { parseCaip19Id, tryGetChainIdFromToken } from '../caip/tokens'; +import { getChainIdFromToken, parseCaip19Id, tryGetChainIdFromToken } from '../caip/tokens'; import { getProvider } from '../multiProvider'; import { useStore } from '../store'; import { TransferFormValues } from '../transfer/types'; @@ -77,9 +77,9 @@ export function useDestinationBalance( // This searches for the route where the origin chain is destinationCaip2Id // and the destination chain is originCaip2Id and where the origin is a base token. - const targetBaseCaip19Id = tokenRoutes[destinationCaip2Id][originCaip2Id].find((r) => - r.baseCaip19Id.startsWith(destinationCaip2Id), - )!.baseCaip19Id; + const targetBaseCaip19Id = tokenRoutes[destinationCaip2Id][originCaip2Id].find( + (r) => getChainIdFromToken(r.baseTokenCaip19Id) === destinationCaip2Id, + )!.baseTokenCaip19Id; const route = getTokenRoute( destinationCaip2Id, originCaip2Id, From 101496635145cdd3e67f403dd3f0ad742f48a5c8 Mon Sep 17 00:00:00 2001 From: J M Rossy Date: Tue, 22 Aug 2023 13:19:26 -0400 Subject: [PATCH 27/58] Setup Jest unit testing Create new CollateralToCollateral route type Refactor route computation into separate file Add unit tests for route computation --- .eslintignore | 3 +- .github/workflows/ci.yml | 29 +- jest.config.js | 16 + package.json | 3 + src/features/tokens/SelectOrInputTokenIds.tsx | 2 +- .../tokens/adapters/AdapterFactory.ts | 16 +- src/features/tokens/routes/fetch.test.ts | 252 ++ src/features/tokens/routes/fetch.ts | 166 ++ src/features/tokens/routes/hooks.ts | 150 +- src/features/tokens/routes/types.ts | 5 +- src/features/tokens/routes/utils.ts | 28 +- src/features/transfer/useTokenTransfer.ts | 12 +- yarn.lock | 2208 ++++++++++++++++- 13 files changed, 2682 insertions(+), 208 deletions(-) create mode 100644 jest.config.js create mode 100644 src/features/tokens/routes/fetch.test.ts create mode 100644 src/features/tokens/routes/fetch.ts diff --git a/.eslintignore b/.eslintignore index a72b3c43..cc71deb1 100644 --- a/.eslintignore +++ b/.eslintignore @@ -4,4 +4,5 @@ build coverage postcss.config.js next.config.js -tailwind.config.js \ No newline at end of file +tailwind.config.js +jest.config.js \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 279433bb..be539c14 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,7 +19,6 @@ jobs: with: path: .//node_modules key: ${{ runner.os }}-yarn-cache-${{ hashFiles('./yarn.lock') }} - - name: yarn-install # Check out the lockfile from main, reinstall, and then # verify the lockfile matches what was committed. @@ -37,19 +36,16 @@ jobs: needs: [install] steps: - uses: actions/checkout@v2 - - name: yarn-cache uses: actions/cache@v2 with: path: .//node_modules key: ${{ runner.os }}-yarn-cache-${{ hashFiles('./yarn.lock') }} - - name: build-cache uses: actions/cache@v2 with: path: ./* key: ${{ github.sha }} - - name: build run: yarn run build env: @@ -64,7 +60,6 @@ jobs: with: path: .//node_modules key: ${{ runner.os }}-yarn-cache-${{ hashFiles('./yarn.lock') }} - - name: prettier run: | yarn run prettier @@ -83,19 +78,17 @@ jobs: with: path: .//node_modules key: ${{ runner.os }}-yarn-cache-${{ hashFiles('./yarn.lock') }} - - name: lint run: yarn run lint - # test: - # runs-on: ubuntu-latest - # needs: [build] - # steps: - # - uses: actions/checkout@v2 - # - uses: actions/cache@v2 - # with: - # path: ./* - # key: ${{ github.sha }} - - # - name: test - # run: yarn run test + test: + runs-on: ubuntu-latest + needs: [build] + steps: + - uses: actions/checkout@v2 + - uses: actions/cache@v2 + with: + path: .//node_modules + key: ${{ runner.os }}-yarn-cache-${{ hashFiles('./yarn.lock') }} + - name: test + run: yarn run test diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 00000000..b1d5d03c --- /dev/null +++ b/jest.config.js @@ -0,0 +1,16 @@ +const nextJest = require('next/jest') + +const createJestConfig = nextJest({ + // Provide the path to your Next.js app to load next.config.js and .env files in your test environment + dir: './', +}) + +// Add any custom config to be passed to Jest +/** @type {import('jest').Config} */ +const customJestConfig = { + // Add more setup options before each test is run + // setupFilesAfterEnv: ['/jest.setup.js'], +} + +// createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async +module.exports = createJestConfig(customJestConfig) \ No newline at end of file diff --git a/package.json b/package.json index 24dca102..fc4c1152 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ }, "devDependencies": { "@trivago/prettier-plugin-sort-imports": "^4.1.1", + "@types/jest": "^29.5.3", "@types/node": "^18.11.18", "@types/react": "^18.2.7", "@types/react-dom": "^18.2.4", @@ -40,6 +41,7 @@ "eslint": "^8.41.0", "eslint-config-next": "^13.4.3", "eslint-config-prettier": "^8.8.0", + "jest": "^29.6.3", "postcss": "^8.4.23", "prettier": "^2.8.8", "tailwindcss": "^3.3.2", @@ -62,6 +64,7 @@ "typecheck": "tsc", "lint": "next lint", "start": "next start", + "test": "jest", "prettier": "prettier --write ./src" }, "types": "dist/src/index.d.ts", diff --git a/src/features/tokens/SelectOrInputTokenIds.tsx b/src/features/tokens/SelectOrInputTokenIds.tsx index 149e85b2..37661bb8 100644 --- a/src/features/tokens/SelectOrInputTokenIds.tsx +++ b/src/features/tokens/SelectOrInputTokenIds.tsx @@ -24,7 +24,7 @@ export function SelectOrInputTokenIds({ const route = getTokenRoute(originCaip2Id, destinationCaip2Id, tokenCaip19Id, tokenRoutes); let activeToken = '' as TokenCaip19Id; - if (route?.type === RouteType.BaseToSynthetic) { + if (route?.type === RouteType.CollateralToSynthetic) { // If the origin is the base chain, use the collateralized token for balance checking activeToken = tokenCaip19Id; } else if (route) { diff --git a/src/features/tokens/adapters/AdapterFactory.ts b/src/features/tokens/adapters/AdapterFactory.ts index 3d6d20bc..7de66b60 100644 --- a/src/features/tokens/adapters/AdapterFactory.ts +++ b/src/features/tokens/adapters/AdapterFactory.ts @@ -12,7 +12,13 @@ import { parseCaip19Id, } from '../../caip/tokens'; import { getMultiProvider, getProvider } from '../../multiProvider'; -import { Route, RouteType } from '../routes/types'; +import { Route } from '../routes/types'; +import { + isRouteFromCollateral, + isRouteFromSynthetic, + isRouteToCollateral, + isRouteToSynthetic, +} from '../routes/utils'; import { EvmHypCollateralAdapter, @@ -75,7 +81,7 @@ export class AdapterFactory { static HypTokenAdapterFromRouteOrigin(route: Route) { const { type, originCaip2Id, originRouterAddress, baseTokenCaip19Id } = route; - if (type === RouteType.BaseToSynthetic) { + if (isRouteFromCollateral(route)) { return AdapterFactory.selectHypAdapter( originCaip2Id, originRouterAddress, @@ -83,7 +89,7 @@ export class AdapterFactory { EvmHypCollateralAdapter, isNativeToken(baseTokenCaip19Id) ? SealevelHypNativeAdapter : SealevelHypCollateralAdapter, ); - } else if (type === RouteType.SyntheticToBase || type === RouteType.SyntheticToSynthetic) { + } else if (isRouteFromSynthetic(route)) { return AdapterFactory.selectHypAdapter( originCaip2Id, originRouterAddress, @@ -98,7 +104,7 @@ export class AdapterFactory { static HypTokenAdapterFromRouteDest(route: Route) { const { type, destCaip2Id, destRouterAddress, baseTokenCaip19Id } = route; - if (type === RouteType.SyntheticToBase) { + if (isRouteToCollateral(route)) { return AdapterFactory.selectHypAdapter( destCaip2Id, destRouterAddress, @@ -106,7 +112,7 @@ export class AdapterFactory { EvmHypCollateralAdapter, isNativeToken(baseTokenCaip19Id) ? SealevelHypNativeAdapter : SealevelHypCollateralAdapter, ); - } else if (type === RouteType.BaseToSynthetic || type === RouteType.SyntheticToSynthetic) { + } else if (isRouteToSynthetic(route)) { return AdapterFactory.selectHypAdapter( destCaip2Id, destRouterAddress, diff --git a/src/features/tokens/routes/fetch.test.ts b/src/features/tokens/routes/fetch.test.ts new file mode 100644 index 00000000..1fabfb59 --- /dev/null +++ b/src/features/tokens/routes/fetch.test.ts @@ -0,0 +1,252 @@ +import { TokenType } from '@hyperlane-xyz/hyperlane-token'; + +import { SOL_ZERO_ADDRESS } from '../../../consts/values'; + +import { computeTokenRoutes } from './fetch'; + +describe('computeTokenRoutes', () => { + it('Handles empty list', () => { + const routesMap = computeTokenRoutes([]); + expect(routesMap).toBeTruthy(); + expect(Object.values(routesMap).length).toBe(0); + }); + + it('Handles basic 3-node route', () => { + const routesMap = computeTokenRoutes([ + { + type: TokenType.collateral, + tokenCaip19Id: 'ethereum:5/erc20:0xb4fbf271143f4fbf7b91a5ded31805e42b2208d6', + routerAddress: '0x145de8760021c4ac6676376691b78038d3DE9097', + name: 'Weth', + symbol: 'WETH', + decimals: 18, + hypTokens: [ + { + decimals: 18, + tokenCaip19Id: 'ethereum:11155111/erc20:0xDcbc0faAA269Cf649AC8950838664BB7B355BD6B', + }, + { + decimals: 18, + tokenCaip19Id: 'ethereum:44787/erc20:0xDcbc0faAA269Cf649AC8950838664BB7B355BD6B', + }, + ], + }, + ]); + expect(routesMap).toEqual({ + 'ethereum:5': { + 'ethereum:11155111': [ + { + type: 'collateralToSynthetic', + baseTokenCaip19Id: 'ethereum:5/erc20:0xb4fbf271143f4fbf7b91a5ded31805e42b2208d6', + baseRouterAddress: '0x145de8760021c4ac6676376691b78038d3DE9097', + originCaip2Id: 'ethereum:5', + originRouterAddress: '0x145de8760021c4ac6676376691b78038d3DE9097', + originDecimals: 18, + destCaip2Id: 'ethereum:11155111', + destRouterAddress: '0xDcbc0faAA269Cf649AC8950838664BB7B355BD6B', + destDecimals: 18, + }, + ], + 'ethereum:44787': [ + { + type: 'collateralToSynthetic', + baseTokenCaip19Id: 'ethereum:5/erc20:0xb4fbf271143f4fbf7b91a5ded31805e42b2208d6', + baseRouterAddress: '0x145de8760021c4ac6676376691b78038d3DE9097', + originCaip2Id: 'ethereum:5', + originRouterAddress: '0x145de8760021c4ac6676376691b78038d3DE9097', + originDecimals: 18, + destCaip2Id: 'ethereum:44787', + destRouterAddress: '0xDcbc0faAA269Cf649AC8950838664BB7B355BD6B', + destDecimals: 18, + }, + ], + }, + 'ethereum:11155111': { + 'ethereum:5': [ + { + type: 'syntheticToCollateral', + baseTokenCaip19Id: 'ethereum:5/erc20:0xb4fbf271143f4fbf7b91a5ded31805e42b2208d6', + baseRouterAddress: '0x145de8760021c4ac6676376691b78038d3DE9097', + originCaip2Id: 'ethereum:11155111', + originRouterAddress: '0xDcbc0faAA269Cf649AC8950838664BB7B355BD6B', + originDecimals: 18, + destCaip2Id: 'ethereum:5', + destRouterAddress: '0x145de8760021c4ac6676376691b78038d3DE9097', + destDecimals: 18, + }, + ], + 'ethereum:44787': [ + { + type: 'syntheticToSynthetic', + baseTokenCaip19Id: 'ethereum:5/erc20:0xb4fbf271143f4fbf7b91a5ded31805e42b2208d6', + baseRouterAddress: '0x145de8760021c4ac6676376691b78038d3DE9097', + originCaip2Id: 'ethereum:11155111', + originRouterAddress: '0xDcbc0faAA269Cf649AC8950838664BB7B355BD6B', + originDecimals: 18, + destCaip2Id: 'ethereum:44787', + destRouterAddress: '0xDcbc0faAA269Cf649AC8950838664BB7B355BD6B', + destDecimals: 18, + }, + ], + }, + 'ethereum:44787': { + 'ethereum:5': [ + { + type: 'syntheticToCollateral', + baseTokenCaip19Id: 'ethereum:5/erc20:0xb4fbf271143f4fbf7b91a5ded31805e42b2208d6', + baseRouterAddress: '0x145de8760021c4ac6676376691b78038d3DE9097', + originCaip2Id: 'ethereum:44787', + originRouterAddress: '0xDcbc0faAA269Cf649AC8950838664BB7B355BD6B', + originDecimals: 18, + destCaip2Id: 'ethereum:5', + destRouterAddress: '0x145de8760021c4ac6676376691b78038d3DE9097', + destDecimals: 18, + }, + ], + 'ethereum:11155111': [ + { + type: 'syntheticToSynthetic', + baseTokenCaip19Id: 'ethereum:5/erc20:0xb4fbf271143f4fbf7b91a5ded31805e42b2208d6', + baseRouterAddress: '0x145de8760021c4ac6676376691b78038d3DE9097', + originCaip2Id: 'ethereum:44787', + originRouterAddress: '0xDcbc0faAA269Cf649AC8950838664BB7B355BD6B', + originDecimals: 18, + destCaip2Id: 'ethereum:11155111', + destRouterAddress: '0xDcbc0faAA269Cf649AC8950838664BB7B355BD6B', + destDecimals: 18, + }, + ], + }, + }); + }); + + it('Handles multi-collateral route', () => { + const routesMap = computeTokenRoutes([ + { + type: TokenType.collateral, + tokenCaip19Id: 'ethereum:5/erc20:0xb4fbf271143f4fbf7b91a5ded31805e42b2208d6', + routerAddress: '0x145de8760021c4ac6676376691b78038d3DE9097', + name: 'Weth', + symbol: 'WETH', + decimals: 18, + hypTokens: [ + { + decimals: 18, + tokenCaip19Id: 'ethereum:11155111/erc20:0xDcbc0faAA269Cf649AC8950838664BB7B355BD6B', + }, + { + decimals: 6, + tokenCaip19Id: 'sealevel:1399811151/native:PJH5QAbxAqrrnSXfH3GHR8icua8CDFZmo97z91xmpvx', + }, + ], + }, + { + type: TokenType.native, + tokenCaip19Id: `sealevel:1399811151/native:${SOL_ZERO_ADDRESS}`, + routerAddress: 'PJH5QAbxAqrrnSXfH3GHR8icua8CDFZmo97z91xmpvx', + name: 'Zebec', + symbol: 'ZBC', + decimals: 6, + hypTokens: [ + { + decimals: 18, + tokenCaip19Id: 'ethereum:11155111/erc20:0xDcbc0faAA269Cf649AC8950838664BB7B355BD6B', + }, + { + decimals: 18, + tokenCaip19Id: 'ethereum:5/erc20:0x145de8760021c4ac6676376691b78038d3DE9097', + }, + ], + }, + ]); + expect(routesMap).toEqual({ + 'ethereum:5': { + 'ethereum:11155111': [ + { + type: 'collateralToSynthetic', + baseTokenCaip19Id: 'ethereum:5/erc20:0xb4fbf271143f4fbf7b91a5ded31805e42b2208d6', + baseRouterAddress: '0x145de8760021c4ac6676376691b78038d3DE9097', + originCaip2Id: 'ethereum:5', + originRouterAddress: '0x145de8760021c4ac6676376691b78038d3DE9097', + originDecimals: 18, + destCaip2Id: 'ethereum:11155111', + destRouterAddress: '0xDcbc0faAA269Cf649AC8950838664BB7B355BD6B', + destDecimals: 18, + }, + ], + 'sealevel:1399811151': [ + { + type: 'collateralToCollateral', + baseTokenCaip19Id: 'ethereum:5/erc20:0xb4fbf271143f4fbf7b91a5ded31805e42b2208d6', + baseRouterAddress: '0x145de8760021c4ac6676376691b78038d3DE9097', + originCaip2Id: 'ethereum:5', + originRouterAddress: '0x145de8760021c4ac6676376691b78038d3DE9097', + originDecimals: 18, + destCaip2Id: 'sealevel:1399811151', + destRouterAddress: 'PJH5QAbxAqrrnSXfH3GHR8icua8CDFZmo97z91xmpvx', + destDecimals: 6, + }, + ], + }, + 'ethereum:11155111': { + 'ethereum:5': [ + { + type: 'syntheticToCollateral', + baseTokenCaip19Id: 'ethereum:5/erc20:0xb4fbf271143f4fbf7b91a5ded31805e42b2208d6', + baseRouterAddress: '0x145de8760021c4ac6676376691b78038d3DE9097', + originCaip2Id: 'ethereum:11155111', + originRouterAddress: '0xDcbc0faAA269Cf649AC8950838664BB7B355BD6B', + originDecimals: 18, + destCaip2Id: 'ethereum:5', + destRouterAddress: '0x145de8760021c4ac6676376691b78038d3DE9097', + destDecimals: 18, + }, + ], + 'sealevel:1399811151': [ + { + type: 'syntheticToCollateral', + baseTokenCaip19Id: + 'sealevel:1399811151/native:00000000000000000000000000000000000000000000', + baseRouterAddress: 'PJH5QAbxAqrrnSXfH3GHR8icua8CDFZmo97z91xmpvx', + originCaip2Id: 'ethereum:11155111', + originRouterAddress: '0xDcbc0faAA269Cf649AC8950838664BB7B355BD6B', + originDecimals: 18, + destCaip2Id: 'sealevel:1399811151', + destRouterAddress: 'PJH5QAbxAqrrnSXfH3GHR8icua8CDFZmo97z91xmpvx', + destDecimals: 6, + }, + ], + }, + 'sealevel:1399811151': { + 'ethereum:5': [ + { + type: 'collateralToCollateral', + baseTokenCaip19Id: + 'sealevel:1399811151/native:00000000000000000000000000000000000000000000', + baseRouterAddress: 'PJH5QAbxAqrrnSXfH3GHR8icua8CDFZmo97z91xmpvx', + originCaip2Id: 'sealevel:1399811151', + originRouterAddress: 'PJH5QAbxAqrrnSXfH3GHR8icua8CDFZmo97z91xmpvx', + originDecimals: 6, + destCaip2Id: 'ethereum:5', + destRouterAddress: '0x145de8760021c4ac6676376691b78038d3DE9097', + destDecimals: 18, + }, + ], + 'ethereum:11155111': [ + { + type: 'collateralToSynthetic', + baseTokenCaip19Id: + 'sealevel:1399811151/native:00000000000000000000000000000000000000000000', + baseRouterAddress: 'PJH5QAbxAqrrnSXfH3GHR8icua8CDFZmo97z91xmpvx', + originCaip2Id: 'sealevel:1399811151', + originRouterAddress: 'PJH5QAbxAqrrnSXfH3GHR8icua8CDFZmo97z91xmpvx', + originDecimals: 6, + destCaip2Id: 'ethereum:11155111', + destRouterAddress: '0xDcbc0faAA269Cf649AC8950838664BB7B355BD6B', + destDecimals: 18, + }, + ], + }, + }); + }); +}); diff --git a/src/features/tokens/routes/fetch.ts b/src/features/tokens/routes/fetch.ts new file mode 100644 index 00000000..acb47501 --- /dev/null +++ b/src/features/tokens/routes/fetch.ts @@ -0,0 +1,166 @@ +import { ProtocolType } from '@hyperlane-xyz/sdk'; + +import { areAddressesEqual, bytesToProtocolAddress } from '../../../utils/addresses'; +import { logger } from '../../../utils/logger'; +import { getCaip2Id } from '../../caip/chains'; +import { + getCaip19Id, + getChainIdFromToken, + isNonFungibleToken, + parseCaip19Id, + resolveAssetNamespace, +} from '../../caip/tokens'; +import { getMultiProvider } from '../../multiProvider'; +import { AdapterFactory } from '../adapters/AdapterFactory'; +import { TokenMetadata, TokenMetadataWithHypTokens } from '../types'; + +import { RouteType, RoutesMap } from './types'; + +export async function fetchRemoteHypTokens( + baseToken: TokenMetadata, + allTokens: TokenMetadata[], +): Promise { + const { + symbol: baseSymbol, + tokenCaip19Id: baseTokenCaip19Id, + routerAddress: baseRouter, + } = baseToken; + const isNft = isNonFungibleToken(baseTokenCaip19Id); + logger.info(`Fetching remote tokens for symbol ${baseSymbol} (${baseTokenCaip19Id})`); + + const baseAdapter = AdapterFactory.HypCollateralAdapterFromAddress(baseTokenCaip19Id, baseRouter); + + const remoteRouters = await baseAdapter.getAllRouters(); + logger.info(`Router addresses found:`, remoteRouters); + + const multiProvider = getMultiProvider(); + const hypTokens = await Promise.all( + remoteRouters.map(async (router) => { + const destMetadata = multiProvider.getChainMetadata(router.domain); + const protocol = destMetadata.protocol || ProtocolType.Ethereum; + const chainCaip2Id = getCaip2Id(protocol, multiProvider.getChainId(router.domain)); + const namespace = resolveAssetNamespace(protocol, false, isNft, true); + const formattedAddress = bytesToProtocolAddress(router.address, protocol); + const tokenCaip19Id = getCaip19Id(chainCaip2Id, namespace, formattedAddress); + if (isNft) return { tokenCaip19Id, decimals: 0 }; + // Attempt to find the decimals from the token list + const routerMetadata = allTokens.find((token) => + areAddressesEqual(formattedAddress, token.routerAddress), + ); + if (routerMetadata) return { tokenCaip19Id, decimals: routerMetadata.decimals }; + // Otherwise try to query the contract + const remoteAdapter = AdapterFactory.HypSyntheticTokenAdapterFromAddress( + baseTokenCaip19Id, + chainCaip2Id, + formattedAddress, + ); + const metadata = await remoteAdapter.getMetadata(); + return { tokenCaip19Id, decimals: metadata.decimals }; + }), + ); + return { ...baseToken, hypTokens }; +} + +// Process token list to populates routesCache with all possible token routes (e.g. router pairs) +export function computeTokenRoutes(tokens: TokenMetadataWithHypTokens[]) { + const tokenRoutes: RoutesMap = {}; + + // Instantiate map structure + const allChainIds = getChainsFromTokens(tokens); + for (const origin of allChainIds) { + tokenRoutes[origin] = {}; + for (const dest of allChainIds) { + if (origin === dest) continue; + tokenRoutes[origin][dest] = []; + } + } + + // Compute all possible routes, in both directions + for (const token of tokens) { + for (const remoteHypToken of token.hypTokens) { + const { + tokenCaip19Id: baseTokenCaip19Id, + routerAddress: baseRouterAddress, + decimals: baseDecimals, + } = token; + const baseChainCaip2Id = getChainIdFromToken(baseTokenCaip19Id); + const { chainCaip2Id: remoteCaip2Id, address: remoteRouterAddress } = parseCaip19Id( + remoteHypToken.tokenCaip19Id, + ); + const remoteDecimals = remoteHypToken.decimals; + // Check if the token list contains the dest router address, meaning it's also a base collateral token + const isRemoteCollateral = tokensHasRouter(tokens, remoteRouterAddress); + const commonRouteProps = { baseTokenCaip19Id, baseRouterAddress }; + + // Register a route from the base to the remote + tokenRoutes[baseChainCaip2Id][remoteCaip2Id]?.push({ + type: isRemoteCollateral + ? RouteType.CollateralToCollateral + : RouteType.CollateralToSynthetic, + ...commonRouteProps, + originCaip2Id: baseChainCaip2Id, + originRouterAddress: baseRouterAddress, + originDecimals: baseDecimals, + destCaip2Id: remoteCaip2Id, + destRouterAddress: remoteRouterAddress, + destDecimals: remoteDecimals, + }); + + // If the remote is not a synthetic (i.e. it's a native/collateral token with it's own config) + // then stop here to avoid duplicate route entries. + if (isRemoteCollateral) continue; + + // Register a route back from the synthetic remote to the base + tokenRoutes[remoteCaip2Id][baseChainCaip2Id]?.push({ + type: RouteType.SyntheticToCollateral, + ...commonRouteProps, + originCaip2Id: remoteCaip2Id, + originRouterAddress: remoteRouterAddress, + originDecimals: remoteDecimals, + destCaip2Id: baseChainCaip2Id, + destRouterAddress: baseRouterAddress, + destDecimals: baseDecimals, + }); + + // Now create routes from the remote synthetic token to all other hypTokens + // This assumes the synthetics were all enrolled to connect to each other + // which is the deployer's default behavior + for (const otherHypToken of token.hypTokens) { + const { chainCaip2Id: otherSynCaip2Id, address: otherHypTokenAddress } = parseCaip19Id( + otherHypToken.tokenCaip19Id, + ); + // Skip if it's same hypToken as parent loop (no route to self) + // or if if remote isn't a synthetic + if (otherHypToken === remoteHypToken || tokensHasRouter(tokens, otherHypTokenAddress)) + continue; + + tokenRoutes[remoteCaip2Id][otherSynCaip2Id]?.push({ + type: RouteType.SyntheticToSynthetic, + ...commonRouteProps, + originCaip2Id: remoteCaip2Id, + originRouterAddress: remoteRouterAddress, + originDecimals: remoteDecimals, + destCaip2Id: otherSynCaip2Id, + destRouterAddress: otherHypTokenAddress, + destDecimals: otherHypToken.decimals, + }); + } + } + } + return tokenRoutes; +} + +function getChainsFromTokens(tokens: TokenMetadataWithHypTokens[]): ChainCaip2Id[] { + const chains = new Set(); + for (const token of tokens) { + chains.add(getChainIdFromToken(token.tokenCaip19Id)); + for (const hypToken of token.hypTokens) { + chains.add(getChainIdFromToken(hypToken.tokenCaip19Id)); + } + } + return Array.from(chains); +} + +function tokensHasRouter(tokens: TokenMetadataWithHypTokens[], router: Address) { + return !!tokens.find((t) => areAddressesEqual(t.routerAddress, router)); +} diff --git a/src/features/tokens/routes/hooks.ts b/src/features/tokens/routes/hooks.ts index e309bb0d..b393be97 100644 --- a/src/features/tokens/routes/hooks.ts +++ b/src/features/tokens/routes/hooks.ts @@ -1,24 +1,13 @@ import { useQuery } from '@tanstack/react-query'; import { useMemo } from 'react'; -import { ProtocolType } from '@hyperlane-xyz/sdk'; - -import { areAddressesEqual, bytesToProtocolAddress } from '../../../utils/addresses'; import { logger } from '../../../utils/logger'; -import { getCaip2Id } from '../../caip/chains'; -import { - getCaip19Id, - getChainIdFromToken, - isNonFungibleToken, - parseCaip19Id, - resolveAssetNamespace, -} from '../../caip/tokens'; -import { getMultiProvider } from '../../multiProvider'; -import { AdapterFactory } from '../adapters/AdapterFactory'; +import { getChainIdFromToken } from '../../caip/tokens'; import { getTokens, parseTokens } from '../metadata'; -import { TokenMetadata, TokenMetadataWithHypTokens } from '../types'; +import { TokenMetadataWithHypTokens } from '../types'; -import { RouteType, RoutesMap } from './types'; +import { computeTokenRoutes, fetchRemoteHypTokens } from './fetch'; +import { RoutesMap } from './types'; export function useTokenRoutes() { const { @@ -44,137 +33,6 @@ export function useTokenRoutes() { return { isLoading, error, tokenRoutes }; } -async function fetchRemoteHypTokens( - baseToken: TokenMetadata, - allTokens: TokenMetadata[], -): Promise { - const { - symbol: baseSymbol, - tokenCaip19Id: baseTokenCaip19Id, - routerAddress: baseRouter, - } = baseToken; - const isNft = isNonFungibleToken(baseTokenCaip19Id); - logger.info(`Fetching remote tokens for symbol ${baseSymbol} (${baseTokenCaip19Id})`); - - const baseAdapter = AdapterFactory.HypCollateralAdapterFromAddress(baseTokenCaip19Id, baseRouter); - - const remoteRouters = await baseAdapter.getAllRouters(); - logger.info(`Router addresses found:`, remoteRouters); - - const multiProvider = getMultiProvider(); - const hypTokens = await Promise.all( - remoteRouters.map(async (router) => { - const destMetadata = multiProvider.getChainMetadata(router.domain); - const protocol = destMetadata.protocol || ProtocolType.Ethereum; - const chainCaip2Id = getCaip2Id(protocol, multiProvider.getChainId(router.domain)); - const namespace = resolveAssetNamespace(protocol, false, isNft, true); - const formattedAddress = bytesToProtocolAddress(router.address, protocol); - const tokenCaip19Id = getCaip19Id(chainCaip2Id, namespace, formattedAddress); - if (isNft) return { tokenCaip19Id, decimals: 0 }; - // Attempt to find the decimals from the token list - const routerMetadata = allTokens.find((token) => - areAddressesEqual(formattedAddress, token.routerAddress), - ); - if (routerMetadata) return { tokenCaip19Id, decimals: routerMetadata.decimals }; - // Otherwise try to query the contract - const remoteAdapter = AdapterFactory.HypSyntheticTokenAdapterFromAddress( - baseTokenCaip19Id, - chainCaip2Id, - formattedAddress, - ); - const metadata = await remoteAdapter.getMetadata(); - return { tokenCaip19Id, decimals: metadata.decimals }; - }), - ); - return { ...baseToken, hypTokens }; -} - -// Process token list to populates routesCache with all possible token routes (e.g. router pairs) -function computeTokenRoutes(tokens: TokenMetadataWithHypTokens[]) { - const tokenRoutes: RoutesMap = {}; - - // Instantiate map structure - const allChainIds = getChainsFromTokens(tokens); - for (const origin of allChainIds) { - tokenRoutes[origin] = {}; - for (const dest of allChainIds) { - if (origin === dest) continue; - tokenRoutes[origin][dest] = []; - } - } - - // Compute all possible routes, in both directions - for (const token of tokens) { - for (const hypToken of token.hypTokens) { - const { - tokenCaip19Id: baseTokenCaip19Id, - routerAddress: baseRouterAddress, - decimals: baseDecimals, - } = token; - const baseChainCaip2Id = getChainIdFromToken(baseTokenCaip19Id); - const { chainCaip2Id: syntheticCaip2Id, address: syntheticRouterAddress } = parseCaip19Id( - hypToken.tokenCaip19Id, - ); - const syntheticDecimals = hypToken.decimals; - - const commonRouteProps = { - baseTokenCaip19Id, - baseRouterAddress, - }; - tokenRoutes[baseChainCaip2Id][syntheticCaip2Id]?.push({ - type: RouteType.BaseToSynthetic, - ...commonRouteProps, - originCaip2Id: baseChainCaip2Id, - originRouterAddress: baseRouterAddress, - originDecimals: baseDecimals, - destCaip2Id: syntheticCaip2Id, - destRouterAddress: syntheticRouterAddress, - destDecimals: syntheticDecimals, - }); - tokenRoutes[syntheticCaip2Id][baseChainCaip2Id]?.push({ - type: RouteType.SyntheticToBase, - ...commonRouteProps, - originCaip2Id: syntheticCaip2Id, - originRouterAddress: syntheticRouterAddress, - originDecimals: syntheticDecimals, - destCaip2Id: baseChainCaip2Id, - destRouterAddress: baseRouterAddress, - destDecimals: baseDecimals, - }); - - for (const otherHypToken of token.hypTokens) { - // Skip if it's same hypToken as parent loop (no route to self) - if (otherHypToken === hypToken) continue; - const { chainCaip2Id: otherSynCaip2Id, address: otherHypTokenAddress } = parseCaip19Id( - otherHypToken.tokenCaip19Id, - ); - tokenRoutes[syntheticCaip2Id][otherSynCaip2Id]?.push({ - type: RouteType.SyntheticToSynthetic, - ...commonRouteProps, - originCaip2Id: syntheticCaip2Id, - originRouterAddress: syntheticRouterAddress, - originDecimals: syntheticDecimals, - destCaip2Id: otherSynCaip2Id, - destRouterAddress: otherHypTokenAddress, - destDecimals: otherHypToken.decimals, - }); - } - } - } - return tokenRoutes; -} - -function getChainsFromTokens(tokens: TokenMetadataWithHypTokens[]): ChainCaip2Id[] { - const chains = new Set(); - for (const token of tokens) { - chains.add(getChainIdFromToken(token.tokenCaip19Id)); - for (const hypToken of token.hypTokens) { - chains.add(getChainIdFromToken(hypToken.tokenCaip19Id)); - } - } - return Array.from(chains); -} - export function useRouteChains(tokenRoutes: RoutesMap): ChainCaip2Id[] { return useMemo(() => { const allCaip2Ids = Object.keys(tokenRoutes) as ChainCaip2Id[]; diff --git a/src/features/tokens/routes/types.ts b/src/features/tokens/routes/types.ts index 07590623..31772af0 100644 --- a/src/features/tokens/routes/types.ts +++ b/src/features/tokens/routes/types.ts @@ -1,7 +1,8 @@ export enum RouteType { - BaseToSynthetic = 'baseToSynthetic', + CollateralToCollateral = 'collateralToCollateral', + CollateralToSynthetic = 'collateralToSynthetic', SyntheticToSynthetic = 'syntheticToSynthetic', - SyntheticToBase = 'syntheticToBase', + SyntheticToCollateral = 'syntheticToCollateral', } export interface Route { diff --git a/src/features/tokens/routes/utils.ts b/src/features/tokens/routes/utils.ts index d60b3859..81e6774f 100644 --- a/src/features/tokens/routes/utils.ts +++ b/src/features/tokens/routes/utils.ts @@ -1,4 +1,4 @@ -import { Route, RoutesMap } from './types'; +import { Route, RouteType, RoutesMap } from './types'; export function getTokenRoutes( originCaip2Id: ChainCaip2Id, @@ -28,3 +28,29 @@ export function hasTokenRoute( ): boolean { return !!getTokenRoute(originCaip2Id, destinationCaip2Id, tokenCaip19Id, tokenRoutes); } + +export function isRouteToCollateral(route: Route) { + return ( + route.type === RouteType.CollateralToCollateral || + route.type === RouteType.SyntheticToCollateral + ); +} + +export function isRouteFromCollateral(route: Route) { + return ( + route.type === RouteType.CollateralToCollateral || + route.type === RouteType.CollateralToSynthetic + ); +} + +export function isRouteToSynthetic(route: Route) { + return ( + route.type === RouteType.CollateralToSynthetic || route.type === RouteType.SyntheticToSynthetic + ); +} + +export function isRouteFromSynthetic(route: Route) { + return ( + route.type === RouteType.SyntheticToCollateral || route.type === RouteType.SyntheticToSynthetic + ); +} diff --git a/src/features/transfer/useTokenTransfer.ts b/src/features/transfer/useTokenTransfer.ts index 7d362975..1c663b7e 100644 --- a/src/features/transfer/useTokenTransfer.ts +++ b/src/features/transfer/useTokenTransfer.ts @@ -14,8 +14,8 @@ import { getMultiProvider } from '../multiProvider'; import { AppState, useStore } from '../store'; import { AdapterFactory } from '../tokens/adapters/AdapterFactory'; import { IHypTokenAdapter } from '../tokens/adapters/ITokenAdapter'; -import { Route, RouteType, RoutesMap } from '../tokens/routes/types'; -import { getTokenRoute } from '../tokens/routes/utils'; +import { Route, RoutesMap } from '../tokens/routes/types'; +import { getTokenRoute, isRouteFromCollateral, isRouteToCollateral } from '../tokens/routes/utils'; import { AccountInfo, ActiveChainInfo, @@ -183,7 +183,7 @@ async function executeTransfer({ // it's possible that the collateral contract balance is insufficient to // cover the remote transfer. This ensures the balance is sufficient or throws. async function ensureSufficientCollateral(route: Route, weiAmount: string, isNft?: boolean) { - if (route.type !== RouteType.SyntheticToBase || isNft) return; + if (!isRouteToCollateral(route) || isNft) return; const adapter = AdapterFactory.TokenAdapterFromAddress(route.baseTokenCaip19Id); logger.debug('Checking collateral balance for token', route.baseTokenCaip19Id); const balance = await adapter.getBalance(route.baseRouterAddress); @@ -215,7 +215,7 @@ async function executeEvmTransfer({ updateStatus, sendTransaction, }: ExecuteTransferParams) { - const { type: routeType, baseRouterAddress, originCaip2Id, baseTokenCaip19Id } = tokenRoute; + const { baseRouterAddress, originCaip2Id, baseTokenCaip19Id } = tokenRoute; if (isTransferApproveRequired(tokenRoute, baseTokenCaip19Id)) { updateStatus(TransferStatus.CreatingApprove); @@ -244,7 +244,7 @@ async function executeEvmTransfer({ logger.debug('Quoted gas payment', gasPayment); // If sending native tokens (e.g. Eth), the gasPayment must be added to the tx value and sent together const txValue = - routeType === RouteType.BaseToSynthetic && isNativeToken(baseTokenCaip19Id) + isRouteFromCollateral(tokenRoute) && isNativeToken(baseTokenCaip19Id) ? BigNumber.from(gasPayment).add(weiAmountOrId) : gasPayment; const transferTxRequest = (await hypTokenAdapter.populateTransferRemoteTx({ @@ -313,7 +313,7 @@ async function executeSealevelTransfer({ export function isTransferApproveRequired(route: Route, tokenCaip19Id: TokenCaip19Id) { return ( !isNativeToken(tokenCaip19Id) && - route.type === RouteType.BaseToSynthetic && + isRouteFromCollateral(route) && getProtocolType(route.originCaip2Id) === ProtocolType.Ethereum ); } diff --git a/yarn.lock b/yarn.lock index 24c92df0..f1112877 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12,6 +12,16 @@ __metadata: languageName: node linkType: hard +"@ampproject/remapping@npm:^2.2.0": + version: 2.2.1 + resolution: "@ampproject/remapping@npm:2.2.1" + dependencies: + "@jridgewell/gen-mapping": ^0.3.0 + "@jridgewell/trace-mapping": ^0.3.9 + checksum: 03c04fd526acc64a1f4df22651186f3e5ef0a9d6d6530ce4482ec9841269cf7a11dbb8af79237c282d721c5312024ff17529cd72cc4768c11e999b58e2302079 + languageName: node + linkType: hard + "@apocentre/alias-sampling@npm:^0.5.3": version: 0.5.3 resolution: "@apocentre/alias-sampling@npm:0.5.3" @@ -19,6 +29,16 @@ __metadata: languageName: node linkType: hard +"@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.12.13, @babel/code-frame@npm:^7.22.10, @babel/code-frame@npm:^7.22.5": + version: 7.22.10 + resolution: "@babel/code-frame@npm:7.22.10" + dependencies: + "@babel/highlight": ^7.22.10 + chalk: ^2.4.2 + checksum: 89a06534ad19759da6203a71bad120b1d7b2ddc016c8e07d4c56b35dea25e7396c6da60a754e8532a86733092b131ae7f661dbe6ba5d165ea777555daa2ed3c9 + languageName: node + linkType: hard + "@babel/code-frame@npm:^7.16.7, @babel/code-frame@npm:^7.18.6": version: 7.18.6 resolution: "@babel/code-frame@npm:7.18.6" @@ -28,6 +48,36 @@ __metadata: languageName: node linkType: hard +"@babel/compat-data@npm:^7.22.9": + version: 7.22.9 + resolution: "@babel/compat-data@npm:7.22.9" + checksum: bed77d9044ce948b4327b30dd0de0779fa9f3a7ed1f2d31638714ed00229fa71fc4d1617ae0eb1fad419338d3658d0e9a5a083297451e09e73e078d0347ff808 + languageName: node + linkType: hard + +"@babel/core@npm:^7.11.6, @babel/core@npm:^7.12.3": + version: 7.22.10 + resolution: "@babel/core@npm:7.22.10" + dependencies: + "@ampproject/remapping": ^2.2.0 + "@babel/code-frame": ^7.22.10 + "@babel/generator": ^7.22.10 + "@babel/helper-compilation-targets": ^7.22.10 + "@babel/helper-module-transforms": ^7.22.9 + "@babel/helpers": ^7.22.10 + "@babel/parser": ^7.22.10 + "@babel/template": ^7.22.5 + "@babel/traverse": ^7.22.10 + "@babel/types": ^7.22.10 + convert-source-map: ^1.7.0 + debug: ^4.1.0 + gensync: ^1.0.0-beta.2 + json5: ^2.2.2 + semver: ^6.3.1 + checksum: cc4efa09209fe1f733cf512e9e4bb50870b191ab2dee8014e34cd6e731f204e48476cc53b4bbd0825d4d342304d577ae43ff5fd8ab3896080673c343321acb32 + languageName: node + linkType: hard + "@babel/generator@npm:7.17.7": version: 7.17.7 resolution: "@babel/generator@npm:7.17.7" @@ -50,6 +100,31 @@ __metadata: languageName: node linkType: hard +"@babel/generator@npm:^7.22.10, @babel/generator@npm:^7.7.2": + version: 7.22.10 + resolution: "@babel/generator@npm:7.22.10" + dependencies: + "@babel/types": ^7.22.10 + "@jridgewell/gen-mapping": ^0.3.2 + "@jridgewell/trace-mapping": ^0.3.17 + jsesc: ^2.5.1 + checksum: 59a79730abdff9070692834bd3af179e7a9413fa2ff7f83dff3eb888765aeaeb2bfc7b0238a49613ed56e1af05956eff303cc139f2407eda8df974813e486074 + languageName: node + linkType: hard + +"@babel/helper-compilation-targets@npm:^7.22.10": + version: 7.22.10 + resolution: "@babel/helper-compilation-targets@npm:7.22.10" + dependencies: + "@babel/compat-data": ^7.22.9 + "@babel/helper-validator-option": ^7.22.5 + browserslist: ^4.21.9 + lru-cache: ^5.1.1 + semver: ^6.3.1 + checksum: f6f1896816392bcff671bbe6e277307729aee53befb4a66ea126e2a91eda78d819a70d06fa384c74ef46c1595544b94dca50bef6c78438d9ffd31776dafbd435 + languageName: node + linkType: hard + "@babel/helper-environment-visitor@npm:^7.16.7": version: 7.18.9 resolution: "@babel/helper-environment-visitor@npm:7.18.9" @@ -57,6 +132,13 @@ __metadata: languageName: node linkType: hard +"@babel/helper-environment-visitor@npm:^7.22.5": + version: 7.22.5 + resolution: "@babel/helper-environment-visitor@npm:7.22.5" + checksum: 248532077d732a34cd0844eb7b078ff917c3a8ec81a7f133593f71a860a582f05b60f818dc5049c2212e5baa12289c27889a4b81d56ef409b4863db49646c4b1 + languageName: node + linkType: hard + "@babel/helper-function-name@npm:^7.16.7": version: 7.19.0 resolution: "@babel/helper-function-name@npm:7.19.0" @@ -67,6 +149,16 @@ __metadata: languageName: node linkType: hard +"@babel/helper-function-name@npm:^7.22.5": + version: 7.22.5 + resolution: "@babel/helper-function-name@npm:7.22.5" + dependencies: + "@babel/template": ^7.22.5 + "@babel/types": ^7.22.5 + checksum: 6b1f6ce1b1f4e513bf2c8385a557ea0dd7fa37971b9002ad19268ca4384bbe90c09681fe4c076013f33deabc63a53b341ed91e792de741b4b35e01c00238177a + languageName: node + linkType: hard + "@babel/helper-hoist-variables@npm:^7.16.7": version: 7.18.6 resolution: "@babel/helper-hoist-variables@npm:7.18.6" @@ -76,6 +168,55 @@ __metadata: languageName: node linkType: hard +"@babel/helper-hoist-variables@npm:^7.22.5": + version: 7.22.5 + resolution: "@babel/helper-hoist-variables@npm:7.22.5" + dependencies: + "@babel/types": ^7.22.5 + checksum: 394ca191b4ac908a76e7c50ab52102669efe3a1c277033e49467913c7ed6f7c64d7eacbeabf3bed39ea1f41731e22993f763b1edce0f74ff8563fd1f380d92cc + languageName: node + linkType: hard + +"@babel/helper-module-imports@npm:^7.22.5": + version: 7.22.5 + resolution: "@babel/helper-module-imports@npm:7.22.5" + dependencies: + "@babel/types": ^7.22.5 + checksum: 9ac2b0404fa38b80bdf2653fbeaf8e8a43ccb41bd505f9741d820ed95d3c4e037c62a1bcdcb6c9527d7798d2e595924c4d025daed73283badc180ada2c9c49ad + languageName: node + linkType: hard + +"@babel/helper-module-transforms@npm:^7.22.9": + version: 7.22.9 + resolution: "@babel/helper-module-transforms@npm:7.22.9" + dependencies: + "@babel/helper-environment-visitor": ^7.22.5 + "@babel/helper-module-imports": ^7.22.5 + "@babel/helper-simple-access": ^7.22.5 + "@babel/helper-split-export-declaration": ^7.22.6 + "@babel/helper-validator-identifier": ^7.22.5 + peerDependencies: + "@babel/core": ^7.0.0 + checksum: 2751f77660518cf4ff027514d6f4794f04598c6393be7b04b8e46c6e21606e11c19f3f57ab6129a9c21bacdf8b3ffe3af87bb401d972f34af2d0ffde02ac3001 + languageName: node + linkType: hard + +"@babel/helper-plugin-utils@npm:^7.0.0, @babel/helper-plugin-utils@npm:^7.10.4, @babel/helper-plugin-utils@npm:^7.12.13, @babel/helper-plugin-utils@npm:^7.14.5, @babel/helper-plugin-utils@npm:^7.22.5, @babel/helper-plugin-utils@npm:^7.8.0": + version: 7.22.5 + resolution: "@babel/helper-plugin-utils@npm:7.22.5" + checksum: c0fc7227076b6041acd2f0e818145d2e8c41968cc52fb5ca70eed48e21b8fe6dd88a0a91cbddf4951e33647336eb5ae184747ca706817ca3bef5e9e905151ff5 + languageName: node + linkType: hard + +"@babel/helper-simple-access@npm:^7.22.5": + version: 7.22.5 + resolution: "@babel/helper-simple-access@npm:7.22.5" + dependencies: + "@babel/types": ^7.22.5 + checksum: fe9686714caf7d70aedb46c3cce090f8b915b206e09225f1e4dbc416786c2fdbbee40b38b23c268b7ccef749dd2db35f255338fb4f2444429874d900dede5ad2 + languageName: node + linkType: hard + "@babel/helper-split-export-declaration@npm:^7.16.7": version: 7.18.6 resolution: "@babel/helper-split-export-declaration@npm:7.18.6" @@ -85,6 +226,15 @@ __metadata: languageName: node linkType: hard +"@babel/helper-split-export-declaration@npm:^7.22.6": + version: 7.22.6 + resolution: "@babel/helper-split-export-declaration@npm:7.22.6" + dependencies: + "@babel/types": ^7.22.5 + checksum: e141cace583b19d9195f9c2b8e17a3ae913b7ee9b8120246d0f9ca349ca6f03cb2c001fd5ec57488c544347c0bb584afec66c936511e447fd20a360e591ac921 + languageName: node + linkType: hard + "@babel/helper-string-parser@npm:^7.19.4": version: 7.19.4 resolution: "@babel/helper-string-parser@npm:7.19.4" @@ -92,6 +242,13 @@ __metadata: languageName: node linkType: hard +"@babel/helper-string-parser@npm:^7.22.5": + version: 7.22.5 + resolution: "@babel/helper-string-parser@npm:7.22.5" + checksum: 836851ca5ec813077bbb303acc992d75a360267aa3b5de7134d220411c852a6f17de7c0d0b8c8dcc0f567f67874c00f4528672b2a4f1bc978a3ada64c8c78467 + languageName: node + linkType: hard + "@babel/helper-validator-identifier@npm:^7.16.7, @babel/helper-validator-identifier@npm:^7.19.1": version: 7.19.1 resolution: "@babel/helper-validator-identifier@npm:7.19.1" @@ -106,6 +263,31 @@ __metadata: languageName: node linkType: hard +"@babel/helper-validator-identifier@npm:^7.22.5": + version: 7.22.5 + resolution: "@babel/helper-validator-identifier@npm:7.22.5" + checksum: 7f0f30113474a28298c12161763b49de5018732290ca4de13cdaefd4fd0d635a6fe3f6686c37a02905fb1e64f21a5ee2b55140cf7b070e729f1bd66866506aea + languageName: node + linkType: hard + +"@babel/helper-validator-option@npm:^7.22.5": + version: 7.22.5 + resolution: "@babel/helper-validator-option@npm:7.22.5" + checksum: bbeca8a85ee86990215c0424997438b388b8d642d69b9f86c375a174d3cdeb270efafd1ff128bc7a1d370923d13b6e45829ba8581c027620e83e3a80c5c414b3 + languageName: node + linkType: hard + +"@babel/helpers@npm:^7.22.10": + version: 7.22.10 + resolution: "@babel/helpers@npm:7.22.10" + dependencies: + "@babel/template": ^7.22.5 + "@babel/traverse": ^7.22.10 + "@babel/types": ^7.22.10 + checksum: 3b1219e362df390b6c5d94b75a53fc1c2eb42927ced0b8022d6a29b833a839696206b9bdad45b4805d05591df49fc16b6fb7db758c9c2ecfe99e3e94cb13020f + languageName: node + linkType: hard + "@babel/highlight@npm:^7.18.6": version: 7.18.6 resolution: "@babel/highlight@npm:7.18.6" @@ -117,6 +299,26 @@ __metadata: languageName: node linkType: hard +"@babel/highlight@npm:^7.22.10": + version: 7.22.10 + resolution: "@babel/highlight@npm:7.22.10" + dependencies: + "@babel/helper-validator-identifier": ^7.22.5 + chalk: ^2.4.2 + js-tokens: ^4.0.0 + checksum: f714a1e1a72dd9d72f6383f4f30fd342e21a8df32d984a4ea8f5eab691bb6ba6db2f8823d4b4cf135d98869e7a98925b81306aa32ee3c429f8cfa52c75889e1b + languageName: node + linkType: hard + +"@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.14.7, @babel/parser@npm:^7.20.7, @babel/parser@npm:^7.22.10, @babel/parser@npm:^7.22.5": + version: 7.22.10 + resolution: "@babel/parser@npm:7.22.10" + bin: + parser: ./bin/babel-parser.js + checksum: af51567b7d3cdf523bc608eae057397486c7fa6c2e5753027c01fe5c36f0767b2d01ce3049b222841326cc5b8c7fda1d810ac1a01af0a97bb04679e2ef9f7049 + languageName: node + linkType: hard + "@babel/parser@npm:^7.17.3, @babel/parser@npm:^7.18.10": version: 7.20.3 resolution: "@babel/parser@npm:7.20.3" @@ -135,6 +337,160 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-syntax-async-generators@npm:^7.8.4": + version: 7.8.4 + resolution: "@babel/plugin-syntax-async-generators@npm:7.8.4" + dependencies: + "@babel/helper-plugin-utils": ^7.8.0 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 7ed1c1d9b9e5b64ef028ea5e755c0be2d4e5e4e3d6cf7df757b9a8c4cfa4193d268176d0f1f7fbecdda6fe722885c7fda681f480f3741d8a2d26854736f05367 + languageName: node + linkType: hard + +"@babel/plugin-syntax-bigint@npm:^7.8.3": + version: 7.8.3 + resolution: "@babel/plugin-syntax-bigint@npm:7.8.3" + dependencies: + "@babel/helper-plugin-utils": ^7.8.0 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 3a10849d83e47aec50f367a9e56a6b22d662ddce643334b087f9828f4c3dd73bdc5909aaeabe123fed78515767f9ca43498a0e621c438d1cd2802d7fae3c9648 + languageName: node + linkType: hard + +"@babel/plugin-syntax-class-properties@npm:^7.8.3": + version: 7.12.13 + resolution: "@babel/plugin-syntax-class-properties@npm:7.12.13" + dependencies: + "@babel/helper-plugin-utils": ^7.12.13 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 24f34b196d6342f28d4bad303612d7ff566ab0a013ce89e775d98d6f832969462e7235f3e7eaf17678a533d4be0ba45d3ae34ab4e5a9dcbda5d98d49e5efa2fc + languageName: node + linkType: hard + +"@babel/plugin-syntax-import-meta@npm:^7.8.3": + version: 7.10.4 + resolution: "@babel/plugin-syntax-import-meta@npm:7.10.4" + dependencies: + "@babel/helper-plugin-utils": ^7.10.4 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 166ac1125d10b9c0c430e4156249a13858c0366d38844883d75d27389621ebe651115cb2ceb6dc011534d5055719fa1727b59f39e1ab3ca97820eef3dcab5b9b + languageName: node + linkType: hard + +"@babel/plugin-syntax-json-strings@npm:^7.8.3": + version: 7.8.3 + resolution: "@babel/plugin-syntax-json-strings@npm:7.8.3" + dependencies: + "@babel/helper-plugin-utils": ^7.8.0 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: bf5aea1f3188c9a507e16efe030efb996853ca3cadd6512c51db7233cc58f3ac89ff8c6bdfb01d30843b161cfe7d321e1bf28da82f7ab8d7e6bc5464666f354a + languageName: node + linkType: hard + +"@babel/plugin-syntax-jsx@npm:^7.7.2": + version: 7.22.5 + resolution: "@babel/plugin-syntax-jsx@npm:7.22.5" + dependencies: + "@babel/helper-plugin-utils": ^7.22.5 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 8829d30c2617ab31393d99cec2978e41f014f4ac6f01a1cecf4c4dd8320c3ec12fdc3ce121126b2d8d32f6887e99ca1a0bad53dedb1e6ad165640b92b24980ce + languageName: node + linkType: hard + +"@babel/plugin-syntax-logical-assignment-operators@npm:^7.8.3": + version: 7.10.4 + resolution: "@babel/plugin-syntax-logical-assignment-operators@npm:7.10.4" + dependencies: + "@babel/helper-plugin-utils": ^7.10.4 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: aff33577037e34e515911255cdbb1fd39efee33658aa00b8a5fd3a4b903585112d037cce1cc9e4632f0487dc554486106b79ccd5ea63a2e00df4363f6d4ff886 + languageName: node + linkType: hard + +"@babel/plugin-syntax-nullish-coalescing-operator@npm:^7.8.3": + version: 7.8.3 + resolution: "@babel/plugin-syntax-nullish-coalescing-operator@npm:7.8.3" + dependencies: + "@babel/helper-plugin-utils": ^7.8.0 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 87aca4918916020d1fedba54c0e232de408df2644a425d153be368313fdde40d96088feed6c4e5ab72aac89be5d07fef2ddf329a15109c5eb65df006bf2580d1 + languageName: node + linkType: hard + +"@babel/plugin-syntax-numeric-separator@npm:^7.8.3": + version: 7.10.4 + resolution: "@babel/plugin-syntax-numeric-separator@npm:7.10.4" + dependencies: + "@babel/helper-plugin-utils": ^7.10.4 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 01ec5547bd0497f76cc903ff4d6b02abc8c05f301c88d2622b6d834e33a5651aa7c7a3d80d8d57656a4588f7276eba357f6b7e006482f5b564b7a6488de493a1 + languageName: node + linkType: hard + +"@babel/plugin-syntax-object-rest-spread@npm:^7.8.3": + version: 7.8.3 + resolution: "@babel/plugin-syntax-object-rest-spread@npm:7.8.3" + dependencies: + "@babel/helper-plugin-utils": ^7.8.0 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: fddcf581a57f77e80eb6b981b10658421bc321ba5f0a5b754118c6a92a5448f12a0c336f77b8abf734841e102e5126d69110a306eadb03ca3e1547cab31f5cbf + languageName: node + linkType: hard + +"@babel/plugin-syntax-optional-catch-binding@npm:^7.8.3": + version: 7.8.3 + resolution: "@babel/plugin-syntax-optional-catch-binding@npm:7.8.3" + dependencies: + "@babel/helper-plugin-utils": ^7.8.0 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 910d90e72bc90ea1ce698e89c1027fed8845212d5ab588e35ef91f13b93143845f94e2539d831dc8d8ededc14ec02f04f7bd6a8179edd43a326c784e7ed7f0b9 + languageName: node + linkType: hard + +"@babel/plugin-syntax-optional-chaining@npm:^7.8.3": + version: 7.8.3 + resolution: "@babel/plugin-syntax-optional-chaining@npm:7.8.3" + dependencies: + "@babel/helper-plugin-utils": ^7.8.0 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: eef94d53a1453361553c1f98b68d17782861a04a392840341bc91780838dd4e695209c783631cf0de14c635758beafb6a3a65399846ffa4386bff90639347f30 + languageName: node + linkType: hard + +"@babel/plugin-syntax-top-level-await@npm:^7.8.3": + version: 7.14.5 + resolution: "@babel/plugin-syntax-top-level-await@npm:7.14.5" + dependencies: + "@babel/helper-plugin-utils": ^7.14.5 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: bbd1a56b095be7820029b209677b194db9b1d26691fe999856462e66b25b281f031f3dfd91b1619e9dcf95bebe336211833b854d0fb8780d618e35667c2d0d7e + languageName: node + linkType: hard + +"@babel/plugin-syntax-typescript@npm:^7.7.2": + version: 7.22.5 + resolution: "@babel/plugin-syntax-typescript@npm:7.22.5" + dependencies: + "@babel/helper-plugin-utils": ^7.22.5 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 8ab7718fbb026d64da93681a57797d60326097fd7cb930380c8bffd9eb101689e90142c760a14b51e8e69c88a73ba3da956cb4520a3b0c65743aee5c71ef360a + languageName: node + linkType: hard + "@babel/runtime-corejs3@npm:^7.10.2": version: 7.18.6 resolution: "@babel/runtime-corejs3@npm:7.18.6" @@ -183,6 +539,17 @@ __metadata: languageName: node linkType: hard +"@babel/template@npm:^7.22.5, @babel/template@npm:^7.3.3": + version: 7.22.5 + resolution: "@babel/template@npm:7.22.5" + dependencies: + "@babel/code-frame": ^7.22.5 + "@babel/parser": ^7.22.5 + "@babel/types": ^7.22.5 + checksum: c5746410164039aca61829cdb42e9a55410f43cace6f51ca443313f3d0bdfa9a5a330d0b0df73dc17ef885c72104234ae05efede37c1cc8a72dc9f93425977a3 + languageName: node + linkType: hard + "@babel/traverse@npm:7.17.3": version: 7.17.3 resolution: "@babel/traverse@npm:7.17.3" @@ -201,6 +568,24 @@ __metadata: languageName: node linkType: hard +"@babel/traverse@npm:^7.22.10": + version: 7.22.10 + resolution: "@babel/traverse@npm:7.22.10" + dependencies: + "@babel/code-frame": ^7.22.10 + "@babel/generator": ^7.22.10 + "@babel/helper-environment-visitor": ^7.22.5 + "@babel/helper-function-name": ^7.22.5 + "@babel/helper-hoist-variables": ^7.22.5 + "@babel/helper-split-export-declaration": ^7.22.6 + "@babel/parser": ^7.22.10 + "@babel/types": ^7.22.10 + debug: ^4.1.0 + globals: ^11.1.0 + checksum: 9f7b358563bfb0f57ac4ed639f50e5c29a36b821a1ce1eea0c7db084f5b925e3275846d0de63bde01ca407c85d9804e0efbe370d92cd2baaafde3bd13b0f4cdb + languageName: node + linkType: hard + "@babel/types@npm:7.17.0": version: 7.17.0 resolution: "@babel/types@npm:7.17.0" @@ -211,6 +596,17 @@ __metadata: languageName: node linkType: hard +"@babel/types@npm:^7.0.0, @babel/types@npm:^7.20.7, @babel/types@npm:^7.22.10, @babel/types@npm:^7.22.5, @babel/types@npm:^7.3.3": + version: 7.22.10 + resolution: "@babel/types@npm:7.22.10" + dependencies: + "@babel/helper-string-parser": ^7.22.5 + "@babel/helper-validator-identifier": ^7.22.5 + to-fast-properties: ^2.0.0 + checksum: 095c4f4b7503fa816e4094113f0ec2351ef96ff32012010b771693066ff628c7c664b21c6bd3fb93aeb46fe7c61f6b3a3c9e4ed0034d6a2481201c417371c8af + languageName: node + linkType: hard + "@babel/types@npm:^7.17.0, @babel/types@npm:^7.18.10, @babel/types@npm:^7.19.0, @babel/types@npm:^7.20.2": version: 7.20.2 resolution: "@babel/types@npm:7.20.2" @@ -232,6 +628,13 @@ __metadata: languageName: node linkType: hard +"@bcoe/v8-coverage@npm:^0.2.3": + version: 0.2.3 + resolution: "@bcoe/v8-coverage@npm:0.2.3" + checksum: 850f9305536d0f2bd13e9e0881cb5f02e4f93fad1189f7b2d4bebf694e3206924eadee1068130d43c11b750efcc9405f88a8e42ef098b6d75239c0f047de1a27 + languageName: node + linkType: hard + "@blocto/sdk@npm:^0.2.22": version: 0.2.22 resolution: "@blocto/sdk@npm:0.2.22" @@ -925,6 +1328,7 @@ __metadata: "@solana/web3.js": ^1.77.0 "@tanstack/react-query": ^4.29.7 "@trivago/prettier-plugin-sort-imports": ^4.1.1 + "@types/jest": ^29.5.3 "@types/node": ^18.11.18 "@types/react": ^18.2.7 "@types/react-dom": ^18.2.4 @@ -938,6 +1342,7 @@ __metadata: eslint-config-prettier: ^8.8.0 ethers: ^5.7.2 formik: ^2.2.9 + jest: ^29.6.3 next: ^13.2.4 postcss: ^8.4.23 prettier: ^2.8.8 @@ -964,6 +1369,256 @@ __metadata: languageName: node linkType: hard +"@istanbuljs/load-nyc-config@npm:^1.0.0": + version: 1.1.0 + resolution: "@istanbuljs/load-nyc-config@npm:1.1.0" + dependencies: + camelcase: ^5.3.1 + find-up: ^4.1.0 + get-package-type: ^0.1.0 + js-yaml: ^3.13.1 + resolve-from: ^5.0.0 + checksum: d578da5e2e804d5c93228450a1380e1a3c691de4953acc162f387b717258512a3e07b83510a936d9fab03eac90817473917e24f5d16297af3867f59328d58568 + languageName: node + linkType: hard + +"@istanbuljs/schema@npm:^0.1.2": + version: 0.1.3 + resolution: "@istanbuljs/schema@npm:0.1.3" + checksum: 5282759d961d61350f33d9118d16bcaed914ebf8061a52f4fa474b2cb08720c9c81d165e13b82f2e5a8a212cc5af482f0c6fc1ac27b9e067e5394c9a6ed186c9 + languageName: node + linkType: hard + +"@jest/console@npm:^29.6.3": + version: 29.6.3 + resolution: "@jest/console@npm:29.6.3" + dependencies: + "@jest/types": ^29.6.3 + "@types/node": "*" + chalk: ^4.0.0 + jest-message-util: ^29.6.3 + jest-util: ^29.6.3 + slash: ^3.0.0 + checksum: a30b380166944ac06d36a50a36f05e65022b97064efd3ace7113d1dfc30d96966af578266f69817afa9d6ec679f8ceb6ae905352c07e5ad23d3c307fc0060174 + languageName: node + linkType: hard + +"@jest/core@npm:^29.6.3": + version: 29.6.3 + resolution: "@jest/core@npm:29.6.3" + dependencies: + "@jest/console": ^29.6.3 + "@jest/reporters": ^29.6.3 + "@jest/test-result": ^29.6.3 + "@jest/transform": ^29.6.3 + "@jest/types": ^29.6.3 + "@types/node": "*" + ansi-escapes: ^4.2.1 + chalk: ^4.0.0 + ci-info: ^3.2.0 + exit: ^0.1.2 + graceful-fs: ^4.2.9 + jest-changed-files: ^29.6.3 + jest-config: ^29.6.3 + jest-haste-map: ^29.6.3 + jest-message-util: ^29.6.3 + jest-regex-util: ^29.6.3 + jest-resolve: ^29.6.3 + jest-resolve-dependencies: ^29.6.3 + jest-runner: ^29.6.3 + jest-runtime: ^29.6.3 + jest-snapshot: ^29.6.3 + jest-util: ^29.6.3 + jest-validate: ^29.6.3 + jest-watcher: ^29.6.3 + micromatch: ^4.0.4 + pretty-format: ^29.6.3 + slash: ^3.0.0 + strip-ansi: ^6.0.0 + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + checksum: 8ec37ce75f52dc85dfe703d4f8de31acf2134d1056127d075a700cf3668bad0cccc17f742b39f0053f8c12455075018bd3551093c0b3e082d593980093cb6ce9 + languageName: node + linkType: hard + +"@jest/environment@npm:^29.6.3": + version: 29.6.3 + resolution: "@jest/environment@npm:29.6.3" + dependencies: + "@jest/fake-timers": ^29.6.3 + "@jest/types": ^29.6.3 + "@types/node": "*" + jest-mock: ^29.6.3 + checksum: 96aaf9baaa58fbacbdfbde9591297f25f9d6f5566cf10cd07d744a4a25b1d82b6cfb89f217a45ccce2cc50ec6c7e3c9a0122908d6b827985a1679afb5e10b7b1 + languageName: node + linkType: hard + +"@jest/expect-utils@npm:^29.6.3": + version: 29.6.3 + resolution: "@jest/expect-utils@npm:29.6.3" + dependencies: + jest-get-type: ^29.6.3 + checksum: aeb0c2a485df09fdb51f866d58e232010cde888a7e6e1f9b395df236918e09e98407eb8281a3d41d2b115d9ff740d100b75100d521717ba903abeacb26e2a192 + languageName: node + linkType: hard + +"@jest/expect@npm:^29.6.3": + version: 29.6.3 + resolution: "@jest/expect@npm:29.6.3" + dependencies: + expect: ^29.6.3 + jest-snapshot: ^29.6.3 + checksum: 40c3fc53aa9f86e10129fcaec243405a4b4c398a8d65a3133f97d39331f065c3833c352b133377f003b2e9acc70909d72ac91698c219a883b857b7cda559b199 + languageName: node + linkType: hard + +"@jest/fake-timers@npm:^29.6.3": + version: 29.6.3 + resolution: "@jest/fake-timers@npm:29.6.3" + dependencies: + "@jest/types": ^29.6.3 + "@sinonjs/fake-timers": ^10.0.2 + "@types/node": "*" + jest-message-util: ^29.6.3 + jest-mock: ^29.6.3 + jest-util: ^29.6.3 + checksum: 60be71159bb92c8b8da593fac2b2fff50c0760c26c3b17237561a2818382d3c797bd119a1707ec1d3e9b77e8e3d6513fe88f0c668d6ca26fb2c01ab475620888 + languageName: node + linkType: hard + +"@jest/globals@npm:^29.6.3": + version: 29.6.3 + resolution: "@jest/globals@npm:29.6.3" + dependencies: + "@jest/environment": ^29.6.3 + "@jest/expect": ^29.6.3 + "@jest/types": ^29.6.3 + jest-mock: ^29.6.3 + checksum: c90ad4e85c4c7fa42e4c61fc6bba854dc7e12c3579b4412fe879e712bf3675e92a771d2ac4ba2a48304a4dab34182e62e9d62f36ca13ddf8dff3cca911ddfbbb + languageName: node + linkType: hard + +"@jest/reporters@npm:^29.6.3": + version: 29.6.3 + resolution: "@jest/reporters@npm:29.6.3" + dependencies: + "@bcoe/v8-coverage": ^0.2.3 + "@jest/console": ^29.6.3 + "@jest/test-result": ^29.6.3 + "@jest/transform": ^29.6.3 + "@jest/types": ^29.6.3 + "@jridgewell/trace-mapping": ^0.3.18 + "@types/node": "*" + chalk: ^4.0.0 + collect-v8-coverage: ^1.0.0 + exit: ^0.1.2 + glob: ^7.1.3 + graceful-fs: ^4.2.9 + istanbul-lib-coverage: ^3.0.0 + istanbul-lib-instrument: ^6.0.0 + istanbul-lib-report: ^3.0.0 + istanbul-lib-source-maps: ^4.0.0 + istanbul-reports: ^3.1.3 + jest-message-util: ^29.6.3 + jest-util: ^29.6.3 + jest-worker: ^29.6.3 + slash: ^3.0.0 + string-length: ^4.0.1 + strip-ansi: ^6.0.0 + v8-to-istanbul: ^9.0.1 + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + checksum: 8899240f018874148a24886ac78ada6dda4b7fc621fed904b276b324b981c2294d2036df92fb87411f2abb914faa351098eeb814d7685dcfa37c7c27b54660a4 + languageName: node + linkType: hard + +"@jest/schemas@npm:^29.6.3": + version: 29.6.3 + resolution: "@jest/schemas@npm:29.6.3" + dependencies: + "@sinclair/typebox": ^0.27.8 + checksum: 910040425f0fc93cd13e68c750b7885590b8839066dfa0cd78e7def07bbb708ad869381f725945d66f2284de5663bbecf63e8fdd856e2ae6e261ba30b1687e93 + languageName: node + linkType: hard + +"@jest/source-map@npm:^29.6.3": + version: 29.6.3 + resolution: "@jest/source-map@npm:29.6.3" + dependencies: + "@jridgewell/trace-mapping": ^0.3.18 + callsites: ^3.0.0 + graceful-fs: ^4.2.9 + checksum: bcc5a8697d471396c0003b0bfa09722c3cd879ad697eb9c431e6164e2ea7008238a01a07193dfe3cbb48b1d258eb7251f6efcea36f64e1ebc464ea3c03ae2deb + languageName: node + linkType: hard + +"@jest/test-result@npm:^29.6.3": + version: 29.6.3 + resolution: "@jest/test-result@npm:29.6.3" + dependencies: + "@jest/console": ^29.6.3 + "@jest/types": ^29.6.3 + "@types/istanbul-lib-coverage": ^2.0.0 + collect-v8-coverage: ^1.0.0 + checksum: 0f8164520587555f4e0c5b3e0843ae8ae43c517301c2986b9ff24ca58215f407164b99f3ccfde778dc3fb299c3bb8922a3dd81cf3ccf0ff646806df61d3d2d78 + languageName: node + linkType: hard + +"@jest/test-sequencer@npm:^29.6.3": + version: 29.6.3 + resolution: "@jest/test-sequencer@npm:29.6.3" + dependencies: + "@jest/test-result": ^29.6.3 + graceful-fs: ^4.2.9 + jest-haste-map: ^29.6.3 + slash: ^3.0.0 + checksum: 71b5fee13e28b2006b4bdea62181dd6b7a537531ac027b1230ad96a5a0c7837a4c008e9cbeebee630b0c7cc22187fede48cb18fec79209ff641492c994db8259 + languageName: node + linkType: hard + +"@jest/transform@npm:^29.6.3": + version: 29.6.3 + resolution: "@jest/transform@npm:29.6.3" + dependencies: + "@babel/core": ^7.11.6 + "@jest/types": ^29.6.3 + "@jridgewell/trace-mapping": ^0.3.18 + babel-plugin-istanbul: ^6.1.1 + chalk: ^4.0.0 + convert-source-map: ^2.0.0 + fast-json-stable-stringify: ^2.1.0 + graceful-fs: ^4.2.9 + jest-haste-map: ^29.6.3 + jest-regex-util: ^29.6.3 + jest-util: ^29.6.3 + micromatch: ^4.0.4 + pirates: ^4.0.4 + slash: ^3.0.0 + write-file-atomic: ^4.0.2 + checksum: edc47e960a71dab5ad8f0480fc4c1b05f2950c12e5aeb62bacfd46929dd5c7101dd2fa521a2e59c62a90849118039949f0230282a485de8dc373aac711f1bff9 + languageName: node + linkType: hard + +"@jest/types@npm:^29.6.3": + version: 29.6.3 + resolution: "@jest/types@npm:29.6.3" + dependencies: + "@jest/schemas": ^29.6.3 + "@types/istanbul-lib-coverage": ^2.0.0 + "@types/istanbul-reports": ^3.0.0 + "@types/node": "*" + "@types/yargs": ^17.0.8 + chalk: ^4.0.0 + checksum: a0bcf15dbb0eca6bdd8ce61a3fb055349d40268622a7670a3b2eb3c3dbafe9eb26af59938366d520b86907b9505b0f9b29b85cec11579a9e580694b87cd90fcc + languageName: node + linkType: hard + "@jnwng/walletconnect-solana@npm:^0.1.5": version: 0.1.5 resolution: "@jnwng/walletconnect-solana@npm:0.1.5" @@ -978,6 +1633,17 @@ __metadata: languageName: node linkType: hard +"@jridgewell/gen-mapping@npm:^0.3.0": + version: 0.3.3 + resolution: "@jridgewell/gen-mapping@npm:0.3.3" + dependencies: + "@jridgewell/set-array": ^1.0.1 + "@jridgewell/sourcemap-codec": ^1.4.10 + "@jridgewell/trace-mapping": ^0.3.9 + checksum: 4a74944bd31f22354fc01c3da32e83c19e519e3bbadafa114f6da4522ea77dd0c2842607e923a591d60a76699d819a2fbb6f3552e277efdb9b58b081390b60ab + languageName: node + linkType: hard + "@jridgewell/gen-mapping@npm:^0.3.2": version: 0.3.2 resolution: "@jridgewell/gen-mapping@npm:0.3.2" @@ -996,6 +1662,13 @@ __metadata: languageName: node linkType: hard +"@jridgewell/resolve-uri@npm:^3.1.0": + version: 3.1.1 + resolution: "@jridgewell/resolve-uri@npm:3.1.1" + checksum: f5b441fe7900eab4f9155b3b93f9800a916257f4e8563afbcd3b5a5337b55e52bd8ae6735453b1b745457d9f6cdb16d74cd6220bbdd98cf153239e13f6cbb653 + languageName: node + linkType: hard + "@jridgewell/set-array@npm:^1.0.1": version: 1.1.2 resolution: "@jridgewell/set-array@npm:1.1.2" @@ -1010,6 +1683,13 @@ __metadata: languageName: node linkType: hard +"@jridgewell/sourcemap-codec@npm:^1.4.14": + version: 1.4.15 + resolution: "@jridgewell/sourcemap-codec@npm:1.4.15" + checksum: b881c7e503db3fc7f3c1f35a1dd2655a188cc51a3612d76efc8a6eb74728bef5606e6758ee77423e564092b4a518aba569bbb21c9bac5ab7a35b0c6ae7e344c8 + languageName: node + linkType: hard + "@jridgewell/trace-mapping@npm:0.3.9": version: 0.3.9 resolution: "@jridgewell/trace-mapping@npm:0.3.9" @@ -1020,6 +1700,16 @@ __metadata: languageName: node linkType: hard +"@jridgewell/trace-mapping@npm:^0.3.12, @jridgewell/trace-mapping@npm:^0.3.17, @jridgewell/trace-mapping@npm:^0.3.18": + version: 0.3.19 + resolution: "@jridgewell/trace-mapping@npm:0.3.19" + dependencies: + "@jridgewell/resolve-uri": ^3.1.0 + "@jridgewell/sourcemap-codec": ^1.4.14 + checksum: 956a6f0f6fec060fb48c6bf1f5ec2064e13cd38c8be3873877d4b92b4a27ba58289a34071752671262a3e3c202abcc3fa2aac64d8447b4b0fa1ba3c9047f1c20 + languageName: node + linkType: hard + "@jridgewell/trace-mapping@npm:^0.3.9": version: 0.3.14 resolution: "@jridgewell/trace-mapping@npm:0.3.14" @@ -1719,6 +2409,31 @@ __metadata: languageName: node linkType: hard +"@sinclair/typebox@npm:^0.27.8": + version: 0.27.8 + resolution: "@sinclair/typebox@npm:0.27.8" + checksum: 00bd7362a3439021aa1ea51b0e0d0a0e8ca1351a3d54c606b115fdcc49b51b16db6e5f43b4fe7a28c38688523e22a94d49dd31168868b655f0d4d50f032d07a1 + languageName: node + linkType: hard + +"@sinonjs/commons@npm:^3.0.0": + version: 3.0.0 + resolution: "@sinonjs/commons@npm:3.0.0" + dependencies: + type-detect: 4.0.8 + checksum: b4b5b73d4df4560fb8c0c7b38c7ad4aeabedd362f3373859d804c988c725889cde33550e4bcc7cd316a30f5152a2d1d43db71b6d0c38f5feef71fd8d016763f8 + languageName: node + linkType: hard + +"@sinonjs/fake-timers@npm:^10.0.2": + version: 10.3.0 + resolution: "@sinonjs/fake-timers@npm:10.3.0" + dependencies: + "@sinonjs/commons": ^3.0.0 + checksum: 614d30cb4d5201550c940945d44c9e0b6d64a888ff2cd5b357f95ad6721070d6b8839cd10e15b76bf5e14af0bcc1d8f9ec00d49a46318f1f669a4bec1d7f3148 + languageName: node + linkType: hard + "@socket.io/component-emitter@npm:~3.1.0": version: 3.1.0 resolution: "@socket.io/component-emitter@npm:3.1.0" @@ -3039,6 +3754,47 @@ __metadata: languageName: node linkType: hard +"@types/babel__core@npm:^7.1.14": + version: 7.20.1 + resolution: "@types/babel__core@npm:7.20.1" + dependencies: + "@babel/parser": ^7.20.7 + "@babel/types": ^7.20.7 + "@types/babel__generator": "*" + "@types/babel__template": "*" + "@types/babel__traverse": "*" + checksum: 9fcd9691a33074802d9057ff70b0e3ff3778f52470475b68698a0f6714fbe2ccb36c16b43dc924eb978cd8a81c1f845e5ff4699e7a47606043b539eb8c6331a8 + languageName: node + linkType: hard + +"@types/babel__generator@npm:*": + version: 7.6.4 + resolution: "@types/babel__generator@npm:7.6.4" + dependencies: + "@babel/types": ^7.0.0 + checksum: 20effbbb5f8a3a0211e95959d06ae70c097fb6191011b73b38fe86deebefad8e09ee014605e0fd3cdaedc73d158be555866810e9166e1f09e4cfd880b874dcb0 + languageName: node + linkType: hard + +"@types/babel__template@npm:*": + version: 7.4.1 + resolution: "@types/babel__template@npm:7.4.1" + dependencies: + "@babel/parser": ^7.1.0 + "@babel/types": ^7.0.0 + checksum: 649fe8b42c2876be1fd28c6ed9b276f78152d5904ec290b6c861d9ef324206e0a5c242e8305c421ac52ecf6358fa7e32ab7a692f55370484825c1df29b1596ee + languageName: node + linkType: hard + +"@types/babel__traverse@npm:*, @types/babel__traverse@npm:^7.0.6": + version: 7.20.1 + resolution: "@types/babel__traverse@npm:7.20.1" + dependencies: + "@babel/types": ^7.20.7 + checksum: 58341e23c649c0eba134a1682d4f20d027fad290d92e5740faa1279978f6ed476fc467ae51ce17a877e2566d805aeac64eae541168994367761ec883a4150221 + languageName: node + linkType: hard + "@types/coingecko-api@npm:^1.0.10": version: 1.0.10 resolution: "@types/coingecko-api@npm:1.0.10" @@ -3064,6 +3820,50 @@ __metadata: languageName: node linkType: hard +"@types/graceful-fs@npm:^4.1.3": + version: 4.1.6 + resolution: "@types/graceful-fs@npm:4.1.6" + dependencies: + "@types/node": "*" + checksum: c3070ccdc9ca0f40df747bced1c96c71a61992d6f7c767e8fd24bb6a3c2de26e8b84135ede000b7e79db530a23e7e88dcd9db60eee6395d0f4ce1dae91369dd4 + languageName: node + linkType: hard + +"@types/istanbul-lib-coverage@npm:*, @types/istanbul-lib-coverage@npm:^2.0.0, @types/istanbul-lib-coverage@npm:^2.0.1": + version: 2.0.4 + resolution: "@types/istanbul-lib-coverage@npm:2.0.4" + checksum: a25d7589ee65c94d31464c16b72a9dc81dfa0bea9d3e105ae03882d616e2a0712a9c101a599ec482d297c3591e16336962878cb3eb1a0a62d5b76d277a890ce7 + languageName: node + linkType: hard + +"@types/istanbul-lib-report@npm:*": + version: 3.0.0 + resolution: "@types/istanbul-lib-report@npm:3.0.0" + dependencies: + "@types/istanbul-lib-coverage": "*" + checksum: 656398b62dc288e1b5226f8880af98087233cdb90100655c989a09f3052b5775bf98ba58a16c5ae642fb66c61aba402e07a9f2bff1d1569e3b306026c59f3f36 + languageName: node + linkType: hard + +"@types/istanbul-reports@npm:^3.0.0": + version: 3.0.1 + resolution: "@types/istanbul-reports@npm:3.0.1" + dependencies: + "@types/istanbul-lib-report": "*" + checksum: f1ad54bc68f37f60b30c7915886b92f86b847033e597f9b34f2415acdbe5ed742fa559a0a40050d74cdba3b6a63c342cac1f3a64dba5b68b66a6941f4abd7903 + languageName: node + linkType: hard + +"@types/jest@npm:^29.5.3": + version: 29.5.3 + resolution: "@types/jest@npm:29.5.3" + dependencies: + expect: ^29.0.0 + pretty-format: ^29.0.0 + checksum: e36bb92e0b9e5ea7d6f8832baa42f087fc1697f6cd30ec309a07ea4c268e06ec460f1f0cfd2581daf5eff5763475190ec1ad8ac6520c49ccfe4f5c0a48bfa676 + languageName: node + linkType: hard + "@types/json-schema@npm:^7.0.9": version: 7.0.11 resolution: "@types/json-schema@npm:7.0.11" @@ -3158,6 +3958,13 @@ __metadata: languageName: node linkType: hard +"@types/stack-utils@npm:^2.0.0": + version: 2.0.1 + resolution: "@types/stack-utils@npm:2.0.1" + checksum: 205fdbe3326b7046d7eaf5e494d8084f2659086a266f3f9cf00bccc549c8e36e407f88168ad4383c8b07099957ad669f75f2532ed4bc70be2b037330f7bae019 + languageName: node + linkType: hard + "@types/trusted-types@npm:^2.0.2": version: 2.0.3 resolution: "@types/trusted-types@npm:2.0.3" @@ -3174,6 +3981,22 @@ __metadata: languageName: node linkType: hard +"@types/yargs-parser@npm:*": + version: 21.0.0 + resolution: "@types/yargs-parser@npm:21.0.0" + checksum: b2f4c8d12ac18a567440379909127cf2cec393daffb73f246d0a25df36ea983b93b7e9e824251f959e9f928cbc7c1aab6728d0a0ff15d6145f66cec2be67d9a2 + languageName: node + linkType: hard + +"@types/yargs@npm:^17.0.8": + version: 17.0.24 + resolution: "@types/yargs@npm:17.0.24" + dependencies: + "@types/yargs-parser": "*" + checksum: 5f3ac4dc4f6e211c1627340160fbe2fd247ceba002190da6cf9155af1798450501d628c9165a183f30a224fc68fa5e700490d740ff4c73e2cdef95bc4e8ba7bf + languageName: node + linkType: hard + "@typescript-eslint/eslint-plugin@npm:^5.59.7": version: 5.59.7 resolution: "@typescript-eslint/eslint-plugin@npm:5.59.7" @@ -4278,6 +5101,15 @@ __metadata: languageName: node linkType: hard +"ansi-escapes@npm:^4.2.1": + version: 4.3.2 + resolution: "ansi-escapes@npm:4.3.2" + dependencies: + type-fest: ^0.21.3 + checksum: 93111c42189c0a6bed9cdb4d7f2829548e943827ee8479c74d6e0b22ee127b2a21d3f8b5ca57723b8ef78ce011fbfc2784350eb2bde3ccfccf2f575fa8489815 + languageName: node + linkType: hard + "ansi-regex@npm:^4.1.0": version: 4.1.1 resolution: "ansi-regex@npm:4.1.1" @@ -4310,6 +5142,13 @@ __metadata: languageName: node linkType: hard +"ansi-styles@npm:^5.0.0": + version: 5.2.0 + resolution: "ansi-styles@npm:5.2.0" + checksum: d7f4e97ce0623aea6bc0d90dcd28881ee04cba06c570b97fd3391bd7a268eedfd9d5e2dd4fdcbdd82b8105df5faf6f24aaedc08eaf3da898e702db5948f63469 + languageName: node + linkType: hard + "any-promise@npm:^1.0.0": version: 1.3.0 resolution: "any-promise@npm:1.3.0" @@ -4317,6 +5156,16 @@ __metadata: languageName: node linkType: hard +"anymatch@npm:^3.0.3": + version: 3.1.3 + resolution: "anymatch@npm:3.1.3" + dependencies: + normalize-path: ^3.0.0 + picomatch: ^2.0.4 + checksum: 3e044fd6d1d26545f235a9fe4d7a534e2029d8e59fa7fd9f2a6eb21230f6b5380ea1eaf55136e60cbf8e613544b3b766e7a6fa2102e2a3a117505466e3025dc2 + languageName: node + linkType: hard + "anymatch@npm:~3.1.2": version: 3.1.2 resolution: "anymatch@npm:3.1.2" @@ -4358,6 +5207,15 @@ __metadata: languageName: node linkType: hard +"argparse@npm:^1.0.7": + version: 1.0.10 + resolution: "argparse@npm:1.0.10" + dependencies: + sprintf-js: ~1.0.2 + checksum: 7ca6e45583a28de7258e39e13d81e925cfa25d7d4aacbf806a382d3c02fcb13403a07fb8aeef949f10a7cfe4a62da0e2e807b348a5980554cc28ee573ef95945 + languageName: node + linkType: hard + "argparse@npm:^2.0.1": version: 2.0.1 resolution: "argparse@npm:2.0.1" @@ -4523,6 +5381,82 @@ __metadata: languageName: node linkType: hard +"babel-jest@npm:^29.6.3": + version: 29.6.3 + resolution: "babel-jest@npm:29.6.3" + dependencies: + "@jest/transform": ^29.6.3 + "@types/babel__core": ^7.1.14 + babel-plugin-istanbul: ^6.1.1 + babel-preset-jest: ^29.6.3 + chalk: ^4.0.0 + graceful-fs: ^4.2.9 + slash: ^3.0.0 + peerDependencies: + "@babel/core": ^7.8.0 + checksum: 8b4b85d829d8ee010f0c8381cb9d67842da905c32183c1fc6e1e8833447a79b969f8279759d44197bb77001239dc41a49fff0e8222d8e8577f47a8d0428d178e + languageName: node + linkType: hard + +"babel-plugin-istanbul@npm:^6.1.1": + version: 6.1.1 + resolution: "babel-plugin-istanbul@npm:6.1.1" + dependencies: + "@babel/helper-plugin-utils": ^7.0.0 + "@istanbuljs/load-nyc-config": ^1.0.0 + "@istanbuljs/schema": ^0.1.2 + istanbul-lib-instrument: ^5.0.4 + test-exclude: ^6.0.0 + checksum: cb4fd95738219f232f0aece1116628cccff16db891713c4ccb501cddbbf9272951a5df81f2f2658dfdf4b3e7b236a9d5cbcf04d5d8c07dd5077297339598061a + languageName: node + linkType: hard + +"babel-plugin-jest-hoist@npm:^29.6.3": + version: 29.6.3 + resolution: "babel-plugin-jest-hoist@npm:29.6.3" + dependencies: + "@babel/template": ^7.3.3 + "@babel/types": ^7.3.3 + "@types/babel__core": ^7.1.14 + "@types/babel__traverse": ^7.0.6 + checksum: 51250f22815a7318f17214a9d44650ba89551e6d4f47a2dc259128428324b52f5a73979d010cefd921fd5a720d8c1d55ad74ff601cd94c7bd44d5f6292fde2d1 + languageName: node + linkType: hard + +"babel-preset-current-node-syntax@npm:^1.0.0": + version: 1.0.1 + resolution: "babel-preset-current-node-syntax@npm:1.0.1" + dependencies: + "@babel/plugin-syntax-async-generators": ^7.8.4 + "@babel/plugin-syntax-bigint": ^7.8.3 + "@babel/plugin-syntax-class-properties": ^7.8.3 + "@babel/plugin-syntax-import-meta": ^7.8.3 + "@babel/plugin-syntax-json-strings": ^7.8.3 + "@babel/plugin-syntax-logical-assignment-operators": ^7.8.3 + "@babel/plugin-syntax-nullish-coalescing-operator": ^7.8.3 + "@babel/plugin-syntax-numeric-separator": ^7.8.3 + "@babel/plugin-syntax-object-rest-spread": ^7.8.3 + "@babel/plugin-syntax-optional-catch-binding": ^7.8.3 + "@babel/plugin-syntax-optional-chaining": ^7.8.3 + "@babel/plugin-syntax-top-level-await": ^7.8.3 + peerDependencies: + "@babel/core": ^7.0.0 + checksum: d118c2742498c5492c095bc8541f4076b253e705b5f1ad9a2e7d302d81a84866f0070346662355c8e25fc02caa28dc2da8d69bcd67794a0d60c4d6fab6913cc8 + languageName: node + linkType: hard + +"babel-preset-jest@npm:^29.6.3": + version: 29.6.3 + resolution: "babel-preset-jest@npm:29.6.3" + dependencies: + babel-plugin-jest-hoist: ^29.6.3 + babel-preset-current-node-syntax: ^1.0.0 + peerDependencies: + "@babel/core": ^7.0.0 + checksum: aa4ff2a8a728d9d698ed521e3461a109a1e66202b13d3494e41eea30729a5e7cc03b3a2d56c594423a135429c37bf63a9fa8b0b9ce275298be3095a88c69f6fb + languageName: node + linkType: hard + "balanced-match@npm:^1.0.0": version: 1.0.2 resolution: "balanced-match@npm:1.0.2" @@ -4752,6 +5686,20 @@ __metadata: languageName: node linkType: hard +"browserslist@npm:^4.21.9": + version: 4.21.10 + resolution: "browserslist@npm:4.21.10" + dependencies: + caniuse-lite: ^1.0.30001517 + electron-to-chromium: ^1.4.477 + node-releases: ^2.0.13 + update-browserslist-db: ^1.0.11 + bin: + browserslist: cli.js + checksum: 1e27c0f111a35d1dd0e8fc2c61781b0daefabc2c9471b0b10537ce54843014bceb2a1ce4571af1a82b2bf1e6e6e05d38865916689a158f03bc2c7a4ec2577db8 + languageName: node + linkType: hard + "bs58@npm:^4.0.0, bs58@npm:^4.0.1": version: 4.0.1 resolution: "bs58@npm:4.0.1" @@ -4781,6 +5729,15 @@ __metadata: languageName: node linkType: hard +"bser@npm:2.1.1": + version: 2.1.1 + resolution: "bser@npm:2.1.1" + dependencies: + node-int64: ^0.4.0 + checksum: 9ba4dc58ce86300c862bffc3ae91f00b2a03b01ee07f3564beeeaf82aa243b8b03ba53f123b0b842c190d4399b94697970c8e7cf7b1ea44b61aa28c3526a4449 + languageName: node + linkType: hard + "buffer-alloc-unsafe@npm:^1.1.0": version: 1.1.0 resolution: "buffer-alloc-unsafe@npm:1.1.0" @@ -4805,7 +5762,7 @@ __metadata: languageName: node linkType: hard -"buffer-from@npm:^1.1.1": +"buffer-from@npm:^1.0.0, buffer-from@npm:^1.1.1": version: 1.1.2 resolution: "buffer-from@npm:1.1.2" checksum: 0448524a562b37d4d7ed9efd91685a5b77a50672c556ea254ac9a6d30e3403a517d8981f10e565db24e8339413b43c97ca2951f10e399c6125a0d8911f5679bb @@ -4909,13 +5866,20 @@ __metadata: languageName: node linkType: hard -"camelcase@npm:^5.0.0": +"camelcase@npm:^5.0.0, camelcase@npm:^5.3.1": version: 5.3.1 resolution: "camelcase@npm:5.3.1" checksum: e6effce26b9404e3c0f301498184f243811c30dfe6d0b9051863bd8e4034d09c8c2923794f280d6827e5aa055f6c434115ff97864a16a963366fb35fd673024b languageName: node linkType: hard +"camelcase@npm:^6.2.0": + version: 6.3.0 + resolution: "camelcase@npm:6.3.0" + checksum: 8c96818a9076434998511251dcb2761a94817ea17dbdc37f47ac080bd088fc62c7369429a19e2178b993497132c8cbcf5cc1f44ba963e76782ba469c0474938d + languageName: node + linkType: hard + "caniuse-lite@npm:^1.0.30001406": version: 1.0.30001418 resolution: "caniuse-lite@npm:1.0.30001418" @@ -4930,6 +5894,13 @@ __metadata: languageName: node linkType: hard +"caniuse-lite@npm:^1.0.30001517": + version: 1.0.30001522 + resolution: "caniuse-lite@npm:1.0.30001522" + checksum: 56e3551c02ae595085114073cf242f7d9d54d32255c80893ca9098a44f44fc6eef353936f234f31c7f4cb894dd2b6c9c4626e30649ee29e04d70aa127eeefeb0 + languageName: node + linkType: hard + "cbor-sync@npm:^1.0.4": version: 1.0.4 resolution: "cbor-sync@npm:1.0.4" @@ -4937,7 +5908,7 @@ __metadata: languageName: node linkType: hard -"chalk@npm:^2.0.0": +"chalk@npm:^2.0.0, chalk@npm:^2.4.2": version: 2.4.2 resolution: "chalk@npm:2.4.2" dependencies: @@ -4958,6 +5929,13 @@ __metadata: languageName: node linkType: hard +"char-regex@npm:^1.0.2": + version: 1.0.2 + resolution: "char-regex@npm:1.0.2" + checksum: b563e4b6039b15213114626621e7a3d12f31008bdce20f9c741d69987f62aeaace7ec30f6018890ad77b2e9b4d95324c9f5acfca58a9441e3b1dcdd1e2525d17 + languageName: node + linkType: hard + "chokidar@npm:^3.5.3": version: 3.5.3 resolution: "chokidar@npm:3.5.3" @@ -4984,6 +5962,13 @@ __metadata: languageName: node linkType: hard +"ci-info@npm:^3.2.0": + version: 3.8.0 + resolution: "ci-info@npm:3.8.0" + checksum: d0a4d3160497cae54294974a7246202244fff031b0a6ea20dd57b10ec510aa17399c41a1b0982142c105f3255aff2173e5c0dd7302ee1b2f28ba3debda375098 + languageName: node + linkType: hard + "cipher-base@npm:^1.0.0, cipher-base@npm:^1.0.1, cipher-base@npm:^1.0.3": version: 1.0.4 resolution: "cipher-base@npm:1.0.4" @@ -4994,6 +5979,13 @@ __metadata: languageName: node linkType: hard +"cjs-module-lexer@npm:^1.0.0": + version: 1.2.3 + resolution: "cjs-module-lexer@npm:1.2.3" + checksum: 5ea3cb867a9bb609b6d476cd86590d105f3cfd6514db38ff71f63992ab40939c2feb68967faa15a6d2b1f90daa6416b79ea2de486e9e2485a6f8b66a21b4fb0a + languageName: node + linkType: hard + "clean-stack@npm:^2.0.0": version: 2.2.0 resolution: "clean-stack@npm:2.2.0" @@ -5030,6 +6022,17 @@ __metadata: languageName: node linkType: hard +"cliui@npm:^8.0.1": + version: 8.0.1 + resolution: "cliui@npm:8.0.1" + dependencies: + string-width: ^4.2.0 + strip-ansi: ^6.0.1 + wrap-ansi: ^7.0.0 + checksum: 79648b3b0045f2e285b76fb2e24e207c6db44323581e421c3acbd0e86454cba1b37aea976ab50195a49e7384b871e6dfb2247ad7dec53c02454ac6497394cb56 + languageName: node + linkType: hard + "clsx@npm:1.1.1": version: 1.1.1 resolution: "clsx@npm:1.1.1" @@ -5044,6 +6047,13 @@ __metadata: languageName: node linkType: hard +"co@npm:^4.6.0": + version: 4.6.0 + resolution: "co@npm:4.6.0" + checksum: 5210d9223010eb95b29df06a91116f2cf7c8e0748a9013ed853b53f362ea0e822f1e5bb054fb3cefc645239a4cf966af1f6133a3b43f40d591f3b68ed6cf0510 + languageName: node + linkType: hard + "coingecko-api@npm:^1.0.10": version: 1.0.10 resolution: "coingecko-api@npm:1.0.10" @@ -5051,6 +6061,13 @@ __metadata: languageName: node linkType: hard +"collect-v8-coverage@npm:^1.0.0": + version: 1.0.2 + resolution: "collect-v8-coverage@npm:1.0.2" + checksum: c10f41c39ab84629d16f9f6137bc8a63d332244383fc368caf2d2052b5e04c20cd1fd70f66fcf4e2422b84c8226598b776d39d5f2d2a51867cc1ed5d1982b4da + languageName: node + linkType: hard + "color-convert@npm:^1.9.0": version: 1.9.3 resolution: "color-convert@npm:1.9.3" @@ -5120,6 +6137,20 @@ __metadata: languageName: node linkType: hard +"convert-source-map@npm:^1.6.0, convert-source-map@npm:^1.7.0": + version: 1.9.0 + resolution: "convert-source-map@npm:1.9.0" + checksum: dc55a1f28ddd0e9485ef13565f8f756b342f9a46c4ae18b843fe3c30c675d058d6a4823eff86d472f187b176f0adf51ea7b69ea38be34be4a63cbbf91b0593c8 + languageName: node + linkType: hard + +"convert-source-map@npm:^2.0.0": + version: 2.0.0 + resolution: "convert-source-map@npm:2.0.0" + checksum: 63ae9933be5a2b8d4509daca5124e20c14d023c820258e484e32dc324d34c2754e71297c94a05784064ad27615037ef677e3f0c00469fb55f409d2bb21261035 + languageName: node + linkType: hard + "copy-to-clipboard@npm:^3.3.1, copy-to-clipboard@npm:^3.3.3": version: 3.3.3 resolution: "copy-to-clipboard@npm:3.3.3" @@ -5316,6 +6347,18 @@ __metadata: languageName: node linkType: hard +"dedent@npm:^1.0.0": + version: 1.5.1 + resolution: "dedent@npm:1.5.1" + peerDependencies: + babel-plugin-macros: ^3.1.0 + peerDependenciesMeta: + babel-plugin-macros: + optional: true + checksum: c3c300a14edf1bdf5a873f9e4b22e839d62490bc5c8d6169c1f15858a1a76733d06a9a56930e963d677a2ceeca4b6b0894cc5ea2f501aa382ca5b92af3413c2a + languageName: node + linkType: hard + "deep-is@npm:^0.1.3": version: 0.1.4 resolution: "deep-is@npm:0.1.4" @@ -5406,6 +6449,13 @@ __metadata: languageName: node linkType: hard +"detect-newline@npm:^3.0.0": + version: 3.1.0 + resolution: "detect-newline@npm:3.1.0" + checksum: ae6cd429c41ad01b164c59ea36f264a2c479598e61cba7c99da24175a7ab80ddf066420f2bec9a1c57a6bead411b4655ff15ad7d281c000a89791f48cbe939e7 + languageName: node + linkType: hard + "detect-node-es@npm:^1.1.0": version: 1.1.0 resolution: "detect-node-es@npm:1.1.0" @@ -5420,6 +6470,13 @@ __metadata: languageName: node linkType: hard +"diff-sequences@npm:^29.6.3": + version: 29.6.3 + resolution: "diff-sequences@npm:29.6.3" + checksum: f4914158e1f2276343d98ff5b31fc004e7304f5470bf0f1adb2ac6955d85a531a6458d33e87667f98f6ae52ebd3891bb47d420bb48a5bd8b7a27ee25b20e33aa + languageName: node + linkType: hard + "diff@npm:^4.0.1": version: 4.0.2 resolution: "diff@npm:4.0.2" @@ -5507,6 +6564,13 @@ __metadata: languageName: node linkType: hard +"electron-to-chromium@npm:^1.4.477": + version: 1.4.498 + resolution: "electron-to-chromium@npm:1.4.498" + checksum: 01962ae42e9097c321cb6ff63ca97dfd36457050727893d1768e6eb1b7d5a48ece568b94b1128fd0211f7ce3a31aca0c17eb72b1292d9b5ef7b0664d90dfe3aa + languageName: node + linkType: hard + "elliptic@npm:6.5.4, elliptic@npm:^6.5.3, elliptic@npm:^6.5.4": version: 6.5.4 resolution: "elliptic@npm:6.5.4" @@ -5522,6 +6586,13 @@ __metadata: languageName: node linkType: hard +"emittery@npm:^0.13.1": + version: 0.13.1 + resolution: "emittery@npm:0.13.1" + checksum: 2b089ab6306f38feaabf4f6f02792f9ec85fc054fda79f44f6790e61bbf6bc4e1616afb9b232e0c5ec5289a8a452f79bfa6d905a6fd64e94b49981f0934001c6 + languageName: node + linkType: hard + "emoji-regex@npm:^7.0.1": version: 7.0.3 resolution: "emoji-regex@npm:7.0.3" @@ -5612,6 +6683,15 @@ __metadata: languageName: node linkType: hard +"error-ex@npm:^1.3.1": + version: 1.3.2 + resolution: "error-ex@npm:1.3.2" + dependencies: + is-arrayish: ^0.2.1 + checksum: c1c2b8b65f9c91b0f9d75f0debaa7ec5b35c266c2cac5de412c1a6de86d4cbae04ae44e510378cb14d032d0645a36925d0186f8bb7367bcc629db256b743a001 + languageName: node + linkType: hard + "es-abstract@npm:^1.19.0, es-abstract@npm:^1.19.1, es-abstract@npm:^1.19.2, es-abstract@npm:^1.19.5, es-abstract@npm:^1.20.0": version: 1.20.1 resolution: "es-abstract@npm:1.20.1" @@ -5700,6 +6780,13 @@ __metadata: languageName: node linkType: hard +"escape-string-regexp@npm:^2.0.0": + version: 2.0.0 + resolution: "escape-string-regexp@npm:2.0.0" + checksum: 9f8a2d5743677c16e85c810e3024d54f0c8dea6424fad3c79ef6666e81dd0846f7437f5e729dfcdac8981bc9e5294c39b4580814d114076b8d36318f46ae4395 + languageName: node + linkType: hard + "escape-string-regexp@npm:^4.0.0": version: 4.0.0 resolution: "escape-string-regexp@npm:4.0.0" @@ -5952,6 +7039,16 @@ __metadata: languageName: node linkType: hard +"esprima@npm:^4.0.0": + version: 4.0.1 + resolution: "esprima@npm:4.0.1" + bin: + esparse: ./bin/esparse.js + esvalidate: ./bin/esvalidate.js + checksum: b45bc805a613dbea2835278c306b91aff6173c8d034223fa81498c77dcbce3b2931bf6006db816f62eacd9fd4ea975dfd85a5b7f3c6402cfd050d4ca3c13a628 + languageName: node + linkType: hard + "esquery@npm:^1.4.2": version: 1.5.0 resolution: "esquery@npm:1.5.0" @@ -6126,6 +7223,23 @@ __metadata: languageName: node linkType: hard +"execa@npm:^5.0.0": + version: 5.1.1 + resolution: "execa@npm:5.1.1" + dependencies: + cross-spawn: ^7.0.3 + get-stream: ^6.0.0 + human-signals: ^2.1.0 + is-stream: ^2.0.0 + merge-stream: ^2.0.0 + npm-run-path: ^4.0.1 + onetime: ^5.1.2 + signal-exit: ^3.0.3 + strip-final-newline: ^2.0.0 + checksum: fba9022c8c8c15ed862847e94c252b3d946036d7547af310e344a527e59021fd8b6bb0723883ea87044dc4f0201f949046993124a42ccb0855cae5bf8c786343 + languageName: node + linkType: hard + "exenv@npm:^1.2.0": version: 1.2.2 resolution: "exenv@npm:1.2.2" @@ -6133,6 +7247,26 @@ __metadata: languageName: node linkType: hard +"exit@npm:^0.1.2": + version: 0.1.2 + resolution: "exit@npm:0.1.2" + checksum: abc407f07a875c3961e4781dfcb743b58d6c93de9ab263f4f8c9d23bb6da5f9b7764fc773f86b43dd88030444d5ab8abcb611cb680fba8ca075362b77114bba3 + languageName: node + linkType: hard + +"expect@npm:^29.0.0, expect@npm:^29.6.3": + version: 29.6.3 + resolution: "expect@npm:29.6.3" + dependencies: + "@jest/expect-utils": ^29.6.3 + jest-get-type: ^29.6.3 + jest-matcher-utils: ^29.6.3 + jest-message-util: ^29.6.3 + jest-util: ^29.6.3 + checksum: c72de87abbc9acc17c66f42fcac8be4dff256f871f1800c3aaa004c74f95f61866cf80e8f2ddacc3f2df290fd58b0cba8adb3a0dee3a09dd5d39f97f63d2aae8 + languageName: node + linkType: hard + "eyes@npm:^0.1.8": version: 0.1.8 resolution: "eyes@npm:0.1.8" @@ -6173,7 +7307,7 @@ __metadata: languageName: node linkType: hard -"fast-json-stable-stringify@npm:^2.0.0": +"fast-json-stable-stringify@npm:^2.0.0, fast-json-stable-stringify@npm:^2.1.0": version: 2.1.0 resolution: "fast-json-stable-stringify@npm:2.1.0" checksum: b191531e36c607977e5b1c47811158733c34ccb3bfde92c44798929e9b4154884378536d26ad90dfecd32e1ffc09c545d23535ad91b3161a27ddbb8ebe0cbecb @@ -6217,6 +7351,15 @@ __metadata: languageName: node linkType: hard +"fb-watchman@npm:^2.0.0": + version: 2.0.2 + resolution: "fb-watchman@npm:2.0.2" + dependencies: + bser: 2.1.1 + checksum: b15a124cef28916fe07b400eb87cbc73ca082c142abf7ca8e8de6af43eca79ca7bd13eb4d4d48240b3bd3136eaac40d16e42d6edf87a8e5d1dd8070626860c78 + languageName: node + linkType: hard + "file-entry-cache@npm:^6.0.1": version: 6.0.1 resolution: "file-entry-cache@npm:6.0.1" @@ -6267,7 +7410,7 @@ __metadata: languageName: node linkType: hard -"find-up@npm:^4.1.0": +"find-up@npm:^4.0.0, find-up@npm:^4.1.0": version: 4.1.0 resolution: "find-up@npm:4.1.0" dependencies: @@ -6363,6 +7506,16 @@ __metadata: languageName: node linkType: hard +"fsevents@npm:^2.3.2": + version: 2.3.3 + resolution: "fsevents@npm:2.3.3" + dependencies: + node-gyp: latest + checksum: 11e6ea6fea15e42461fc55b4b0e4a0a3c654faa567f1877dbd353f39156f69def97a69936d1746619d656c4b93de2238bf731f6085a03a50cabf287c9d024317 + conditions: os=darwin + languageName: node + linkType: hard + "fsevents@npm:~2.3.2": version: 2.3.2 resolution: "fsevents@npm:2.3.2" @@ -6373,6 +7526,15 @@ __metadata: languageName: node linkType: hard +"fsevents@patch:fsevents@^2.3.2#~builtin": + version: 2.3.3 + resolution: "fsevents@patch:fsevents@npm%3A2.3.3#~builtin::version=2.3.3&hash=18f3a7" + dependencies: + node-gyp: latest + conditions: os=darwin + languageName: node + linkType: hard + "fsevents@patch:fsevents@~2.3.2#~builtin": version: 2.3.2 resolution: "fsevents@patch:fsevents@npm%3A2.3.2#~builtin::version=2.3.2&hash=18f3a7" @@ -6424,7 +7586,14 @@ __metadata: languageName: node linkType: hard -"get-caller-file@npm:^2.0.1": +"gensync@npm:^1.0.0-beta.2": + version: 1.0.0-beta.2 + resolution: "gensync@npm:1.0.0-beta.2" + checksum: a7437e58c6be12aa6c90f7730eac7fa9833dc78872b4ad2963d2031b00a3367a93f98aec75f9aaac7220848e4026d67a8655e870b24f20a543d103c0d65952ec + languageName: node + linkType: hard + +"get-caller-file@npm:^2.0.1, get-caller-file@npm:^2.0.5": version: 2.0.5 resolution: "get-caller-file@npm:2.0.5" checksum: b9769a836d2a98c3ee734a88ba712e62703f1df31b94b784762c433c27a386dd6029ff55c2a920c392e33657d80191edbf18c61487e198844844516f843496b9 @@ -6449,6 +7618,20 @@ __metadata: languageName: node linkType: hard +"get-package-type@npm:^0.1.0": + version: 0.1.0 + resolution: "get-package-type@npm:0.1.0" + checksum: bba0811116d11e56d702682ddef7c73ba3481f114590e705fc549f4d868972263896af313c57a25c076e3c0d567e11d919a64ba1b30c879be985fc9d44f96148 + languageName: node + linkType: hard + +"get-stream@npm:^6.0.0": + version: 6.0.1 + resolution: "get-stream@npm:6.0.1" + checksum: e04ecece32c92eebf5b8c940f51468cd53554dcbb0ea725b2748be583c9523d00128137966afce410b9b051eb2ef16d657cd2b120ca8edafcf5a65e81af63cad + languageName: node + linkType: hard + "get-symbol-description@npm:^1.0.0": version: 1.0.0 resolution: "get-symbol-description@npm:1.0.0" @@ -6603,6 +7786,13 @@ __metadata: languageName: node linkType: hard +"graceful-fs@npm:^4.2.9": + version: 4.2.11 + resolution: "graceful-fs@npm:4.2.11" + checksum: ac85f94da92d8eb6b7f5a8b20ce65e43d66761c55ce85ac96df6865308390da45a8d3f0296dd3a663de65d30ba497bd46c696cc1e248c72b13d6d567138a4fc7 + languageName: node + linkType: hard + "grapheme-splitter@npm:^1.0.4": version: 1.0.4 resolution: "grapheme-splitter@npm:1.0.4" @@ -6727,6 +7917,13 @@ __metadata: languageName: node linkType: hard +"html-escaper@npm:^2.0.0": + version: 2.0.2 + resolution: "html-escaper@npm:2.0.2" + checksum: d2df2da3ad40ca9ee3a39c5cc6475ef67c8f83c234475f24d8e9ce0dc80a2c82df8e1d6fa78ddd1e9022a586ea1bd247a615e80a5cd9273d90111ddda7d9e974 + languageName: node + linkType: hard + "http-cache-semantics@npm:^4.1.0": version: 4.1.0 resolution: "http-cache-semantics@npm:4.1.0" @@ -6755,6 +7952,13 @@ __metadata: languageName: node linkType: hard +"human-signals@npm:^2.1.0": + version: 2.1.0 + resolution: "human-signals@npm:2.1.0" + checksum: b87fd89fce72391625271454e70f67fe405277415b48bcc0117ca73d31fa23a4241787afdc8d67f5a116cf37258c052f59ea82daffa72364d61351423848e3b8 + languageName: node + linkType: hard + "humanize-ms@npm:^1.2.1": version: 1.2.1 resolution: "humanize-ms@npm:1.2.1" @@ -6797,6 +8001,18 @@ __metadata: languageName: node linkType: hard +"import-local@npm:^3.0.2": + version: 3.1.0 + resolution: "import-local@npm:3.1.0" + dependencies: + pkg-dir: ^4.2.0 + resolve-cwd: ^3.0.0 + bin: + import-local-fixture: fixtures/cli.js + checksum: bfcdb63b5e3c0e245e347f3107564035b128a414c4da1172a20dc67db2504e05ede4ac2eee1252359f78b0bfd7b19ef180aec427c2fce6493ae782d73a04cddd + languageName: node + linkType: hard + "imurmurhash@npm:^0.1.4": version: 0.1.4 resolution: "imurmurhash@npm:0.1.4" @@ -6872,6 +8088,13 @@ __metadata: languageName: node linkType: hard +"is-arrayish@npm:^0.2.1": + version: 0.2.1 + resolution: "is-arrayish@npm:0.2.1" + checksum: eef4417e3c10e60e2c810b6084942b3ead455af16c4509959a27e490e7aee87cfb3f38e01bbde92220b528a0ee1a18d52b787e1458ee86174d8c7f0e58cd488f + languageName: node + linkType: hard + "is-bigint@npm:^1.0.1": version: 1.0.4 resolution: "is-bigint@npm:1.0.4" @@ -6973,6 +8196,13 @@ __metadata: languageName: node linkType: hard +"is-generator-fn@npm:^2.0.0": + version: 2.1.0 + resolution: "is-generator-fn@npm:2.1.0" + checksum: a6ad5492cf9d1746f73b6744e0c43c0020510b59d56ddcb78a91cbc173f09b5e6beff53d75c9c5a29feb618bfef2bf458e025ecf3a57ad2268e2fb2569f56215 + languageName: node + linkType: hard + "is-generator-function@npm:^1.0.7": version: 1.0.10 resolution: "is-generator-function@npm:1.0.10" @@ -7064,7 +8294,7 @@ __metadata: languageName: node linkType: hard -"is-stream@npm:^2.0.1": +"is-stream@npm:^2.0.0, is-stream@npm:^2.0.1": version: 2.0.1 resolution: "is-stream@npm:2.0.1" checksum: b8e05ccdf96ac330ea83c12450304d4a591f9958c11fd17bed240af8d5ffe08aedafa4c0f4cfccd4d28dc9d4d129daca1023633d5c11601a6cbc77521f6fae66 @@ -7150,6 +8380,71 @@ __metadata: languageName: node linkType: hard +"istanbul-lib-coverage@npm:^3.0.0, istanbul-lib-coverage@npm:^3.2.0": + version: 3.2.0 + resolution: "istanbul-lib-coverage@npm:3.2.0" + checksum: a2a545033b9d56da04a8571ed05c8120bf10e9bce01cf8633a3a2b0d1d83dff4ac4fe78d6d5673c27fc29b7f21a41d75f83a36be09f82a61c367b56aa73c1ff9 + languageName: node + linkType: hard + +"istanbul-lib-instrument@npm:^5.0.4": + version: 5.2.1 + resolution: "istanbul-lib-instrument@npm:5.2.1" + dependencies: + "@babel/core": ^7.12.3 + "@babel/parser": ^7.14.7 + "@istanbuljs/schema": ^0.1.2 + istanbul-lib-coverage: ^3.2.0 + semver: ^6.3.0 + checksum: bf16f1803ba5e51b28bbd49ed955a736488381e09375d830e42ddeb403855b2006f850711d95ad726f2ba3f1ae8e7366de7e51d2b9ac67dc4d80191ef7ddf272 + languageName: node + linkType: hard + +"istanbul-lib-instrument@npm:^6.0.0": + version: 6.0.0 + resolution: "istanbul-lib-instrument@npm:6.0.0" + dependencies: + "@babel/core": ^7.12.3 + "@babel/parser": ^7.14.7 + "@istanbuljs/schema": ^0.1.2 + istanbul-lib-coverage: ^3.2.0 + semver: ^7.5.4 + checksum: b9dc3723a769e65dbe1b912f935088ffc07cf393fa78a3ce79022c91aabb0ad01405ffd56083cdd822e514798e9daae3ea7bfe85633b094ecb335d28eb0a3f97 + languageName: node + linkType: hard + +"istanbul-lib-report@npm:^3.0.0": + version: 3.0.1 + resolution: "istanbul-lib-report@npm:3.0.1" + dependencies: + istanbul-lib-coverage: ^3.0.0 + make-dir: ^4.0.0 + supports-color: ^7.1.0 + checksum: fd17a1b879e7faf9bb1dc8f80b2a16e9f5b7b8498fe6ed580a618c34df0bfe53d2abd35bf8a0a00e628fb7405462576427c7df20bbe4148d19c14b431c974b21 + languageName: node + linkType: hard + +"istanbul-lib-source-maps@npm:^4.0.0": + version: 4.0.1 + resolution: "istanbul-lib-source-maps@npm:4.0.1" + dependencies: + debug: ^4.1.1 + istanbul-lib-coverage: ^3.0.0 + source-map: ^0.6.1 + checksum: 21ad3df45db4b81852b662b8d4161f6446cd250c1ddc70ef96a585e2e85c26ed7cd9c2a396a71533cfb981d1a645508bc9618cae431e55d01a0628e7dec62ef2 + languageName: node + linkType: hard + +"istanbul-reports@npm:^3.1.3": + version: 3.1.6 + resolution: "istanbul-reports@npm:3.1.6" + dependencies: + html-escaper: ^2.0.0 + istanbul-lib-report: ^3.0.0 + checksum: 44c4c0582f287f02341e9720997f9e82c071627e1e862895745d5f52ec72c9b9f38e1d12370015d2a71dcead794f34c7732aaef3fab80a24bc617a21c3d911d6 + languageName: node + linkType: hard + "javascript-natural-sort@npm:0.7.1": version: 0.7.1 resolution: "javascript-natural-sort@npm:0.7.1" @@ -7180,25 +8475,465 @@ __metadata: languageName: node linkType: hard -"jayson@npm:^4.1.0": - version: 4.1.0 - resolution: "jayson@npm:4.1.0" +"jayson@npm:^4.1.0": + version: 4.1.0 + resolution: "jayson@npm:4.1.0" + dependencies: + "@types/connect": ^3.4.33 + "@types/node": ^12.12.54 + "@types/ws": ^7.4.4 + JSONStream: ^1.3.5 + commander: ^2.20.3 + delay: ^5.0.0 + es6-promisify: ^5.0.0 + eyes: ^0.1.8 + isomorphic-ws: ^4.0.1 + json-stringify-safe: ^5.0.1 + uuid: ^8.3.2 + ws: ^7.4.5 + bin: + jayson: bin/jayson.js + checksum: 86464322fbdc6db65d2bb4fc278cb6c86fad5c2a506065490d39459f09ba0d30f2b4fb740b33828a1424791419b6c8bd295dc54d361a4ad959bf70cc62b1ca7e + languageName: node + linkType: hard + +"jest-changed-files@npm:^29.6.3": + version: 29.6.3 + resolution: "jest-changed-files@npm:29.6.3" + dependencies: + execa: ^5.0.0 + jest-util: ^29.6.3 + p-limit: ^3.1.0 + checksum: 55bc820a70c220a02fec214d5c48d5e0d829549e5c7b9959776b4ca3f76f5ff20c7c8ff816a847822766f1d712477ab3027f7a66ec61bf65de3f852e878b4dfd + languageName: node + linkType: hard + +"jest-circus@npm:^29.6.3": + version: 29.6.3 + resolution: "jest-circus@npm:29.6.3" + dependencies: + "@jest/environment": ^29.6.3 + "@jest/expect": ^29.6.3 + "@jest/test-result": ^29.6.3 + "@jest/types": ^29.6.3 + "@types/node": "*" + chalk: ^4.0.0 + co: ^4.6.0 + dedent: ^1.0.0 + is-generator-fn: ^2.0.0 + jest-each: ^29.6.3 + jest-matcher-utils: ^29.6.3 + jest-message-util: ^29.6.3 + jest-runtime: ^29.6.3 + jest-snapshot: ^29.6.3 + jest-util: ^29.6.3 + p-limit: ^3.1.0 + pretty-format: ^29.6.3 + pure-rand: ^6.0.0 + slash: ^3.0.0 + stack-utils: ^2.0.3 + checksum: 65b76f853d1bd2ddc74ec5d9a37cff3d04d436e675b0ded52167ba9e5dfb9d6fbca8572c9f255d379ad332e87770bac3da6dbcabcaf840ee2ba6e0cde5b8c20e + languageName: node + linkType: hard + +"jest-cli@npm:^29.6.3": + version: 29.6.3 + resolution: "jest-cli@npm:29.6.3" + dependencies: + "@jest/core": ^29.6.3 + "@jest/test-result": ^29.6.3 + "@jest/types": ^29.6.3 + chalk: ^4.0.0 + exit: ^0.1.2 + graceful-fs: ^4.2.9 + import-local: ^3.0.2 + jest-config: ^29.6.3 + jest-util: ^29.6.3 + jest-validate: ^29.6.3 + prompts: ^2.0.1 + yargs: ^17.3.1 + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + bin: + jest: bin/jest.js + checksum: 69c422f1522b25756afb5a27b4b01a710d0f5ba52c592903b1ab47103ee2414ac9a9fff36a976092bb595980ba5c45f128e33b5d6ebc666c8a6973474bbf1443 + languageName: node + linkType: hard + +"jest-config@npm:^29.6.3": + version: 29.6.3 + resolution: "jest-config@npm:29.6.3" + dependencies: + "@babel/core": ^7.11.6 + "@jest/test-sequencer": ^29.6.3 + "@jest/types": ^29.6.3 + babel-jest: ^29.6.3 + chalk: ^4.0.0 + ci-info: ^3.2.0 + deepmerge: ^4.2.2 + glob: ^7.1.3 + graceful-fs: ^4.2.9 + jest-circus: ^29.6.3 + jest-environment-node: ^29.6.3 + jest-get-type: ^29.6.3 + jest-regex-util: ^29.6.3 + jest-resolve: ^29.6.3 + jest-runner: ^29.6.3 + jest-util: ^29.6.3 + jest-validate: ^29.6.3 + micromatch: ^4.0.4 + parse-json: ^5.2.0 + pretty-format: ^29.6.3 + slash: ^3.0.0 + strip-json-comments: ^3.1.1 + peerDependencies: + "@types/node": "*" + ts-node: ">=9.0.0" + peerDependenciesMeta: + "@types/node": + optional: true + ts-node: + optional: true + checksum: c3505411b89e5d046fbd294bb6e9ccc8c64a7efcf9d546450bec25512db4cbb67c8d102e4a58fa8ef8eac73052d1259533d9012b483469581ad5ed4cc5faa39f + languageName: node + linkType: hard + +"jest-diff@npm:^29.6.3": + version: 29.6.3 + resolution: "jest-diff@npm:29.6.3" + dependencies: + chalk: ^4.0.0 + diff-sequences: ^29.6.3 + jest-get-type: ^29.6.3 + pretty-format: ^29.6.3 + checksum: 23b0a88efeab36566386f059f3da340754d2860969cbc34805154e2377714e37e3130e21a791fc68008fb460bbf5edd7ec43c16d96d15797b32ccfae5160fe37 + languageName: node + linkType: hard + +"jest-docblock@npm:^29.6.3": + version: 29.6.3 + resolution: "jest-docblock@npm:29.6.3" + dependencies: + detect-newline: ^3.0.0 + checksum: 6f3213a1e79e7eedafeb462acfa9a41303f9c0167893b140f6818fa16d7eb6bf3f9b9cf4669097ca6b7154847793489ecd6b4f6cfb0e416b88cfa3b4b36715b6 + languageName: node + linkType: hard + +"jest-each@npm:^29.6.3": + version: 29.6.3 + resolution: "jest-each@npm:29.6.3" + dependencies: + "@jest/types": ^29.6.3 + chalk: ^4.0.0 + jest-get-type: ^29.6.3 + jest-util: ^29.6.3 + pretty-format: ^29.6.3 + checksum: fe06e80b3554e2a8464f5f5c61943e02db1f8a7177139cb55b3201a1d1513cb089d8800401f102729a31bf8dd6f88229044e6088fea9dd5647ed11e841b6b88c + languageName: node + linkType: hard + +"jest-environment-node@npm:^29.6.3": + version: 29.6.3 + resolution: "jest-environment-node@npm:29.6.3" + dependencies: + "@jest/environment": ^29.6.3 + "@jest/fake-timers": ^29.6.3 + "@jest/types": ^29.6.3 + "@types/node": "*" + jest-mock: ^29.6.3 + jest-util: ^29.6.3 + checksum: c215d8d94d95ba0353677c8b6c7c46d3f612bfd6becafa90e842ab99cb4ba2243c7f0309f1518ea2879820d39c0f3ec0d678e9ebb41055ed6eedbeb123f2897c + languageName: node + linkType: hard + +"jest-get-type@npm:^29.6.3": + version: 29.6.3 + resolution: "jest-get-type@npm:29.6.3" + checksum: 88ac9102d4679d768accae29f1e75f592b760b44277df288ad76ce5bf038c3f5ce3719dea8aa0f035dac30e9eb034b848ce716b9183ad7cc222d029f03e92205 + languageName: node + linkType: hard + +"jest-haste-map@npm:^29.6.3": + version: 29.6.3 + resolution: "jest-haste-map@npm:29.6.3" + dependencies: + "@jest/types": ^29.6.3 + "@types/graceful-fs": ^4.1.3 + "@types/node": "*" + anymatch: ^3.0.3 + fb-watchman: ^2.0.0 + fsevents: ^2.3.2 + graceful-fs: ^4.2.9 + jest-regex-util: ^29.6.3 + jest-util: ^29.6.3 + jest-worker: ^29.6.3 + micromatch: ^4.0.4 + walker: ^1.0.8 + dependenciesMeta: + fsevents: + optional: true + checksum: d72b81442cf54c5962009502b4001e53b7e40ecd1717bb5d17d5b0badc89cf5529b8be5d2804442d25ee6a70809de150e554b074029170b0e86a32b7560ce430 + languageName: node + linkType: hard + +"jest-leak-detector@npm:^29.6.3": + version: 29.6.3 + resolution: "jest-leak-detector@npm:29.6.3" + dependencies: + jest-get-type: ^29.6.3 + pretty-format: ^29.6.3 + checksum: 27548fcfc7602fe1b88f8600185e35ffff71751f3631e52bbfdfc72776f5a13a430185cf02fc632b41320a74f99ae90e40ce101c8887509f0f919608a7175129 + languageName: node + linkType: hard + +"jest-matcher-utils@npm:^29.6.3": + version: 29.6.3 + resolution: "jest-matcher-utils@npm:29.6.3" + dependencies: + chalk: ^4.0.0 + jest-diff: ^29.6.3 + jest-get-type: ^29.6.3 + pretty-format: ^29.6.3 + checksum: d4965d5cc079799bc0a9075daea7a964768d4db55f0388ef879642215399c955ae9a22c967496813c908763b487f97e40701a1eb4ed5b0b7529c447b6d33e652 + languageName: node + linkType: hard + +"jest-message-util@npm:^29.6.3": + version: 29.6.3 + resolution: "jest-message-util@npm:29.6.3" + dependencies: + "@babel/code-frame": ^7.12.13 + "@jest/types": ^29.6.3 + "@types/stack-utils": ^2.0.0 + chalk: ^4.0.0 + graceful-fs: ^4.2.9 + micromatch: ^4.0.4 + pretty-format: ^29.6.3 + slash: ^3.0.0 + stack-utils: ^2.0.3 + checksum: 59f5229a06c073a8877ba4d2e304cc07d63b0062bf5764d4bed14364403889e77f1825d1bd9017c19a840847d17dffd414dc06f1fcb537b5f9e03dbc65b84ada + languageName: node + linkType: hard + +"jest-mock@npm:^29.6.3": + version: 29.6.3 + resolution: "jest-mock@npm:29.6.3" + dependencies: + "@jest/types": ^29.6.3 + "@types/node": "*" + jest-util: ^29.6.3 + checksum: 35772968010c0afb1bb1ef78570b9cbea907c6f967d24b4e95e1a596a1000c63d60e225fb9ddfdd5218674da4aa61d92a09927fc26310cecbbfaa8278d919e32 + languageName: node + linkType: hard + +"jest-pnp-resolver@npm:^1.2.2": + version: 1.2.3 + resolution: "jest-pnp-resolver@npm:1.2.3" + peerDependencies: + jest-resolve: "*" + peerDependenciesMeta: + jest-resolve: + optional: true + checksum: db1a8ab2cb97ca19c01b1cfa9a9c8c69a143fde833c14df1fab0766f411b1148ff0df878adea09007ac6a2085ec116ba9a996a6ad104b1e58c20adbf88eed9b2 + languageName: node + linkType: hard + +"jest-regex-util@npm:^29.6.3": + version: 29.6.3 + resolution: "jest-regex-util@npm:29.6.3" + checksum: 0518beeb9bf1228261695e54f0feaad3606df26a19764bc19541e0fc6e2a3737191904607fb72f3f2ce85d9c16b28df79b7b1ec9443aa08c3ef0e9efda6f8f2a + languageName: node + linkType: hard + +"jest-resolve-dependencies@npm:^29.6.3": + version: 29.6.3 + resolution: "jest-resolve-dependencies@npm:29.6.3" + dependencies: + jest-regex-util: ^29.6.3 + jest-snapshot: ^29.6.3 + checksum: db0e57158cc085926f1e0dd63919cc78b87dc7e5644cd40f6b4b0bdcc228f3872b5520477db9a67889f4bcf658c5b85303fef89eee1df60d02a662c356021c2f + languageName: node + linkType: hard + +"jest-resolve@npm:^29.6.3": + version: 29.6.3 + resolution: "jest-resolve@npm:29.6.3" + dependencies: + chalk: ^4.0.0 + graceful-fs: ^4.2.9 + jest-haste-map: ^29.6.3 + jest-pnp-resolver: ^1.2.2 + jest-util: ^29.6.3 + jest-validate: ^29.6.3 + resolve: ^1.20.0 + resolve.exports: ^2.0.0 + slash: ^3.0.0 + checksum: 94594aab55b957e4f13fec248a18c99a6d8eb4842aa33ea5ef77179604df206d3fff1c59393a8984f179d0a7c6b98322d260b356076cdc2e74f2ebf1d9fba74a + languageName: node + linkType: hard + +"jest-runner@npm:^29.6.3": + version: 29.6.3 + resolution: "jest-runner@npm:29.6.3" + dependencies: + "@jest/console": ^29.6.3 + "@jest/environment": ^29.6.3 + "@jest/test-result": ^29.6.3 + "@jest/transform": ^29.6.3 + "@jest/types": ^29.6.3 + "@types/node": "*" + chalk: ^4.0.0 + emittery: ^0.13.1 + graceful-fs: ^4.2.9 + jest-docblock: ^29.6.3 + jest-environment-node: ^29.6.3 + jest-haste-map: ^29.6.3 + jest-leak-detector: ^29.6.3 + jest-message-util: ^29.6.3 + jest-resolve: ^29.6.3 + jest-runtime: ^29.6.3 + jest-util: ^29.6.3 + jest-watcher: ^29.6.3 + jest-worker: ^29.6.3 + p-limit: ^3.1.0 + source-map-support: 0.5.13 + checksum: 9f10100f1a558ec78d24e131494d9b3736633f788f3edcd30dbce7257c0cee6f62fec08ab99dbb684ddcc7dbb5ca846711b140ca6090a9547c5900a0e3da53f8 + languageName: node + linkType: hard + +"jest-runtime@npm:^29.6.3": + version: 29.6.3 + resolution: "jest-runtime@npm:29.6.3" + dependencies: + "@jest/environment": ^29.6.3 + "@jest/fake-timers": ^29.6.3 + "@jest/globals": ^29.6.3 + "@jest/source-map": ^29.6.3 + "@jest/test-result": ^29.6.3 + "@jest/transform": ^29.6.3 + "@jest/types": ^29.6.3 + "@types/node": "*" + chalk: ^4.0.0 + cjs-module-lexer: ^1.0.0 + collect-v8-coverage: ^1.0.0 + glob: ^7.1.3 + graceful-fs: ^4.2.9 + jest-haste-map: ^29.6.3 + jest-message-util: ^29.6.3 + jest-mock: ^29.6.3 + jest-regex-util: ^29.6.3 + jest-resolve: ^29.6.3 + jest-snapshot: ^29.6.3 + jest-util: ^29.6.3 + slash: ^3.0.0 + strip-bom: ^4.0.0 + checksum: 8743c61a2354dbce87282bfcbc11049f7d30d25ecd5f475ce56c1b7d926debb21b04db284d4d65a14283893a696442c66e923b35742fb02cc9f940a0a41ca49e + languageName: node + linkType: hard + +"jest-snapshot@npm:^29.6.3": + version: 29.6.3 + resolution: "jest-snapshot@npm:29.6.3" + dependencies: + "@babel/core": ^7.11.6 + "@babel/generator": ^7.7.2 + "@babel/plugin-syntax-jsx": ^7.7.2 + "@babel/plugin-syntax-typescript": ^7.7.2 + "@babel/types": ^7.3.3 + "@jest/expect-utils": ^29.6.3 + "@jest/transform": ^29.6.3 + "@jest/types": ^29.6.3 + babel-preset-current-node-syntax: ^1.0.0 + chalk: ^4.0.0 + expect: ^29.6.3 + graceful-fs: ^4.2.9 + jest-diff: ^29.6.3 + jest-get-type: ^29.6.3 + jest-matcher-utils: ^29.6.3 + jest-message-util: ^29.6.3 + jest-util: ^29.6.3 + natural-compare: ^1.4.0 + pretty-format: ^29.6.3 + semver: ^7.5.3 + checksum: c63631d2c18adc678455b9aa6e569cb1ea227e97aaa8628e154b39c95ca626d89e88d62c82e07d66cc83a1fddda1f7153506dd0f49d3411bbbecb52272ed72f5 + languageName: node + linkType: hard + +"jest-util@npm:^29.6.3": + version: 29.6.3 + resolution: "jest-util@npm:29.6.3" + dependencies: + "@jest/types": ^29.6.3 + "@types/node": "*" + chalk: ^4.0.0 + ci-info: ^3.2.0 + graceful-fs: ^4.2.9 + picomatch: ^2.2.3 + checksum: 7bf3ba3ac67ac6ceff7d8fdd23a86768e23ddd9133ecd9140ef87cc0c28708effabaf67a6cd45cd9d90a63d645a522ed0825d09ee59ac4c03b9c473b1fef4c7c + languageName: node + linkType: hard + +"jest-validate@npm:^29.6.3": + version: 29.6.3 + resolution: "jest-validate@npm:29.6.3" + dependencies: + "@jest/types": ^29.6.3 + camelcase: ^6.2.0 + chalk: ^4.0.0 + jest-get-type: ^29.6.3 + leven: ^3.1.0 + pretty-format: ^29.6.3 + checksum: caa489ed11080441c636b8035ab71bafbdc0c052b1e452855e4d2dd24ac15e497710a270ea6fc5ef8926b22c1ce4d6e07ec2dc193f0810cff5851d7a2222c045 + languageName: node + linkType: hard + +"jest-watcher@npm:^29.6.3": + version: 29.6.3 + resolution: "jest-watcher@npm:29.6.3" + dependencies: + "@jest/test-result": ^29.6.3 + "@jest/types": ^29.6.3 + "@types/node": "*" + ansi-escapes: ^4.2.1 + chalk: ^4.0.0 + emittery: ^0.13.1 + jest-util: ^29.6.3 + string-length: ^4.0.1 + checksum: d31ab2076342d45959d5a7d9fdd88c0c5d52c2ea6fb3b1eabe7f8c28177d90355331beb4d844e171ed9e0341a2da901b7eefaa122505ba0f0ac88e58d29b3374 + languageName: node + linkType: hard + +"jest-worker@npm:^29.6.3": + version: 29.6.3 + resolution: "jest-worker@npm:29.6.3" dependencies: - "@types/connect": ^3.4.33 - "@types/node": ^12.12.54 - "@types/ws": ^7.4.4 - JSONStream: ^1.3.5 - commander: ^2.20.3 - delay: ^5.0.0 - es6-promisify: ^5.0.0 - eyes: ^0.1.8 - isomorphic-ws: ^4.0.1 - json-stringify-safe: ^5.0.1 - uuid: ^8.3.2 - ws: ^7.4.5 + "@types/node": "*" + jest-util: ^29.6.3 + merge-stream: ^2.0.0 + supports-color: ^8.0.0 + checksum: 8ffb24a2d4c70ed3032034a2601defccc19353d854d89459f58793c6c8f170f88038c6722073c8047c5734c8ec8d4902ebc955f4f7acb433c2499adf616388fc + languageName: node + linkType: hard + +"jest@npm:^29.6.3": + version: 29.6.3 + resolution: "jest@npm:29.6.3" + dependencies: + "@jest/core": ^29.6.3 + "@jest/types": ^29.6.3 + import-local: ^3.0.2 + jest-cli: ^29.6.3 + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true bin: - jayson: bin/jayson.js - checksum: 86464322fbdc6db65d2bb4fc278cb6c86fad5c2a506065490d39459f09ba0d30f2b4fb740b33828a1424791419b6c8bd295dc54d361a4ad959bf70cc62b1ca7e + jest: bin/jest.js + checksum: dd4f53fb84f28b665b47c628222e5d3b624e9e0afa79b22afceef4f2a53dc0d8f0edd7ca254917ace5c94c3a7bf58c108563234c4fe34e86c679ce99633cfbe6 languageName: node linkType: hard @@ -7232,6 +8967,18 @@ __metadata: languageName: node linkType: hard +"js-yaml@npm:^3.13.1": + version: 3.14.1 + resolution: "js-yaml@npm:3.14.1" + dependencies: + argparse: ^1.0.7 + esprima: ^4.0.0 + bin: + js-yaml: bin/js-yaml.js + checksum: bef146085f472d44dee30ec34e5cf36bf89164f5d585435a3d3da89e52622dff0b188a580e4ad091c3341889e14cb88cac6e4deb16dc5b1e9623bb0601fc255c + languageName: node + linkType: hard + "js-yaml@npm:^4.1.0": version: 4.1.0 resolution: "js-yaml@npm:4.1.0" @@ -7259,6 +9006,13 @@ __metadata: languageName: node linkType: hard +"json-parse-even-better-errors@npm:^2.3.0": + version: 2.3.1 + resolution: "json-parse-even-better-errors@npm:2.3.1" + checksum: 798ed4cf3354a2d9ccd78e86d2169515a0097a5c133337807cdf7f1fc32e1391d207ccfc276518cc1d7d8d4db93288b8a50ba4293d212ad1336e52a8ec0a941f + languageName: node + linkType: hard + "json-rpc-engine@npm:6.1.0, json-rpc-engine@npm:^6.1.0": version: 6.1.0 resolution: "json-rpc-engine@npm:6.1.0" @@ -7317,6 +9071,15 @@ __metadata: languageName: node linkType: hard +"json5@npm:^2.2.2": + version: 2.2.3 + resolution: "json5@npm:2.2.3" + bin: + json5: lib/cli.js + checksum: 2a7436a93393830bce797d4626275152e37e877b265e94ca69c99e3d20c2b9dab021279146a39cdb700e71b2dd32a4cebd1514cd57cee102b1af906ce5040349 + languageName: node + linkType: hard + "jsonify@npm:^0.0.1": version: 0.0.1 resolution: "jsonify@npm:0.0.1" @@ -7379,6 +9142,13 @@ __metadata: languageName: node linkType: hard +"kleur@npm:^3.0.3": + version: 3.0.3 + resolution: "kleur@npm:3.0.3" + checksum: df82cd1e172f957bae9c536286265a5cdbd5eeca487cb0a3b2a7b41ef959fc61f8e7c0e9aeea9c114ccf2c166b6a8dd45a46fd619c1c569d210ecd2765ad5169 + languageName: node + linkType: hard + "language-subtag-registry@npm:~0.3.2": version: 0.3.22 resolution: "language-subtag-registry@npm:0.3.22" @@ -7395,6 +9165,13 @@ __metadata: languageName: node linkType: hard +"leven@npm:^3.1.0": + version: 3.1.0 + resolution: "leven@npm:3.1.0" + checksum: 638401d534585261b6003db9d99afd244dfe82d75ddb6db5c0df412842d5ab30b2ef18de471aaec70fe69a46f17b4ae3c7f01d8a4e6580ef7adb9f4273ad1e55 + languageName: node + linkType: hard + "levn@npm:^0.4.1": version: 0.4.1 resolution: "levn@npm:0.4.1" @@ -7541,6 +9318,15 @@ __metadata: languageName: node linkType: hard +"lru-cache@npm:^5.1.1": + version: 5.1.1 + resolution: "lru-cache@npm:5.1.1" + dependencies: + yallist: ^3.0.2 + checksum: c154ae1cbb0c2206d1501a0e94df349653c92c8cbb25236d7e85190bcaf4567a03ac6eb43166fabfa36fd35623694da7233e88d9601fbf411a9a481d85dbd2cb + languageName: node + linkType: hard + "lru-cache@npm:^6.0.0": version: 6.0.0 resolution: "lru-cache@npm:6.0.0" @@ -7557,6 +9343,15 @@ __metadata: languageName: node linkType: hard +"make-dir@npm:^4.0.0": + version: 4.0.0 + resolution: "make-dir@npm:4.0.0" + dependencies: + semver: ^7.5.3 + checksum: bf0731a2dd3aab4db6f3de1585cea0b746bb73eb5a02e3d8d72757e376e64e6ada190b1eddcde5b2f24a81b688a9897efd5018737d05e02e2a671dda9cff8a8a + languageName: node + linkType: hard + "make-error@npm:^1.1.1": version: 1.3.6 resolution: "make-error@npm:1.3.6" @@ -7588,6 +9383,15 @@ __metadata: languageName: node linkType: hard +"makeerror@npm:1.0.12": + version: 1.0.12 + resolution: "makeerror@npm:1.0.12" + dependencies: + tmpl: 1.0.5 + checksum: b38a025a12c8146d6eeea5a7f2bf27d51d8ad6064da8ca9405fcf7bf9b54acd43e3b30ddd7abb9b1bfa4ddb266019133313482570ddb207de568f71ecfcf6060 + languageName: node + linkType: hard + "md5.js@npm:^1.3.4": version: 1.3.5 resolution: "md5.js@npm:1.3.5" @@ -7617,6 +9421,13 @@ __metadata: languageName: node linkType: hard +"merge-stream@npm:^2.0.0": + version: 2.0.0 + resolution: "merge-stream@npm:2.0.0" + checksum: 6fa4dcc8d86629705cea944a4b88ef4cb0e07656ebf223fa287443256414283dd25d91c1cd84c77987f2aec5927af1a9db6085757cb43d90eb170ebf4b47f4f4 + languageName: node + linkType: hard + "merge2@npm:^1.3.0, merge2@npm:^1.4.1": version: 1.4.1 resolution: "merge2@npm:1.4.1" @@ -7660,6 +9471,13 @@ __metadata: languageName: node linkType: hard +"mimic-fn@npm:^2.1.0": + version: 2.1.0 + resolution: "mimic-fn@npm:2.1.0" + checksum: d2421a3444848ce7f84bd49115ddacff29c15745db73f54041edc906c14b131a38d05298dae3081667627a59b2eb1ca4b436ff2e1b80f69679522410418b478a + languageName: node + linkType: hard + "minimalistic-assert@npm:^1.0.0, minimalistic-assert@npm:^1.0.1": version: 1.0.1 resolution: "minimalistic-assert@npm:1.0.1" @@ -8020,6 +9838,20 @@ __metadata: languageName: node linkType: hard +"node-int64@npm:^0.4.0": + version: 0.4.0 + resolution: "node-int64@npm:0.4.0" + checksum: d0b30b1ee6d961851c60d5eaa745d30b5c95d94bc0e74b81e5292f7c42a49e3af87f1eb9e89f59456f80645d679202537de751b7d72e9e40ceea40c5e449057e + languageName: node + linkType: hard + +"node-releases@npm:^2.0.13": + version: 2.0.13 + resolution: "node-releases@npm:2.0.13" + checksum: 17ec8f315dba62710cae71a8dad3cd0288ba943d2ece43504b3b1aa8625bf138637798ab470b1d9035b0545996f63000a8a926e0f6d35d0996424f8b6d36dda3 + languageName: node + linkType: hard + "node-releases@npm:^2.0.8": version: 2.0.10 resolution: "node-releases@npm:2.0.10" @@ -8052,6 +9884,15 @@ __metadata: languageName: node linkType: hard +"npm-run-path@npm:^4.0.1": + version: 4.0.1 + resolution: "npm-run-path@npm:4.0.1" + dependencies: + path-key: ^3.0.0 + checksum: 5374c0cea4b0bbfdfae62da7bbdf1e1558d338335f4cacf2515c282ff358ff27b2ecb91ffa5330a8b14390ac66a1e146e10700440c1ab868208430f56b5f4d23 + languageName: node + linkType: hard + "npmlog@npm:^6.0.0": version: 6.0.2 resolution: "npmlog@npm:6.0.2" @@ -8180,6 +10021,15 @@ __metadata: languageName: node linkType: hard +"onetime@npm:^5.1.2": + version: 5.1.2 + resolution: "onetime@npm:5.1.2" + dependencies: + mimic-fn: ^2.1.0 + checksum: 2478859ef817fc5d4e9c2f9e5728512ddd1dbc9fb7829ad263765bb6d3b91ce699d6e2332eef6b7dff183c2f490bd3349f1666427eaba4469fba0ac38dfd0d34 + languageName: node + linkType: hard + "open@npm:^8.4.0": version: 8.4.0 resolution: "open@npm:8.4.0" @@ -8230,7 +10080,7 @@ __metadata: languageName: node linkType: hard -"p-limit@npm:^3.0.2": +"p-limit@npm:^3.0.2, p-limit@npm:^3.1.0": version: 3.1.0 resolution: "p-limit@npm:3.1.0" dependencies: @@ -8320,6 +10170,18 @@ __metadata: languageName: node linkType: hard +"parse-json@npm:^5.2.0": + version: 5.2.0 + resolution: "parse-json@npm:5.2.0" + dependencies: + "@babel/code-frame": ^7.0.0 + error-ex: ^1.3.1 + json-parse-even-better-errors: ^2.3.0 + lines-and-columns: ^1.1.6 + checksum: 62085b17d64da57f40f6afc2ac1f4d95def18c4323577e1eced571db75d9ab59b297d1d10582920f84b15985cbfc6b6d450ccbf317644cfa176f3ed982ad87e2 + languageName: node + linkType: hard + "path-exists@npm:^3.0.0": version: 3.0.0 resolution: "path-exists@npm:3.0.0" @@ -8341,7 +10203,7 @@ __metadata: languageName: node linkType: hard -"path-key@npm:^3.1.0": +"path-key@npm:^3.0.0, path-key@npm:^3.1.0": version: 3.1.1 resolution: "path-key@npm:3.1.1" checksum: 55cd7a9dd4b343412a8386a743f9c746ef196e57c823d90ca3ab917f90ab9f13dd0ded27252ba49dbdfcab2b091d998bc446f6220cd3cea65db407502a740020 @@ -8382,7 +10244,7 @@ __metadata: languageName: node linkType: hard -"picomatch@npm:^2.0.4, picomatch@npm:^2.2.1, picomatch@npm:^2.3.1": +"picomatch@npm:^2.0.4, picomatch@npm:^2.2.1, picomatch@npm:^2.2.3, picomatch@npm:^2.3.1": version: 2.3.1 resolution: "picomatch@npm:2.3.1" checksum: 050c865ce81119c4822c45d3c84f1ced46f93a0126febae20737bd05ca20589c564d6e9226977df859ed5e03dc73f02584a2b0faad36e896936238238b0446cf @@ -8455,6 +10317,22 @@ __metadata: languageName: node linkType: hard +"pirates@npm:^4.0.4": + version: 4.0.6 + resolution: "pirates@npm:4.0.6" + checksum: 46a65fefaf19c6f57460388a5af9ab81e3d7fd0e7bc44ca59d753cb5c4d0df97c6c6e583674869762101836d68675f027d60f841c105d72734df9dfca97cbcc6 + languageName: node + linkType: hard + +"pkg-dir@npm:^4.2.0": + version: 4.2.0 + resolution: "pkg-dir@npm:4.2.0" + dependencies: + find-up: ^4.0.0 + checksum: 9863e3f35132bf99ae1636d31ff1e1e3501251d480336edb1c211133c8d58906bed80f154a1d723652df1fda91e01c7442c2eeaf9dc83157c7ae89087e43c8d6 + languageName: node + linkType: hard + "pngjs@npm:^3.3.0": version: 3.4.0 resolution: "pngjs@npm:3.4.0" @@ -8598,6 +10476,17 @@ __metadata: languageName: node linkType: hard +"pretty-format@npm:^29.0.0, pretty-format@npm:^29.6.3": + version: 29.6.3 + resolution: "pretty-format@npm:29.6.3" + dependencies: + "@jest/schemas": ^29.6.3 + ansi-styles: ^5.0.0 + react-is: ^18.0.0 + checksum: 4e1c0db48e65571c22e80ff92123925ff8b3a2a89b71c3a1683cfde711004d492de32fe60c6bc10eea8bf6c678e5cbe544ac6c56cb8096e1eb7caf856928b1c4 + languageName: node + linkType: hard + "process-warning@npm:^1.0.0": version: 1.0.0 resolution: "process-warning@npm:1.0.0" @@ -8629,6 +10518,16 @@ __metadata: languageName: node linkType: hard +"prompts@npm:^2.0.1": + version: 2.4.2 + resolution: "prompts@npm:2.4.2" + dependencies: + kleur: ^3.0.3 + sisteransi: ^1.0.5 + checksum: d8fd1fe63820be2412c13bfc5d0a01909acc1f0367e32396962e737cb2fc52d004f3302475d5ce7d18a1e8a79985f93ff04ee03007d091029c3f9104bffc007d + languageName: node + linkType: hard + "prop-types@npm:^15.6.0, prop-types@npm:^15.6.2, prop-types@npm:^15.7.2, prop-types@npm:^15.8.1": version: 15.8.1 resolution: "prop-types@npm:15.8.1" @@ -8678,6 +10577,13 @@ __metadata: languageName: node linkType: hard +"pure-rand@npm:^6.0.0": + version: 6.0.2 + resolution: "pure-rand@npm:6.0.2" + checksum: 79de33876a4f515d759c48e98d00756bbd916b4ea260cc572d7adfa4b62cace9952e89f0241d0410214554503d25061140fe325c66f845213d2b1728ba8d413e + languageName: node + linkType: hard + "qr.js@npm:0.0.0": version: 0.0.0 resolution: "qr.js@npm:0.0.0" @@ -8863,6 +10769,13 @@ __metadata: languageName: node linkType: hard +"react-is@npm:^18.0.0": + version: 18.2.0 + resolution: "react-is@npm:18.2.0" + checksum: e72d0ba81b5922759e4aff17e0252bd29988f9642ed817f56b25a3e217e13eea8a7f2322af99a06edb779da12d5d636e9fda473d620df9a3da0df2a74141d53e + languageName: node + linkType: hard + "react-lifecycles-compat@npm:^3.0.0": version: 3.0.4 resolution: "react-lifecycles-compat@npm:3.0.4" @@ -9088,6 +11001,15 @@ __metadata: languageName: node linkType: hard +"resolve-cwd@npm:^3.0.0": + version: 3.0.0 + resolution: "resolve-cwd@npm:3.0.0" + dependencies: + resolve-from: ^5.0.0 + checksum: 546e0816012d65778e580ad62b29e975a642989108d9a3c5beabfb2304192fa3c9f9146fbdfe213563c6ff51975ae41bac1d3c6e047dd9572c94863a057b4d81 + languageName: node + linkType: hard + "resolve-from@npm:^4.0.0": version: 4.0.0 resolution: "resolve-from@npm:4.0.0" @@ -9095,6 +11017,20 @@ __metadata: languageName: node linkType: hard +"resolve-from@npm:^5.0.0": + version: 5.0.0 + resolution: "resolve-from@npm:5.0.0" + checksum: 4ceeb9113e1b1372d0cd969f3468fa042daa1dd9527b1b6bb88acb6ab55d8b9cd65dbf18819f9f9ddf0db804990901dcdaade80a215e7b2c23daae38e64f5bdf + languageName: node + linkType: hard + +"resolve.exports@npm:^2.0.0": + version: 2.0.2 + resolution: "resolve.exports@npm:2.0.2" + checksum: 1c7778ca1b86a94f8ab4055d196c7d87d1874b96df4d7c3e67bbf793140f0717fd506dcafd62785b079cd6086b9264424ad634fb904409764c3509c3df1653f2 + languageName: node + linkType: hard + "resolve@npm:^1.1.7, resolve@npm:^1.20.0, resolve@npm:^1.22.0": version: 1.22.1 resolution: "resolve@npm:1.22.1" @@ -9355,6 +11291,15 @@ __metadata: languageName: node linkType: hard +"semver@npm:^6.3.1": + version: 6.3.1 + resolution: "semver@npm:6.3.1" + bin: + semver: bin/semver.js + checksum: ae47d06de28836adb9d3e25f22a92943477371292d9b665fb023fae278d345d508ca1958232af086d85e0155aee22e313e100971898bbb8d5d89b8b1d4054ca2 + languageName: node + linkType: hard + "semver@npm:^7.3.5, semver@npm:^7.3.7": version: 7.3.7 resolution: "semver@npm:7.3.7" @@ -9377,6 +11322,17 @@ __metadata: languageName: node linkType: hard +"semver@npm:^7.5.3, semver@npm:^7.5.4": + version: 7.5.4 + resolution: "semver@npm:7.5.4" + dependencies: + lru-cache: ^6.0.0 + bin: + semver: bin/semver.js + checksum: 12d8ad952fa353b0995bf180cdac205a4068b759a140e5d3c608317098b3575ac2f1e09182206bf2eb26120e1c0ed8fb92c48c592f6099680de56bb071423ca3 + languageName: node + linkType: hard + "set-blocking@npm:^2.0.0": version: 2.0.0 resolution: "set-blocking@npm:2.0.0" @@ -9423,13 +11379,20 @@ __metadata: languageName: node linkType: hard -"signal-exit@npm:^3.0.7": +"signal-exit@npm:^3.0.3, signal-exit@npm:^3.0.7": version: 3.0.7 resolution: "signal-exit@npm:3.0.7" checksum: a2f098f247adc367dffc27845853e9959b9e88b01cb301658cfe4194352d8d2bb32e18467c786a7fe15f1d44b233ea35633d076d5e737870b7139949d1ab6318 languageName: node linkType: hard +"sisteransi@npm:^1.0.5": + version: 1.0.5 + resolution: "sisteransi@npm:1.0.5" + checksum: aba6438f46d2bfcef94cf112c835ab395172c75f67453fe05c340c770d3c402363018ae1ab4172a1026a90c47eaccf3af7b6ff6fa749a680c2929bd7fa2b37a4 + languageName: node + linkType: hard + "slash@npm:^3.0.0": version: 3.0.0 resolution: "slash@npm:3.0.0" @@ -9510,6 +11473,16 @@ __metadata: languageName: node linkType: hard +"source-map-support@npm:0.5.13": + version: 0.5.13 + resolution: "source-map-support@npm:0.5.13" + dependencies: + buffer-from: ^1.0.0 + source-map: ^0.6.0 + checksum: 933550047b6c1a2328599a21d8b7666507427c0f5ef5eaadd56b5da0fd9505e239053c66fe181bf1df469a3b7af9d775778eee283cbb7ae16b902ddc09e93a97 + languageName: node + linkType: hard + "source-map@npm:^0.5.0": version: 0.5.7 resolution: "source-map@npm:0.5.7" @@ -9517,6 +11490,13 @@ __metadata: languageName: node linkType: hard +"source-map@npm:^0.6.0, source-map@npm:^0.6.1": + version: 0.6.1 + resolution: "source-map@npm:0.6.1" + checksum: 59ce8640cf3f3124f64ac289012c2b8bd377c238e316fb323ea22fbfe83da07d81e000071d7242cad7a23cd91c7de98e4df8830ec3f133cb6133a5f6e9f67bc2 + languageName: node + linkType: hard + "split-on-first@npm:^1.0.0": version: 1.1.0 resolution: "split-on-first@npm:1.1.0" @@ -9531,6 +11511,13 @@ __metadata: languageName: node linkType: hard +"sprintf-js@npm:~1.0.2": + version: 1.0.3 + resolution: "sprintf-js@npm:1.0.3" + checksum: 19d79aec211f09b99ec3099b5b2ae2f6e9cdefe50bc91ac4c69144b6d3928a640bb6ae5b3def70c2e85a2c3d9f5ec2719921e3a59d3ca3ef4b2fd1a4656a0df3 + languageName: node + linkType: hard + "ssri@npm:^9.0.0": version: 9.0.1 resolution: "ssri@npm:9.0.1" @@ -9540,6 +11527,15 @@ __metadata: languageName: node linkType: hard +"stack-utils@npm:^2.0.3": + version: 2.0.6 + resolution: "stack-utils@npm:2.0.6" + dependencies: + escape-string-regexp: ^2.0.0 + checksum: 052bf4d25bbf5f78e06c1d5e67de2e088b06871fa04107ca8d3f0e9d9263326e2942c8bedee3545795fc77d787d443a538345eef74db2f8e35db3558c6f91ff7 + languageName: node + linkType: hard + "stream-browserify@npm:^3.0.0": version: 3.0.0 resolution: "stream-browserify@npm:3.0.0" @@ -9564,6 +11560,16 @@ __metadata: languageName: node linkType: hard +"string-length@npm:^4.0.1": + version: 4.0.2 + resolution: "string-length@npm:4.0.2" + dependencies: + char-regex: ^1.0.2 + strip-ansi: ^6.0.0 + checksum: ce85533ef5113fcb7e522bcf9e62cb33871aa99b3729cec5595f4447f660b0cefd542ca6df4150c97a677d58b0cb727a3fe09ac1de94071d05526c73579bf505 + languageName: node + linkType: hard + "string-width@npm:^1.0.2 || 2 || 3 || 4, string-width@npm:^4.1.0, string-width@npm:^4.2.0, string-width@npm:^4.2.3": version: 4.2.3 resolution: "string-width@npm:4.2.3" @@ -9658,6 +11664,20 @@ __metadata: languageName: node linkType: hard +"strip-bom@npm:^4.0.0": + version: 4.0.0 + resolution: "strip-bom@npm:4.0.0" + checksum: 9dbcfbaf503c57c06af15fe2c8176fb1bf3af5ff65003851a102749f875a6dbe0ab3b30115eccf6e805e9d756830d3e40ec508b62b3f1ddf3761a20ebe29d3f3 + languageName: node + linkType: hard + +"strip-final-newline@npm:^2.0.0": + version: 2.0.0 + resolution: "strip-final-newline@npm:2.0.0" + checksum: 69412b5e25731e1938184b5d489c32e340605bb611d6140344abc3421b7f3c6f9984b21dff296dfcf056681b82caa3bb4cc996a965ce37bcfad663e92eae9c64 + languageName: node + linkType: hard + "strip-json-comments@npm:^3.1.0, strip-json-comments@npm:^3.1.1": version: 3.1.1 resolution: "strip-json-comments@npm:3.1.1" @@ -9731,6 +11751,15 @@ __metadata: languageName: node linkType: hard +"supports-color@npm:^8.0.0": + version: 8.1.1 + resolution: "supports-color@npm:8.1.1" + dependencies: + has-flag: ^4.0.0 + checksum: c052193a7e43c6cdc741eb7f378df605636e01ad434badf7324f17fb60c69a880d8d8fcdcb562cf94c2350e57b937d7425ab5b8326c67c2adc48f7c87c1db406 + languageName: node + linkType: hard + "supports-preserve-symlinks-flag@npm:^1.0.0": version: 1.0.0 resolution: "supports-preserve-symlinks-flag@npm:1.0.0" @@ -9803,6 +11832,17 @@ __metadata: languageName: node linkType: hard +"test-exclude@npm:^6.0.0": + version: 6.0.0 + resolution: "test-exclude@npm:6.0.0" + dependencies: + "@istanbuljs/schema": ^0.1.2 + glob: ^7.1.4 + minimatch: ^3.0.4 + checksum: 3b34a3d77165a2cb82b34014b3aba93b1c4637a5011807557dc2f3da826c59975a5ccad765721c4648b39817e3472789f9b0fa98fc854c5c1c7a1e632aacdc28 + languageName: node + linkType: hard + "text-encoding-utf-8@npm:^1.0.2": version: 1.0.2 resolution: "text-encoding-utf-8@npm:1.0.2" @@ -9868,6 +11908,13 @@ __metadata: languageName: node linkType: hard +"tmpl@npm:1.0.5": + version: 1.0.5 + resolution: "tmpl@npm:1.0.5" + checksum: cd922d9b853c00fe414c5a774817be65b058d54a2d01ebb415840960406c669a0fc632f66df885e24cb022ec812739199ccbdb8d1164c3e513f85bfca5ab2873 + languageName: node + linkType: hard + "to-fast-properties@npm:^2.0.0": version: 2.0.0 resolution: "to-fast-properties@npm:2.0.0" @@ -10003,6 +12050,13 @@ __metadata: languageName: node linkType: hard +"type-detect@npm:4.0.8": + version: 4.0.8 + resolution: "type-detect@npm:4.0.8" + checksum: 62b5628bff67c0eb0b66afa371bd73e230399a8d2ad30d852716efcc4656a7516904570cd8631a49a3ce57c10225adf5d0cbdcb47f6b0255fe6557c453925a15 + languageName: node + linkType: hard + "type-fest@npm:^0.20.2": version: 0.20.2 resolution: "type-fest@npm:0.20.2" @@ -10010,6 +12064,13 @@ __metadata: languageName: node linkType: hard +"type-fest@npm:^0.21.3": + version: 0.21.3 + resolution: "type-fest@npm:0.21.3" + checksum: e6b32a3b3877f04339bae01c193b273c62ba7bfc9e325b8703c4ee1b32dc8fe4ef5dfa54bf78265e069f7667d058e360ae0f37be5af9f153b22382cd55a9afe0 + languageName: node + linkType: hard + "typedarray-to-buffer@npm:3.1.5": version: 3.1.5 resolution: "typedarray-to-buffer@npm:3.1.5" @@ -10119,6 +12180,20 @@ __metadata: languageName: node linkType: hard +"update-browserslist-db@npm:^1.0.11": + version: 1.0.11 + resolution: "update-browserslist-db@npm:1.0.11" + dependencies: + escalade: ^3.1.1 + picocolors: ^1.0.0 + peerDependencies: + browserslist: ">= 4.21.0" + bin: + update-browserslist-db: cli.js + checksum: b98327518f9a345c7cad5437afae4d2ae7d865f9779554baf2a200fdf4bac4969076b679b1115434bd6557376bdd37ca7583d0f9b8f8e302d7d4cc1e91b5f231 + languageName: node + linkType: hard + "uri-js@npm:^4.2.2": version: 4.4.1 resolution: "uri-js@npm:4.4.1" @@ -10228,6 +12303,17 @@ __metadata: languageName: node linkType: hard +"v8-to-istanbul@npm:^9.0.1": + version: 9.1.0 + resolution: "v8-to-istanbul@npm:9.1.0" + dependencies: + "@jridgewell/trace-mapping": ^0.3.12 + "@types/istanbul-lib-coverage": ^2.0.1 + convert-source-map: ^1.6.0 + checksum: 2069d59ee46cf8d83b4adfd8a5c1a90834caffa9f675e4360f1157ffc8578ef0f763c8f32d128334424159bb6b01f3876acd39cd13297b2769405a9da241f8d1 + languageName: node + linkType: hard + "valtio@npm:1.10.6": version: 1.10.6 resolution: "valtio@npm:1.10.6" @@ -10264,6 +12350,15 @@ __metadata: languageName: node linkType: hard +"walker@npm:^1.0.8": + version: 1.0.8 + resolution: "walker@npm:1.0.8" + dependencies: + makeerror: 1.0.12 + checksum: ad7a257ea1e662e57ef2e018f97b3c02a7240ad5093c392186ce0bcf1f1a60bbadd520d073b9beb921ed99f64f065efb63dfc8eec689a80e569f93c1c5d5e16c + languageName: node + linkType: hard + "warning@npm:^4.0.3": version: 4.0.3 resolution: "warning@npm:4.0.3" @@ -10383,6 +12478,17 @@ __metadata: languageName: node linkType: hard +"wrap-ansi@npm:^7.0.0": + version: 7.0.0 + resolution: "wrap-ansi@npm:7.0.0" + dependencies: + ansi-styles: ^4.0.0 + string-width: ^4.1.0 + strip-ansi: ^6.0.0 + checksum: a790b846fd4505de962ba728a21aaeda189b8ee1c7568ca5e817d85930e06ef8d1689d49dbf0e881e8ef84436af3a88bc49115c2e2788d841ff1b8b5b51a608b + languageName: node + linkType: hard + "wrappy@npm:1": version: 1.0.2 resolution: "wrappy@npm:1.0.2" @@ -10390,6 +12496,16 @@ __metadata: languageName: node linkType: hard +"write-file-atomic@npm:^4.0.2": + version: 4.0.2 + resolution: "write-file-atomic@npm:4.0.2" + dependencies: + imurmurhash: ^0.1.4 + signal-exit: ^3.0.7 + checksum: 5da60bd4eeeb935eec97ead3df6e28e5917a6bd317478e4a85a5285e8480b8ed96032bbcc6ecd07b236142a24f3ca871c924ec4a6575e623ec1b11bf8c1c253c + languageName: node + linkType: hard + "ws@npm:7.4.6": version: 7.4.6 resolution: "ws@npm:7.4.6" @@ -10471,6 +12587,20 @@ __metadata: languageName: node linkType: hard +"y18n@npm:^5.0.5": + version: 5.0.8 + resolution: "y18n@npm:5.0.8" + checksum: 54f0fb95621ee60898a38c572c515659e51cc9d9f787fb109cef6fde4befbe1c4602dc999d30110feee37456ad0f1660fa2edcfde6a9a740f86a290999550d30 + languageName: node + linkType: hard + +"yallist@npm:^3.0.2": + version: 3.1.1 + resolution: "yallist@npm:3.1.1" + checksum: 48f7bb00dc19fc635a13a39fe547f527b10c9290e7b3e836b9a8f1ca04d4d342e85714416b3c2ab74949c9c66f9cebb0473e6bc353b79035356103b47641285d + languageName: node + linkType: hard + "yallist@npm:^4.0.0": version: 4.0.0 resolution: "yallist@npm:4.0.0" @@ -10505,6 +12635,13 @@ __metadata: languageName: node linkType: hard +"yargs-parser@npm:^21.1.1": + version: 21.1.1 + resolution: "yargs-parser@npm:21.1.1" + checksum: ed2d96a616a9e3e1cc7d204c62ecc61f7aaab633dcbfab2c6df50f7f87b393993fe6640d017759fe112d0cb1e0119f2b4150a87305cc873fd90831c6a58ccf1c + languageName: node + linkType: hard + "yargs@npm:^13.2.4": version: 13.3.2 resolution: "yargs@npm:13.3.2" @@ -10542,6 +12679,21 @@ __metadata: languageName: node linkType: hard +"yargs@npm:^17.3.1": + version: 17.7.2 + resolution: "yargs@npm:17.7.2" + dependencies: + cliui: ^8.0.1 + escalade: ^3.1.1 + get-caller-file: ^2.0.5 + require-directory: ^2.1.1 + string-width: ^4.2.3 + y18n: ^5.0.5 + yargs-parser: ^21.1.1 + checksum: 73b572e863aa4a8cbef323dd911d79d193b772defd5a51aab0aca2d446655216f5002c42c5306033968193bdbf892a7a4c110b0d77954a7fdf563e653967b56a + languageName: node + linkType: hard + "yn@npm:3.1.1": version: 3.1.1 resolution: "yn@npm:3.1.1" From 4d4dffe119ed20c346c1a54d03d1dc1102aff60e Mon Sep 17 00:00:00 2001 From: J M Rossy Date: Tue, 22 Aug 2023 16:58:59 -0400 Subject: [PATCH 28/58] Port fixes for collateral balance checking --- .../tokens/adapters/AdapterFactory.ts | 9 +-- src/features/tokens/routes/fetch.test.ts | 29 ++++++--- src/features/tokens/routes/fetch.ts | 65 +++++++++---------- src/features/tokens/routes/types.ts | 6 +- src/features/tokens/types.ts | 2 +- src/features/transfer/useTokenTransfer.ts | 18 +++-- 6 files changed, 71 insertions(+), 58 deletions(-) diff --git a/src/features/tokens/adapters/AdapterFactory.ts b/src/features/tokens/adapters/AdapterFactory.ts index 7de66b60..d8d30ebc 100644 --- a/src/features/tokens/adapters/AdapterFactory.ts +++ b/src/features/tokens/adapters/AdapterFactory.ts @@ -103,20 +103,21 @@ export class AdapterFactory { } static HypTokenAdapterFromRouteDest(route: Route) { - const { type, destCaip2Id, destRouterAddress, baseTokenCaip19Id } = route; + const { type, destCaip2Id, destRouterAddress, destTokenCaip19Id, baseTokenCaip19Id } = route; + const tokenCaip19Id = destTokenCaip19Id || baseTokenCaip19Id; if (isRouteToCollateral(route)) { return AdapterFactory.selectHypAdapter( destCaip2Id, destRouterAddress, - baseTokenCaip19Id, + tokenCaip19Id, EvmHypCollateralAdapter, - isNativeToken(baseTokenCaip19Id) ? SealevelHypNativeAdapter : SealevelHypCollateralAdapter, + isNativeToken(tokenCaip19Id) ? SealevelHypNativeAdapter : SealevelHypCollateralAdapter, ); } else if (isRouteToSynthetic(route)) { return AdapterFactory.selectHypAdapter( destCaip2Id, destRouterAddress, - baseTokenCaip19Id, + tokenCaip19Id, EvmHypSyntheticAdapter, SealevelHypSyntheticAdapter, ); diff --git a/src/features/tokens/routes/fetch.test.ts b/src/features/tokens/routes/fetch.test.ts index 1fabfb59..3b548f63 100644 --- a/src/features/tokens/routes/fetch.test.ts +++ b/src/features/tokens/routes/fetch.test.ts @@ -23,11 +23,13 @@ describe('computeTokenRoutes', () => { hypTokens: [ { decimals: 18, - tokenCaip19Id: 'ethereum:11155111/erc20:0xDcbc0faAA269Cf649AC8950838664BB7B355BD6B', + chain: 'ethereum:11155111', + router: '0xDcbc0faAA269Cf649AC8950838664BB7B355BD6B', }, { decimals: 18, - tokenCaip19Id: 'ethereum:44787/erc20:0xDcbc0faAA269Cf649AC8950838664BB7B355BD6B', + chain: 'ethereum:44787', + router: '0xEcbc0faAA269Cf649AC8950838664BB7B355BD6C', }, ], }, @@ -56,7 +58,7 @@ describe('computeTokenRoutes', () => { originRouterAddress: '0x145de8760021c4ac6676376691b78038d3DE9097', originDecimals: 18, destCaip2Id: 'ethereum:44787', - destRouterAddress: '0xDcbc0faAA269Cf649AC8950838664BB7B355BD6B', + destRouterAddress: '0xEcbc0faAA269Cf649AC8950838664BB7B355BD6C', destDecimals: 18, }, ], @@ -84,7 +86,7 @@ describe('computeTokenRoutes', () => { originRouterAddress: '0xDcbc0faAA269Cf649AC8950838664BB7B355BD6B', originDecimals: 18, destCaip2Id: 'ethereum:44787', - destRouterAddress: '0xDcbc0faAA269Cf649AC8950838664BB7B355BD6B', + destRouterAddress: '0xEcbc0faAA269Cf649AC8950838664BB7B355BD6C', destDecimals: 18, }, ], @@ -96,7 +98,7 @@ describe('computeTokenRoutes', () => { baseTokenCaip19Id: 'ethereum:5/erc20:0xb4fbf271143f4fbf7b91a5ded31805e42b2208d6', baseRouterAddress: '0x145de8760021c4ac6676376691b78038d3DE9097', originCaip2Id: 'ethereum:44787', - originRouterAddress: '0xDcbc0faAA269Cf649AC8950838664BB7B355BD6B', + originRouterAddress: '0xEcbc0faAA269Cf649AC8950838664BB7B355BD6C', originDecimals: 18, destCaip2Id: 'ethereum:5', destRouterAddress: '0x145de8760021c4ac6676376691b78038d3DE9097', @@ -109,7 +111,7 @@ describe('computeTokenRoutes', () => { baseTokenCaip19Id: 'ethereum:5/erc20:0xb4fbf271143f4fbf7b91a5ded31805e42b2208d6', baseRouterAddress: '0x145de8760021c4ac6676376691b78038d3DE9097', originCaip2Id: 'ethereum:44787', - originRouterAddress: '0xDcbc0faAA269Cf649AC8950838664BB7B355BD6B', + originRouterAddress: '0xEcbc0faAA269Cf649AC8950838664BB7B355BD6C', originDecimals: 18, destCaip2Id: 'ethereum:11155111', destRouterAddress: '0xDcbc0faAA269Cf649AC8950838664BB7B355BD6B', @@ -132,11 +134,13 @@ describe('computeTokenRoutes', () => { hypTokens: [ { decimals: 18, - tokenCaip19Id: 'ethereum:11155111/erc20:0xDcbc0faAA269Cf649AC8950838664BB7B355BD6B', + chain: 'ethereum:11155111', + router: '0xDcbc0faAA269Cf649AC8950838664BB7B355BD6B', }, { decimals: 6, - tokenCaip19Id: 'sealevel:1399811151/native:PJH5QAbxAqrrnSXfH3GHR8icua8CDFZmo97z91xmpvx', + chain: 'sealevel:1399811151', + router: 'PJH5QAbxAqrrnSXfH3GHR8icua8CDFZmo97z91xmpvx', }, ], }, @@ -150,11 +154,13 @@ describe('computeTokenRoutes', () => { hypTokens: [ { decimals: 18, - tokenCaip19Id: 'ethereum:11155111/erc20:0xDcbc0faAA269Cf649AC8950838664BB7B355BD6B', + chain: 'ethereum:11155111', + router: '0xDcbc0faAA269Cf649AC8950838664BB7B355BD6B', }, { decimals: 18, - tokenCaip19Id: 'ethereum:5/erc20:0x145de8760021c4ac6676376691b78038d3DE9097', + chain: 'ethereum:5', + router: '0x145de8760021c4ac6676376691b78038d3DE9097', }, ], }, @@ -185,6 +191,8 @@ describe('computeTokenRoutes', () => { destCaip2Id: 'sealevel:1399811151', destRouterAddress: 'PJH5QAbxAqrrnSXfH3GHR8icua8CDFZmo97z91xmpvx', destDecimals: 6, + destTokenCaip19Id: + 'sealevel:1399811151/native:00000000000000000000000000000000000000000000', }, ], }, @@ -230,6 +238,7 @@ describe('computeTokenRoutes', () => { destCaip2Id: 'ethereum:5', destRouterAddress: '0x145de8760021c4ac6676376691b78038d3DE9097', destDecimals: 18, + destTokenCaip19Id: 'ethereum:5/erc20:0xb4fbf271143f4fbf7b91a5ded31805e42b2208d6', }, ], 'ethereum:11155111': [ diff --git a/src/features/tokens/routes/fetch.ts b/src/features/tokens/routes/fetch.ts index acb47501..d41a185c 100644 --- a/src/features/tokens/routes/fetch.ts +++ b/src/features/tokens/routes/fetch.ts @@ -3,13 +3,7 @@ import { ProtocolType } from '@hyperlane-xyz/sdk'; import { areAddressesEqual, bytesToProtocolAddress } from '../../../utils/addresses'; import { logger } from '../../../utils/logger'; import { getCaip2Id } from '../../caip/chains'; -import { - getCaip19Id, - getChainIdFromToken, - isNonFungibleToken, - parseCaip19Id, - resolveAssetNamespace, -} from '../../caip/tokens'; +import { getChainIdFromToken, isNonFungibleToken } from '../../caip/tokens'; import { getMultiProvider } from '../../multiProvider'; import { AdapterFactory } from '../adapters/AdapterFactory'; import { TokenMetadata, TokenMetadataWithHypTokens } from '../types'; @@ -38,24 +32,23 @@ export async function fetchRemoteHypTokens( remoteRouters.map(async (router) => { const destMetadata = multiProvider.getChainMetadata(router.domain); const protocol = destMetadata.protocol || ProtocolType.Ethereum; - const chainCaip2Id = getCaip2Id(protocol, multiProvider.getChainId(router.domain)); - const namespace = resolveAssetNamespace(protocol, false, isNft, true); + const chain = getCaip2Id(protocol, multiProvider.getChainId(router.domain)); const formattedAddress = bytesToProtocolAddress(router.address, protocol); - const tokenCaip19Id = getCaip19Id(chainCaip2Id, namespace, formattedAddress); - if (isNft) return { tokenCaip19Id, decimals: 0 }; + if (isNft) return { chain, router: formattedAddress, decimals: 0 }; // Attempt to find the decimals from the token list const routerMetadata = allTokens.find((token) => areAddressesEqual(formattedAddress, token.routerAddress), ); - if (routerMetadata) return { tokenCaip19Id, decimals: routerMetadata.decimals }; + if (routerMetadata) + return { chain, router: formattedAddress, decimals: routerMetadata.decimals }; // Otherwise try to query the contract const remoteAdapter = AdapterFactory.HypSyntheticTokenAdapterFromAddress( baseTokenCaip19Id, - chainCaip2Id, + chain, formattedAddress, ); const metadata = await remoteAdapter.getMetadata(); - return { tokenCaip19Id, decimals: metadata.decimals }; + return { chain, router: formattedAddress, decimals: metadata.decimals }; }), ); return { ...baseToken, hypTokens }; @@ -84,37 +77,39 @@ export function computeTokenRoutes(tokens: TokenMetadataWithHypTokens[]) { decimals: baseDecimals, } = token; const baseChainCaip2Id = getChainIdFromToken(baseTokenCaip19Id); - const { chainCaip2Id: remoteCaip2Id, address: remoteRouterAddress } = parseCaip19Id( - remoteHypToken.tokenCaip19Id, - ); - const remoteDecimals = remoteHypToken.decimals; + const { + chain: remoteChainCaip2Id, + router: remoteRouterAddress, + decimals: remoteDecimals, + } = remoteHypToken; // Check if the token list contains the dest router address, meaning it's also a base collateral token - const isRemoteCollateral = tokensHasRouter(tokens, remoteRouterAddress); + const remoteBaseTokenConfig = findTokenByRouter(tokens, remoteRouterAddress); const commonRouteProps = { baseTokenCaip19Id, baseRouterAddress }; // Register a route from the base to the remote - tokenRoutes[baseChainCaip2Id][remoteCaip2Id]?.push({ - type: isRemoteCollateral + tokenRoutes[baseChainCaip2Id][remoteChainCaip2Id]?.push({ + type: remoteBaseTokenConfig ? RouteType.CollateralToCollateral : RouteType.CollateralToSynthetic, ...commonRouteProps, originCaip2Id: baseChainCaip2Id, originRouterAddress: baseRouterAddress, originDecimals: baseDecimals, - destCaip2Id: remoteCaip2Id, + destCaip2Id: remoteChainCaip2Id, destRouterAddress: remoteRouterAddress, destDecimals: remoteDecimals, + destTokenCaip19Id: remoteBaseTokenConfig ? remoteBaseTokenConfig.tokenCaip19Id : undefined, }); // If the remote is not a synthetic (i.e. it's a native/collateral token with it's own config) // then stop here to avoid duplicate route entries. - if (isRemoteCollateral) continue; + if (remoteBaseTokenConfig) continue; // Register a route back from the synthetic remote to the base - tokenRoutes[remoteCaip2Id][baseChainCaip2Id]?.push({ + tokenRoutes[remoteChainCaip2Id][baseChainCaip2Id]?.push({ type: RouteType.SyntheticToCollateral, ...commonRouteProps, - originCaip2Id: remoteCaip2Id, + originCaip2Id: remoteChainCaip2Id, originRouterAddress: remoteRouterAddress, originDecimals: remoteDecimals, destCaip2Id: baseChainCaip2Id, @@ -126,18 +121,16 @@ export function computeTokenRoutes(tokens: TokenMetadataWithHypTokens[]) { // This assumes the synthetics were all enrolled to connect to each other // which is the deployer's default behavior for (const otherHypToken of token.hypTokens) { - const { chainCaip2Id: otherSynCaip2Id, address: otherHypTokenAddress } = parseCaip19Id( - otherHypToken.tokenCaip19Id, - ); + const { chain: otherSynCaip2Id, router: otherHypTokenAddress } = otherHypToken; // Skip if it's same hypToken as parent loop (no route to self) - // or if if remote isn't a synthetic - if (otherHypToken === remoteHypToken || tokensHasRouter(tokens, otherHypTokenAddress)) - continue; + if (otherHypToken === remoteHypToken) continue; + // Also skip if remote isn't a synthetic (i.e. has a collateral/native config) + if (findTokenByRouter(tokens, otherHypTokenAddress)) continue; - tokenRoutes[remoteCaip2Id][otherSynCaip2Id]?.push({ + tokenRoutes[remoteChainCaip2Id][otherSynCaip2Id]?.push({ type: RouteType.SyntheticToSynthetic, ...commonRouteProps, - originCaip2Id: remoteCaip2Id, + originCaip2Id: remoteChainCaip2Id, originRouterAddress: remoteRouterAddress, originDecimals: remoteDecimals, destCaip2Id: otherSynCaip2Id, @@ -155,12 +148,12 @@ function getChainsFromTokens(tokens: TokenMetadataWithHypTokens[]): ChainCaip2Id for (const token of tokens) { chains.add(getChainIdFromToken(token.tokenCaip19Id)); for (const hypToken of token.hypTokens) { - chains.add(getChainIdFromToken(hypToken.tokenCaip19Id)); + chains.add(hypToken.chain); } } return Array.from(chains); } -function tokensHasRouter(tokens: TokenMetadataWithHypTokens[], router: Address) { - return !!tokens.find((t) => areAddressesEqual(t.routerAddress, router)); +function findTokenByRouter(tokens: TokenMetadataWithHypTokens[], router: Address) { + return tokens.find((t) => areAddressesEqual(t.routerAddress, router)); } diff --git a/src/features/tokens/routes/types.ts b/src/features/tokens/routes/types.ts index 31772af0..5939c3fc 100644 --- a/src/features/tokens/routes/types.ts +++ b/src/features/tokens/routes/types.ts @@ -7,7 +7,8 @@ export enum RouteType { export interface Route { type: RouteType; - baseTokenCaip19Id: TokenCaip19Id; // i.e. the underlying 'collateralized' token + // The underlying 'collateralized' token: + baseTokenCaip19Id: TokenCaip19Id; baseRouterAddress: Address; originCaip2Id: ChainCaip2Id; originRouterAddress: Address; @@ -15,6 +16,9 @@ export interface Route { destCaip2Id: ChainCaip2Id; destRouterAddress: Address; destDecimals: number; + // The underlying token on the destination chain + // Only set for CollateralToCollateral routes (b.c. sealevel need it) + destTokenCaip19Id?: TokenCaip19Id; } export type RoutesMap = Record>; diff --git a/src/features/tokens/types.ts b/src/features/tokens/types.ts index 92ca2ad1..2f816ec8 100644 --- a/src/features/tokens/types.ts +++ b/src/features/tokens/types.ts @@ -88,7 +88,7 @@ export type TokenMetadata = CollateralTokenMetadata | NativeTokenMetadata; * Extended types including synthetic hyp token addresses */ interface HypTokens { - hypTokens: Array<{ tokenCaip19Id: TokenCaip19Id; decimals: number }>; + hypTokens: Array<{ chain: ChainCaip2Id; router: Address; decimals: number }>; } type NativeTokenMetadataWithHypTokens = NativeTokenMetadata & HypTokens; diff --git a/src/features/transfer/useTokenTransfer.ts b/src/features/transfer/useTokenTransfer.ts index 1c663b7e..cee5683b 100644 --- a/src/features/transfer/useTokenTransfer.ts +++ b/src/features/transfer/useTokenTransfer.ts @@ -6,7 +6,7 @@ import { toast } from 'react-toastify'; import { HyperlaneCore, ProtocolType } from '@hyperlane-xyz/sdk'; import { toastTxSuccess } from '../../components/toast/TxSuccessToast'; -import { toWei } from '../../utils/amount'; +import { convertDecimals, toWei } from '../../utils/amount'; import { logger } from '../../utils/logger'; import { getProtocolType, parseCaip2Id } from '../caip/chains'; import { isNativeToken, isNonFungibleToken } from '../caip/tokens'; @@ -184,11 +184,17 @@ async function executeTransfer({ // cover the remote transfer. This ensures the balance is sufficient or throws. async function ensureSufficientCollateral(route: Route, weiAmount: string, isNft?: boolean) { if (!isRouteToCollateral(route) || isNft) return; - const adapter = AdapterFactory.TokenAdapterFromAddress(route.baseTokenCaip19Id); - logger.debug('Checking collateral balance for token', route.baseTokenCaip19Id); - const balance = await adapter.getBalance(route.baseRouterAddress); - if (BigNumber.from(balance).lt(weiAmount)) { - throw new Error('Collateral contract has insufficient balance'); + logger.debug('Ensuring collateral balance for route', route); + const adapter = AdapterFactory.HypTokenAdapterFromRouteDest(route); + const destinationBalance = await adapter.getBalance(route.destRouterAddress); + const destinationBalanceInOriginDecimals = convertDecimals( + route.destDecimals, + route.originDecimals, + destinationBalance, + ); + if (destinationBalanceInOriginDecimals.lt(weiAmount)) { + toast.error('Collateral contract balance insufficient for transfer'); + throw new Error('Insufficient collateral balance'); } } From c2bd6da86395125af43abf72657d92ef895ec472 Mon Sep 17 00:00:00 2001 From: J M Rossy Date: Tue, 22 Aug 2023 17:06:52 -0400 Subject: [PATCH 29/58] Remove balance and route hacks --- src/features/tokens/balances.tsx | 23 ++++------------------- src/features/tokens/routes/utils.ts | 7 +------ 2 files changed, 5 insertions(+), 25 deletions(-) diff --git a/src/features/tokens/balances.tsx b/src/features/tokens/balances.tsx index d756160d..fe205a47 100644 --- a/src/features/tokens/balances.tsx +++ b/src/features/tokens/balances.tsx @@ -4,7 +4,7 @@ import { useEffect } from 'react'; import { areAddressesEqual, isValidAddress } from '../../utils/addresses'; import { logger } from '../../utils/logger'; import { getProtocolType } from '../caip/chains'; -import { getChainIdFromToken, parseCaip19Id, tryGetChainIdFromToken } from '../caip/tokens'; +import { parseCaip19Id, tryGetChainIdFromToken } from '../caip/tokens'; import { getProvider } from '../multiProvider'; import { useStore } from '../store'; import { TransferFormValues } from '../transfer/types'; @@ -71,27 +71,12 @@ export function useDestinationBalance( tokenRoutes, ], queryFn: async () => { - // NOTE: this is a hack to accommodate destination balances, specifically the case - // when the destination is a Sealevel chain and is a non-synthetic warp route. - // This only really works with the specific setup of tokens.ts. - - // This searches for the route where the origin chain is destinationCaip2Id - // and the destination chain is originCaip2Id and where the origin is a base token. - const targetBaseCaip19Id = tokenRoutes[destinationCaip2Id][originCaip2Id].find( - (r) => getChainIdFromToken(r.baseTokenCaip19Id) === destinationCaip2Id, - )!.baseTokenCaip19Id; - const route = getTokenRoute( - destinationCaip2Id, - originCaip2Id, - targetBaseCaip19Id, - tokenRoutes, - ); + const route = getTokenRoute(originCaip2Id, destinationCaip2Id, tokenCaip19Id, tokenRoutes); const protocol = getProtocolType(destinationCaip2Id); if (!route || !recipientAddress || !isValidAddress(recipientAddress, protocol)) return null; - - const adapter = AdapterFactory.HypTokenAdapterFromRouteOrigin(route); + const adapter = AdapterFactory.HypTokenAdapterFromRouteDest(route); const balance = await adapter.getBalance(recipientAddress); - return { balance, decimals: route.originDecimals }; + return { balance, decimals: route.destDecimals }; }, refetchInterval: 5000, }); diff --git a/src/features/tokens/routes/utils.ts b/src/features/tokens/routes/utils.ts index 6e130b25..81e6774f 100644 --- a/src/features/tokens/routes/utils.ts +++ b/src/features/tokens/routes/utils.ts @@ -1,5 +1,3 @@ -import { getChainIdFromToken } from '../../caip/tokens'; - import { Route, RouteType, RoutesMap } from './types'; export function getTokenRoutes( @@ -28,10 +26,7 @@ export function hasTokenRoute( tokenCaip19Id: TokenCaip19Id, tokenRoutes: RoutesMap, ): boolean { - const tokenRoute = getTokenRoute(originCaip2Id, destinationCaip2Id, tokenCaip19Id, tokenRoutes); - // This will break things if there are other warp routes configured! - // This only looks for routes in which the origin is the base token. - return !!tokenRoute && getChainIdFromToken(tokenCaip19Id) === originCaip2Id; + return !!getTokenRoute(originCaip2Id, destinationCaip2Id, tokenCaip19Id, tokenRoutes); } export function isRouteToCollateral(route: Route) { From c9e2e1b0788e2ad2687516150480f9fe98646387 Mon Sep 17 00:00:00 2001 From: J M Rossy Date: Tue, 22 Aug 2023 17:07:29 -0400 Subject: [PATCH 30/58] Remove dead const --- src/features/transfer/useTokenTransfer.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/features/transfer/useTokenTransfer.ts b/src/features/transfer/useTokenTransfer.ts index 1db2ae4f..d59d723f 100644 --- a/src/features/transfer/useTokenTransfer.ts +++ b/src/features/transfer/useTokenTransfer.ts @@ -27,8 +27,6 @@ import { import { TransferContext, TransferFormValues, TransferStatus } from './types'; -const COLLATERAL_CONTRACT_BALANCE_INSUFFICIENT_ERROR = 'Collateral contract balance insufficient'; - export function useTokenTransfer(onDone?: () => void) { const { transfers, addTransfer, updateTransferStatus } = useStore((s) => ({ transfers: s.transfers, From 8b6bc70d8805cf6bb4581aebcfca0949bbf8b204 Mon Sep 17 00:00:00 2001 From: J M Rossy Date: Tue, 22 Aug 2023 17:11:44 -0400 Subject: [PATCH 31/58] Revert minor chains.ts change --- src/consts/chains.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/consts/chains.ts b/src/consts/chains.ts index fd8c9491..927c2808 100644 --- a/src/consts/chains.ts +++ b/src/consts/chains.ts @@ -39,7 +39,7 @@ export const chains: ChainMap = { ...solana, rpcUrls: [ { - http: process.env.NEXT_PUBLIC_SOLANA_RPC_URL ?? 'https://api.mainnet-beta.solana.com', + http: process.env.NEXT_PUBLIC_SOLANA_RPC_URL || 'https://api.mainnet-beta.solana.com', }, ], // TODO move up to SDK From e791e29f08d176fefa6e69ba2d3046b3d62ebf76 Mon Sep 17 00:00:00 2001 From: J M Rossy Date: Tue, 22 Aug 2023 17:16:04 -0400 Subject: [PATCH 32/58] Fix build error from merge conflict --- src/features/transfer/useTokenTransfer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/features/transfer/useTokenTransfer.ts b/src/features/transfer/useTokenTransfer.ts index d59d723f..cee5683b 100644 --- a/src/features/transfer/useTokenTransfer.ts +++ b/src/features/transfer/useTokenTransfer.ts @@ -126,7 +126,7 @@ async function executeTransfer({ params: values, }); - await ensureSufficientCollateral(tokenRoutes, tokenRoute, weiAmountOrId, isNft); + await ensureSufficientCollateral(tokenRoute, weiAmountOrId, isNft); const hypTokenAdapter = AdapterFactory.HypTokenAdapterFromRouteOrigin(tokenRoute); From 0235fc025c1dbbd3d8b2b78d72a4cf863469b810 Mon Sep 17 00:00:00 2001 From: J M Rossy Date: Fri, 25 Aug 2023 13:22:48 -0400 Subject: [PATCH 33/58] Update zebec token name and logo --- src/consts/tokens.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/consts/tokens.ts b/src/consts/tokens.ts index 4fa734f6..eaa3a357 100644 --- a/src/consts/tokens.ts +++ b/src/consts/tokens.ts @@ -7,9 +7,10 @@ export const tokenList: WarpTokenConfig = [ chainId: 97, address: '0x64544969ed7ebf5f083679233325356ebe738930', hypCollateralAddress: '0x31b5234A896FbC4b3e2F7237592D054716762131', - symbol: 'ZBC', + symbol: 'wZBC', name: 'Zebec', decimals: 18, + logoURI: '/logos/zebec.png', }, // proteustestnet @@ -17,9 +18,10 @@ export const tokenList: WarpTokenConfig = [ type: 'native', chainId: 88002, hypNativeAddress: '0x34A9af13c5555BAD0783C220911b9ef59CfDBCEf', - symbol: 'ZBC', + symbol: 'wZBC', name: 'Zebec', decimals: 18, + logoURI: '/logos/zebec.png', }, // solanadevnet @@ -29,8 +31,9 @@ export const tokenList: WarpTokenConfig = [ address: 'Gh9ZwEmdLJ8DscKNTkTqPbNwLNNBjuSzaG9Vp2KGtKJr', hypCollateralAddress: 'PJH5QAbxAqrrnSXfH3GHR8icua8CDFZmo97z91xmpvx', name: 'Zebec', - symbol: 'ZBC', + symbol: 'wZBC', decimals: 6, isSpl2022: false, + logoURI: '/logos/zebec.png', }, ]; From 266742f1525030691550ffe61f1dc0ae471a3db5 Mon Sep 17 00:00:00 2001 From: J M Rossy Date: Mon, 28 Aug 2023 11:31:57 -0400 Subject: [PATCH 34/58] Remove learn more link --- src/components/tip/TipCard.tsx | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/src/components/tip/TipCard.tsx b/src/components/tip/TipCard.tsx index d4d779ff..9a3eeee2 100644 --- a/src/components/tip/TipCard.tsx +++ b/src/components/tip/TipCard.tsx @@ -1,10 +1,7 @@ -import Image from 'next/image'; import { useState } from 'react'; import { IconButton } from '../../components/buttons/IconButton'; import { config } from '../../consts/config'; -import { links } from '../../consts/links'; -import InfoCircle from '../../images/icons/info-circle.svg'; import XCircle from '../../images/icons/x-circle.svg'; export function TipCard() { @@ -14,19 +11,10 @@ export function TipCard() {

⚠️ Nautilus Bridge is in deposit-only mode.

Date: Mon, 28 Aug 2023 13:26:42 -0400 Subject: [PATCH 35/58] Set default network to sol mainnet --- src/features/wallet/SolanaWalletContext.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/features/wallet/SolanaWalletContext.tsx b/src/features/wallet/SolanaWalletContext.tsx index 09e88382..0a960731 100644 --- a/src/features/wallet/SolanaWalletContext.tsx +++ b/src/features/wallet/SolanaWalletContext.tsx @@ -16,7 +16,7 @@ import { logger } from '../../utils/logger'; export function SolanaWalletContext({ children }: PropsWithChildren) { // TODO support multiple networks - const network = WalletAdapterNetwork.Devnet; + const network = WalletAdapterNetwork.Mainnet; const endpoint = useMemo(() => clusterApiUrl(network), [network]); const wallets = useMemo( () => [ From a6eed4be24a70092ca9b641cb1aa78ea970f5fa4 Mon Sep 17 00:00:00 2001 From: Trevor Porter Date: Tue, 5 Sep 2023 11:35:41 +0100 Subject: [PATCH 36/58] Fix bug whenever a very small amount is passed in --- src/utils/amount.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/utils/amount.ts b/src/utils/amount.ts index 2bdebe3c..45f8daac 100644 --- a/src/utils/amount.ts +++ b/src/utils/amount.ts @@ -44,7 +44,12 @@ export function toWei( decimals = STANDARD_TOKEN_DECIMALS, ): BigNumber { if (!value) return new BigNumber(0); - const valueString = value.toString().trim(); + // First convert to a BigNumber, and then call `toString` with the + // explicit radix 10 such that the result is formatted as a base-10 string + // and not in scientific notation. + const valueBN = new BigNumber(value); + const valueString = valueBN.toString(10).trim(); + console.log('valueString', value, typeof value, valueBN, valueString); const components = valueString.split('.'); if (components.length === 1) { return new BigNumber(parseUnits(valueString, decimals).toString()); From 3c64770ffbe0becf6e13009e368acb2aaf9a6dcd Mon Sep 17 00:00:00 2001 From: Trevor Porter Date: Tue, 5 Sep 2023 11:36:01 +0100 Subject: [PATCH 37/58] Rm console.log --- src/utils/amount.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/utils/amount.ts b/src/utils/amount.ts index 45f8daac..b93926ba 100644 --- a/src/utils/amount.ts +++ b/src/utils/amount.ts @@ -49,7 +49,6 @@ export function toWei( // and not in scientific notation. const valueBN = new BigNumber(value); const valueString = valueBN.toString(10).trim(); - console.log('valueString', value, typeof value, valueBN, valueString); const components = valueString.split('.'); if (components.length === 1) { return new BigNumber(parseUnits(valueString, decimals).toString()); From e1ab10338d9e9acc9fd816bfd1b1f103b77cc924 Mon Sep 17 00:00:00 2001 From: Trevor Porter Date: Tue, 5 Sep 2023 12:08:05 +0100 Subject: [PATCH 38/58] Add new warps --- src/consts/tokens.ts | 51 +++++++++++++++++++++ src/features/transfer/TransferTokenForm.tsx | 4 +- src/utils/amount.ts | 2 +- 3 files changed, 54 insertions(+), 3 deletions(-) diff --git a/src/consts/tokens.ts b/src/consts/tokens.ts index 0fe20596..47f2a6bb 100644 --- a/src/consts/tokens.ts +++ b/src/consts/tokens.ts @@ -36,4 +36,55 @@ export const tokenList: WarpTokenConfig = [ isSpl2022: false, logoURI: '/logos/zebec.png', }, + + { + chainId: 56, + name: 'Ethereum Token', + symbol: 'ETH', + decimals: 18, + type: 'collateral', + address: '0x2170ed0880ac9a755fd29b2688956bd959f933f8', + hypCollateralAddress: '0x2a6822dc5639b3fe70de6b65b9ff872e554162fa', + isNft: false, + }, + { + chainId: 56, + name: 'USD Coin', + symbol: 'USDC', + decimals: 18, + type: 'collateral', + address: '0x8ac76a51cc950d9822d68b83fe1ad97b32cd580d', + hypCollateralAddress: '0x6937a62f93a56D2AE9392Fa1649b830ca37F3ea4', + isNft: false, + }, + { + chainId: 56, + name: 'Tether USD', + symbol: 'USDT', + decimals: 18, + type: 'collateral', + address: '0x55d398326f99059ff775485246999027b3197955', + hypCollateralAddress: '0xb7d36720a16A1F9Cfc1f7910Ac49f03965401a36', + isNft: false, + }, + { + chainId: 56, + name: 'BTCB Token', // May be worth overriding this to "Bitcoin" + symbol: 'BTCB', // Also same for this and "BTC" + decimals: 18, + type: 'collateral', + address: '0x7130d2a12b9bcbfae4f2634d864a1ee1ce3ead9c', + hypCollateralAddress: '0xB3545006A532E8C23ebC4e33d5ab2232Cafc35Ad', + isNft: false, + }, + { + chainId: 56, + name: 'PoseiSwap Token', + symbol: 'POSE', + decimals: 18, + type: 'collateral', + address: '0xd7518e8cfd7448201155bbbedeed88888e3575ae', + hypCollateralAddress: '0x807D2C6c3d64873Cc729dfC65fB717C3E05e682f', + isNft: false, + }, ]; diff --git a/src/features/transfer/TransferTokenForm.tsx b/src/features/transfer/TransferTokenForm.tsx index a9126946..673269cf 100644 --- a/src/features/transfer/TransferTokenForm.tsx +++ b/src/features/transfer/TransferTokenForm.tsx @@ -276,8 +276,8 @@ function TokenBalance({ balance?: string | null; decimals?: number; }) { - const value = !decimals ? fromWei(balance, decimals) : fromWeiRounded(balance, decimals); - return
{`${label}: ${value}`}
; + const value = !decimals ? fromWei(balance, decimals) : fromWeiRounded(balance, decimals, false); + return
{`${label}: ${value}`}
; } function ButtonSection({ diff --git a/src/utils/amount.ts b/src/utils/amount.ts index b93926ba..0ccd719b 100644 --- a/src/utils/amount.ts +++ b/src/utils/amount.ts @@ -32,7 +32,7 @@ export function fromWeiRounded( // If amount is less than min value if (amount.lt(MIN_ROUNDED_VALUE)) { if (roundDownIfSmall) return '0'; - else return MIN_ROUNDED_VALUE.toString(); + return amount.toString(10); } const displayDecimals = amount.gte(10000) ? 2 : DISPLAY_DECIMALS; From fa2ed1012e267ae6c61b734ede7ff98ae08fa7c1 Mon Sep 17 00:00:00 2001 From: J M Rossy Date: Tue, 5 Sep 2023 11:10:49 -0400 Subject: [PATCH 39/58] Add token logos --- public/logos/POSE.jpg | Bin 0 -> 30749 bytes public/logos/USDC.svg | 5 +++++ public/logos/bitcoin.svg | 8 ++++++++ public/logos/tether.svg | 1 + src/consts/tokens.ts | 5 +++++ 5 files changed, 19 insertions(+) create mode 100644 public/logos/POSE.jpg create mode 100644 public/logos/USDC.svg create mode 100644 public/logos/bitcoin.svg create mode 100644 public/logos/tether.svg diff --git a/public/logos/POSE.jpg b/public/logos/POSE.jpg new file mode 100644 index 0000000000000000000000000000000000000000..8d8828d7de851e506adf1f18b24f3e602af12294 GIT binary patch literal 30749 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 literal 0 HcmV?d00001 diff --git a/public/logos/USDC.svg b/public/logos/USDC.svg new file mode 100644 index 00000000..697dfda9 --- /dev/null +++ b/public/logos/USDC.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/public/logos/bitcoin.svg b/public/logos/bitcoin.svg new file mode 100644 index 00000000..f9926c31 --- /dev/null +++ b/public/logos/bitcoin.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/public/logos/tether.svg b/public/logos/tether.svg new file mode 100644 index 00000000..533bcaa1 --- /dev/null +++ b/public/logos/tether.svg @@ -0,0 +1 @@ +tether-usdt \ No newline at end of file diff --git a/src/consts/tokens.ts b/src/consts/tokens.ts index 47f2a6bb..7639c9ba 100644 --- a/src/consts/tokens.ts +++ b/src/consts/tokens.ts @@ -46,6 +46,7 @@ export const tokenList: WarpTokenConfig = [ address: '0x2170ed0880ac9a755fd29b2688956bd959f933f8', hypCollateralAddress: '0x2a6822dc5639b3fe70de6b65b9ff872e554162fa', isNft: false, + logoURI: '/logos/weth.png', }, { chainId: 56, @@ -56,6 +57,7 @@ export const tokenList: WarpTokenConfig = [ address: '0x8ac76a51cc950d9822d68b83fe1ad97b32cd580d', hypCollateralAddress: '0x6937a62f93a56D2AE9392Fa1649b830ca37F3ea4', isNft: false, + logoURI: '/logos/USDC.svg', }, { chainId: 56, @@ -66,6 +68,7 @@ export const tokenList: WarpTokenConfig = [ address: '0x55d398326f99059ff775485246999027b3197955', hypCollateralAddress: '0xb7d36720a16A1F9Cfc1f7910Ac49f03965401a36', isNft: false, + logoURI: '/logos/tether.svg', }, { chainId: 56, @@ -76,6 +79,7 @@ export const tokenList: WarpTokenConfig = [ address: '0x7130d2a12b9bcbfae4f2634d864a1ee1ce3ead9c', hypCollateralAddress: '0xB3545006A532E8C23ebC4e33d5ab2232Cafc35Ad', isNft: false, + logoURI: '/logos/bitcoin.svg', }, { chainId: 56, @@ -86,5 +90,6 @@ export const tokenList: WarpTokenConfig = [ address: '0xd7518e8cfd7448201155bbbedeed88888e3575ae', hypCollateralAddress: '0x807D2C6c3d64873Cc729dfC65fB717C3E05e682f', isNft: false, + logoURI: '/logos/POSE.jpg', }, ]; From 19d52d329132cf6fa494103450952625fd9f6be0 Mon Sep 17 00:00:00 2001 From: Trevor Porter Date: Wed, 6 Sep 2023 17:23:52 +0100 Subject: [PATCH 40/58] Rm POSE --- src/consts/tokens.ts | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/src/consts/tokens.ts b/src/consts/tokens.ts index 7639c9ba..be344c6e 100644 --- a/src/consts/tokens.ts +++ b/src/consts/tokens.ts @@ -72,8 +72,8 @@ export const tokenList: WarpTokenConfig = [ }, { chainId: 56, - name: 'BTCB Token', // May be worth overriding this to "Bitcoin" - symbol: 'BTCB', // Also same for this and "BTC" + name: 'BTCB Token', + symbol: 'BTCB', decimals: 18, type: 'collateral', address: '0x7130d2a12b9bcbfae4f2634d864a1ee1ce3ead9c', @@ -81,15 +81,20 @@ export const tokenList: WarpTokenConfig = [ isNft: false, logoURI: '/logos/bitcoin.svg', }, - { - chainId: 56, - name: 'PoseiSwap Token', - symbol: 'POSE', - decimals: 18, - type: 'collateral', - address: '0xd7518e8cfd7448201155bbbedeed88888e3575ae', - hypCollateralAddress: '0x807D2C6c3d64873Cc729dfC65fB717C3E05e682f', - isNft: false, - logoURI: '/logos/POSE.jpg', - }, + + // POSE not enabled for now, apparently they already created a + // version of POSE on Nautilus & details of bridging need to be + // ironed out. + + // { + // chainId: 56, + // name: 'PoseiSwap Token', + // symbol: 'POSE', + // decimals: 18, + // type: 'collateral', + // address: '0xd7518e8cfd7448201155bbbedeed88888e3575ae', + // hypCollateralAddress: '0x807D2C6c3d64873Cc729dfC65fB717C3E05e682f', + // isNft: false, + // logoURI: '/logos/POSE.jpg', + // }, ]; From c0f5d8414b582e31e9b7ca81852e72f48f72f541 Mon Sep 17 00:00:00 2001 From: Nam Chu Hoai Date: Thu, 7 Sep 2023 08:58:56 -0400 Subject: [PATCH 41/58] Update TipCard.tsx --- src/components/tip/TipCard.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/tip/TipCard.tsx b/src/components/tip/TipCard.tsx index 9a3eeee2..6e82798c 100644 --- a/src/components/tip/TipCard.tsx +++ b/src/components/tip/TipCard.tsx @@ -13,7 +13,7 @@ export function TipCard() {

Currently, you can bridge from BSC and Solana to Nautilus. Transfers originating Nautilus - are expected to go live September 1st. + are expected to go live soon.

From 703c17c1593ca813803c93d54048438812add4e7 Mon Sep 17 00:00:00 2001 From: Trevor Porter Date: Wed, 27 Sep 2023 11:04:33 +0100 Subject: [PATCH 42/58] Add warning if not enough funds --- src/features/transfer/useTokenTransfer.ts | 60 ++++++++++++++++++++--- 1 file changed, 53 insertions(+), 7 deletions(-) diff --git a/src/features/transfer/useTokenTransfer.ts b/src/features/transfer/useTokenTransfer.ts index f57ee625..8f3a356c 100644 --- a/src/features/transfer/useTokenTransfer.ts +++ b/src/features/transfer/useTokenTransfer.ts @@ -3,9 +3,15 @@ import { BigNumber, PopulatedTransaction as EvmTransaction, providers } from 'et import { useCallback, useState } from 'react'; import { toast } from 'react-toastify'; -import { IHypTokenAdapter } from '@hyperlane-xyz/hyperlane-token'; +import { IHypTokenAdapter, ITokenAdapter } from '@hyperlane-xyz/hyperlane-token'; import { HyperlaneCore } from '@hyperlane-xyz/sdk'; -import { ProtocolType, convertDecimals, toWei } from '@hyperlane-xyz/utils'; +import { + ProtocolType, + convertDecimals, + fromWei, + fromWeiRounded, + toWei, +} from '@hyperlane-xyz/utils'; import { toastTxSuccess } from '../../components/toast/TxSuccessToast'; import { logger } from '../../utils/logger'; @@ -104,7 +110,7 @@ async function executeTransfer({ try { const { originCaip2Id, destinationCaip2Id, tokenCaip19Id, amount, recipientAddress } = values; - const { protocol: originProtocol } = parseCaip2Id(originCaip2Id); + const { protocol: originProtocol, reference: originReference } = parseCaip2Id(originCaip2Id); const { reference: destReference } = parseCaip2Id(destinationCaip2Id); const destinationDomainId = getMultiProvider().getDomainId(destReference); @@ -126,6 +132,9 @@ async function executeTransfer({ await ensureSufficientCollateral(tokenRoute, weiAmountOrId, isNft); const hypTokenAdapter = AdapterFactory.HypTokenAdapterFromRouteOrigin(tokenRoute); + const originNativeTokenAdapter = AdapterFactory.TokenAdapterFromAddress( + `${originProtocol}:${originReference}/native:0x0000000000000000000000000000000000000000`, + ); const triggerParams: ExecuteTransferParams = { weiAmountOrId, @@ -133,6 +142,7 @@ async function executeTransfer({ recipientAddress, tokenRoute, hypTokenAdapter, + originNativeTokenAdapter, activeAccount: activeAccounts.accounts[originProtocol], activeChain: activeChains.chains[originProtocol], updateStatus: (s: TransferStatus) => { @@ -200,6 +210,7 @@ interface ExecuteTransferParams { recipientAddress: Address; tokenRoute: Route; hypTokenAdapter: IHypTokenAdapter; + originNativeTokenAdapter: ITokenAdapter; activeAccount: AccountInfo; activeChain: ActiveChainInfo; updateStatus: (s: TransferStatus) => void; @@ -212,6 +223,7 @@ async function executeEvmTransfer({ recipientAddress, tokenRoute, hypTokenAdapter, + originNativeTokenAdapter, activeAccount, activeChain, updateStatus, @@ -249,10 +261,44 @@ async function executeEvmTransfer({ const gasPayment = await hypTokenAdapter.quoteGasPayment(destinationDomainId); logger.debug('Quoted gas payment', gasPayment); // If sending native tokens (e.g. Eth), the gasPayment must be added to the tx value and sent together - const txValue = - isRouteFromCollateral(tokenRoute) && isNativeToken(baseTokenCaip19Id) - ? BigNumber.from(gasPayment).add(weiAmountOrId) - : gasPayment; + const isWarpingNativeToken = + isRouteFromCollateral(tokenRoute) && isNativeToken(baseTokenCaip19Id); + const txValue = isWarpingNativeToken ? BigNumber.from(gasPayment).add(weiAmountOrId) : gasPayment; + + if (activeAccount.address) { + const originNativeBalance = await originNativeTokenAdapter.getBalance(activeAccount.address); + + // Note this doesn't consider gas fees on the origin chain that are + // also required by the tx. + if (BigNumber.from(originNativeBalance).lte(txValue)) { + const { reference: originReference } = parseCaip2Id(originCaip2Id); + // getMetadata() throws for Native tokens, so we rely on the multiprovider instead + const { symbol: originNativeTokenSymbol, decimals: originNativeTokenDecimals } = + getMultiProvider().getChainMetadata(originReference).nativeToken || {}; + + const prettyNativeTokenString = (amount: string) => { + const prettyAmount = !originNativeTokenDecimals + ? fromWei(amount, originNativeTokenDecimals) + : fromWeiRounded(amount, originNativeTokenDecimals, false); + + return `${prettyAmount}${originNativeTokenSymbol ? ` ${originNativeTokenSymbol}` : ''}`; + }; + + const prettyGasPayment = prettyNativeTokenString(gasPayment); + const prettyBalance = prettyNativeTokenString(originNativeBalance); + + let toastError: string; + if (isWarpingNativeToken) { + const prettyTxValue = prettyNativeTokenString(txValue.toString()); + toastError = `Native token balance insufficient to cover destination gas fees. Please add more native tokens, or bridge fewer tokens. Destination gas fees: ${prettyGasPayment}. Total required balance, including bridged tokens: ${prettyTxValue}. Current balance: ${prettyBalance}.`; + } else { + toastError = `Native token balance insufficient to cover destination gas fees. Please add more native tokens. Destination gas fees required: ${prettyGasPayment}. Current balance: ${prettyBalance}`; + } + toast.error(toastError, { autoClose: false }); + throw new Error('Insufficient native token balance for interchain gas payment'); + } + } + const transferTxRequest = (await hypTokenAdapter.populateTransferRemoteTx({ weiAmountOrId, recipient: recipientAddress, From 38c1fca2ffa9ed40c4198ff4962a66d288dbffcc Mon Sep 17 00:00:00 2001 From: Trevor Porter Date: Wed, 27 Sep 2023 11:59:53 +0100 Subject: [PATCH 43/58] Can't change env vars on vercel atm --- src/consts/config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/consts/config.ts b/src/consts/config.ts index a56d3a29..a679a696 100644 --- a/src/consts/config.ts +++ b/src/consts/config.ts @@ -20,7 +20,7 @@ export const config: Config = Object.freeze({ debug: isDevMode, version, explorerApiKeys, - showTipBox: !!withdrawalWhitelist, + showTipBox: false, // !!withdrawalWhitelist, showDisabledTokens: false, walletConnectProjectId, withdrawalWhitelist, From 6b7ef95e45fa7abdb769ba937a5f9616bd36449f Mon Sep 17 00:00:00 2001 From: Trevor Porter Date: Wed, 27 Sep 2023 12:03:31 +0100 Subject: [PATCH 44/58] Remove deposit only mode part --- src/features/transfer/TransferTokenForm.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/features/transfer/TransferTokenForm.tsx b/src/features/transfer/TransferTokenForm.tsx index 5837a978..e36319b5 100644 --- a/src/features/transfer/TransferTokenForm.tsx +++ b/src/features/transfer/TransferTokenForm.tsx @@ -482,12 +482,12 @@ function validateFormValues( } } - if ( - config.withdrawalWhitelist && - !config.withdrawalWhitelist.split(',').includes(destinationCaip2Id) - ) { - return { destinationCaip2Id: 'Bridge is in deposit-only mode' }; - } + // if ( + // config.withdrawalWhitelist && + // !config.withdrawalWhitelist.split(',').includes(destinationCaip2Id) + // ) { + // return { destinationCaip2Id: 'Bridge is in deposit-only mode' }; + // } if ( config.transferBlacklist && From 0ba8413f0d9f5a17cf1d1afc4bfedd2c5915d7d6 Mon Sep 17 00:00:00 2001 From: Trevor Porter Date: Wed, 27 Sep 2023 14:42:47 +0100 Subject: [PATCH 45/58] New POSE token --- src/consts/tokens.ts | 27 +++++++++++---------------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/src/consts/tokens.ts b/src/consts/tokens.ts index be344c6e..ca39b0af 100644 --- a/src/consts/tokens.ts +++ b/src/consts/tokens.ts @@ -81,20 +81,15 @@ export const tokenList: WarpTokenConfig = [ isNft: false, logoURI: '/logos/bitcoin.svg', }, - - // POSE not enabled for now, apparently they already created a - // version of POSE on Nautilus & details of bridging need to be - // ironed out. - - // { - // chainId: 56, - // name: 'PoseiSwap Token', - // symbol: 'POSE', - // decimals: 18, - // type: 'collateral', - // address: '0xd7518e8cfd7448201155bbbedeed88888e3575ae', - // hypCollateralAddress: '0x807D2C6c3d64873Cc729dfC65fB717C3E05e682f', - // isNft: false, - // logoURI: '/logos/POSE.jpg', - // }, + { + chainId: 22222, + name: 'PoseiSwap Token', + symbol: 'POSE', + decimals: 18, + type: 'collateral', + address: '0xB883935B47B0508b479CEE642DB4b9E2de387920', + hypCollateralAddress: '0x79B1257BDBCaeF98Da685A7c225b6e61a119Cb7a', + isNft: false, + logoURI: '/logos/POSE.jpg', + }, ]; From b8deeb88b368cd5d7767c07e1c8c62bfd72b82f1 Mon Sep 17 00:00:00 2001 From: Trevor Porter Date: Thu, 28 Sep 2023 12:40:17 +0100 Subject: [PATCH 46/58] Only wZBC on Solana --- src/consts/tokens.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/consts/tokens.ts b/src/consts/tokens.ts index ca39b0af..fcb975be 100644 --- a/src/consts/tokens.ts +++ b/src/consts/tokens.ts @@ -8,7 +8,7 @@ export const tokenList: WarpTokenConfig = [ address: '0x37a56cdcD83Dce2868f721De58cB3830C44C6303', hypCollateralAddress: '0xC27980812E2E66491FD457D488509b7E04144b98', name: 'Zebec', - symbol: 'wZBC', + symbol: 'ZBC', decimals: 9, logoURI: '/logos/zebec.png', }, @@ -19,7 +19,7 @@ export const tokenList: WarpTokenConfig = [ chainId: 22222, hypNativeAddress: '0x4501bBE6e731A4bC5c60C03A77435b2f6d5e9Fe7', name: 'Zebec', - symbol: 'wZBC', + symbol: 'ZBC', decimals: 18, logoURI: '/logos/zebec.png', }, From 87cd43cce8b1c846332110abf8e6b341dc3f4b77 Mon Sep 17 00:00:00 2001 From: Trevor Porter Date: Tue, 3 Oct 2023 11:52:15 +0100 Subject: [PATCH 47/58] Use token 1.5.1-beta2, which will increase the compute limit to 500k --- package.json | 2 +- yarn.lock | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index f62bd609..96c5b17b 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "author": "J M Rossy", "dependencies": { "@headlessui/react": "^1.7.14", - "@hyperlane-xyz/hyperlane-token": "^1.5.1", + "@hyperlane-xyz/hyperlane-token": "^1.5.1-beta2", "@hyperlane-xyz/sdk": "^1.5.1", "@hyperlane-xyz/utils": "^1.5.1", "@hyperlane-xyz/widgets": "^1.5.0", diff --git a/yarn.lock b/yarn.lock index 36a40d90..88bd3bd6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1324,7 +1324,7 @@ __metadata: languageName: node linkType: hard -"@hyperlane-xyz/hyperlane-token@npm:^1.5.1": +"@hyperlane-xyz/hyperlane-token@npm:^1.5.1-beta2": version: 1.5.1 resolution: "@hyperlane-xyz/hyperlane-token@npm:1.5.1" dependencies: @@ -1374,7 +1374,7 @@ __metadata: resolution: "@hyperlane-xyz/warp-ui-template@workspace:." dependencies: "@headlessui/react": ^1.7.14 - "@hyperlane-xyz/hyperlane-token": ^1.5.1 + "@hyperlane-xyz/hyperlane-token": ^1.5.1-beta2 "@hyperlane-xyz/sdk": ^1.5.1 "@hyperlane-xyz/utils": ^1.5.1 "@hyperlane-xyz/widgets": ^1.5.0 From 7276b1017a59ea1ea5f4cd21c4a06d984107cf38 Mon Sep 17 00:00:00 2001 From: J M Rossy Date: Thu, 14 Dec 2023 11:54:00 -0500 Subject: [PATCH 48/58] Disable ledger connector --- src/features/wallet/EvmWalletContext.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/features/wallet/EvmWalletContext.tsx b/src/features/wallet/EvmWalletContext.tsx index b675c4cb..438a57b7 100644 --- a/src/features/wallet/EvmWalletContext.tsx +++ b/src/features/wallet/EvmWalletContext.tsx @@ -3,8 +3,7 @@ import '@rainbow-me/rainbowkit/styles.css'; import { argentWallet, coinbaseWallet, - injectedWallet, - ledgerWallet, + injectedWallet, // ledgerWallet, metaMaskWallet, omniWallet, rainbowWallet, @@ -35,7 +34,7 @@ const connectors = connectorsForWallets([ metaMaskWallet(connectorConfig), injectedWallet(connectorConfig), walletConnectWallet(connectorConfig), - ledgerWallet(connectorConfig), + // ledgerWallet(connectorConfig), ], }, { From 2a1f74d98b76e2383566f68111cbdc0b892ff358 Mon Sep 17 00:00:00 2001 From: J M Rossy Date: Tue, 9 Jan 2024 13:25:33 -0500 Subject: [PATCH 49/58] Update tip card text --- src/components/tip/TipCard.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/tip/TipCard.tsx b/src/components/tip/TipCard.tsx index 6e82798c..e70a0289 100644 --- a/src/components/tip/TipCard.tsx +++ b/src/components/tip/TipCard.tsx @@ -12,8 +12,7 @@ export function TipCard() {

⚠️ Nautilus Bridge is in deposit-only mode.

- Currently, you can bridge from BSC and Solana to Nautilus. Transfers originating Nautilus - are expected to go live soon. + Currently, bridge transfers to Nautilus must originate from BSC or Solana.

From 07b76e8f6b963f1b748cb9e48f105a19d85e1f52 Mon Sep 17 00:00:00 2001 From: J M Rossy Date: Thu, 1 Feb 2024 11:28:54 -0500 Subject: [PATCH 50/58] Replace favicon --- CUSTOMIZE.md | 1 - public/favicon.ico | Bin 15406 -> 0 bytes public/favicon.png | Bin 0 -> 34930 bytes src/pages/_document.tsx | 2 +- 4 files changed, 1 insertion(+), 2 deletions(-) delete mode 100644 public/favicon.ico create mode 100644 public/favicon.png diff --git a/CUSTOMIZE.md b/CUSTOMIZE.md index 1c3f84dc..9c27db9e 100644 --- a/CUSTOMIZE.md +++ b/CUSTOMIZE.md @@ -43,7 +43,6 @@ The links used in the footer can be found here: `./src/consts/links.ts` ### Public assets / Favicons The images and manifest files under `./public` should also be updated. -The current collection was generated with [RealFaviconGenerator](https://realfavicongenerator.net) ### Fonts diff --git a/public/favicon.ico b/public/favicon.ico deleted file mode 100644 index 619c4dbbe685a573d27b16fa5f37d6f5b3c48ee2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15406 zcmeHO4Qv!e6n^|j0D)Tq6$AcMB}ybhjS3p0LWn;BdaeA4h+2UlDk26&F#?StpjfaN zAsV4oF`$JAm}syh0{)PQmA2g8U60zk-d%qviXb5Em7nu_cb#@Q?`L;!uPtVqyv*#( zy!XC&GyCSv+bdBQ>PC`8DAQ?PccM{5l%5`b9@d|z0Bso=E&VqW9Y`Y@hB06PN4OnD z)IYDCcKKzRgK`>Zrr%DFcx9U6v(q9xzgR8P{GuZ2hPF(vgBI61=rOO8p02jj6QD&= zg?4abNeNw1W2ZdeU*NaV2(55D9_oqt2U|&|gMKGHpjOU*_#q2eF8XA8Q;m#h{H<8* z^h%_R(^LDTHD^jWf*PkuvoQWkP_2Sb%%>T1xlJjfIkVs0jaEyrjruilKiiQpL&im= z9PE*8YWT$%@V=|$M?LUI1@#sEvGj|6=Pxt>Jm-`$Fo!Xs|BSW&h^8z{sUXbbVDoq> zLrK0eDhBRSL73OWfXVkN9JN3zeV{mo?ohV(0Y%ZMtfw+ahKCA=qvUfrW^6T>t%d@kfI#JHV)2h9d=A^g-tv~%A&&>`Npbq=~7^{LfPx~KjS zrSW@>@pC|{k5^JZ@P~XFeBxK!4_q1WCF80qXe{`~iTSa9;8_Z?qJ29sX4lDd9rz}@ zZ8QjS=7mbk3v(PV_{`F@Kq3nSgVeR&L+N!TG%(TFRP^Kgk+6Tm=Y>qCQU3|F!e^s9 zQ=u)T195E+W>Pl&o&%*Qq~<^X*SQa}r?)WLc&(TBZH5 zs43g>9iMfW(=3#mQJ!HNtE7TN`kOMPg^}@<^7*XQa#AUy#ua%Vv2`51cJ>?6~Af+t$3S3k?*`&dH#cB>=Kf3F`=|H4 z?aX|fC*jLN4k&J(#7;-tGL1z3!tJ)xeemNuaNhom@+0`#4T#59Bj!KsaZ)T1;YBaRQ{CTnc;B;+iww$TKXMHo98{aX>= z&cyRJ&??SPhS4J*6pj(|pywu+iv}Y1^E%F;)v(=c&}$gK0Q{e#{G4^6??F7T#T~mh z+MK}j4CrgdkM`-nT*WqNaMK_+az>o5tgoarj;m^IbZrHmG46TL7e~wKO2q6vq32c3 z3o$0xdky3+=X{mFjE49RP%mEv4fd3h#CE9zMuDA6)&<=&pm!~B6yT1VhqZfwd8$gt z!hBrk_&$`6M~)0*rw3)~0luMr7Y%7p5&J83nsw6xrUgt3bT$jX7j|Kv%Dy?M@bR4u zXDmGUe$gN2!YAUIhV}w0Pnd-IUV(^7C)~M=Fp7w_yO_XTvrEuFNn00 z7W`tLA%^(}_1#rvgkLap?4KU|Vjd`uBZi&i_fxNqnnjZM`Th7C{%fiD*4p0lFaST# zf%j8hdt+yP2H_9+DZJb7wD(>z5tOpK?&NP+d`2nltESGe z_^eo1qqQ52U+BQM$jP|hoYFCeAGO5Wslwlq`^yLZW=(UD>D&nodni@-c^we{-wNzL zP561w*=U|v$3$f+@w0Bc%kOK(@1QM-aEty_;}5NYL&fg`6z@9bT*kRufiYg0YW%tQ zEfwr9Yg~gWJ|E-Z7JaG4&w3DVY|?~ZCO>@FaDg#i*&g_TC07%E{I(n4XKs&&Tl6*I zdw*j#zUQkX{EqtBKqh`4pn*;->-`qav>dhM!T%9NeghGBlP%9q955tNj1<`)uv%6o ztJf%g6TeZG)F;QZzdpMe#bw&xC`;;-W7=Pz-HhTg?QfJN^~t&1?XPdvCfEPL0{;Sl COsF>i diff --git a/public/favicon.png b/public/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..6d0e36c19c9e375b56bbdebdf7b9a07be93f35a3 GIT binary patch literal 34930 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 literal 0 HcmV?d00001 diff --git a/src/pages/_document.tsx b/src/pages/_document.tsx index 72893bc7..a095b645 100644 --- a/src/pages/_document.tsx +++ b/src/pages/_document.tsx @@ -11,7 +11,7 @@ export default function Document() { - + From 249e4a7765df46006264b4733f6d992bd3162e03 Mon Sep 17 00:00:00 2001 From: Avi Atkin <103125634+avious00@users.noreply.github.com> Date: Wed, 29 May 2024 15:33:17 -0400 Subject: [PATCH 51/58] twitter link from hyperlane_xyz to hyperlane --- src/consts/links.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/consts/links.ts b/src/consts/links.ts index cd25d672..aad2c7ff 100644 --- a/src/consts/links.ts +++ b/src/consts/links.ts @@ -6,6 +6,6 @@ export const links = { docs: 'https://docs.hyperlane.xyz', gasDocs: 'https://docs.hyperlane.xyz/docs/protocol/interchain-gas-payments', chains: 'https://docs.hyperlane.xyz/docs/resources/domains', - twitter: 'https://twitter.com/hyperlane_xyz', + twitter: 'https://twitter.com/hyperlane', blog: 'https://medium.com/hyperlane', }; From 5a1625a8375c2fdab9168d3268580a8d13f0f167 Mon Sep 17 00:00:00 2001 From: Connor McEwen Date: Wed, 21 Aug 2024 13:01:07 -0700 Subject: [PATCH 52/58] update rpc url --- src/consts/chains.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/consts/chains.ts b/src/consts/chains.ts index e54c9014..3ad71c2a 100644 --- a/src/consts/chains.ts +++ b/src/consts/chains.ts @@ -88,7 +88,7 @@ export const chains: ChainMap = { }, rpcUrls: [ { - http: 'https://api.nautilus.nautchain.xyz', + http: 'https://api.evm.nautilus.prod.eclipsenetwork.xyz', }, ], blocks: { From e0f2b645323e3c67985115d070f67d070a16af20 Mon Sep 17 00:00:00 2001 From: Connor McEwen Date: Thu, 29 Aug 2024 15:02:35 -0700 Subject: [PATCH 53/58] add warning --- src/components/tip/TipCard.tsx | 22 +++------------------- 1 file changed, 3 insertions(+), 19 deletions(-) diff --git a/src/components/tip/TipCard.tsx b/src/components/tip/TipCard.tsx index e70a0289..60fb7d26 100644 --- a/src/components/tip/TipCard.tsx +++ b/src/components/tip/TipCard.tsx @@ -1,28 +1,12 @@ -import { useState } from 'react'; - -import { IconButton } from '../../components/buttons/IconButton'; -import { config } from '../../consts/config'; -import XCircle from '../../images/icons/x-circle.svg'; - export function TipCard() { - const [show, setShow] = useState(config.showTipBox); - if (!show) return null; return ( -
-

⚠️ Nautilus Bridge is in deposit-only mode.

+
+

⚠️ Nautilus Bridge is shutting down September 6

- Currently, bridge transfers to Nautilus must originate from BSC or Solana. + You must move funds from Nautilus Chain to BSC or Solana or funds will be lost.

-
- setShow(false)} - title="Hide tip" - classes="hover:rotate-90" - /> -
); } From 7f3e83c24b682fb69a43f69d95cb3054b736739e Mon Sep 17 00:00:00 2001 From: Connor McEwen Date: Thu, 29 Aug 2024 15:08:20 -0700 Subject: [PATCH 54/58] clarify language --- src/components/tip/TipCard.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/tip/TipCard.tsx b/src/components/tip/TipCard.tsx index 60fb7d26..4e99b731 100644 --- a/src/components/tip/TipCard.tsx +++ b/src/components/tip/TipCard.tsx @@ -1,7 +1,7 @@ export function TipCard() { return (
-

⚠️ Nautilus Bridge is shutting down September 6

+

⚠️ Nautilus Chain is shutting down September 6

You must move funds from Nautilus Chain to BSC or Solana or funds will be lost. From 6f4743e62bb04ee63b817449740587e68ead5af5 Mon Sep 17 00:00:00 2001 From: J M Rossy Date: Fri, 30 Aug 2024 10:53:46 -0400 Subject: [PATCH 55/58] Add destination blacklist and update tip card text --- src/components/tip/TipCard.tsx | 3 ++- src/consts/config.ts | 4 ++++ src/features/transfer/TransferTokenForm.tsx | 7 +++++++ 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/components/tip/TipCard.tsx b/src/components/tip/TipCard.tsx index 4e99b731..8009b2f7 100644 --- a/src/components/tip/TipCard.tsx +++ b/src/components/tip/TipCard.tsx @@ -4,7 +4,8 @@ export function TipCard() {

⚠️ Nautilus Chain is shutting down September 6

- You must move funds from Nautilus Chain to BSC or Solana or funds will be lost. + You must move your funds from Nautilus Chain to BSC or Solana, otherwise the funds will be + lost.

diff --git a/src/consts/config.ts b/src/consts/config.ts index a56d3a29..60f63b52 100644 --- a/src/consts/config.ts +++ b/src/consts/config.ts @@ -4,6 +4,7 @@ const explorerApiKeys = JSON.parse(process?.env?.EXPLORER_API_KEYS || '{}'); const walletConnectProjectId = process?.env?.NEXT_PUBLIC_WALLET_CONNECT_ID || ''; const withdrawalWhitelist = process?.env?.NEXT_PUBLIC_BLOCK_WITHDRAWAL_WHITELIST || ''; const transferBlacklist = process?.env?.NEXT_PUBLIC_TRANSFER_BLACKLIST || ''; +const destinationBlacklist = process?.env?.NEXT_PUBLIC_DESTINATION_BLACKLIST || ''; interface Config { debug: boolean; // Enables some debug features in the app @@ -14,6 +15,8 @@ interface Config { walletConnectProjectId: string; // Project ID provided by walletconnect withdrawalWhitelist: string; // comma-separated list of CAIP2 chain IDs to which transfers are supported transferBlacklist: string; // comma-separated list of routes between which transfers are disabled. Expects Caip2Id-Caip2Id (e.g. ethereum:1-sealevel:1399811149) + // This is slightly redundant with the transfer blacklist above but we want to block transfers INTO nautilus but not out, which is different + destinationBlacklist: string; // comma-separated list of CAIP2 chain IDs to which transfers are disabled } export const config: Config = Object.freeze({ @@ -25,4 +28,5 @@ export const config: Config = Object.freeze({ walletConnectProjectId, withdrawalWhitelist, transferBlacklist, + destinationBlacklist, }); diff --git a/src/features/transfer/TransferTokenForm.tsx b/src/features/transfer/TransferTokenForm.tsx index de29a530..260b2db9 100644 --- a/src/features/transfer/TransferTokenForm.tsx +++ b/src/features/transfer/TransferTokenForm.tsx @@ -538,6 +538,13 @@ function validateFormValues( return { destinationCaip2Id: 'Route is not currently allowed' }; } + if ( + config.destinationBlacklist && + config.destinationBlacklist.split(',').includes(destinationCaip2Id) + ) { + return { destinationCaip2Id: 'Transfers to this chain are not allowed' }; + } + return {}; } From cde7b649901bc4a5d73996e47241cfe245c0e42f Mon Sep 17 00:00:00 2001 From: Mo Hussan <22501692+Mo-Hussain@users.noreply.github.com> Date: Fri, 6 Sep 2024 17:16:50 +0100 Subject: [PATCH 56/58] feat(warpui): Nautilus shutdown --- src/components/nav/Header.tsx | 4 ---- src/pages/index.tsx | 33 +++++++++++++++++++++++++++------ 2 files changed, 27 insertions(+), 10 deletions(-) diff --git a/src/components/nav/Header.tsx b/src/components/nav/Header.tsx index e22f343e..bef32c37 100644 --- a/src/components/nav/Header.tsx +++ b/src/components/nav/Header.tsx @@ -1,7 +1,6 @@ import Image from 'next/image'; import Link from 'next/link'; -import { WalletControlBar } from '../../features/wallet/WalletControlBar'; import Title from '../../images/logos/app-title.svg'; export function Header() { @@ -11,9 +10,6 @@ export function Header() { -
- -
); diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 326980a1..3da28da3 100755 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -1,13 +1,34 @@ import type { NextPage } from 'next'; -import { TipCard } from '../components/tip/TipCard'; -import { TransferTokenCard } from '../features/transfer/TransferTokenCard'; - const Home: NextPage = () => { return ( -
- - +
+
+

Nautilus chain has shutdown.

+

+ Please see the announcement in this{' '} + + tweet. + +

+

+ Affected? Email at{' '} + + nautilus@hyperlane.xyz + {' '} + for assistance. +

+
); }; From 9ef3d0d1586f4871e8da6717131dd285d61fb062 Mon Sep 17 00:00:00 2001 From: Connor McEwen Date: Fri, 6 Sep 2024 12:40:34 -0400 Subject: [PATCH 57/58] Update src/pages/index.tsx Co-authored-by: J M Rossy --- src/pages/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 3da28da3..6cf686f6 100755 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -17,7 +17,7 @@ const Home: NextPage = () => {

- Affected? Email at{' '} + Need help? Email{' '} Date: Mon, 21 Oct 2024 17:33:43 +0100 Subject: [PATCH 58/58] Revert "feat(warpui): Nautilus shutdown" --- src/components/nav/Header.tsx | 4 ++++ src/pages/index.tsx | 33 ++++++--------------------------- 2 files changed, 10 insertions(+), 27 deletions(-) diff --git a/src/components/nav/Header.tsx b/src/components/nav/Header.tsx index bef32c37..e22f343e 100644 --- a/src/components/nav/Header.tsx +++ b/src/components/nav/Header.tsx @@ -1,6 +1,7 @@ import Image from 'next/image'; import Link from 'next/link'; +import { WalletControlBar } from '../../features/wallet/WalletControlBar'; import Title from '../../images/logos/app-title.svg'; export function Header() { @@ -10,6 +11,9 @@ export function Header() { +

); diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 6cf686f6..326980a1 100755 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -1,34 +1,13 @@ import type { NextPage } from 'next'; +import { TipCard } from '../components/tip/TipCard'; +import { TransferTokenCard } from '../features/transfer/TransferTokenCard'; + const Home: NextPage = () => { return ( -