diff --git a/IPFS.json b/IPFS.json index 4b00e2064..0cf6852f6 100644 --- a/IPFS.json +++ b/IPFS.json @@ -4,6 +4,10 @@ "leastSafeVersion": "0.76.1", "config": { "enabledWithdrawalDexes": ["one-inch", "paraswap", "bebop", "jumper"], + "withdrawalDex": { + "integration": "cowswap", + "enabled": true + }, "multiChainBanner": [ 324, 10, 42161, 137, 8453, 5000, 59144, 534352, 56, 34443, 48900, 1868, 130, 1135, 1923 @@ -26,9 +30,10 @@ "featureFlags": { "disableSendCalls": false, "dgBannerEnabled": true, - "rewardsMaintenance": true, + "rewardsMaintenance": false, "holidayDecorEnabled": false, - "forceAllowance": false + "forceAllowance": false, + "amountBannerEnabled": true }, "pages": { "/earn": { @@ -47,7 +52,10 @@ "cid": "bafybeiecvujvs74xvxgpwctmbfkcucazyaudmwuiw4wfv6ys7uio7o376u", "leastSafeVersion": "0.76.1", "config": { - "enabledWithdrawalDexes": ["one-inch", "paraswap", "bebop", "jumper"], + "withdrawalDex": { + "integration": "cowswap", + "enabled": true + }, "multiChainBanner": [ 324, 10, 42161, 137, 8453, 5000, 59144, 534352, 56, 34443, 48900, 1868, 130, 1135, 1923 @@ -72,7 +80,8 @@ "dgBannerEnabled": true, "rewardsMaintenance": false, "holidayDecorEnabled": false, - "forceAllowance": false + "forceAllowance": false, + "amountBannerEnabled": true }, "pages": { "/earn": { @@ -92,7 +101,10 @@ "cid": "bafybeibbsoqlofslw273b4ih2pdxfaz2zbjmred2ijog725tcmfoewix7y", "leastSafeVersion": "0.36.1", "config": { - "enabledWithdrawalDexes": ["one-inch", "paraswap", "bebop", "jumper"], + "withdrawalDex": { + "integration": "cowswap", + "enabled": true + }, "multiChainBanner": [], "featureFlags": { "ledgerLiveL2": true @@ -110,7 +122,10 @@ "cid": "", "leastSafeVersion": "0.36.1", "config": { - "enabledWithdrawalDexes": ["one-inch", "paraswap", "bebop", "jumper"], + "withdrawalDex": { + "integration": "cowswap", + "enabled": true + }, "multiChainBanner": [], "featureFlags": { "ledgerLiveL2": true @@ -139,7 +154,10 @@ "cid": "", "leastSafeVersion": "0.36.1", "config": { - "enabledWithdrawalDexes": ["one-inch", "paraswap", "bebop", "jumper"], + "withdrawalDex": { + "integration": "cowswap", + "enabled": true + }, "multiChainBanner": [], "featureFlags": { "ledgerLiveL2": true, @@ -148,7 +166,8 @@ "dgWarningState": false, "rewardsMaintenance": false, "holidayDecorEnabled": false, - "forceAllowance": false + "forceAllowance": false, + "amountBannerEnabled": true }, "earnVaultsBanner": { "showOnStakeForm": true, diff --git a/abi/wsteth-abi.ts b/abi/wsteth-abi.ts new file mode 100644 index 000000000..17fed83f1 --- /dev/null +++ b/abi/wsteth-abi.ts @@ -0,0 +1,9 @@ +export const wstethABI = [ + { + inputs: [], + name: 'stEthPerToken', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, +] as const; diff --git a/assets/icons/balancer-banner-icon.svg b/assets/icons/balancer-banner-icon.svg deleted file mode 100644 index 5f6da086b..000000000 --- a/assets/icons/balancer-banner-icon.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/assets/icons/bebop.svg b/assets/icons/bebop.svg deleted file mode 100644 index a9044170a..000000000 --- a/assets/icons/bebop.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/assets/icons/cowswap-circle.svg b/assets/icons/cowswap-circle.svg deleted file mode 100644 index a7906a9ec..000000000 --- a/assets/icons/cowswap-circle.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/assets/icons/curve.svg b/assets/icons/curve.svg deleted file mode 100644 index f4e6f5f53..000000000 --- a/assets/icons/curve.svg +++ /dev/null @@ -1,1523 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/assets/icons/jumper.svg b/assets/icons/jumper.svg deleted file mode 100644 index d0dd04c21..000000000 --- a/assets/icons/jumper.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/assets/icons/paraswap-circle.svg b/assets/icons/paraswap-circle.svg deleted file mode 100644 index 087243ac3..000000000 --- a/assets/icons/paraswap-circle.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/assets/icons/velora.svg b/assets/icons/velora.svg deleted file mode 100644 index b77226506..000000000 --- a/assets/icons/velora.svg +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/assets/partner/cow-swap.svg b/assets/partner/cow-swap.svg new file mode 100644 index 000000000..6f809433a --- /dev/null +++ b/assets/partner/cow-swap.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/oneinch-circle.svg b/assets/partner/oneinch-circle.svg similarity index 100% rename from assets/icons/oneinch-circle.svg rename to assets/partner/oneinch-circle.svg diff --git a/assets/icons/open-ocean.svg b/assets/partner/open-ocean.svg similarity index 100% rename from assets/icons/open-ocean.svg rename to assets/partner/open-ocean.svg diff --git a/config/csp/index.ts b/config/csp/index.ts index d1bd9b36f..369903e94 100644 --- a/config/csp/index.ts +++ b/config/csp/index.ts @@ -50,6 +50,7 @@ export const contentSecurityPolicy: ContentSecurityPolicyOption = { }), childSrc: [ "'self'", + 'https://swap.cow.fi', // CowSwap widget iframe baseUrl 'https://*.walletconnect.org', 'https://*.walletconnect.com', ], diff --git a/config/external-config/frontend-fallback.ts b/config/external-config/frontend-fallback.ts index 31372e49d..02d2c3823 100644 --- a/config/external-config/frontend-fallback.ts +++ b/config/external-config/frontend-fallback.ts @@ -8,7 +8,6 @@ import { getManifestKey, isManifestValid, } from 'config/external-config'; -import { getDexConfig } from 'features/withdrawals/request/withdrawal-rates'; import { EARN_VAULTS } from 'features/earn/consts'; import FallbackLocalManifest from 'IPFS.json'; @@ -30,9 +29,13 @@ export const getBackwardCompatibleConfig = ( ); return { - enabledWithdrawalDexes: config.enabledWithdrawalDexes?.filter( - (dex) => !!getDexConfig(dex), - ), + withdrawalDex: { + ...(config?.withdrawalDex ?? { + // if none provided, disable with default integration + enabled: false, + integration: 'cowswap', + }), + }, featureFlags: { ...(config?.featureFlags ?? {}) }, multiChainBanner: config?.multiChainBanner ?? [], earnVaultsBanner: config?.earnVaultsBanner ?? {}, @@ -50,8 +53,7 @@ export const overrideManifestConfig = ( ): ManifestEntry['config'] => { return { ...config, - enabledWithdrawalDexes: - override.enabledWithdrawalDexes ?? config.enabledWithdrawalDexes, + withdrawalDex: { ...config.withdrawalDex, ...override.withdrawalDex }, featureFlags: { ...config.featureFlags, ...override.featureFlags }, multiChainBanner: override.multiChainBanner ?? config.multiChainBanner, earnVaults: override.earnVaults ?? config.earnVaults, @@ -66,10 +68,22 @@ export const getFallbackedManifestEntry = ( defaultChain: number, manifestOverride?: string, ): ManifestEntry => { - const isValid = isManifestValid(prefetchedManifest, defaultChain); - return isValid - ? prefetchedManifest[getManifestKey(defaultChain, manifestOverride)] - : (FallbackLocalManifest as unknown as Manifest)[defaultChain]; + const manifestKey = getManifestKey(defaultChain, manifestOverride); + const localFallback = (FallbackLocalManifest as unknown as Manifest)[ + manifestKey + ]; + + const isValid = isManifestValid(prefetchedManifest, manifestKey); + + const fallbacked = isValid ? prefetchedManifest[manifestKey] : localFallback; + + if (!fallbacked) { + throw new Error( + `[getFallbackedManifestEntry] No valid manifest entry for ${manifestKey} found in remote or local fallback IPFS.json`, + ); + } + + return fallbacked; }; export const useFallbackManifestEntry = ( diff --git a/config/external-config/index.ts b/config/external-config/index.ts index b901aaffa..520084640 100644 --- a/config/external-config/index.ts +++ b/config/external-config/index.ts @@ -12,9 +12,5 @@ export { isManifestValid, getManifestKey, isManifestEntryValid, - isEnabledDexesValid, - isFeatureFlagsValid, - isMultiChainBannerValid, - isPagesValid, shouldRedirectToRoot, } from './utils'; diff --git a/config/external-config/types.ts b/config/external-config/types.ts index 43eb85c16..15e59be3a 100644 --- a/config/external-config/types.ts +++ b/config/external-config/types.ts @@ -1,5 +1,4 @@ import type { UseQueryResult } from '@tanstack/react-query'; -import type { DexWithdrawalApi } from 'features/withdrawals/request/withdrawal-rates'; import type { EarnVaultKey } from 'features/earn/consts'; export type Manifest = Record; @@ -28,8 +27,21 @@ export type EarnVaultConfigEntry = { disabled?: boolean; }; +export type WithdrawalDexIntegration = 'cowswap'; + +type MustIncludeAll = + Exclude extends never ? U : never; + +export type WithdrawalDexIntegrationList = MustIncludeAll< + WithdrawalDexIntegration, + ['cowswap'] +>; + export type ManifestConfig = { - enabledWithdrawalDexes: DexWithdrawalApi[]; + withdrawalDex: { + integration: WithdrawalDexIntegration; + enabled: boolean; + }; multiChainBanner: number[]; earnVaults: EarnVaultConfigEntry[]; earnVaultsBanner: { @@ -44,6 +56,7 @@ export type ManifestConfig = { rewardsMaintenance?: boolean; holidayDecorEnabled?: boolean; forceAllowance?: boolean; + amountBannerEnabled?: boolean; }; pages: { [page in ManifestConfigPage]?: { diff --git a/config/external-config/utils.ts b/config/external-config/utils.ts index 13a6f24fd..aa6dd580a 100644 --- a/config/external-config/utils.ts +++ b/config/external-config/utils.ts @@ -12,7 +12,11 @@ export const getManifestKey = ( manifestOverride?: string, ) => `${defaultChain}` + - (typeof manifestOverride === 'string' ? `-${manifestOverride}` : ''); + (typeof manifestOverride === 'string' && manifestOverride + ? `-${manifestOverride}` + : ''); + +// TODO: rework to zod schema validation export const isMultiChainBannerValid = (config: object) => { // allow empty config @@ -40,27 +44,6 @@ export const isFeatureFlagsValid = (config: object) => { return !(typeof config.featureFlags !== 'object'); }; -export const isEnabledDexesValid = (config: object) => { - if ( - !( - 'enabledWithdrawalDexes' in config && - Array.isArray(config.enabledWithdrawalDexes) - ) - ) - return false; - - const enabledWithdrawalDexes = config.enabledWithdrawalDexes; - - if ( - !enabledWithdrawalDexes.every( - (dex) => typeof dex === 'string' && dex !== '', - ) - ) - return false; - - return new Set(enabledWithdrawalDexes).size === enabledWithdrawalDexes.length; -}; - export const isPagesValid = (config: object) => { if (!('pages' in config)) { return true; @@ -75,6 +58,21 @@ export const isPagesValid = (config: object) => { return false; }; +export const isWithdrawalDexValid = (config: object) => { + if (!('withdrawalDex' in config)) { + return true; + } + + const withdrawalDex = config.withdrawalDex as ManifestConfig['withdrawalDex']; + if (withdrawalDex && typeof withdrawalDex === 'object') { + const isValid = + typeof withdrawalDex.enabled === 'boolean' && + typeof withdrawalDex.integration === 'string'; + return isValid; + } + return false; +}; + export const isManifestEntryValid = ( entry?: unknown, ): entry is ManifestEntry => { @@ -90,10 +88,10 @@ export const isManifestEntryValid = ( const config = entry.config; return [ - isEnabledDexesValid, isMultiChainBannerValid, isFeatureFlagsValid, isPagesValid, + isWithdrawalDexValid, ] .map((validator) => validator(config)) .every((isValid) => isValid); @@ -103,13 +101,10 @@ export const isManifestEntryValid = ( export const isManifestValid = ( manifest: unknown, - chain: number, + key: string, ): manifest is Manifest => { - const stringChain = chain.toString(); - if (manifest && typeof manifest === 'object' && stringChain in manifest) - return isManifestEntryValid( - (manifest as Record)[stringChain], - ); + if (manifest && typeof manifest === 'object' && key in manifest) + return isManifestEntryValid((manifest as Record)[key]); return false; }; diff --git a/config/networks/networks-map.ts b/config/networks/networks-map.ts index 0fdcfefb3..304bc80e3 100644 --- a/config/networks/networks-map.ts +++ b/config/networks/networks-map.ts @@ -26,8 +26,10 @@ export const API_NAMES = {}; export const CONTRACT_NAMES = { // Main Lido contract lido: 'lido', + lidoLocator: 'lidoLocator', wsteth: 'wsteth', withdrawalQueue: 'withdrawalQueue', + daoAgent: 'daoAgent', // SI wstethReferralStaker: 'wstethReferralStaker', // DualGovernance @@ -41,15 +43,21 @@ export const CONTRACT_NAMES = { // Aux contracts aggregatorEthUsdPriceFeed: 'aggregatorEthUsdPriceFeed', aggregatorStEthUsdPriceFeed: 'aggregatorStEthUsdPriceFeed', + aggregatorUsdcUsdPriceFeed: 'aggregatorUsdcUsdPriceFeed', + aggregatorUsdtUsdPriceFeed: 'aggregatorUsdtUsdPriceFeed', + aggregatorDaiUsdPriceFeed: 'aggregatorDaiUsdPriceFeed', + aggregatorBtcUsdPriceFeed: 'aggregatorBtcUsdPriceFeed', stakingRouter: 'stakingRouter', stethCurve: 'stethCurve', - lidoLocator: 'lidoLocator', + ensPublicResolver: 'ensPublicResolver', ensRegistry: 'ensRegistry', // 3rd party tokens weth: 'weth', usdc: 'usdc', usdt: 'usdt', + usds: 'usds', + wbtc: 'wbtc', // GGV ggvVault: 'ggvVault', ggvTeller: 'ggvTeller', diff --git a/consts/api.ts b/consts/api.ts index f840ceecc..708c3a05c 100644 --- a/consts/api.ts +++ b/consts/api.ts @@ -18,6 +18,7 @@ export const enum ETH_API_ROUTES { STETH_STATS = '/v1/protocol/steth/stats', STETH_SMA_APR = '/v1/protocol/steth/apr/sma', SWAP_ONE_INCH = '/v1/swap/one-inch', + /* @deprecated */ SWAP_JUMPER = '/v1/swap/jumper', CURVE_APR = '/v1/pool/curve/steth-eth/apr/last', } diff --git a/consts/external-links.ts b/consts/external-links.ts index a62599c78..970da97e5 100644 --- a/consts/external-links.ts +++ b/consts/external-links.ts @@ -5,6 +5,11 @@ export const LINK_ADD_NFT_GUIDE = `${config.helpOrigin}/en/articles/7858367-how- export const OPEN_OCEAN_REFERRAL_ADDRESS = '0xbb1263222b2c020f155d409dba05c4a3861f18f8'; +const GITHUB_RAW_MAIN = + 'https://raw.githubusercontent.com/lidofinance/ethereum-staking-widget/main'; + // for dev and local testing you can set to 'http://localhost:3000/runtime/IPFS.json' and have file at /public/runtime/IPFS.json -export const IPFS_MANIFEST_URL = - 'https://raw.githubusercontent.com/lidofinance/ethereum-staking-widget/main/IPFS.json'; +export const IPFS_MANIFEST_URL = `${GITHUB_RAW_MAIN}/IPFS.json`; + +export const DEX_SELL_TOKEN_LIST_URL = `${GITHUB_RAW_MAIN}/public/token-lists/withdrawals-dex-sell-tokenlist.json`; +export const DEX_BUY_TOKEN_LIST_URL = `${GITHUB_RAW_MAIN}/public/token-lists/withdrawals-dex-buy-tokenlist.json`; diff --git a/consts/matomo/matomo-click-events.ts b/consts/matomo/matomo-click-events.ts index 20caac062..3471cf024 100644 --- a/consts/matomo/matomo-click-events.ts +++ b/consts/matomo/matomo-click-events.ts @@ -81,12 +81,7 @@ export const enum MATOMO_CLICK_EVENTS_TYPES { withdrawalMaxInput = 'withdrawalMaxInput', withdrawalOtherFactorsTooltipMode = 'withdrawalOtherFactorsTooltipMode', withdrawalFAQtooltipEthAmount = 'withdrawalFAQtooltipEthAmount', - withdrawalGoTo1inch = 'withdrawalGoTo1inch', - withdrawalGoToBebop = 'withdrawalGoToBebop', - withdrawalGoToCowSwap = 'withdrawalGoToCowSwap', - withdrawalGoToParaswap = 'withdrawalGoToParaswap', - withdrawalGoToOpenOcean = 'withdrawalGoToOpenOcean', - withdrawalGoToJumper = 'withdrawalGoToJumper', + withdrawalEtherscanSuccessTemplate = 'withdrawalEtherscanSuccessTemplate', withdrawalGuideSuccessTemplate = 'withdrawalGuideSuccessTemplate', @@ -455,36 +450,6 @@ export const MATOMO_CLICK_EVENTS: Record< 'Push «FAQ» in tooltip near ETH amount on Request tab', 'eth_withdrawals_request_FAQ_tooltip_eth_amount', ], - [MATOMO_CLICK_EVENTS_TYPES.withdrawalGoTo1inch]: [ - 'Ethereum_Staking_Widget_Withdraw_Use_Aggregators', - 'Click on «Go to 1inch» in aggregators list on Request tab', - 'eth_withdrawals_request_go_to_1inch', - ], - [MATOMO_CLICK_EVENTS_TYPES.withdrawalGoToBebop]: [ - 'Ethereum_Staking_Widget_Withdraw_Use_Aggregators', - 'Click on «Go to Bebop» in aggregators list on Request tab', - 'eth_withdrawals_request_go_to_Bebop', - ], - [MATOMO_CLICK_EVENTS_TYPES.withdrawalGoToCowSwap]: [ - 'Ethereum_Staking_Widget_Withdraw_Use_Aggregators', - 'Click on «Go to CowSwap» in aggregators list on Request tab', - 'eth_withdrawals_request_go_to_CowSwap', - ], - [MATOMO_CLICK_EVENTS_TYPES.withdrawalGoToParaswap]: [ - 'Ethereum_Staking_Widget_Withdraw_Use_Aggregators', - 'Click on «Go to Paraswap» in aggregators list on Request tab', - 'eth_withdrawals_request_go_to_Paraswap', - ], - [MATOMO_CLICK_EVENTS_TYPES.withdrawalGoToOpenOcean]: [ - 'Ethereum_Staking_Widget_Withdraw_Use_Aggregators', - 'Click on «Go to OpenOcean in aggregators list on Request tab', - 'eth_withdrawals_request_go_to_OpenOcean', - ], - [MATOMO_CLICK_EVENTS_TYPES.withdrawalGoToJumper]: [ - 'Ethereum_Staking_Widget_Withdraw_Use_Aggregators', - 'Click on «Go to Jumper» in aggregators list on Request tab', - 'eth_withdrawals_request_go_to_Jumper', - ], [MATOMO_CLICK_EVENTS_TYPES.withdrawalEtherscanSuccessTemplate]: [ 'Ethereum_Withdrawals_Widget', 'Click on "Etherscan" on success template after withdrawal request', diff --git a/consts/matomo/matomo-tx-events.ts b/consts/matomo/matomo-tx-events.ts index 2f627fd92..e015f9114 100644 --- a/consts/matomo/matomo-tx-events.ts +++ b/consts/matomo/matomo-tx-events.ts @@ -20,6 +20,12 @@ export const enum MATOMO_TX_EVENTS_TYPES { withdrawalRequestFinish = 'withdrawalRequestFinish', withdrawalClaimStart = 'withdrawalClaimStart', withdrawalClaimFinish = 'withdrawalClaimFinish', + + // Dex withdrawal + withdrawalDexSwapStart = 'withdrawalDexSwapStart', + withdrawalDexSwapPosted = 'withdrawalDexSwapPosted', + withdrawalDexSwapFinish = 'withdrawalDexSwapFinish', + withdrawalDexSwapCancel = 'withdrawalDexSwapCancel', } export const MATOMO_TX_EVENTS: Record = @@ -99,4 +105,26 @@ export const MATOMO_TX_EVENTS: Record = 'Finish withdrawal claim request', 'eth_widget_withdrawal_claim_finish', ], + + // dex withdrawal events + [MATOMO_TX_EVENTS_TYPES.withdrawalDexSwapStart]: [ + 'Ethereum_Staking_Widget', + 'Start swap', + 'eth_widget_withdrawal_start_swap', + ], + [MATOMO_TX_EVENTS_TYPES.withdrawalDexSwapPosted]: [ + 'Ethereum_Staking_Widget', + 'Swap order signed and posted', + 'eth_widget_withdrawal_swap_order_signed_and_posted', + ], + [MATOMO_TX_EVENTS_TYPES.withdrawalDexSwapFinish]: [ + 'Ethereum_Staking_Widget', + 'Swap order succesfully finished', + 'eth_widget_withdrawal_swap_order_sucessfully_finished', + ], + [MATOMO_TX_EVENTS_TYPES.withdrawalDexSwapCancel]: [ + 'Ethereum_Staking_Widget', + 'Swap order rejected', + 'eth_widget_withdrawal_swap_rejected', + ], }; diff --git a/features/earn/shared/v2/vault-card/styles.tsx b/features/earn/shared/v2/vault-card/styles.tsx index 4b9e06099..8e307757b 100644 --- a/features/earn/shared/v2/vault-card/styles.tsx +++ b/features/earn/shared/v2/vault-card/styles.tsx @@ -15,8 +15,17 @@ export const CardWrapper = styled(Block)<{ $variant: 'eth' | 'usd' | 'default'; }>` position: relative; + isolation: isolate; overflow: hidden; color: var(--lido-color-text); + transition: box-shadow 0.1s ease; + border: 1px solid + ${({ theme }) => (theme.name === 'dark' ? '#34343D' : '#fff')}; + + &:hover { + box-shadow: 0px 4px 64px 0px + ${({ theme }) => (theme.name === 'dark' ? '#000' : '#a7c9eb66')}; + } &::before { content: ''; @@ -30,11 +39,17 @@ export const CardWrapper = styled(Block)<{ filter: blur(10px); background: ${({ $variant }) => getBackgroundGradient($variant)}; pointer-events: none; + z-index: -1; + + ${({ theme }) => theme.mediaQueries.md} { + top: -60px; + right: 0; + left: calc(50% - 120px); + } } & > * { position: relative; - z-index: 1; } `; @@ -80,6 +95,8 @@ export const CardTitle = styled.div` export const CardTitleBadge = styled(Badge)` height: 32px; user-select: none; + position: relative; + z-index: 2; `; export const ChevronsUpIcon = styled(ChevronsUp)` @@ -216,6 +233,9 @@ export const StyledTooltip = styled(Tooltip)` `; export const BadgeStyled = styled.span` + position: relative; + z-index: 2; + ${({ theme }) => theme.mediaQueries.md} { order: 1; } @@ -226,3 +246,11 @@ export const TitleTextStyled = styled.span` order: 2; } `; + +export const CardOverlayLink = styled.a` + && { + position: absolute; + inset: 0; + z-index: 1; + } +`; diff --git a/features/earn/shared/v2/vault-card/vault-card.tsx b/features/earn/shared/v2/vault-card/vault-card.tsx index 2c9c864b0..4b2991726 100644 --- a/features/earn/shared/v2/vault-card/vault-card.tsx +++ b/features/earn/shared/v2/vault-card/vault-card.tsx @@ -3,6 +3,7 @@ import { Button } from '@lidofinance/lido-ui'; import { CardWrapper, + CardOverlayLink, CardHeader, CardHeaderContent, CardTitle, @@ -80,8 +81,17 @@ export const VaultCard: React.FC = ({ (vault) => vault.name === urlSlug, )?.deprecated; + const depositHref = `${EARN_PATH}/${urlSlug}/${EARN_VAULT_DEPOSIT_SLUG}`; + return ( + @@ -115,11 +125,16 @@ export const VaultCard: React.FC = ({ {stats.apxLabel} - {stats.apxHint} + + {stats.apxHint} + - * + @@ -131,7 +146,7 @@ export const VaultCard: React.FC = ({ - {position && ( + {!!position?.balance && ( My position @@ -153,11 +168,7 @@ export const VaultCard: React.FC = ({ )} - + diff --git a/features/earn/shared/v2/vault-chart/apy-data/metavault-apy.ts b/features/earn/shared/v2/vault-chart/apy-data/metavault-apy.ts index 699550de4..f4ed65e62 100644 --- a/features/earn/shared/v2/vault-chart/apy-data/metavault-apy.ts +++ b/features/earn/shared/v2/vault-chart/apy-data/metavault-apy.ts @@ -38,6 +38,9 @@ export type MetavaultChartFetchedData = z.infer< export const METAVAULT_CURRENT_DATA_SCHEMA = z.object({ totalTvl: z.object({ usd: z.string(), + amount: z.string(), + decimals: z.number(), + usd_decimals: z.number(), }), lastUpdate: UNIX_TIMESTAMP_SCHEMA, }); diff --git a/features/earn/shared/v2/vault-chart/hooks/use-chart-data.ts b/features/earn/shared/v2/vault-chart/hooks/use-chart-data.ts index 6951610db..9623b6747 100644 --- a/features/earn/shared/v2/vault-chart/hooks/use-chart-data.ts +++ b/features/earn/shared/v2/vault-chart/hooks/use-chart-data.ts @@ -71,10 +71,22 @@ export const useChartData = (props: UseChartDataProps) => { const [tvlSeriesData, apySeriesData] = useMemo(() => { if (!data) return [[], []]; - const tvl = data.map((item) => [item.timestampMs, item.tvlUsd]); + const tvl = data.map((item) => + item.tvlUsd != null + ? [item.timestampMs, item.tvl, item.tvlUsd] + : [item.timestampMs, item.tvl], + ); const lastTs = tvl.at(-1)?.[0] ?? 0; if (currentTvlPoint && currentTvlPoint.timestampMs > lastTs) { - tvl.push([currentTvlPoint.timestampMs, currentTvlPoint.tvlUsd]); + tvl.push( + currentTvlPoint.tvlUsd != null + ? [ + currentTvlPoint.timestampMs, + currentTvlPoint.tvl, + currentTvlPoint.tvlUsd, + ] + : [currentTvlPoint.timestampMs, currentTvlPoint.tvl], + ); } return [tvl, data.map((item) => [item.timestampMs, item.apyValue])]; }, [data, currentTvlPoint]); @@ -107,15 +119,15 @@ export const useChartData = (props: UseChartDataProps) => { const yAxisFormatter = useMemo( () => activeChart === CHART_TYPE.tvl - ? (value: number) => formatTvl(value) + ? (value: number) => formatTvl(value, isETHVault) : (value: string) => `${value}%`, - [activeChart], + [activeChart, isETHVault], ); const tooltipFormatter = useMemo( () => (params: TooltipComponentFormatterCallbackParams) => - formatTooltipContent(params, activeChart === CHART_TYPE.tvl), - [activeChart], + formatTooltipContent(params, activeChart === CHART_TYPE.tvl, isETHVault), + [activeChart, isETHVault], ); // alignToVaultTimestamps uses filteredApySeriesData so treasury/staking series diff --git a/features/earn/shared/v2/vault-chart/hooks/use-vault-chart-data.ts b/features/earn/shared/v2/vault-chart/hooks/use-vault-chart-data.ts index 445c9604b..2beb95719 100644 --- a/features/earn/shared/v2/vault-chart/hooks/use-vault-chart-data.ts +++ b/features/earn/shared/v2/vault-chart/hooks/use-vault-chart-data.ts @@ -3,20 +3,21 @@ import { useQuery } from '@tanstack/react-query'; import { formatUnits } from 'viem'; import type { Address } from 'viem'; -import { useEthUsd } from 'shared/hooks/use-eth-usd'; import { unixTimestampToMs } from 'utils/unix-timestamp-to-ms'; +import { useEthUsd } from 'shared/hooks/use-eth-usd'; +import { STRATEGY_LAZY } from 'consts/react-query-strategies'; +import { useEthVaultStats } from 'features/earn/vault-eth/hooks/use-vault-stats'; +import { useUsdVaultStats } from 'features/earn/vault-usd/hooks/use-vault-stats'; -import { - fetchMetavaultChartData, - fetchMetavaultCurrentData, -} from '../apy-data/metavault-apy'; +import { fetchMetavaultChartData } from '../apy-data/metavault-apy'; import { METAVAULT_QUERY_SCOPE } from '../consts'; -import { STRATEGY_LAZY } from 'consts/react-query-strategies'; export type NormalizedVaultChartPoint = { timestampMs: number; - /** TVL already converted to USD. */ - tvlUsd: number; + /** TVL: ETH for ETH vaults, USD for USD vaults. */ + tvl: number; + /** TVL in USD, only populated for ETH vaults (for the tooltip). */ + tvlUsd?: number; /** APY as a plain percentage (e.g. 5.25 for 5.25%). */ apyValue: number; }; @@ -52,33 +53,64 @@ export const useMetavaultChartData = ({ enabled: !!vaultAddress, }); - // ETH price is needed to convert wei TVL to USD for ETH vaults. - // Globally cached — adding this call causes no extra network requests. - const { price: ethPrice, isLoading: isEthPriceLoading } = useEthUsd(); + // ETH price is used to show a USD equivalent in the TVL tooltip for ETH vaults. + // Globally cached — no extra network requests. + const { price: ethPrice } = useEthUsd(); - const { data: currentData, isLoading: isCurrentDataLoading } = useQuery({ - queryKey: [METAVAULT_QUERY_SCOPE, 'current-tvl-data', vaultAddress], - queryFn: async () => { - if (!vaultAddress) return null; - return fetchMetavaultCurrentData(vaultAddress); - }, - enabled: !!vaultAddress, - }); + const { + tvlUsd: ethVaultTvlUsd, + tvlBaseAsset: ethVaultBaseAsset, + tvlBaseAssetDecimals: ethVaultBaseAssetDecimals, + tvlUpdateTimestampMs: ethVaultUpdateTimestampMs, + } = useEthVaultStats(); + + const { + tvlUsd: usdVaultTvlUsd, + tvlUpdateTimestampMs: usdVaultUpdateTimestampMs, + } = useUsdVaultStats(); const currentTvlPoint = useMemo(() => { - if (!currentData) return null; + if (isETHVault) { + if ( + ethVaultBaseAsset == null || + ethVaultTvlUsd == null || + ethVaultUpdateTimestampMs == null + ) + return null; + + return { + timestampMs: ethVaultUpdateTimestampMs, + tvl: Number(formatUnits(ethVaultBaseAsset, ethVaultBaseAssetDecimals)), + tvlUsd: ethVaultTvlUsd, + }; + } + + if (usdVaultTvlUsd == null || usdVaultUpdateTimestampMs == null) + return null; + return { - timestampMs: unixTimestampToMs(currentData.lastUpdate), - tvlUsd: Number(formatUnits(BigInt(currentData.totalTvl.usd), 8)), + timestampMs: usdVaultUpdateTimestampMs, + // USD Vault uses USDC as a base asset, so we can use TVL in USD directly. + tvl: usdVaultTvlUsd, + tvlUsd: undefined, // Not needed for USD vault. }; - }, [currentData]); + }, [ + isETHVault, + usdVaultUpdateTimestampMs, + usdVaultTvlUsd, + ethVaultBaseAsset, + ethVaultTvlUsd, + ethVaultUpdateTimestampMs, + ethVaultBaseAssetDecimals, + ]); const data = useMemo((): NormalizedVaultChartPoint[] | null => { if (!rawData) return null; - // For ETH vault, hold off until ETH price is ready; otherwise tvlUsd would be wrong. - if (isETHVault && !ethPrice) return null; return rawData.map((item) => { + const tvl = Number( + formatUnits(BigInt(item.tvl.amount), item.tvl.decimals), + ); const tvlUsd = isETHVault && ethPrice ? Number( @@ -87,10 +119,11 @@ export const useMetavaultChartData = ({ Number(18n + ethPrice.decimals), ), ) - : Number(formatUnits(BigInt(item.tvl.amount), item.tvl.decimals)); + : undefined; return { timestampMs: unixTimestampToMs(Number(item.timestamp)), + tvl, tvlUsd, apyValue: Number(Number(item.apy.value).toFixed(2)), }; @@ -100,11 +133,7 @@ export const useMetavaultChartData = ({ return { data, currentTvlPoint, - // For ETH vault, the chart is not ready until ETH price is also loaded. - isLoading: - isVaultLoading || - (isETHVault && isEthPriceLoading) || - isCurrentDataLoading, + isLoading: isVaultLoading, isError, }; }; diff --git a/features/earn/shared/v2/vault-chart/utils.ts b/features/earn/shared/v2/vault-chart/utils.ts index 597930cee..6f29c3b4f 100644 --- a/features/earn/shared/v2/vault-chart/utils.ts +++ b/features/earn/shared/v2/vault-chart/utils.ts @@ -18,10 +18,12 @@ import { /** * Format TVL for display. - * Expects `amount` already converted to USD (done by the vault data hook before reaching here). + * For ETH vaults renders as "X ETH"; for USD vaults renders as "$X". */ -export const formatTvl = (amount: number) => - `$${getShortenedNumber(amount, '0')}`; +export const formatTvl = (amount: number, isETH = false) => + isETH + ? `${getShortenedNumber(amount, '0')} ETH` + : `$${getShortenedNumber(amount, '0')}`; /** Format date+time for the tooltip header using the browser's local timezone. */ export const formatDate = (timestamp: number) => @@ -35,16 +37,26 @@ export const formatDate = (timestamp: number) => export const formatTooltipContent = ( params: TooltipComponentFormatterCallbackParams, isTvl: boolean, + isETHVault = false, ): string => { if (!Array.isArray(params) || params.length === 0) return ''; const first = params[0]; const [timestamp] = (first.value as [number, string | number]) ?? []; const lines = params.map(({ marker, seriesName, value }) => { - const rawValue = (value as [number, string | number])[1]; + const values = value as (string | number)[]; + const rawValue = values[1]; + const usdValue = values[2] as number | undefined; const formatted = isTvl - ? formatTvl(rawValue as number) + ? formatTvl(rawValue as number, isETHVault) : `${Number(rawValue).toFixed(2)}%`; - return `
${marker}${seriesName}   ${formatted}
`; + + let line = `
${marker}${seriesName}   ${formatted}`; + if (isTvl && isETHVault && usdValue != null) { + line += `
${formatTvl(usdValue)}
`; + } + line += '
'; + + return line; }); return `${formatDate(timestamp)}
${lines.join('')}
`; }; diff --git a/features/earn/shared/v2/vault-chart/vault-chart.tsx b/features/earn/shared/v2/vault-chart/vault-chart.tsx index 11e9098b4..9051039ed 100644 --- a/features/earn/shared/v2/vault-chart/vault-chart.tsx +++ b/features/earn/shared/v2/vault-chart/vault-chart.tsx @@ -74,7 +74,6 @@ export const VaultChart = (props: VaultChartProps) => { isTreasuryLoading, isStakingLoading, isLoadingError, - seriesData, treasurySeriesData, stakingSeriesData, @@ -109,8 +108,11 @@ export const VaultChart = (props: VaultChartProps) => { trigger: 'axis', confine: true, formatter: tooltipFormatter, + extraCssText: + 'min-width:170px;border-radius:10px;padding:12px;gap: 8px;box-shadow: 0 4px 16px 0 rgba(0, 10, 61, 0.16);', textStyle: { fontFamily: 'Manrope, sans-serif', + color: isDark ? '#fff' : '#273852', }, ...(isDark && { backgroundColor: '#131317', diff --git a/features/earn/shared/v2/vault-page/content/top-section.tsx b/features/earn/shared/v2/vault-page/content/top-section.tsx index cc54b3dc1..80c540a9c 100644 --- a/features/earn/shared/v2/vault-page/content/top-section.tsx +++ b/features/earn/shared/v2/vault-page/content/top-section.tsx @@ -24,7 +24,7 @@ type TopSectionProps = { title: string; description: string; apx?: number | null; - tvl?: number | null; + tvlUsd?: number | null; apxHint?: React.ReactNode; isApxLoading?: boolean; isTvlLoading?: boolean; @@ -36,7 +36,7 @@ export const TopSection: FC = (props) => { title, description, apx, - tvl, + tvlUsd, apxHint, isApxLoading, isTvlLoading, @@ -60,7 +60,7 @@ export const TopSection: FC = (props) => { - APY (7d avg.) + APY* (7d avg.) {apxHint} @@ -73,7 +73,7 @@ export const TopSection: FC = (props) => { TVL - + diff --git a/features/earn/shared/v2/vault-page/styles.tsx b/features/earn/shared/v2/vault-page/styles.tsx index 4e306bed4..3118408e0 100644 --- a/features/earn/shared/v2/vault-page/styles.tsx +++ b/features/earn/shared/v2/vault-page/styles.tsx @@ -104,6 +104,9 @@ export const TableItem = styled.div` export const TableLabel = styled.span` color: var(--lido-color-textSecondary); + display: flex; + align-items: center; + gap: ${({ theme }) => theme.spaceMap.xs}px; `; export const TableValue = styled.span` diff --git a/features/earn/shared/v2/vault-page/vault-page.tsx b/features/earn/shared/v2/vault-page/vault-page.tsx index bb2a936b2..9ee8cac50 100644 --- a/features/earn/shared/v2/vault-page/vault-page.tsx +++ b/features/earn/shared/v2/vault-page/vault-page.tsx @@ -7,7 +7,7 @@ import { type ReactNode, type SVGProps, } from 'react'; -import { Tab } from '@lidofinance/lido-ui'; +import { Tab, Tooltip, Question } from '@lidofinance/lido-ui'; import { type MATOMO_EVENT_TYPE } from 'consts/matomo'; import { trackMatomoEvent } from 'utils/track-matomo-event'; @@ -42,15 +42,16 @@ type VaultIllustration = ComponentType>; export type InfoItem = { label: ReactNode; value?: ReactNode; + tooltip?: ReactNode; }; type Props = { title: string; description: string; apx?: number | null; - tvl?: number | null; apxHint?: React.ReactNode; isApxLoading?: boolean; + tvlUsd?: number | null; isTvlLoading?: boolean; logo: VaultIllustration; sidePanel?: ReactNode; @@ -125,7 +126,7 @@ export const VaultPage: FC = (props) => { title={props.title} description={props.description} apx={props.apx} - tvl={props.tvl} + tvlUsd={props.tvlUsd} apxHint={props.apxHint} isApxLoading={props.isApxLoading} isTvlLoading={props.isTvlLoading} @@ -172,7 +173,14 @@ export const VaultPage: FC = (props) => { {generalInfoLeft.map((item, index) => ( - {item.label} + + {item.label} + {item.tooltip && ( + + + + )} + {item.value != null && ( {item.value} )} @@ -182,7 +190,14 @@ export const VaultPage: FC = (props) => { {generalInfoRight.map((item, index) => ( - {item.label} + + {item.label} + {item.tooltip && ( + + + + )} + {item.value != null && ( {item.value} )} diff --git a/features/earn/vault-dvv/vault-card-dvv-v2.tsx b/features/earn/vault-dvv/vault-card-dvv-v2.tsx index ad591f239..b3f7fe9f2 100644 --- a/features/earn/vault-dvv/vault-card-dvv-v2.tsx +++ b/features/earn/vault-dvv/vault-card-dvv-v2.tsx @@ -22,7 +22,7 @@ export const VaultCardDVV = () => { stats={{ tvl: tvl, apx: apr, - apxLabel: 'APR', + apxLabel: 'APY* (7d avg.)', apxHint: , isLoading: isLoadingStats, }} @@ -35,9 +35,7 @@ export const VaultCardDVV = () => { } : undefined } - ctaLabel={ - sharesBalance && sharesBalance > 0n ? 'Upgrade your assets' : 'View' - } + ctaLabel={sharesBalance && sharesBalance > 0n ? 'Manage' : 'View'} illustration={} depositLinkCallback={() => { trackMatomoEvent(MATOMO_EARN_EVENTS_TYPES.strategyDeposit); diff --git a/features/earn/vault-eth/consts.ts b/features/earn/vault-eth/consts.ts index 55eb074e2..d6629ebf7 100644 --- a/features/earn/vault-eth/consts.ts +++ b/features/earn/vault-eth/consts.ts @@ -52,3 +52,5 @@ export const ETH_VAULT_ROUTES = [ matomoEvent: MATOMO_EARN_EVENTS_TYPES.earnEthWithdrawalTab, }, ]; + +export const ETH_VAULT_BASE_ASSET_DECIMALS = 18; diff --git a/features/earn/vault-eth/hooks/use-vault-stats.ts b/features/earn/vault-eth/hooks/use-vault-stats.ts index 839d6a35f..03015a95f 100644 --- a/features/earn/vault-eth/hooks/use-vault-stats.ts +++ b/features/earn/vault-eth/hooks/use-vault-stats.ts @@ -1,45 +1,25 @@ -import { UNIX_TIMESTAMP_SCHEMA } from 'utils/zod'; -import { formatUnits } from 'viem'; -import { useQuery } from '@tanstack/react-query'; -// import { useEthUsd } from 'shared/hooks/use-eth-usd'; -import { ETH_VAULT_QUERY_SCOPE } from '../consts'; -import { ALLOCATION_SCHEMA, fetchEthVaultStats } from '../utils'; +import { useEthUsd } from 'shared/hooks/use-eth-usd'; +import { unixTimestampToMs } from 'utils/unix-timestamp-to-ms'; import { useEthVaultCollect } from './use-collect'; +import { ETH_VAULT_BASE_ASSET_DECIMALS } from '../consts'; export const useEthVaultStats = () => { - const { data, isLoading } = useQuery({ - queryKey: [ETH_VAULT_QUERY_SCOPE, 'allocations'], - queryFn: async () => { - const fetchedData = await fetchEthVaultStats(); - const allocations = ALLOCATION_SCHEMA.parse(fetchedData.allocations); - const apiUsdTVL = Number( - formatUnits( - BigInt(fetchedData.totalTvl.usd), - fetchedData.totalTvl.usd_decimals, - ), - ); - const lastUpdate = UNIX_TIMESTAMP_SCHEMA.parse(fetchedData.lastUpdate); - - return { allocations, lastUpdate, apiUsdTVL }; - }, - }); - const { data: collectorData, isLoading: isCollectorLoading } = useEthVaultCollect(); const totalTvlWei = collectorData?.totalTvlWei; + const collectorTimestamp = collectorData?.collectorTimestamp; // unix timestamp in seconds - // Temporarily disabled: collector contract TVL (totalTvlWei × ETH price) doesn't yet - // match the vault's actual TVL. Using the API value (apiUsdTVL) instead as - // the source of truth until the discrepancy is resolved. - // const { usdAmount: totalTvlUsd, isLoading: isEthUsdLoading } = - // useEthUsd(totalTvlWei); + const { usdAmount, isLoading: isEthUsdLoading } = useEthUsd(totalTvlWei); + const tvlUpdateTimestampMs = + collectorTimestamp != null + ? unixTimestampToMs(collectorTimestamp) + : undefined; return { - // isLoading: isLoading || isCollectorLoading || isEthUsdLoading, - isLoading: isLoading || isCollectorLoading, - totalTvlUsd: data?.apiUsdTVL, - totalTvlWei, - fetchedPositions: data?.allocations, - lastUpdateTimestamp: data?.lastUpdate, + isLoading: isCollectorLoading || isEthUsdLoading, + tvlUsd: usdAmount, + tvlBaseAsset: totalTvlWei, // uses base asset – ETH + tvlBaseAssetDecimals: ETH_VAULT_BASE_ASSET_DECIMALS, + tvlUpdateTimestampMs, } as const; }; diff --git a/features/earn/vault-eth/vault-card.tsx b/features/earn/vault-eth/vault-card.tsx index 4eeaa147b..a2f3618b6 100644 --- a/features/earn/vault-eth/vault-card.tsx +++ b/features/earn/vault-eth/vault-card.tsx @@ -17,7 +17,7 @@ import { ProtectedTooltip } from './protected-tooltip'; export const EthVaultCard = () => { const { apy, isLoading: isApyLoading } = useEthVaultApy(); - const { totalTvlUsd, isLoading: isTvlLoading } = useEthVaultStats(); + const { tvlUsd, isLoading: isTvlLoading } = useEthVaultStats(); const { data: earnethPositionData, isLoading: isPositionLoading } = useEthVaultPosition(); @@ -30,7 +30,7 @@ export const EthVaultCard = () => { description={ETH_VAULT_DESCRIPTION} urlSlug={EARN_VAULT_ETH_SLUG} stats={{ - tvl: totalTvlUsd, + tvl: tvlUsd, apx: apy, apxLabel: 'APY* (7d avg.)', apxHint: , diff --git a/features/earn/vault-eth/vault-page.tsx b/features/earn/vault-eth/vault-page.tsx index ef1929256..36e606f48 100644 --- a/features/earn/vault-eth/vault-page.tsx +++ b/features/earn/vault-eth/vault-page.tsx @@ -48,8 +48,17 @@ const GENERAL_INFO_LEFT: InfoItem[] = [ { label: 'Last audit date', value: '02 March 2026' }, ]; -const GENERAL_INFO_RIGHT: Array<{ label: ReactNode; value?: ReactNode }> = [ - { label: 'Withdrawal wait time', value: 'up to 72 hours' }, +const GENERAL_INFO_RIGHT: Array<{ + label: ReactNode; + value?: ReactNode; + tooltip?: string; +}> = [ + { + label: 'Withdrawal wait time', + value: 'up to 72 hours', + tooltip: + 'Withdrawals take up to 72 hours to process. Once ready, your funds can be claimed in the Lido UI', + }, { label: ( @@ -101,14 +110,14 @@ export const EthVaultPage: FC<{ action: typeof EARN_VAULT_DEPOSIT_SLUG | typeof EARN_VAULT_WITHDRAW_SLUG; }> = ({ action }) => { const { apy, isLoading: isApyLoading } = useEthVaultApy(); - const { totalTvlUsd, isLoading: isTvlLoading } = useEthVaultStats(); + const { tvlUsd, isLoading: isTvlLoading } = useEthVaultStats(); return ( <> } diff --git a/features/earn/vault-ggv/vault-card-ggv-v2.tsx b/features/earn/vault-ggv/vault-card-ggv-v2.tsx index 3a2093eac..6844081a3 100644 --- a/features/earn/vault-ggv/vault-card-ggv-v2.tsx +++ b/features/earn/vault-ggv/vault-card-ggv-v2.tsx @@ -22,7 +22,7 @@ export const VaultCardGGV = () => { stats={{ tvl: tvl, apx: apy, - apxLabel: 'APY', + apxLabel: 'APY* (7d avg.)', apxHint: , isLoading: isLoadingStats, }} @@ -35,9 +35,7 @@ export const VaultCardGGV = () => { } : undefined } - ctaLabel={ - sharesBalance && sharesBalance > 0n ? 'Upgrade your assets' : 'View' - } + ctaLabel={sharesBalance && sharesBalance > 0n ? 'Manage' : 'View'} illustration={} depositLinkCallback={() => { trackMatomoEvent(MATOMO_EARN_EVENTS_TYPES.strategyDeposit); diff --git a/features/earn/vault-stg/vault-card-stg-v2.tsx b/features/earn/vault-stg/vault-card-stg-v2.tsx index 0b3df8778..b5e76d3bb 100644 --- a/features/earn/vault-stg/vault-card-stg-v2.tsx +++ b/features/earn/vault-stg/vault-card-stg-v2.tsx @@ -36,7 +36,7 @@ export const VaultCardSTG = () => { stats={{ tvl: totalTvlUsd, apx: apy, - apxLabel: 'APY', + apxLabel: 'APY* (7d avg.)', apxHint: , isLoading: isLoadingApy || isLoadingTvlUsd, }} @@ -52,9 +52,7 @@ export const VaultCardSTG = () => { : undefined } ctaLabel={ - strethSharesBalance && strethSharesBalance > 0n - ? 'Upgrade your assets' - : 'View' + strethSharesBalance && strethSharesBalance > 0n ? 'Manage' : 'View' } illustration={} depositLinkCallback={() => { diff --git a/features/earn/vault-usd/consts.ts b/features/earn/vault-usd/consts.ts index 998468f31..bb7a13a7e 100644 --- a/features/earn/vault-usd/consts.ts +++ b/features/earn/vault-usd/consts.ts @@ -17,3 +17,5 @@ export const USD_VAULT_DEPOSIT_TOKENS = [TOKENS.usdc, TOKENS.usdt] as const; export const USD_VAULT_QUERY_SCOPE = 'earn-vault-usd'; export const USD_VAULT_STATS_ORIGIN = 'https://api.mellow.finance'; + +export const USD_VAULT_BASE_ASSET_DECIMALS = 6; diff --git a/features/earn/vault-usd/hooks/use-vault-stats.ts b/features/earn/vault-usd/hooks/use-vault-stats.ts index 8eb0c97ef..fda2d2958 100644 --- a/features/earn/vault-usd/hooks/use-vault-stats.ts +++ b/features/earn/vault-usd/hooks/use-vault-stats.ts @@ -1,32 +1,25 @@ -import { UNIX_TIMESTAMP_SCHEMA } from 'utils/zod'; -import { useQuery } from '@tanstack/react-query'; -import { USD_VAULT_QUERY_SCOPE } from '../consts'; -import { ALLOCATION_SCHEMA, fetchUsdVaultStats } from '../utils'; -import { useUsdVaultCollect } from './use-collect'; import { convertTotalUsdToNumber } from 'features/earn/shared/utils/collector-totalusd'; +import { unixTimestampToMs } from 'utils/unix-timestamp-to-ms'; +import { useUsdVaultCollect } from './use-collect'; +import { USD_VAULT_BASE_ASSET_DECIMALS } from '../consts'; export const useUsdVaultStats = () => { - const { data, isLoading } = useQuery({ - queryKey: [USD_VAULT_QUERY_SCOPE, 'allocations'], - queryFn: async () => { - const fetchedData = await fetchUsdVaultStats(); - const allocations = ALLOCATION_SCHEMA.parse(fetchedData.allocations); - const lastUpdate = UNIX_TIMESTAMP_SCHEMA.parse(fetchedData.lastUpdate); - return { allocations, lastUpdate }; - }, - }); - const { data: collectorData, isLoading: isCollectorLoading } = useUsdVaultCollect(); const totalTvlWei = collectorData?.totalTvlWei; + const collectorTimestamp = collectorData?.collectorTimestamp; // unix timestamp in seconds - const totalTvlUsd = convertTotalUsdToNumber(collectorData?.totalTvlUsd); + const tvlUsd = convertTotalUsdToNumber(collectorData?.totalTvlUsd); + const tvlUpdateTimestampMs = + collectorTimestamp != null + ? unixTimestampToMs(collectorTimestamp) + : undefined; return { - isLoading: isLoading || isCollectorLoading, - totalTvlUsd, - totalTvlWei, - fetchedPositions: data?.allocations, - lastUpdateTimestamp: data?.lastUpdate, + isLoading: isCollectorLoading, + tvlUsd, + tvlBaseAsset: totalTvlWei, // uses base asset – USDC + tvlBaseAssetDecimals: USD_VAULT_BASE_ASSET_DECIMALS, + tvlUpdateTimestampMs, } as const; }; diff --git a/features/earn/vault-usd/vault-card.tsx b/features/earn/vault-usd/vault-card.tsx index 115613d44..9e81e25ce 100644 --- a/features/earn/vault-usd/vault-card.tsx +++ b/features/earn/vault-usd/vault-card.tsx @@ -18,7 +18,7 @@ import { ProtectedTooltip } from './protected-tooltip'; export const UsdVaultCard = () => { const { apy, isLoading: isApyLoading } = useUsdVaultApy(); - const { totalTvlUsd, isLoading: isTvlLoading } = useUsdVaultStats(); + const { tvlUsd, isLoading: isTvlLoading } = useUsdVaultStats(); const { data: usdPositionData, isLoading: isPositionLoading } = useUsdVaultPosition(); @@ -30,7 +30,7 @@ export const UsdVaultCard = () => { description={USD_VAULT_DESCRIPTION} urlSlug={EARN_VAULT_USD_SLUG} stats={{ - tvl: totalTvlUsd, + tvl: tvlUsd, apx: apy, apxLabel: 'APY* (7d avg.)', apxHint: , diff --git a/features/earn/vault-usd/vault-page.tsx b/features/earn/vault-usd/vault-page.tsx index 51cd1323a..0ec5e3722 100644 --- a/features/earn/vault-usd/vault-page.tsx +++ b/features/earn/vault-usd/vault-page.tsx @@ -48,8 +48,17 @@ const GENERAL_INFO_LEFT = [ { label: 'Last audit date', value: '02 March 2026' }, ]; -const GENERAL_INFO_RIGHT: Array<{ label: ReactNode; value?: ReactNode }> = [ - { label: 'Withdrawal wait time', value: 'up to 72 hours' }, +const GENERAL_INFO_RIGHT: Array<{ + label: ReactNode; + value?: ReactNode; + tooltip?: string; +}> = [ + { + label: 'Withdrawal wait time', + value: 'up to 72 hours', + tooltip: + 'Withdrawals take up to 72 hours to process. Once ready, your funds can be claimed in the Lido UI', + }, { label: ( @@ -101,14 +110,14 @@ export const VaultPageUSD: FC<{ action: typeof EARN_VAULT_DEPOSIT_SLUG | typeof EARN_VAULT_WITHDRAW_SLUG; }> = ({ action }) => { const { apy, isLoading: isApyLoading } = useUsdVaultApy(); - const { totalTvlUsd, isLoading: isTvlLoading } = useUsdVaultStats(); + const { tvlUsd, isLoading: isTvlLoading } = useUsdVaultStats(); return ( <> } diff --git a/features/earn/vaults-list/styles.tsx b/features/earn/vaults-list/styles.tsx index c059493ec..eb86fd805 100644 --- a/features/earn/vaults-list/styles.tsx +++ b/features/earn/vaults-list/styles.tsx @@ -1,4 +1,17 @@ import styled from 'styled-components'; +import { AccordionTransparent } from '@lidofinance/lido-ui'; + +export const AccordionTransparentStyled = styled(AccordionTransparent)` + overflow: visible; + + /* + Disabling pointer-events while animation is in progress, + fixes issue with card shadow on hover caused by inline overflow:hidden during animation + */ + &[data-animating] * { + pointer-events: none; + } +`; export const ListSubtitle = styled.div` text-align: center; diff --git a/features/earn/vaults-list/vaults-list.tsx b/features/earn/vaults-list/vaults-list.tsx index 7533d4bb5..7c4aa0015 100644 --- a/features/earn/vaults-list/vaults-list.tsx +++ b/features/earn/vaults-list/vaults-list.tsx @@ -1,5 +1,4 @@ import { useState, type FC } from 'react'; -import { AccordionTransparent } from '@lidofinance/lido-ui'; import { DisclaimerSection, @@ -25,6 +24,7 @@ import { CardsStack, ListSubtitle, ListWrapper, + AccordionTransparentStyled, } from './styles'; const VAULT_CARDS = { @@ -38,6 +38,7 @@ const VAULT_CARDS = { export const EarnVaultsList: FC = () => { const { earnVaultsEnabled } = useEarnState(); const [isDrawerRightOpen, setIsDrawerRightOpen] = useState(false); + const [isAccordionAnimating, setIsAccordionAnimating] = useState(false); const actualVaults = [] as typeof earnVaultsEnabled; const deprecatedVaults = [] as typeof earnVaultsEnabled; @@ -79,7 +80,11 @@ export const EarnVaultsList: FC = () => { {hasDeprecatedVaults && ( - setIsAccordionAnimating(true)} + onExpand={() => setIsAccordionAnimating(false)} + onCollapse={() => setIsAccordionAnimating(false)} summary={ Upgrading vaults @@ -93,7 +98,7 @@ export const EarnVaultsList: FC = () => { return ; })} - + )} diff --git a/features/ipfs/security-status-banner/use-version-status.ts b/features/ipfs/security-status-banner/use-version-status.ts index 595cd0133..4eae8907c 100644 --- a/features/ipfs/security-status-banner/use-version-status.ts +++ b/features/ipfs/security-status-banner/use-version-status.ts @@ -47,36 +47,40 @@ export const useVersionStatus = () => { // ens cid extraction const remoteVersionQueryResult = useRemoteVersion(); + // SECURITY: QA overrides below can only escalate to `true` (show warning), + // never suppress a real `true` to `false`. This prevents hiding security + // banners if QA localStorage keys leak to production. + // update is available // for INFRA - leastSafeVersion is not NO_SAFE_VERSION // for IPFS - ^this and current cid doesn't match - const isUpdateAvailable = overrideWithQAMockBoolean( - Boolean( - remoteVersionQueryResult.data && - ((currentCidQueryResult.data && - remoteVersionQueryResult.data.cid !== currentCidQueryResult.data) || - !config.ipfsMode) && - remoteVersionQueryResult.data.leastSafeVersion !== NO_SAFE_VERSION, - ), - 'mock-qa-helpers-security-banner-is-update-available', + const realIsUpdateAvailable = Boolean( + remoteVersionQueryResult.data && + ((currentCidQueryResult.data && + remoteVersionQueryResult.data.cid !== currentCidQueryResult.data) || + !config.ipfsMode) && + remoteVersionQueryResult.data.leastSafeVersion !== NO_SAFE_VERSION, ); + const isUpdateAvailable = + realIsUpdateAvailable || + overrideWithQAMockBoolean(false, 'mock-qa-helpers-security-banner-is-update-available'); - const isVersionUnsafe = overrideWithQAMockBoolean( - Boolean( - remoteVersionQueryResult.data?.leastSafeVersion && - (remoteVersionQueryResult.data.leastSafeVersion === NO_SAFE_VERSION || - isVersionLess( - buildInfo.version, - remoteVersionQueryResult.data.leastSafeVersion, - )), - ), - 'mock-qa-helpers-security-banner-is-version-unsafe', + const realIsVersionUnsafe = Boolean( + remoteVersionQueryResult.data?.leastSafeVersion && + (remoteVersionQueryResult.data.leastSafeVersion === NO_SAFE_VERSION || + isVersionLess( + buildInfo.version, + remoteVersionQueryResult.data.leastSafeVersion, + )), ); + const isVersionUnsafe = + realIsVersionUnsafe || + overrideWithQAMockBoolean(false, 'mock-qa-helpers-security-banner-is-version-unsafe'); - const isNotVerifiable = overrideWithQAMockBoolean( - !!remoteVersionQueryResult.error, - 'mock-qa-helpers-security-banner-is-not-verifiable', - ); + const realIsNotVerifiable = !!remoteVersionQueryResult.error; + const isNotVerifiable = + realIsNotVerifiable || + overrideWithQAMockBoolean(false, 'mock-qa-helpers-security-banner-is-not-verifiable'); // disconnect wallet and disallow connection for unsafe versions useEffect(() => { diff --git a/features/stake/stake-form/hooks/use-tx-modal-stages-stake.tsx b/features/stake/stake-form/hooks/use-tx-modal-stages-stake.tsx index 43295bdac..674b09fa5 100644 --- a/features/stake/stake-form/hooks/use-tx-modal-stages-stake.tsx +++ b/features/stake/stake-form/hooks/use-tx-modal-stages-stake.tsx @@ -8,6 +8,7 @@ import { TxStageSignOperationAmount } from 'shared/transaction-modal/tx-stages-c import { TxStageOperationSucceedBalanceShown } from 'shared/transaction-modal/tx-stages-composed/tx-stage-operation-succeed-balance-shown'; import { EarnUpToBanner } from 'shared/banners/earn-up-to-banner'; import { MATOMO_CLICK_EVENTS_TYPES } from 'consts/matomo'; +import { AmountBanner } from 'shared/banners/amount-banners'; const STAGE_OPERATION_ARGS = { token: 'ETH', @@ -39,7 +40,7 @@ const getTxModalStagesStake = (transitStage: TransactionModalTransitStage) => ({ />, ), - success: (balance: bigint, txHash?: Hash) => + success: (balance: bigint, preStakeBalance: bigint, txHash?: Hash) => transitStage( ({ balanceToken={'stETH'} operationText={'Staking'} footer={ - + + + } />, { diff --git a/features/stake/stake-form/use-stake.ts b/features/stake/stake-form/use-stake.ts index 62df59c35..76a7690f3 100644 --- a/features/stake/stake-form/use-stake.ts +++ b/features/stake/stake-form/use-stake.ts @@ -55,6 +55,7 @@ export const useStake = ({ onConfirm, onRetry }: StakeOptions) => { referral, stake.core.rpcProvider, ); + const preStakeBalance = await stETH.balance(address); const onStakeTxConfirmed = async () => { const [, balance] = await Promise.all([ @@ -91,7 +92,7 @@ export const useStake = ({ onConfirm, onRetry }: StakeOptions) => { if (featureFlags.holidayDecorEnabled) { bells(); } - txModalStages.success(balance, txHash); + txModalStages.success(balance, preStakeBalance, txHash); trackMatomoEvent(MATOMO_TX_EVENTS_TYPES.stakingFinish); }, onFailure: ({ error }) => txModalStages.failed(error, onRetry), diff --git a/features/stake/stake.tsx b/features/stake/stake.tsx index c87b40983..dbb82c7bd 100644 --- a/features/stake/stake.tsx +++ b/features/stake/stake.tsx @@ -6,7 +6,6 @@ import { AprDisclaimer, LegalDisclaimer, } from 'shared/components'; - import { StakeFaq } from './stake-faq/stake-faq'; import { LidoStats } from './lido-stats/lido-stats'; import { StakeForm } from './stake-form'; diff --git a/features/stake/swap-discount-banner/styles.ts b/features/stake/swap-discount-banner/styles.ts index 8caea19c0..26ed81ab6 100644 --- a/features/stake/swap-discount-banner/styles.ts +++ b/features/stake/swap-discount-banner/styles.ts @@ -1,7 +1,7 @@ import styled from 'styled-components'; import BgSrc from 'assets/icons/swap-banner-bg.svg'; -import OpenOcean from 'assets/icons/open-ocean.svg'; -import OneInch from 'assets/icons/oneinch-circle.svg'; +import OpenOcean from 'assets/partner/open-ocean.svg'; +import OneInch from 'assets/partner/oneinch-circle.svg'; export const Wrap = styled.div` position: relative; diff --git a/features/withdrawals/request/dex-disclaimer.tsx b/features/withdrawals/request/dex-disclaimer.tsx new file mode 100644 index 000000000..be25b77a2 --- /dev/null +++ b/features/withdrawals/request/dex-disclaimer.tsx @@ -0,0 +1,9 @@ +import { useConfig } from 'config'; + +export const DexDisclaimer = () => { + const isDexEnabled = useConfig().externalConfig.withdrawalDex.enabled; + + if (!isDexEnabled) return null; + + return

Withdrawals performed by swaps are powered by CowSwap.

; +}; diff --git a/features/withdrawals/request/form/options/dex-option/consts.ts b/features/withdrawals/request/form/options/dex-option/consts.ts new file mode 100644 index 000000000..2d8bf08cd --- /dev/null +++ b/features/withdrawals/request/form/options/dex-option/consts.ts @@ -0,0 +1,14 @@ +export const BLOCKED_RPC_METHODS = new Set([ + // Signing arbitrary data (phishing) + 'eth_sign', + // Manipulation of the network + 'wallet_addEthereumChain', + 'wallet_switchEthereumChain', + // Debugging (leak of private data) + 'debug_traceTransaction', + 'debug_traceCall', +]); + +export const MAX_SLIPPAGE = 300; // 3% (bps) +export const PARTNER_FEE_BPS = 30; // 0.3% — Lido DAO treasury +export const WHEN_PRICE_IMPACT_IS_HIGH_THAN = 3; // 3% diff --git a/features/withdrawals/request/form/options/dex-option/dex-option.tsx b/features/withdrawals/request/form/options/dex-option/dex-option.tsx new file mode 100644 index 000000000..a72522dc3 --- /dev/null +++ b/features/withdrawals/request/form/options/dex-option/dex-option.tsx @@ -0,0 +1,305 @@ +import { + CowSwapWidget, + CowSwapWidgetPalette, + CowSwapWidgetParams, + CowSwapWidgetProps, + TradeType, +} from '@cowprotocol/widget-react'; +import { useCallback, useMemo, useState } from 'react'; +import { useQuery } from '@tanstack/react-query'; +// eslint-disable-next-line import/no-extraneous-dependencies +import { CowWidgetEvents, OnTradeParamsPayload } from '@cowprotocol/events'; + +import { LOCALE } from 'config/groups/locale'; +import { STRATEGY_IMMUTABLE } from 'consts/react-query-strategies'; +import { useTheme } from 'styled-components'; +import { useWalletClient } from 'wagmi'; +import { useAddressValidation } from 'providers/address-validation-provider'; +import { themeDark, themeLight } from '@lidofinance/lido-ui'; +import { + useDappStatus, + useEthereumBalance, + useStethBalance, + useWstethBalance, +} from 'modules/web3'; +import { getContractAddress } from 'config/networks/contract-address'; +import invariant from 'tiny-invariant'; +import { trackMatomoEvent } from 'utils/track-matomo-event'; +import { MATOMO_TX_EVENTS_TYPES } from 'consts/matomo'; +import { + DEX_SELL_TOKEN_LIST_URL, + DEX_BUY_TOKEN_LIST_URL, +} from 'consts/external-links'; + +import { + MAX_SLIPPAGE, + PARTNER_FEE_BPS, + WHEN_PRICE_IMPACT_IS_HIGH_THAN, +} from './consts'; +import { LoaderStyled, DexWrapper } from './styles'; +import { useCowSwapEthereumProvider } from './hooks/use-cow-swap-ethereum-provider'; +import { useCspBlocked } from './hooks/use-csp-blocked'; + +import { useTradeGuard, TradeGuardModal } from './trade-guard'; + +const cowSwapThemeDark: CowSwapWidgetPalette = { + baseTheme: 'dark', + primary: themeDark.colors.primary, + background: themeDark.colors.background, + paper: '#2D2D35', + text: themeDark.colors.text, + warning: themeDark.colors.warning, + alert: themeDark.colors.warningBackground, + danger: themeDark.colors.error, + info: themeDark.colors.warning, + success: themeDark.colors.success, + // boxShadow: '0 12px 12px 0 rgba(5, 43, 101, 0.06)', // TODO: wait fix from CowSwap team + boxShadow: 'none', +}; + +const cowSwapThemeLight: CowSwapWidgetPalette = { + baseTheme: 'light', + primary: themeLight.colors.primary, + background: '#F2F2F2', + paper: themeLight.colors.foreground, + text: themeLight.colors.text, + warning: themeLight.colors.warning, + alert: themeLight.colors.warning, + danger: themeLight.colors.error, + info: themeLight.colors.warning, + success: themeLight.colors.success, + // boxShadow: '0 12px 12px 0 rgba(5, 43, 101, 0.06)', // TODO: wait fix from CowSwap team + boxShadow: 'none', +}; + +export const DexOption = () => { + const { refetch: refetchSteth } = useStethBalance(); + const { refetch: refetchWsteth } = useWstethBalance(); + const { refetch: refetchEth } = useEthereumBalance(); + // state to trigger refreshes to memoized params + const [refreshId, setRefreshId] = useState(0); + const [isLoading, setIsLoading] = useState(true); + + useCspBlocked(); + + const { isTestnet, chainId } = useDappStatus(); + const { validateAddress } = useAddressValidation(); + const { data: walletClient } = useWalletClient(); + const { name: themeName } = useTheme(); + + const refreshBalances = useCallback(() => { + void Promise.allSettled([refetchSteth(), refetchWsteth(), refetchEth()]); + }, [refetchEth, refetchSteth, refetchWsteth]); + + // Fall back to self-hosted token lists if GitHub is unavailable + const { data: isGithubAvailable = true } = useQuery({ + queryKey: ['dex-token-list-availability'], + ...STRATEGY_IMMUTABLE, + queryFn: () => + fetch(DEX_SELL_TOKEN_LIST_URL, { method: 'HEAD' }) + .then((res) => res.ok) + .catch(() => false), + }); + + const daoAgentAddress = getContractAddress(chainId, 'daoAgent'); + invariant( + daoAgentAddress, + 'DAO Agent address is not defined for current network', + ); + + const { + modalState, + handleModalClose, + validateTrade, + validateApproval, + reportTradeParams, + checkSellLimit, + verifySignedOrder, + } = useTradeGuard({ + walletAddress: walletClient?.account.address, + isTestnet, + }); + + const validate = useCallback(async () => { + const isValid = await validateAddress(walletClient?.account.address); + return isValid; + }, [validateAddress, walletClient?.account.address]); + + const params = useMemo( + () => ({ + // + // Core options + // + appCode: 'Lido Staking Widget', + standaloneMode: false, + // for testnets only sepolia + chainId: isTestnet ? 11155111 : 1, + // test app + baseUrl: 'https://swap.cow.fi', + + // + // Trading options + // + + tradeType: TradeType.SWAP, + enabledTradeTypes: [TradeType.SWAP], + sell: { + asset: 'STETH', + }, + buy: { + asset: 'ETH', + }, + sellTokenLists: [ + isGithubAvailable + ? DEX_SELL_TOKEN_LIST_URL + : `${window.location.origin}/token-lists/withdrawals-dex-sell-tokenlist.json`, + ], + buyTokenLists: [ + isGithubAvailable + ? DEX_BUY_TOKEN_LIST_URL + : `${window.location.origin}/token-lists/withdrawals-dex-buy-tokenlist.json`, + ], + slippage: { + max: MAX_SLIPPAGE, + }, + partnerFee: { + bps: PARTNER_FEE_BPS, + recipient: daoAgentAddress, + }, + disableTrade: { + whenPriceImpactIsUnknown: true, + whenPriceImpactIsHigherThan: WHEN_PRICE_IMPACT_IS_HIGH_THAN, + }, + disableCrossChainSwap: true, + + // + // UI options + // + + width: '100%', + height: '432px', + theme: themeName === 'dark' ? cowSwapThemeDark : cowSwapThemeLight, + + disablePostedOrderConfirmationModal: true, + disableTokenImport: true, + disablePostTradeTips: true, + hideRecentTokens: true, + hideFavoriteTokens: true, + disableToastMessages: true, + disableProgressBar: false, + sounds: { + postOrder: null, + orderExecuted: null, + orderError: null, + }, + hideBridgeInfo: false, + hideOrdersTable: false, + hideNetworkSelector: true, + locale: LOCALE, + + hooks: { + onBeforeApproval: async () => { + if (!(await checkSellLimit())) return false; + if (!(await validateApproval())) return false; + + return await validate(); + }, + onBeforeWrapOrUnwrap: async () => { + if (!(await checkSellLimit())) return false; + + return await validate(); + }, + onBeforeTrade: async (payload) => { + if (!(await checkSellLimit())) return false; + if (!(await validateTrade(payload))) return false; + + trackMatomoEvent(MATOMO_TX_EVENTS_TYPES.withdrawalDexSwapStart); + return await validate(); + }, + onBeforeOrderCancel: async () => { + return await validate(); + }, + onBeforeOrdersCancel: async () => { + return await validate(); + }, + }, + }), + // eslint-disable-next-line react-hooks/exhaustive-deps + [ + isTestnet, + daoAgentAddress, + themeName, + validate, + validateTrade, + validateApproval, + isGithubAvailable, + refreshId, + ], + ); + + const provider = useCowSwapEthereumProvider(verifySignedOrder); + + const listeners: CowSwapWidgetProps['listeners'] = useMemo(() => { + const handlers: CowSwapWidgetProps['listeners'] = [ + { + event: CowWidgetEvents.ON_POSTED_ORDER, + handler: async () => { + trackMatomoEvent(MATOMO_TX_EVENTS_TYPES.withdrawalDexSwapPosted); + }, + }, + { + event: CowWidgetEvents.ON_FULFILLED_ORDER, + handler: () => { + refreshBalances(); + trackMatomoEvent(MATOMO_TX_EVENTS_TYPES.withdrawalDexSwapFinish); + }, + }, + { + event: CowWidgetEvents.ON_CANCELLED_ORDER, + handler: () => { + trackMatomoEvent(MATOMO_TX_EVENTS_TYPES.withdrawalDexSwapCancel); + }, + }, + { + event: CowWidgetEvents.ON_EXPIRED_ORDER, + handler: () => { + trackMatomoEvent(MATOMO_TX_EVENTS_TYPES.withdrawalDexSwapCancel); + }, + }, + { + event: CowWidgetEvents.ON_CHANGE_TRADE_PARAMS, + handler: (params: OnTradeParamsPayload) => { + reportTradeParams(params); + + // Workaround: refresh params if user changes sell token + const { sellToken } = params; + if ( + !sellToken || + sellToken.symbol.toLowerCase() === 'steth' || + sellToken.symbol.toLowerCase() === 'wsteth' + ) + return; + setRefreshId((id) => id + 1); + }, + }, + ]; + + return handlers; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [refreshBalances]); + + return ( + <> + + setIsLoading(false)} + /> + + + + + ); +}; diff --git a/features/withdrawals/request/form/options/dex-option/error-boundary.tsx b/features/withdrawals/request/form/options/dex-option/error-boundary.tsx new file mode 100644 index 000000000..dd98a0ec4 --- /dev/null +++ b/features/withdrawals/request/form/options/dex-option/error-boundary.tsx @@ -0,0 +1,29 @@ +import dynamic from 'next/dynamic'; +import { ErrorBoundary } from 'react-error-boundary'; +import { FallbackContainer } from './styles'; + +// @cowprotocol/widget-lib accesses `document` at module scope, +// so the entire CowSwap widget tree must be loaded client-side only. +const DexOption = dynamic(() => import('./dex-option').then((m) => m.DexOption), { + ssr: false, +}); + +const Fallback = () => { + return ( + +

DEX temporarily unavailable

+ + This feature relies on an external provider that is currently not + responding. Please refresh the page or try again later. + +
+ ); +}; + +export const DexOptionWithErrorBoundary = () => { + return ( + + + + ); +}; diff --git a/features/withdrawals/request/form/options/dex-option/hooks/use-cow-swap-ethereum-provider.ts b/features/withdrawals/request/form/options/dex-option/hooks/use-cow-swap-ethereum-provider.ts new file mode 100644 index 000000000..5ff2d97cc --- /dev/null +++ b/features/withdrawals/request/form/options/dex-option/hooks/use-cow-swap-ethereum-provider.ts @@ -0,0 +1,53 @@ +import { useMemo } from 'react'; +import { EthereumProvider, JsonRpcRequest } from '@cowprotocol/widget-react'; +import { ConnectorEventMap, useConnection, useWalletClient } from 'wagmi'; + +import { BLOCKED_RPC_METHODS } from '../consts'; +import { parseOrderFromSignRequest } from '../trade-guard/utils/verify-order'; +import type { OrderFields } from '../trade-guard/utils/verify-order'; + +type VerifyOrder = (order: OrderFields) => string | null; + +export const useCowSwapEthereumProvider = ( + verifySignedOrder: VerifyOrder, +): EthereumProvider | undefined => { + const { data: walletClient } = useWalletClient(); + const { connector } = useConnection(); + + return useMemo(() => { + if (!walletClient || !connector) return undefined; + + return { + request: (args: JsonRpcRequest): Promise => { + if (BLOCKED_RPC_METHODS.has(args.method)) { + return Promise.reject( + new Error(`RPC method "${args.method}" is not allowed`), + ); + } + + // Defense-in-depth: verify CowSwap order before signing + if (args.method === 'eth_signTypedData_v4') { + const order = parseOrderFromSignRequest(args.params); + if (order) { + const error = verifySignedOrder(order); + if (error) { + return Promise.reject( + new Error(`Order signing rejected: ${error}`), + ); + } + } + } + + return walletClient.request( + args as Parameters[0], + ); + }, + on: (eventName: string, handler: unknown) => { + connector.emitter.on( + eventName as keyof ConnectorEventMap, + handler as never, + ); + }, + }; + }, [walletClient, connector, verifySignedOrder]); +}; diff --git a/features/withdrawals/request/form/options/dex-option/hooks/use-csp-blocked.ts b/features/withdrawals/request/form/options/dex-option/hooks/use-csp-blocked.ts new file mode 100644 index 000000000..19a0f831a --- /dev/null +++ b/features/withdrawals/request/form/options/dex-option/hooks/use-csp-blocked.ts @@ -0,0 +1,26 @@ +import { useState, useEffect } from 'react'; + +export const useCspBlocked = () => { + const [cspBlocked, setCspBlocked] = useState(null); + + useEffect(() => { + const handler = (e: SecurityPolicyViolationEvent) => { + if ( + (e.violatedDirective === 'child-src' || + e.violatedDirective === 'frame-src') && + e.blockedURI.includes('cow.fi') + ) { + setCspBlocked(new Error('CSP blocked CoW widget iframe')); + } + }; + document.addEventListener('securitypolicyviolation', handler); + return () => + document.removeEventListener('securitypolicyviolation', handler); + }, []); + + if (cspBlocked) throw cspBlocked; + + return { + cspBlocked, + }; +}; diff --git a/features/withdrawals/request/form/options/dex-option/index.tsx b/features/withdrawals/request/form/options/dex-option/index.tsx new file mode 100644 index 000000000..236a88d56 --- /dev/null +++ b/features/withdrawals/request/form/options/dex-option/index.tsx @@ -0,0 +1 @@ +export { DexOptionWithErrorBoundary } from './error-boundary'; diff --git a/features/withdrawals/request/form/options/dex-option/styles.ts b/features/withdrawals/request/form/options/dex-option/styles.ts new file mode 100644 index 000000000..0a0d2e85e --- /dev/null +++ b/features/withdrawals/request/form/options/dex-option/styles.ts @@ -0,0 +1,75 @@ +import { Loader } from '@lidofinance/lido-ui'; +import styled, { css, keyframes } from 'styled-components'; + +export const SellAmountWarning = styled.div` + padding: 12px 16px; + border-radius: 10px; + background-color: var(--lido-color-errorBackground, #fde8e8); + color: var(--lido-color-error, #e14d4d); + font-size: 14px; + font-weight: 400; + line-height: 1.4; +`; + +export const FallbackContainer = styled.div` + display: flex; + + height: 200px; + padding: 0 24px; + flex-direction: column; + justify-content: center; + align-items: center; + gap: 4px; + align-self: stretch; + + border-radius: 10px; + background-color: var(--custom-background-secondary); + + h2 { + color: var(--lido-color-text); + font-size: 14px; + font-weight: 700; + } + + span { + color: var(--lido-color-textSecondary); + font-size: 12px; + font-weight: 400; + text-align: center; + } +`; + +export const DexWrapper = styled.div` + position: relative; + width: 100%; + height: 100%; +`; + +const fadeOut = keyframes` + from { opacity: 0.8; } + to { opacity: 0; pointer-events: none; } +`; + +export const LoaderStyled = styled(Loader)<{ $isVisible: boolean }>` + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 100; + display: flex; + align-items: center; + justify-content: center; + background-color: var(--custom-background-secondary); + border-radius: 24px; + + ${({ $isVisible }) => + $isVisible + ? css` + opacity: 0.8; + ` + : css` + animation: ${fadeOut} 0.3s ease forwards; + pointer-events: none; + `} +`; diff --git a/features/withdrawals/request/form/options/dex-option/trade-guard/__tests__/qa-utils.test.ts b/features/withdrawals/request/form/options/dex-option/trade-guard/__tests__/qa-utils.test.ts new file mode 100644 index 000000000..ff3d1ba06 --- /dev/null +++ b/features/withdrawals/request/form/options/dex-option/trade-guard/__tests__/qa-utils.test.ts @@ -0,0 +1,108 @@ +// Mock utils/qa to break the ESM config chain (env-dynamics.mjs) +jest.mock('utils/qa', () => ({ + overrideWithQAMockString: jest.fn((value: string) => value), + overrideWithQAMockNumber: jest.fn((value: number) => value), +})); + +import { overrideWithQAMockString, overrideWithQAMockNumber } from 'utils/qa'; +import { applyQALevelOverride, readThresholds } from '../utils/qa-utils'; +import { DEFAULT_THRESHOLDS } from '../consts'; + +const mockString = overrideWithQAMockString as jest.Mock; +const mockNumber = overrideWithQAMockNumber as jest.Mock; + +afterEach(() => jest.resetAllMocks()); + +// --------------------------------------------------------------------------- +// applyQALevelOverride +// --------------------------------------------------------------------------- +describe('applyQALevelOverride', () => { + it('keeps level when QA returns same value', () => { + mockString.mockReturnValue('blocked'); + expect(applyQALevelOverride('blocked')).toBe('blocked'); + }); + + it('allows escalation: safe → blocked', () => { + mockString.mockReturnValue('blocked'); + expect(applyQALevelOverride('safe')).toBe('blocked'); + }); + + it('blocks de-escalation: blocked → safe', () => { + mockString.mockReturnValue('safe'); + expect(applyQALevelOverride('blocked')).toBe('blocked'); + }); + + it('ignores invalid level strings', () => { + mockString.mockReturnValue('invalid'); + expect(applyQALevelOverride('blocked')).toBe('blocked'); + }); + + it('ignores removed danger level', () => { + mockString.mockReturnValue('danger'); + expect(applyQALevelOverride('safe')).toBe('safe'); + }); +}); + +// --------------------------------------------------------------------------- +// readThresholds — QA clamping +// --------------------------------------------------------------------------- +describe('readThresholds', () => { + it('returns defaults when no QA overrides', () => { + mockNumber.mockImplementation((v: number) => v); + expect(readThresholds()).toEqual(DEFAULT_THRESHOLDS); + }); + + // --- All thresholds: lower = stricter → only allow lower --- + + it('allows lowering oracleDeviationBlock (tighter)', () => { + mockNumber.mockImplementation((v: number, key: string) => + key.includes('oracle-block') ? 2 : v, + ); + const t = readThresholds(); + expect(t.oracleDeviationBlock).toBe(2); + }); + + it('blocks raising oracleDeviationBlock (looser)', () => { + mockNumber.mockImplementation((v: number, key: string) => + key.includes('oracle-block') ? 10 : v, + ); + const t = readThresholds(); + expect(t.oracleDeviationBlock).toBe(DEFAULT_THRESHOLDS.oracleDeviationBlock); + }); + + it('allows lowering maxAllowedSellAmount (tighter)', () => { + mockNumber.mockImplementation((v: number, key: string) => + key.includes('max-sell') ? 1000 : v, + ); + const t = readThresholds(); + expect(t.maxAllowedSellAmount).toBe(1000); + }); + + it('blocks raising maxAllowedSellAmount (looser)', () => { + mockNumber.mockImplementation((v: number, key: string) => + key.includes('max-sell') ? 99999 : v, + ); + const t = readThresholds(); + expect(t.maxAllowedSellAmount).toBe( + DEFAULT_THRESHOLDS.maxAllowedSellAmount, + ); + }); + + it('allows lowering minSellUnitsToTriggerOracle (tighter)', () => { + mockNumber.mockImplementation((v: number, key: string) => + key.includes('min-sell') ? 0.1 : v, + ); + const t = readThresholds(); + expect(t.minSellUnitsToTriggerOracle).toBe(0.1); + }); + + it('blocks raising minSellUnitsToTriggerOracle (looser)', () => { + mockNumber.mockImplementation((v: number, key: string) => + key.includes('min-sell') ? 100 : v, + ); + const t = readThresholds(); + expect(t.minSellUnitsToTriggerOracle).toBe( + DEFAULT_THRESHOLDS.minSellUnitsToTriggerOracle, + ); + }); +}); diff --git a/features/withdrawals/request/form/options/dex-option/trade-guard/__tests__/utils.test.ts b/features/withdrawals/request/form/options/dex-option/trade-guard/__tests__/utils.test.ts new file mode 100644 index 000000000..39475610c --- /dev/null +++ b/features/withdrawals/request/form/options/dex-option/trade-guard/__tests__/utils.test.ts @@ -0,0 +1,380 @@ +import { safeParseDecimal } from '../utils/safe-parse-decimal'; +import { resolveLevel } from '../utils/resolve-level'; +import { analyzeParams } from '../utils/analyze-params'; +import { isValidRound, isInBounds } from '../utils/oracle-utils'; +import { + CHAINLINK_SCALE, + PRICE_BOUNDS, + WSTETH_RATE_MIN, + WSTETH_RATE_MAX, +} from '../consts'; +import type { RoundData } from '../utils/oracle-utils'; +import type { OnTradeParamsPayload } from '../types'; + +// stETH and ETH addresses for test fixtures +const STETH = '0xae7ab96520de3a18e5e111b5eaab095312d7fe84'; +const WSTETH = '0x7f39c581f595b53c5cb19bd0b3f8da6c935e2ca0'; +const ETH = '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee'; +const USDC = '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48'; +const UNKNOWN = '0x0000000000000000000000000000000000000001'; + +const WALLET = '0xUserWalletAddress'; + +const makePayload = ( + overrides: Partial = {}, +): OnTradeParamsPayload => + ({ + sellToken: { address: STETH, symbol: 'stETH' }, + buyToken: { address: ETH, symbol: 'ETH' }, + sellTokenAmount: { units: '10' }, + buyTokenAmount: { units: '10' }, + minimumReceiveBuyAmount: { units: '9.8' }, + recipient: undefined, + ...overrides, + }) as unknown as OnTradeParamsPayload; + +// --------------------------------------------------------------------------- +// safeParseDecimal +// --------------------------------------------------------------------------- +describe('safeParseDecimal', () => { + it('parses valid integers', () => { + expect(safeParseDecimal('42')).toBe(42); + expect(safeParseDecimal('0')).toBe(0); + }); + + it('parses valid decimals', () => { + expect(safeParseDecimal('1.5')).toBe(1.5); + expect(safeParseDecimal('0.001')).toBe(0.001); + }); + + it('returns null for null/undefined/empty', () => { + expect(safeParseDecimal(null)).toBeNull(); + expect(safeParseDecimal(undefined)).toBeNull(); + expect(safeParseDecimal('')).toBeNull(); + }); + + it('rejects scientific notation', () => { + expect(safeParseDecimal('1e18')).toBeNull(); + expect(safeParseDecimal('1E10')).toBeNull(); + expect(safeParseDecimal('1e+5')).toBeNull(); + }); + + it('rejects partial parses', () => { + expect(safeParseDecimal('1000abc')).toBeNull(); + expect(safeParseDecimal('abc')).toBeNull(); + expect(safeParseDecimal('12.34.56')).toBeNull(); + }); + + it('rejects negative numbers', () => { + expect(safeParseDecimal('-1')).toBeNull(); + expect(safeParseDecimal('-0.5')).toBeNull(); + }); + + it('handles large valid numbers', () => { + expect(safeParseDecimal('99999999999999999999')).toBe(1e20); + }); + + it('rejects numbers exceeding 20 integer digits', () => { + expect(safeParseDecimal('123456789012345678901')).toBeNull(); + }); +}); + +// --------------------------------------------------------------------------- +// resolveLevel +// --------------------------------------------------------------------------- +describe('resolveLevel', () => { + it('returns safe when null', () => { + expect(resolveLevel(null)).toBe('safe'); + }); + + it('returns blocked for oracle deviation >= 4%', () => { + expect(resolveLevel(4)).toBe('blocked'); + expect(resolveLevel(4.9)).toBe('blocked'); + expect(resolveLevel(50)).toBe('blocked'); + }); + + it('returns safe for oracle deviation below 4%', () => { + expect(resolveLevel(3.9)).toBe('safe'); + }); +}); + +// --------------------------------------------------------------------------- +// analyzeParams +// --------------------------------------------------------------------------- +describe('analyzeParams', () => { + describe('token validation', () => { + it('returns blocked when sell token is missing', () => { + const payload = makePayload({ sellToken: undefined }); + const result = analyzeParams(payload, WALLET, false); + expect(result.level).toBe('blocked'); + expect(result.isStructural).toBe(true); + expect(result.messages[0]).toContain('Token information unavailable'); + }); + + it('blocks invalid sell token on mainnet', () => { + const payload = makePayload({ + sellToken: { address: UNKNOWN, symbol: 'X' } as never, + }); + const result = analyzeParams(payload, WALLET, false); + expect(result.level).toBe('blocked'); + expect(result.isStructural).toBe(true); + expect(result.messages[0]).toContain('Invalid sell token'); + }); + + it('blocks invalid buy token on mainnet', () => { + const payload = makePayload({ + buyToken: { address: UNKNOWN, symbol: 'X' } as never, + }); + const result = analyzeParams(payload, WALLET, false); + expect(result.level).toBe('blocked'); + expect(result.isStructural).toBe(true); + expect(result.messages[0]).toContain('Invalid buy token'); + }); + + it('skips token whitelist on testnet', () => { + const payload = makePayload({ + sellToken: { address: UNKNOWN, symbol: 'X' } as never, + buyToken: { address: UNKNOWN, symbol: 'Y' } as never, + }); + const result = analyzeParams(payload, WALLET, true); + expect(result.level).toBe('safe'); + }); + + it('accepts valid stETH → ETH pair', () => { + const result = analyzeParams(makePayload(), WALLET, false); + expect(result.level).toBe('safe'); + expect(result.isStructural).toBe(false); + }); + + it('accepts wstETH → USDC pair', () => { + const payload = makePayload({ + sellToken: { address: WSTETH, symbol: 'wstETH' } as never, + buyToken: { address: USDC, symbol: 'USDC' } as never, + }); + const result = analyzeParams(payload, WALLET, false); + expect(result.level).toBe('safe'); + }); + }); + + describe('recipient validation', () => { + it('blocks when recipient mismatches wallet', () => { + const payload = makePayload({ recipient: '0xAttacker' }); + const result = analyzeParams(payload, WALLET, false); + expect(result.level).toBe('blocked'); + expect(result.isStructural).toBe(true); + expect(result.messages[0]).toContain('recipient does not match'); + }); + + it('passes when recipient matches wallet (case-insensitive)', () => { + const payload = makePayload({ + recipient: WALLET.toUpperCase(), + }); + const result = analyzeParams(payload, WALLET, false); + expect(result.level).toBe('safe'); + }); + + it('passes when recipient is undefined', () => { + const payload = makePayload({ recipient: undefined }); + const result = analyzeParams(payload, WALLET, false); + expect(result.level).toBe('safe'); + }); + + it('skips recipient check when wallet is undefined (banner mode)', () => { + const payload = makePayload({ recipient: '0xAnyone' }); + const result = analyzeParams(payload, undefined, false); + expect(result.level).toBe('safe'); + }); + }); + + describe('max sell amount', () => { + it('blocks sell amount > 5000', () => { + const payload = makePayload({ + sellTokenAmount: { units: '5001' } as never, + }); + const result = analyzeParams(payload, WALLET, false); + expect(result.level).toBe('blocked'); + expect(result.isStructural).toBe(true); + expect(result.messages[0]).toContain('capped at'); + }); + + it('allows sell amount = 5000', () => { + const payload = makePayload({ + sellTokenAmount: { units: '5000' } as never, + }); + const result = analyzeParams(payload, WALLET, false); + expect(result.level).toBe('safe'); + }); + }); +}); + +// --------------------------------------------------------------------------- +// isValidRound +// --------------------------------------------------------------------------- +describe('isValidRound', () => { + const NOW = 1700000000n; + const MAX_STALENESS = 3900; // 1h + 5min + + const makeRound = ( + overrides: Partial< + Record< + 'roundId' | 'answer' | 'startedAt' | 'updatedAt' | 'answeredInRound', + bigint + > + > = {}, + ): RoundData => [ + overrides.roundId ?? 100n, + overrides.answer ?? 220000000000n, // $2200 + overrides.startedAt ?? NOW - 60n, + overrides.updatedAt ?? NOW - 60n, + overrides.answeredInRound ?? 100n, + ]; + + it('accepts a valid fresh round', () => { + expect(isValidRound(makeRound(), MAX_STALENESS, NOW)).toBe(true); + }); + + it('rejects answer <= 0', () => { + expect(isValidRound(makeRound({ answer: 0n }), MAX_STALENESS, NOW)).toBe( + false, + ); + expect(isValidRound(makeRound({ answer: -1n }), MAX_STALENESS, NOW)).toBe( + false, + ); + }); + + it('rejects incomplete round (answeredInRound < roundId)', () => { + expect( + isValidRound( + makeRound({ roundId: 100n, answeredInRound: 99n }), + MAX_STALENESS, + NOW, + ), + ).toBe(false); + }); + + it('rejects future timestamps beyond 60s tolerance', () => { + expect( + isValidRound(makeRound({ updatedAt: NOW + 61n }), MAX_STALENESS, NOW), + ).toBe(false); + }); + + it('accepts timestamps within 60s future tolerance', () => { + expect( + isValidRound(makeRound({ updatedAt: NOW + 60n }), MAX_STALENESS, NOW), + ).toBe(true); + }); + + it('rejects stale data beyond maxStaleness', () => { + expect( + isValidRound( + makeRound({ updatedAt: NOW - BigInt(MAX_STALENESS) - 1n }), + MAX_STALENESS, + NOW, + ), + ).toBe(false); + }); + + it('accepts data at exactly maxStaleness', () => { + expect( + isValidRound( + makeRound({ updatedAt: NOW - BigInt(MAX_STALENESS) }), + MAX_STALENESS, + NOW, + ), + ).toBe(true); + }); +}); + +// --------------------------------------------------------------------------- +// PRICE_BOUNDS +// --------------------------------------------------------------------------- +describe('PRICE_BOUNDS', () => { + it('has bounds for all expected feeds', () => { + const expected = [ + 'ETH_USD', + 'STETH_USD', + 'USDC_USD', + 'USDT_USD', + 'DAI_USD', + 'BTC_USD', + ]; + for (const key of expected) { + expect(PRICE_BOUNDS[key]).toBeDefined(); + expect(PRICE_BOUNDS[key].min).toBeLessThan(PRICE_BOUNDS[key].max); + } + }); + + it('ETH bounds accept $2200', () => { + const price = 2200n * CHAINLINK_SCALE; + expect(price >= PRICE_BOUNDS['ETH_USD'].min).toBe(true); + expect(price <= PRICE_BOUNDS['ETH_USD'].max).toBe(true); + }); + + it('USDC bounds accept $1.00', () => { + const price = CHAINLINK_SCALE; // $1.00 + expect(price >= PRICE_BOUNDS['USDC_USD'].min).toBe(true); + expect(price <= PRICE_BOUNDS['USDC_USD'].max).toBe(true); + }); + + it('ETH bounds reject $0', () => { + expect(0n >= PRICE_BOUNDS['ETH_USD'].min).toBe(false); + }); + + it('ETH bounds reject $500 (below $1000 min)', () => { + const price = 500n * CHAINLINK_SCALE; + expect(price >= PRICE_BOUNDS['ETH_USD'].min).toBe(false); + }); + + it('USDC bounds reject $3', () => { + const price = 3n * CHAINLINK_SCALE; + expect(price <= PRICE_BOUNDS['USDC_USD'].max).toBe(false); + }); +}); + +// --------------------------------------------------------------------------- +// isInBounds +// --------------------------------------------------------------------------- +describe('isInBounds', () => { + it('returns true for price within bounds', () => { + expect(isInBounds(2200n * CHAINLINK_SCALE, 'ETH_USD')).toBe(true); + }); + + it('returns false for price below min', () => { + expect(isInBounds(10n * CHAINLINK_SCALE, 'ETH_USD')).toBe(false); + }); + + it('returns false for price above max', () => { + expect(isInBounds(30_000n * CHAINLINK_SCALE, 'ETH_USD')).toBe(false); + }); + + it('returns true at exact min boundary', () => { + expect(isInBounds(PRICE_BOUNDS['ETH_USD'].min, 'ETH_USD')).toBe(true); + }); + + it('returns true at exact max boundary', () => { + expect(isInBounds(PRICE_BOUNDS['ETH_USD'].max, 'ETH_USD')).toBe(true); + }); + + it('returns false (fail-closed) for unknown feed key', () => { + expect(isInBounds(100n * CHAINLINK_SCALE, 'UNKNOWN_FEED')).toBe(false); + }); +}); + +// --------------------------------------------------------------------------- +// wstETH rate bounds +// --------------------------------------------------------------------------- +describe('wstETH rate bounds', () => { + it('accepts current rate ~1.23', () => { + const rate = 1231387616265532089n; + expect(rate >= WSTETH_RATE_MIN).toBe(true); + expect(rate <= WSTETH_RATE_MAX).toBe(true); + }); + + it('rejects rate below 1.0', () => { + expect(999999999999999999n >= WSTETH_RATE_MIN).toBe(false); + }); + + it('rejects rate above 2.0', () => { + expect(2000000000000000001n <= WSTETH_RATE_MAX).toBe(false); + }); +}); diff --git a/features/withdrawals/request/form/options/dex-option/trade-guard/__tests__/verify-order.test.ts b/features/withdrawals/request/form/options/dex-option/trade-guard/__tests__/verify-order.test.ts new file mode 100644 index 000000000..a15234116 --- /dev/null +++ b/features/withdrawals/request/form/options/dex-option/trade-guard/__tests__/verify-order.test.ts @@ -0,0 +1,235 @@ +import { + verifyOrderFields, + verifyOrderAmounts, + parseOrderFromSignRequest, +} from '../utils/verify-order'; +import type { + OrderFields, + ValidatedTradeSnapshot, +} from '../utils/verify-order'; + +const STETH = '0xae7ab96520de3a18e5e111b5eaab095312d7fe84'; +const ETH = '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee'; +const USDC = '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48'; +const UNKNOWN = '0x0000000000000000000000000000000000000001'; +const WALLET = '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045'; + +const makeOrder = (overrides: Partial = {}): OrderFields => ({ + sellToken: STETH, + buyToken: ETH, + receiver: WALLET, + sellAmount: '100000000000000000', // 0.1 stETH in wei + buyAmount: '100000000000000000', + ...overrides, +}); + +const makeSnapshot = ( + overrides: Partial = {}, +): ValidatedTradeSnapshot => ({ + sellToken: STETH, + buyToken: ETH, + sellAmountUnits: '0.1', + buyAmountMinUnits: '0.1', + ...overrides, +}); + +// --------------------------------------------------------------------------- +// verifyOrderFields +// --------------------------------------------------------------------------- +describe('verifyOrderFields', () => { + it('returns null for valid order', () => { + expect(verifyOrderFields(makeOrder(), WALLET, false)).toBeNull(); + }); + + it('rejects receiver mismatch', () => { + const order = makeOrder({ receiver: '0xAttacker' }); + expect(verifyOrderFields(order, WALLET, false)).toContain( + 'receiver does not match', + ); + }); + + it('receiver check is case-insensitive', () => { + const order = makeOrder({ receiver: WALLET.toUpperCase() }); + expect(verifyOrderFields(order, WALLET.toLowerCase(), false)).toBeNull(); + }); + + it('rejects invalid sell token on mainnet', () => { + const order = makeOrder({ sellToken: UNKNOWN }); + expect(verifyOrderFields(order, WALLET, false)).toContain( + 'Invalid sell token', + ); + }); + + it('rejects invalid buy token on mainnet', () => { + const order = makeOrder({ buyToken: UNKNOWN }); + expect(verifyOrderFields(order, WALLET, false)).toContain( + 'Invalid buy token', + ); + }); + + it('skips token whitelist on testnet', () => { + const order = makeOrder({ sellToken: UNKNOWN, buyToken: UNKNOWN }); + expect(verifyOrderFields(order, WALLET, true)).toBeNull(); + }); +}); + +// --------------------------------------------------------------------------- +// verifyOrderAmounts +// --------------------------------------------------------------------------- +describe('verifyOrderAmounts', () => { + it('returns null when amounts match exactly', () => { + expect(verifyOrderAmounts(makeOrder(), makeSnapshot())).toBeNull(); + }); + + it('returns null when order sells less than validated', () => { + const order = makeOrder({ sellAmount: '50000000000000000' }); // 0.05 + expect(verifyOrderAmounts(order, makeSnapshot())).toBeNull(); + }); + + it('returns null when order buys more than validated minimum', () => { + const order = makeOrder({ buyAmount: '200000000000000000' }); // 0.2 + expect(verifyOrderAmounts(order, makeSnapshot())).toBeNull(); + }); + + it('rejects when order sells more than validated', () => { + const order = makeOrder({ sellAmount: '200000000000000000' }); // 0.2 + expect(verifyOrderAmounts(order, makeSnapshot())).toContain( + 'sells more than validated', + ); + }); + + it('rejects when order buys less than validated minimum', () => { + const order = makeOrder({ buyAmount: '50000000000000000' }); // 0.05 + expect(verifyOrderAmounts(order, makeSnapshot())).toContain( + 'minimum receive is less', + ); + }); + + it('rejects sell token mismatch', () => { + const order = makeOrder({ sellToken: USDC }); + expect(verifyOrderAmounts(order, makeSnapshot())).toContain( + 'Sell token in order differs', + ); + }); + + it('rejects buy token mismatch', () => { + const order = makeOrder({ buyToken: USDC }); + expect(verifyOrderAmounts(order, makeSnapshot())).toContain( + 'Buy token in order differs', + ); + }); + + it('handles malformed sellAmount gracefully', () => { + const order = makeOrder({ sellAmount: 'not-a-number' }); + expect(verifyOrderAmounts(order, makeSnapshot())).toContain( + 'Invalid order amount format', + ); + }); + + it('handles malformed buyAmount gracefully', () => { + const order = makeOrder({ buyAmount: '1.5' }); + expect(verifyOrderAmounts(order, makeSnapshot())).toContain( + 'Invalid order amount format', + ); + }); + + it('rejects when token decimals unknown (fail-closed)', () => { + const order = makeOrder({ sellToken: UNKNOWN, sellAmount: '999999' }); + const snapshot = makeSnapshot({ sellToken: UNKNOWN }); + expect(verifyOrderAmounts(order, snapshot)).toContain( + 'token decimals unknown', + ); + }); + + describe('USDC (6 decimals)', () => { + const usdcOrder = makeOrder({ + sellToken: STETH, + buyToken: USDC, + sellAmount: '100000000000000000', // 0.1 stETH + buyAmount: '200000000', // 200 USDC (6 decimals) + }); + const usdcSnapshot = makeSnapshot({ + buyToken: USDC, + buyAmountMinUnits: '200', + }); + + it('accepts exact USDC amount', () => { + expect(verifyOrderAmounts(usdcOrder, usdcSnapshot)).toBeNull(); + }); + + it('rejects USDC amount below minimum', () => { + const order = { ...usdcOrder, buyAmount: '199000000' }; // 199 USDC + expect(verifyOrderAmounts(order, usdcSnapshot)).toContain( + 'minimum receive is less', + ); + }); + }); +}); + +// --------------------------------------------------------------------------- +// parseOrderFromSignRequest +// --------------------------------------------------------------------------- +describe('parseOrderFromSignRequest', () => { + const validTypedData = { + primaryType: 'Order', + message: { + sellToken: STETH, + buyToken: ETH, + receiver: WALLET, + sellAmount: '100000000000000000', + buyAmount: '100000000000000000', + }, + }; + + it('parses valid CowSwap order from object', () => { + const result = parseOrderFromSignRequest([WALLET, validTypedData]); + expect(result).toEqual(validTypedData.message); + }); + + it('parses valid CowSwap order from JSON string', () => { + const result = parseOrderFromSignRequest([ + WALLET, + JSON.stringify(validTypedData), + ]); + expect(result).toEqual(validTypedData.message); + }); + + it('returns null for non-Order primaryType', () => { + const data = { ...validTypedData, primaryType: 'Permit' }; + expect(parseOrderFromSignRequest([WALLET, data])).toBeNull(); + }); + + it('returns null for missing message fields', () => { + const data = { + primaryType: 'Order', + message: { sellToken: STETH }, // incomplete + }; + expect(parseOrderFromSignRequest([WALLET, data])).toBeNull(); + }); + + it('returns null for malformed JSON string', () => { + expect(parseOrderFromSignRequest([WALLET, 'not-json'])).toBeNull(); + }); + + it('returns null for null input', () => { + expect(parseOrderFromSignRequest(null)).toBeNull(); + }); + + it('returns null for empty array', () => { + expect(parseOrderFromSignRequest([])).toBeNull(); + }); + + it('returns null when fields are non-string types', () => { + const data = { + primaryType: 'Order', + message: { + sellToken: 123, + buyToken: ETH, + receiver: WALLET, + sellAmount: '100', + buyAmount: '100', + }, + }; + expect(parseOrderFromSignRequest([WALLET, data])).toBeNull(); + }); +}); diff --git a/features/withdrawals/request/form/options/dex-option/trade-guard/consts.ts b/features/withdrawals/request/form/options/dex-option/trade-guard/consts.ts new file mode 100644 index 000000000..6385fbffe --- /dev/null +++ b/features/withdrawals/request/form/options/dex-option/trade-guard/consts.ts @@ -0,0 +1,125 @@ +import type { Address } from 'viem'; + +import mainnetConfig from 'networks/mainnet.json'; + +import { PARTNER_FEE_BPS } from '../consts'; + +import type { ChainlinkFeedConfig } from './types'; + +// Partner fee as a percentage (0.3%) — subtracted from deviation calculations +// because it's a known fixed cost the user has agreed to, not an unexpected loss +export const PARTNER_FEE_PCT = PARTNER_FEE_BPS / 100; + +// --- Thresholds --- + +export type Thresholds = { + oracleDeviationBlock: number; + maxAllowedSellAmount: number; + minSellUnitsToTriggerOracle: number; +}; + +export const DEFAULT_THRESHOLDS: Thresholds = { + oracleDeviationBlock: 4, + maxAllowedSellAmount: 5_000, + // Minimum sell amount (in token units) to trigger Chainlink oracle verification + minSellUnitsToTriggerOracle: 1, +}; + +// --- Resolve mainnet addresses from network config --- + +const c = mainnetConfig.contracts; + +// --- Valid token addresses (mainnet, lowercased) --- + +// CowSwap native ETH placeholder — not a real contract, no entry in network config +const ETH_PLACEHOLDER = '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee'; + +export const VALID_SELL_TOKENS = new Set([ + c.lido.toLowerCase(), // stETH + c.wsteth.toLowerCase(), // wstETH +]); + +export const VALID_BUY_TOKENS = new Set([ + ETH_PLACEHOLDER, + c.weth.toLowerCase(), + c.usdc.toLowerCase(), + c.usdt.toLowerCase(), + c.usds.toLowerCase(), + c.wbtc.toLowerCase(), +]); + +// --- Chainlink Price Feeds (Mainnet) --- + +// All feeds return USD price with 8 decimals unless noted +export const CHAINLINK_FEEDS: Record = { + ETH_USD: { + address: c.aggregatorEthUsdPriceFeed as Address, + maxStaleness: 3900, // 1h + 5min buffer + }, + STETH_USD: { + address: c.aggregatorStEthUsdPriceFeed as Address, + maxStaleness: 3900, + }, + USDC_USD: { + address: c.aggregatorUsdcUsdPriceFeed as Address, + maxStaleness: 87000, // 24h + 10min buffer + }, + USDT_USD: { + address: c.aggregatorUsdtUsdPriceFeed as Address, + maxStaleness: 87000, + }, + DAI_USD: { + address: c.aggregatorDaiUsdPriceFeed as Address, + maxStaleness: 3900, // 1h, used as proxy for USDS + }, + BTC_USD: { + address: c.aggregatorBtcUsdPriceFeed as Address, + maxStaleness: 3900, // 1h, used for WBTC pricing + }, +}; + +// Map buy token address → Chainlink feed key for USD price +export const BUY_TOKEN_FEED_MAP: Record = { + [ETH_PLACEHOLDER]: 'ETH_USD', + [c.weth.toLowerCase()]: 'ETH_USD', + [c.usdc.toLowerCase()]: 'USDC_USD', + [c.usdt.toLowerCase()]: 'USDT_USD', + [c.usds.toLowerCase()]: 'DAI_USD', // USDS → DAI proxy + [c.wbtc.toLowerCase()]: 'BTC_USD', +}; + +// Sell token address → Chainlink feed key for USD price +export const SELL_TOKEN_FEED_MAP: Record = { + [c.lido.toLowerCase()]: 'STETH_USD', + [c.wsteth.toLowerCase()]: 'STETH_USD', // wstETH → needs rate conversion +}; + +// wstETH address for detecting when extra conversion is needed +export const WSTETH_ADDRESS = c.wsteth.toLowerCase() as Address; + +// +// --------------------------------------------------------------------------- +// Oracle constants +// --------------------------------------------------------------------------- + +// Chainlink scale (8 decimals) +export const CHAINLINK_SCALE = 10n ** 8n; +// wstETH scale (18 decimals) +export const WSTETH_SCALE = 10n ** 18n; +// wstETH rate minimum (18 decimals) +export const WSTETH_RATE_MIN = 10n ** 18n; +// wstETH rate maximum (18 decimals) +export const WSTETH_RATE_MAX = 2n * 10n ** 18n; // 2.0 — ~14 years runway at 5% APR + +// Per-feed price sanity bounds (8 decimals) +export const PRICE_BOUNDS: Record = { + ETH_USD: { min: 1000n * CHAINLINK_SCALE, max: 20_000n * CHAINLINK_SCALE }, + STETH_USD: { min: 1000n * CHAINLINK_SCALE, max: 20_000n * CHAINLINK_SCALE }, + USDC_USD: { min: CHAINLINK_SCALE / 2n, max: 2n * CHAINLINK_SCALE }, + USDT_USD: { min: CHAINLINK_SCALE / 2n, max: 2n * CHAINLINK_SCALE }, + DAI_USD: { min: CHAINLINK_SCALE / 2n, max: 2n * CHAINLINK_SCALE }, + BTC_USD: { + min: 30_000n * CHAINLINK_SCALE, + max: 200_000n * CHAINLINK_SCALE, + }, +}; diff --git a/features/withdrawals/request/form/options/dex-option/trade-guard/index.ts b/features/withdrawals/request/form/options/dex-option/trade-guard/index.ts new file mode 100644 index 000000000..9cb1198c9 --- /dev/null +++ b/features/withdrawals/request/form/options/dex-option/trade-guard/index.ts @@ -0,0 +1,2 @@ +export { useTradeGuard } from './use-trade-guard'; +export { TradeGuardModal } from './trade-guard-modal'; diff --git a/features/withdrawals/request/form/options/dex-option/trade-guard/trade-guard-modal.tsx b/features/withdrawals/request/form/options/dex-option/trade-guard/trade-guard-modal.tsx new file mode 100644 index 000000000..22080422b --- /dev/null +++ b/features/withdrawals/request/form/options/dex-option/trade-guard/trade-guard-modal.tsx @@ -0,0 +1,93 @@ +import { Modal, Button } from '@lidofinance/lido-ui'; +import styled from 'styled-components'; + +import type { TradeGuardLevel } from './types'; + +const ModalContent = styled.div` + text-align: center; + padding: 0 8px 8px; +`; + +const Title = styled.h3<{ $level: 'blocked' | 'limit' }>` + font-size: 18px; + font-weight: 800; + margin: 0 0 12px; + color: ${({ $level }) => + $level === 'limit' + ? 'var(--lido-color-textSecondary, #7a8aa0)' + : 'var(--lido-color-error, #e14d4d)'}; +`; + +const MessageList = styled.div` + text-align: left; + margin: 0 0 24px; + padding-left: 20px; + font-size: 14px; + line-height: 1.6; + color: var(--lido-color-textSecondary); +`; + +const OracleBadge = styled.span` + display: block; + font-size: 12px; + color: var(--lido-color-textSecondary); + margin-bottom: 16px; +`; + +export type TradeGuardModalState = { + open: boolean; + level: TradeGuardLevel; + messages: string[]; + oracleVerified: boolean; +}; + +export const MODAL_INITIAL_STATE: TradeGuardModalState = { + open: false, + level: 'safe', + messages: [], + oracleVerified: false, +}; + +type TradeGuardModalProps = { + state: TradeGuardModalState; + onClose: (result: boolean) => void; +}; + +const TITLE_TEXT: Record = { + blocked: 'Swap unavailable', + limit: 'Swap unavailable', +}; + +export const TradeGuardModal = ({ state, onClose }: TradeGuardModalProps) => { + const { open, level, messages, oracleVerified } = state; + const titleLevel = level === 'limit' ? 'limit' : 'blocked'; + + return ( + onClose(false)}> + + + {TITLE_TEXT[level] ?? TITLE_TEXT.blocked} + + + {oracleVerified && ( + Verified by Chainlink oracle + )} + + + {messages.map((msg) => ( +

{msg}

+ ))} +
+ + +
+
+ ); +}; diff --git a/features/withdrawals/request/form/options/dex-option/trade-guard/types.ts b/features/withdrawals/request/form/options/dex-option/trade-guard/types.ts new file mode 100644 index 000000000..6d1a2fad6 --- /dev/null +++ b/features/withdrawals/request/form/options/dex-option/trade-guard/types.ts @@ -0,0 +1,12 @@ +import type { Address } from 'viem'; +// eslint-disable-next-line import/no-extraneous-dependencies +import type { OnTradeParamsPayload } from '@cowprotocol/events'; + +export type { OnTradeParamsPayload }; + +export type TradeGuardLevel = 'safe' | 'blocked' | 'limit'; + +export type ChainlinkFeedConfig = { + address: Address; + maxStaleness: number; // seconds +}; diff --git a/features/withdrawals/request/form/options/dex-option/trade-guard/use-oracle-rates.ts b/features/withdrawals/request/form/options/dex-option/trade-guard/use-oracle-rates.ts new file mode 100644 index 000000000..ea00bd614 --- /dev/null +++ b/features/withdrawals/request/form/options/dex-option/trade-guard/use-oracle-rates.ts @@ -0,0 +1,135 @@ +import { useCallback } from 'react'; + +import { useMainnetOnlyWagmi } from 'modules/web3'; +import { AggregatorAbi } from 'abi/aggregator-abi'; +import { wstethABI } from 'abi/wsteth-abi'; + +import { + CHAINLINK_FEEDS, + SELL_TOKEN_FEED_MAP, + BUY_TOKEN_FEED_MAP, + WSTETH_ADDRESS, + PARTNER_FEE_PCT, + CHAINLINK_SCALE, + WSTETH_SCALE, + WSTETH_RATE_MIN, + WSTETH_RATE_MAX, +} from './consts'; +import { + safeParseDecimal, + isValidRound, + isInBounds, + type RoundData, +} from './utils'; +import type { OnTradeParamsPayload } from './types'; + +export type OracleResult = + | { ok: true; sellTokenUsd: number; buyTokenUsd: number; deviation: number } + | { ok: false; reason: 'unsupported' | 'unavailable' }; + +const FAIL_UNSUPPORTED: OracleResult = { ok: false, reason: 'unsupported' }; +const FAIL_UNAVAILABLE: OracleResult = { ok: false, reason: 'unavailable' }; + +export const useOracleRates = () => { + const { publicClientMainnet } = useMainnetOnlyWagmi(); + + const verifyWithOracle = useCallback( + async (params: OnTradeParamsPayload): Promise => { + if (!publicClientMainnet) return FAIL_UNAVAILABLE; + + const sellAddr = params.sellToken?.address.toLowerCase(); + const buyAddr = params.buyToken?.address.toLowerCase(); + if (!sellAddr || !buyAddr) return FAIL_UNSUPPORTED; + + const sellFeedKey = SELL_TOKEN_FEED_MAP[sellAddr]; + const buyFeedKey = BUY_TOKEN_FEED_MAP[buyAddr]; + if (!sellFeedKey || !buyFeedKey) return FAIL_UNSUPPORTED; + + const sellFeed = CHAINLINK_FEEDS[sellFeedKey]; + const buyFeed = CHAINLINK_FEEDS[buyFeedKey]; + if (!sellFeed || !buyFeed) return FAIL_UNSUPPORTED; + + try { + const isWsteth = sellAddr === WSTETH_ADDRESS; + + // Individual readContract calls — Multicall3 is not in /api/rpc allowlist + const [sellRoundData, buyRoundData, wstethRate] = await Promise.all([ + publicClientMainnet.readContract({ + address: sellFeed.address, + abi: AggregatorAbi, + functionName: 'latestRoundData', + }) as Promise, + publicClientMainnet.readContract({ + address: buyFeed.address, + abi: AggregatorAbi, + functionName: 'latestRoundData', + }) as Promise, + isWsteth + ? publicClientMainnet.readContract({ + address: WSTETH_ADDRESS, + abi: wstethABI, + functionName: 'stEthPerToken', + }) + : Promise.resolve(null), + ] as const); + + // Validate rounds + const nowSec = BigInt(Math.floor(Date.now() / 1000)); + if (!isValidRound(sellRoundData, sellFeed.maxStaleness, nowSec)) + return FAIL_UNAVAILABLE; + if (!isValidRound(buyRoundData, buyFeed.maxStaleness, nowSec)) + return FAIL_UNAVAILABLE; + + const sellAnswer = sellRoundData[1]; + const buyAnswer = buyRoundData[1]; + + // Sanity bounds + if (!isInBounds(sellAnswer, sellFeedKey)) return FAIL_UNAVAILABLE; + if (!isInBounds(buyAnswer, buyFeedKey)) return FAIL_UNAVAILABLE; + + // wstETH → stETH conversion + let sellPriceScaled = sellAnswer; + if (isWsteth) { + if (wstethRate == null) return FAIL_UNAVAILABLE; + if (wstethRate < WSTETH_RATE_MIN || wstethRate > WSTETH_RATE_MAX) + return FAIL_UNAVAILABLE; + sellPriceScaled = (sellPriceScaled * wstethRate) / WSTETH_SCALE; + } + + // Compute deviation + const sellUnits = safeParseDecimal( + params.sellTokenAmount?.units?.toString(), + ); + const buyUnits = safeParseDecimal( + params.buyTokenAmount?.units?.toString(), + ); + + const isInvalidUnits = + sellUnits === null || + sellUnits <= 0 || + buyUnits === null || + buyUnits <= 0; + + if (isInvalidUnits) return FAIL_UNAVAILABLE; + + const sellTokenUsd = Number(sellPriceScaled) / Number(CHAINLINK_SCALE); + const buyTokenUsd = Number(buyAnswer) / Number(CHAINLINK_SCALE); + const expectedSellUsd = sellUnits * sellTokenUsd; + const actualBuyUsd = buyUnits * buyTokenUsd; + if (expectedSellUsd <= 0) return FAIL_UNAVAILABLE; + + // Subtract partner fee — known fixed cost, not unexpected loss + const deviation = + ((expectedSellUsd - actualBuyUsd) / expectedSellUsd) * 100 - + PARTNER_FEE_PCT; + + return { ok: true, sellTokenUsd, buyTokenUsd, deviation }; + } catch { + return FAIL_UNAVAILABLE; + } + }, + [publicClientMainnet], + ); + + return { verifyWithOracle }; +}; diff --git a/features/withdrawals/request/form/options/dex-option/trade-guard/use-trade-guard.ts b/features/withdrawals/request/form/options/dex-option/trade-guard/use-trade-guard.ts new file mode 100644 index 000000000..f99de3187 --- /dev/null +++ b/features/withdrawals/request/form/options/dex-option/trade-guard/use-trade-guard.ts @@ -0,0 +1,288 @@ +import { useCallback, useRef, useState } from 'react'; + +import type { TradeGuardLevel, OnTradeParamsPayload } from './types'; +import { useOracleRates, type OracleResult } from './use-oracle-rates'; +import type { Thresholds } from './consts'; +import { + safeParseDecimal, + resolveLevel, + analyzeParams, + readThresholds, + applyQALevelOverride, + verifyOrderFields, + verifyOrderAmounts, +} from './utils'; +import type { OrderFields, ValidatedTradeSnapshot } from './utils/verify-order'; +import { + MODAL_INITIAL_STATE, + type TradeGuardModalState, +} from './trade-guard-modal'; + +// --------------------------------------------------------------------------- +// Oracle result → level/messages +// --------------------------------------------------------------------------- + +type OracleOutcome = { + level: TradeGuardLevel; + messages: string[]; + verified: boolean; +}; + +const applyOracleResult = ( + result: OracleResult, + meetsThreshold: boolean, + t: Thresholds, +): OracleOutcome => { + if (result.ok) { + const level = resolveLevel(result.deviation, t); + const messages = + result.deviation >= t.oracleDeviationBlock + ? [ + `Oracle price deviation: ${result.deviation.toFixed(1)}% (Chainlink verification)`, + ] + : []; + + return { level, messages, verified: true }; + } + + if (result.reason === 'unavailable') { + if (meetsThreshold) { + return { + level: 'blocked', + messages: ['Oracle verification temporarily unavailable'], + verified: false, + }; + } + + // Swap is below oracle threshold — no concern + return { level: 'safe', messages: [], verified: false }; + } + + if (result.reason === 'unsupported') { + return { + level: 'blocked', + messages: ['Oracle price verification not available for this token pair'], + verified: false, + }; + } + + return { level: 'safe', messages: [], verified: false }; +}; + +// --------------------------------------------------------------------------- +// Hook +// --------------------------------------------------------------------------- + +type UseTradeGuardOptions = { + walletAddress?: string; + isTestnet?: boolean; +}; + +export const useTradeGuard = ({ + walletAddress, + isTestnet = false, +}: UseTradeGuardOptions) => { + const [modalState, setModalState] = + useState(MODAL_INITIAL_STATE); + const sellExceededRef = useRef(false); + const tokenSymbolRef = useRef(''); + const resolveRef = useRef<((value: boolean) => void) | null>(null); + const lastValidatedTradeRef = useRef(null); + const { verifyWithOracle } = useOracleRates(); + + const handleModalClose = useCallback((result: boolean) => { + resolveRef.current?.(result); + resolveRef.current = null; + // Keep level/messages intact — prevents flicker during close animation + setModalState((prev) => ({ ...prev, open: false })); + }, []); + + const showModal = useCallback( + ( + level: TradeGuardLevel, + messages: string[], + oracleVerified: boolean, + ): Promise => + new Promise((resolve) => { + // Resolve any pending modal before opening a new one + resolveRef.current?.(false); + resolveRef.current = resolve; + setModalState({ open: true, level, messages, oracleVerified }); + }), + [], + ); + + // Swap gate: structural checks → oracle check → modal + const validateTrade = useCallback( + async (payload: OnTradeParamsPayload): Promise => { + // Invalidate previous snapshot — prevents stale data if this call fails + lastValidatedTradeRef.current = null; + + if (!walletAddress) { + await showModal( + 'blocked', + ['Wallet address unavailable — cannot verify swap'], + false, + ); + + return false; + } + + const t = readThresholds(); + const { level, messages, isStructural } = analyzeParams( + payload, + walletAddress, + isTestnet, + t, + ); + + let finalLevel = level; + let finalMessages = messages; + let oracleVerified = false; + + // Oracle verification — skip for structural blocks (oracle is irrelevant + // for token whitelist, recipient mismatch, etc.) + const sellUnits = safeParseDecimal( + payload.sellTokenAmount?.units?.toString(), + ); + // QA clamping already applied in readThresholds() + const meetsThreshold = + sellUnits !== null && sellUnits >= t.minSellUnitsToTriggerOracle; + const shouldCheckOracle = !isTestnet && !isStructural && meetsThreshold; + + if (shouldCheckOracle) { + const result = await verifyWithOracle(payload); + const outcome = applyOracleResult(result, meetsThreshold, t); + finalLevel = outcome.level; + finalMessages = outcome.messages; + oracleVerified = outcome.verified; + } + + // QA overrides + finalLevel = applyQALevelOverride(finalLevel); + + // Gate decision — only safe passes, everything else is blocked + if (finalLevel !== 'safe') { + await showModal(finalLevel, finalMessages, oracleVerified); + return false; + } + + // Store validated params for provider-level EIP-712 verification + lastValidatedTradeRef.current = { + sellToken: payload.sellToken?.address ?? '', + buyToken: payload.buyToken?.address ?? '', + sellAmountUnits: payload.sellTokenAmount?.units?.toString() ?? '', + buyAmountMinUnits: + payload.minimumReceiveBuyAmount?.units?.toString() ?? '', + }; + + return true; + }, + [walletAddress, isTestnet, verifyWithOracle, showModal], + ); + + // --------------------------------------------------------------------------- + // ON_CHANGE_TRADE_PARAMS → store latest payload for pre-approval checks + // --------------------------------------------------------------------------- + + const lastTradeParamsRef = useRef(null); + + /** Call from ON_CHANGE_TRADE_PARAMS to track current params. */ + const reportTradeParams = useCallback( + (payload: OnTradeParamsPayload) => { + lastTradeParamsRef.current = payload; + + // Track sell limit + const t = readThresholds(); + const units = safeParseDecimal(payload.sellTokenAmount?.units?.toString()); + sellExceededRef.current = units !== null && units > t.maxAllowedSellAmount; + tokenSymbolRef.current = payload.sellToken?.symbol ?? ''; + }, + [], + ); + + /** Structural pre-check on approval — uses last ON_CHANGE_TRADE_PARAMS data. */ + const validateApproval = useCallback(async (): Promise => { + if (!walletAddress) { + await showModal( + 'blocked', + ['Wallet address unavailable — cannot verify approval'], + false, + ); + return false; + } + + const payload = lastTradeParamsRef.current; + if (!payload) { + await showModal( + 'blocked', + ['Trade parameters unavailable — cannot verify approval'], + false, + ); + return false; + } + + const t = readThresholds(); + const { level, messages } = analyzeParams( + payload, + walletAddress, + isTestnet, + t, + ); + + if (level !== 'safe') { + await showModal(level, messages, false); + return false; + } + + return true; + }, [walletAddress, isTestnet, showModal]); + + /** Stable callback — safe to call from memoized widget hooks. + * Shows a neutral "limit" modal and returns false when exceeded. */ + const checkSellLimit = useCallback(async (): Promise => { + if (!sellExceededRef.current) return true; + + const t = readThresholds(); + const symbol = tokenSymbolRef.current || 'tokens'; + await showModal( + 'limit', + [ + `Sell amount exceeds maximum allowed (${t.maxAllowedSellAmount.toLocaleString()} ${symbol})`, + ], + false, + ); + + return false; + }, [showModal]); + + /** Verify EIP-712 order against the last validated onBeforeTrade payload. */ + const verifySignedOrder = useCallback( + (order: OrderFields): string | null => { + if (!walletAddress) return 'Wallet address unavailable'; + + // Static checks (receiver, token whitelist) + const staticError = verifyOrderFields(order, walletAddress, isTestnet); + if (staticError) return staticError; + + // Amount checks against last validated payload + const snapshot = lastValidatedTradeRef.current; + if (!snapshot) { + return 'No validated trade on record — order signing rejected'; + } + + return verifyOrderAmounts(order, snapshot); + }, + [walletAddress, isTestnet], + ); + + return { + modalState, + handleModalClose, + validateTrade, + validateApproval, + reportTradeParams, + checkSellLimit, + verifySignedOrder, + }; +}; diff --git a/features/withdrawals/request/form/options/dex-option/trade-guard/utils/analyze-params.ts b/features/withdrawals/request/form/options/dex-option/trade-guard/utils/analyze-params.ts new file mode 100644 index 000000000..2535c092e --- /dev/null +++ b/features/withdrawals/request/form/options/dex-option/trade-guard/utils/analyze-params.ts @@ -0,0 +1,84 @@ +import { + DEFAULT_THRESHOLDS, + VALID_SELL_TOKENS, + VALID_BUY_TOKENS, + type Thresholds, +} from '../consts'; +import type { TradeGuardLevel, OnTradeParamsPayload } from '../types'; + +import { safeParseDecimal } from './safe-parse-decimal'; + +type AnalysisResult = { + level: TradeGuardLevel; + messages: string[]; + /** True when the block is structural (token/recipient/wallet/limit) — oracle is irrelevant */ + isStructural: boolean; +}; + +// Structural validation: token whitelist, recipient, sell limit. +// All price verification is delegated to the Chainlink oracle. +export const analyzeParams = ( + params: OnTradeParamsPayload, + walletAddress: string | undefined, + isTestnet: boolean, + t: Thresholds = DEFAULT_THRESHOLDS, +): AnalysisResult => { + const sellAddr = params.sellToken?.address.toLowerCase(); + const buyAddr = params.buyToken?.address.toLowerCase(); + + if (!sellAddr || !buyAddr) { + return { + level: 'blocked', + messages: [ + 'Token information unavailable — swap cannot be fully verified', + ], + isStructural: true, + }; + } + + // Token whitelist (mainnet only) + if (!isTestnet) { + if (!VALID_SELL_TOKENS.has(sellAddr)) { + return { + level: 'blocked', + messages: ['Invalid sell token detected'], + isStructural: true, + }; + } + if (!VALID_BUY_TOKENS.has(buyAddr)) { + return { + level: 'blocked', + messages: ['Invalid buy token detected'], + isStructural: true, + }; + } + } + + // Recipient validation (wallet may be undefined during banner updates) + if ( + walletAddress && + params.recipient && + params.recipient.toLowerCase() !== walletAddress.toLowerCase() + ) { + return { + level: 'blocked', + messages: ['Swap recipient does not match your wallet address'], + isStructural: true, + }; + } + + // Max sell amount + const sellUnits = safeParseDecimal(params.sellTokenAmount?.units?.toString()); + const symbol = params.sellToken?.symbol; + if (sellUnits !== null && sellUnits > t.maxAllowedSellAmount) { + return { + level: 'blocked', + messages: [ + `Single transactions are capped at ${t.maxAllowedSellAmount.toLocaleString()} ${symbol}. This limit exists to protect you from outsized losses due to slippage, MEV, and execution risk. Split your order into smaller trades to continue.`, + ], + isStructural: true, + }; + } + + return { level: 'safe', messages: [], isStructural: false }; +}; diff --git a/features/withdrawals/request/form/options/dex-option/trade-guard/utils/index.ts b/features/withdrawals/request/form/options/dex-option/trade-guard/utils/index.ts new file mode 100644 index 000000000..f69d00970 --- /dev/null +++ b/features/withdrawals/request/form/options/dex-option/trade-guard/utils/index.ts @@ -0,0 +1,6 @@ +export * from './analyze-params'; +export * from './safe-parse-decimal'; +export * from './resolve-level'; +export * from './qa-utils'; +export * from './oracle-utils'; +export * from './verify-order'; diff --git a/features/withdrawals/request/form/options/dex-option/trade-guard/utils/oracle-utils.ts b/features/withdrawals/request/form/options/dex-option/trade-guard/utils/oracle-utils.ts new file mode 100644 index 000000000..4fd9ff8f2 --- /dev/null +++ b/features/withdrawals/request/form/options/dex-option/trade-guard/utils/oracle-utils.ts @@ -0,0 +1,27 @@ +import { PRICE_BOUNDS } from '../consts'; + +// Pure oracle helpers and constants — no React/wagmi imports. +// Kept separate so they're testable without pulling in wagmi ESM. + +export type RoundData = readonly [bigint, bigint, bigint, bigint, bigint]; + +export const isValidRound = ( + roundData: RoundData, + maxStaleness: number, + nowSec: bigint, +): boolean => { + const [roundId, answer, , updatedAt, answeredInRound] = roundData; + + if (answer <= 0n) return false; + if (answeredInRound < roundId) return false; + if (updatedAt > nowSec + 60n) return false; + + return nowSec - updatedAt <= BigInt(maxStaleness); +}; + +export const isInBounds = (answer: bigint, feedKey: string): boolean => { + const bounds = PRICE_BOUNDS[feedKey]; + if (!bounds) return false; + + return answer >= bounds.min && answer <= bounds.max; +}; diff --git a/features/withdrawals/request/form/options/dex-option/trade-guard/utils/qa-utils.ts b/features/withdrawals/request/form/options/dex-option/trade-guard/utils/qa-utils.ts new file mode 100644 index 000000000..d0b0825ee --- /dev/null +++ b/features/withdrawals/request/form/options/dex-option/trade-guard/utils/qa-utils.ts @@ -0,0 +1,44 @@ +import { overrideWithQAMockString, overrideWithQAMockNumber } from 'utils/qa'; + +import { DEFAULT_THRESHOLDS, type Thresholds } from '../consts'; +import { TradeGuardLevel } from '../types'; + +import { LEVEL_ORDER } from './resolve-level'; + +const QA_KEY_LEVEL = 'mock-qa-helpers-trade-guard-level'; + +// --------------------------------------------------------------------------- +// QA override keys (only active when ENABLE_QA_HELPERS=true) +// +// NOTE: All QA overrides are clamped so they can only tighten protection, +// never relax it. +// --------------------------------------------------------------------------- + +const QA_THRESHOLD_KEYS: Record = { + oracleDeviationBlock: 'mock-qa-helpers-trade-guard-oracle-block', + maxAllowedSellAmount: 'mock-qa-helpers-trade-guard-max-sell', + minSellUnitsToTriggerOracle: 'mock-qa-helpers-trade-guard-min-sell', +}; + +export const applyQALevelOverride = ( + level: TradeGuardLevel, +): TradeGuardLevel => { + const v = overrideWithQAMockString(level, QA_KEY_LEVEL); + if (!LEVEL_ORDER.includes(v as TradeGuardLevel)) return level; + // QA can only escalate severity, never downgrade (e.g. safe→blocked OK, blocked→safe NO) + const qaIdx = LEVEL_ORDER.indexOf(v as TradeGuardLevel); + const curIdx = LEVEL_ORDER.indexOf(level); + return qaIdx >= curIdx ? (v as TradeGuardLevel) : level; +}; + +export const readThresholds = (): Thresholds => { + const t = { ...DEFAULT_THRESHOLDS }; + for (const key of Object.keys(QA_THRESHOLD_KEYS) as (keyof Thresholds)[]) { + const qa = overrideWithQAMockNumber(t[key], QA_THRESHOLD_KEYS[key]); + const dflt = DEFAULT_THRESHOLDS[key]; + // QA overrides can only tighten thresholds, never relax them. + // For all thresholds, lower = stricter. + t[key] = Math.min(qa, dflt); + } + return t; +}; diff --git a/features/withdrawals/request/form/options/dex-option/trade-guard/utils/resolve-level.ts b/features/withdrawals/request/form/options/dex-option/trade-guard/utils/resolve-level.ts new file mode 100644 index 000000000..0ae17ca73 --- /dev/null +++ b/features/withdrawals/request/form/options/dex-option/trade-guard/utils/resolve-level.ts @@ -0,0 +1,14 @@ +import { DEFAULT_THRESHOLDS, type Thresholds } from '../consts'; +import type { TradeGuardLevel } from '../types'; + +export const LEVEL_ORDER: TradeGuardLevel[] = ['safe', 'limit', 'blocked']; + +export const resolveLevel = ( + oracleDev: number | null, + t: Thresholds = DEFAULT_THRESHOLDS, +): TradeGuardLevel => { + if (oracleDev !== null && oracleDev >= t.oracleDeviationBlock) { + return 'blocked'; + } + return 'safe'; +}; diff --git a/features/withdrawals/request/form/options/dex-option/trade-guard/utils/safe-parse-decimal.ts b/features/withdrawals/request/form/options/dex-option/trade-guard/utils/safe-parse-decimal.ts new file mode 100644 index 000000000..b379078e9 --- /dev/null +++ b/features/withdrawals/request/form/options/dex-option/trade-guard/utils/safe-parse-decimal.ts @@ -0,0 +1,13 @@ +// Strict decimal parser — rejects scientific notation and partial parses +const DECIMAL_RE = /^\d{1,20}(\.\d{1,18})?$/; + +export const safeParseDecimal = ( + value: string | undefined | null, +): number | null => { + if (!value) return null; + + if (!DECIMAL_RE.test(value)) return null; + + const n = parseFloat(value); + return Number.isFinite(n) && n >= 0 ? n : null; +}; diff --git a/features/withdrawals/request/form/options/dex-option/trade-guard/utils/verify-order.ts b/features/withdrawals/request/form/options/dex-option/trade-guard/utils/verify-order.ts new file mode 100644 index 000000000..88e700c2f --- /dev/null +++ b/features/withdrawals/request/form/options/dex-option/trade-guard/utils/verify-order.ts @@ -0,0 +1,188 @@ +import { parseUnits } from 'viem'; + +import mainnetConfig from 'networks/mainnet.json'; + +import { VALID_SELL_TOKENS, VALID_BUY_TOKENS } from '../consts'; + +// --------------------------------------------------------------------------- +// Types +// --------------------------------------------------------------------------- + +type OrderFields = { + sellToken: string; + buyToken: string; + receiver: string; + sellAmount: string; + buyAmount: string; +}; + +export type { OrderFields }; + +/** Snapshot of the validated onBeforeTrade payload (human-readable units). */ +export type ValidatedTradeSnapshot = { + sellToken: string; + buyToken: string; + sellAmountUnits: string; + buyAmountMinUnits: string; +}; + +// --------------------------------------------------------------------------- +// Token decimals (all whitelisted tokens) +// --------------------------------------------------------------------------- + +const c = mainnetConfig.contracts; +const ETH_PLACEHOLDER = '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee'; + +const TOKEN_DECIMALS: Record = { + [c.lido.toLowerCase()]: 18, // stETH + [c.wsteth.toLowerCase()]: 18, // wstETH + [ETH_PLACEHOLDER]: 18, // ETH placeholder + [c.weth.toLowerCase()]: 18, // WETH + [c.usdc.toLowerCase()]: 6, // USDC + [c.usdt.toLowerCase()]: 6, // USDT + [c.usds.toLowerCase()]: 18, // USDS + [c.wbtc.toLowerCase()]: 8, // WBTC +}; + +// --------------------------------------------------------------------------- +// Static field verification (addresses only) +// --------------------------------------------------------------------------- + +/** + * Defense-in-depth: verifies that an EIP-712 CowSwap order + * matches the expected constraints before signing. + * + * Returns null if OK, or an error message if verification fails. + */ +export const verifyOrderFields = ( + order: OrderFields, + walletAddress: string, + isTestnet: boolean, +): string | null => { + if (order.receiver.toLowerCase() !== walletAddress.toLowerCase()) { + return 'Order receiver does not match wallet address'; + } + + if (!isTestnet) { + if (!VALID_SELL_TOKENS.has(order.sellToken.toLowerCase())) { + return 'Invalid sell token in order'; + } + if (!VALID_BUY_TOKENS.has(order.buyToken.toLowerCase())) { + return 'Invalid buy token in order'; + } + } + + return null; +}; + +// --------------------------------------------------------------------------- +// Amount verification against validated payload +// --------------------------------------------------------------------------- + +/** + * Converts a human-readable decimal string ("0.1") to raw bigint + * using the token's decimals. Returns null if conversion fails. + */ +const unitsToRaw = (units: string, tokenAddress: string): bigint | null => { + const decimals = TOKEN_DECIMALS[tokenAddress.toLowerCase()]; + if (decimals === undefined) return null; + try { + return parseUnits(units, decimals); + } catch { + return null; + } +}; + +/** + * Verifies that the EIP-712 order amounts match the onBeforeTrade payload + * that was validated by the trade guard. + * + * - sellAmount must equal the validated sellAmountUnits (exact match) + * - buyAmount must be >= the validated buyAmountMinUnits (no worse than shown) + * + * Returns null if OK, or an error message if verification fails. + */ +export const verifyOrderAmounts = ( + order: OrderFields, + snapshot: ValidatedTradeSnapshot, +): string | null => { + // Token addresses must match + if (order.sellToken.toLowerCase() !== snapshot.sellToken.toLowerCase()) { + return 'Sell token in order differs from validated trade'; + } + if (order.buyToken.toLowerCase() !== snapshot.buyToken.toLowerCase()) { + return 'Buy token in order differs from validated trade'; + } + + // Convert validated units to raw for comparison + const expectedSell = unitsToRaw(snapshot.sellAmountUnits, order.sellToken); + const expectedBuyMin = unitsToRaw( + snapshot.buyAmountMinUnits, + order.buyToken, + ); + + let orderSell: bigint; + let orderBuy: bigint; + try { + orderSell = BigInt(order.sellAmount); + orderBuy = BigInt(order.buyAmount); + } catch { + return 'Invalid order amount format'; + } + + // Fail-closed: if we can't convert units, we can't verify amounts + if (expectedSell === null) { + return 'Cannot verify sell amount: token decimals unknown'; + } + if (expectedBuyMin === null) { + return 'Cannot verify buy amount: token decimals unknown'; + } + + // Sell amount: order must not sell more than validated + if (orderSell > expectedSell) { + return `Order sells more than validated (${order.sellAmount} > ${expectedSell})`; + } + + // Buy amount (minimum receive): order must not accept less than validated + if (orderBuy < expectedBuyMin) { + return `Order minimum receive is less than validated (${order.buyAmount} < ${expectedBuyMin})`; + } + + return null; +}; + +// --------------------------------------------------------------------------- +// EIP-712 parsing +// --------------------------------------------------------------------------- + +/** + * Attempts to extract CowSwap Order fields from eth_signTypedData_v4 params. + * Returns null if the request is not a CowSwap Order signing request. + */ +export const parseOrderFromSignRequest = ( + params: unknown, +): OrderFields | null => { + try { + const [, typedDataRaw] = params as [string, string | object]; + const typedData = + typeof typedDataRaw === 'string' + ? JSON.parse(typedDataRaw) + : typedDataRaw; + + if (typedData?.primaryType === 'Order' && typedData?.message) { + const msg = typedData.message; + if ( + typeof msg.sellToken === 'string' && + typeof msg.buyToken === 'string' && + typeof msg.receiver === 'string' && + typeof msg.sellAmount === 'string' && + typeof msg.buyAmount === 'string' + ) { + return msg as OrderFields; + } + } + } catch { + // Not valid typed data — ignore + } + return null; +}; diff --git a/features/withdrawals/request/form/options/dex-options/dex-options.tsx b/features/withdrawals/request/form/options/dex-options/dex-options.tsx deleted file mode 100644 index bd019817e..000000000 --- a/features/withdrawals/request/form/options/dex-options/dex-options.tsx +++ /dev/null @@ -1,146 +0,0 @@ -import { useWithdrawalRates } from 'features/withdrawals/request/withdrawal-rates/use-withdrawal-rates'; -import { useTvlError } from 'features/withdrawals/hooks/useTvlError'; -import { FormatToken } from 'shared/formatters/format-token'; -import { trackMatomoEvent } from 'utils/track-matomo-event'; -import { getDexConfig } from 'features/withdrawals/request/withdrawal-rates'; - -import { - DexOptionBlockLink, - DexOptionBlockTitle, - DexOptionStyled, - DexOptionsContainer, - DexOptionAmount, - DexOptionLoader, - DexWarning, - DexOptionsShowMore, - DexOptionsCheckMarkIcon, -} from './styles'; -import { ReactComponent as AttentionTriangle } from 'assets/icons/attention-triangle.svg'; -import { useMemo, useState } from 'react'; -import { InlineLoaderSmall } from '../styles'; - -const MAX_SHOWN_ELEMENTS = 4; - -type DexOptionProps = { - title: string; - icon: React.FC; - url: string; - loading?: boolean; - toReceive: bigint | null; - onClickGoTo: React.MouseEventHandler; -}; - -const DexOption: React.FC = ({ - title, - icon: Icon, - url, - toReceive, - loading, - onClickGoTo, -}) => { - let amountComponent: React.ReactNode = '-'; - if (loading) { - amountComponent = ; - } else if (toReceive) { - amountComponent = ( - - ); - } - - return ( - - - {title} - - Go to {title} - - {amountComponent} - - ); -}; - -export const DexOptions: React.FC< - React.ComponentProps -> = (props) => { - const [showMore, setShowMore] = useState(false); - const [buttonText, setButtonText] = useState('See all options'); - - const { balanceDiffSteth } = useTvlError(); - const isPausedByTvlError = balanceDiffSteth !== undefined; - - const { data, isLoading, amount, selectedToken, enabledDexes } = - useWithdrawalRates({ - isPaused: isPausedByTvlError, - }); - - const isAnyDexEnabled = enabledDexes.length > 0; - const allowExpand = enabledDexes.length > MAX_SHOWN_ELEMENTS; - - const showLoader = !isPausedByTvlError && isAnyDexEnabled && isLoading; - const showList = !isPausedByTvlError && isAnyDexEnabled && !isLoading; - const showPausedList = isPausedByTvlError; - - const dexesListData = useMemo(() => { - if (showList) return data; - if (showPausedList) { - return enabledDexes.map((dexId) => ({ - ...getDexConfig(dexId), - toReceive: null, - rate: null, - })); - } - return null; - }, [data, enabledDexes, showList, showPausedList]); - - return ( - <> - - setButtonText(showMore ? 'Hide' : 'See all options') - } - {...props} - > - {!isAnyDexEnabled && ( - - -
Aggregator's prices are not available now
-
- )} - {showLoader && enabledDexes.map((_, i) => )} - {(showList || showPausedList) && - dexesListData?.map( - ({ title, toReceive, link, rate, matomoEvent, icon }) => { - return ( - trackMatomoEvent(matomoEvent)} - url={link(amount, selectedToken)} - key={title} - toReceive={rate ? toReceive : null} - /> - ); - }, - )} -
- {allowExpand && ( - { - e.preventDefault(); - setShowMore(!showMore); - }} - > - {buttonText} - - - )} - - ); -}; diff --git a/features/withdrawals/request/form/options/dex-options/index.tsx b/features/withdrawals/request/form/options/dex-options/index.tsx deleted file mode 100644 index 8ceb50ba1..000000000 --- a/features/withdrawals/request/form/options/dex-options/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export { DexOptions } from './dex-options'; diff --git a/features/withdrawals/request/form/options/dex-options/styles.ts b/features/withdrawals/request/form/options/dex-options/styles.ts deleted file mode 100644 index 9654808f3..000000000 --- a/features/withdrawals/request/form/options/dex-options/styles.ts +++ /dev/null @@ -1,148 +0,0 @@ -import styled from 'styled-components'; -import { InlineLoader, ThemeName } from '@lidofinance/lido-ui'; -import ExternalLink from 'assets/icons/external-link-icon.svg'; -import { ReactComponent as ChevronBlue } from 'assets/icons/chevron-blue.svg'; - -export const DexOptionsContainer = styled.div<{ - $maxElements: number; -}>` - --itemHeight: 82px; - --itemGap: 8px; - - display: flex; - flex-direction: column; - gap: var(--itemGap); - overflow-y: hidden; - max-height: calc( - (var(--itemGap) + var(--itemHeight)) * ${({ $maxElements }) => $maxElements} - var( - --itemGap - ) - ); - transition: max-height 0.2s ease-in-out; - - ${({ theme }) => theme.mediaQueries.md} { - max-height: unset; - } -`; - -export const DexOptionsShowMore = styled.button` - display: flex; - - ${({ theme }) => theme.mediaQueries.md} { - display: none; - } - - padding: 2px 16px; - margin: 8px auto 0px; - - flex: 0 1; - - align-self: center; - flex-direction: row; - justify-content: center; - align-items: center; - gap: 8px; - - background: none; - outline: none; - border: none; - color: var(--lido-color-primary); - line-height: 20px; - font-size: 12px; - font-weight: 700; - - cursor: pointer; -`; - -export const DexOptionStyled = styled.div<{ $loading?: boolean }>` - width: 100%; - - min-height: var(--itemHeight); - max-height: var(--itemHeight); - background-color: var(--custom-background-secondary); - border-radius: ${({ theme }) => theme.borderRadiusesMap.lg}px; - padding: 16px 20px; - - display: grid; - gap: 5px 16px; - grid-template: 1fr 1fr / 44px max-content; - - & > svg, - & > img { - grid-row: 1 / 3; - grid-column: 1 / 1; - align-self: center; - width: 44px; - filter: ${({ theme }) => - theme.name === ThemeName.light - ? 'drop-shadow(0px 0px 1px rgba(246, 248, 250, 255))' - : 'unset'}; - } -`; - -export const DexOptionLoader = styled(InlineLoader)` - display: block; - width: 100%; - min-height: 82px; - border-radius: ${({ theme }) => theme.borderRadiusesMap.lg}px; -`; - -export const DexOptionBlockTitle = styled.span` - grid-row: 1; - grid-column: 2; - color: var(--lido-color-text); - font-weight: 400; - font-size: 14px; -`; - -export const DexOptionBlockLink = styled.a` - grid-row: 2; - grid-column: 2; - &::after { - content: ' '; - display: inline-block; - background: url(${ExternalLink}) center / contain no-repeat; - width: 12px; - height: 12px; - margin-left: 8px; - margin-bottom: -1px; - } -`; - -export const DexOptionAmount = styled.span` - grid-row: 1 / 3; - grid-column: 3; - width: 100%; - justify-self: end; - align-self: center; - text-align: end; - - color: var(--lido-color-text); - font-weight: 700; - font-size: 14px; -`; - -export const DexWarning = styled.div` - display: flex; - align-items: center; - justify-content: center; - padding: ${({ theme }) => theme.spaceMap.md}px; - font-weight: 400; - font-size: ${({ theme }) => theme.fontSizesMap.xs}px; - background-color: var(--custom-background-secondary); - border-radius: ${({ theme }) => theme.borderRadiusesMap.lg}px; - - svg { - display: block; - margin-right: ${({ theme }) => theme.spaceMap.xs}px; - } -`; - -export const DexOptionsCheckMarkIcon = styled(ChevronBlue)<{ - $active?: boolean; -}>` - transition: transform 0.3s ease-in-out; - transform: rotateY(180deg); - transform: ${({ $active }) => - $active ? 'rotateZ(180deg)' : 'rotateZ(0deg)'}; -`; diff --git a/features/withdrawals/request/form/options/lido-option.tsx b/features/withdrawals/request/form/options/lido-option.tsx index 05a99d373..de06f433c 100644 --- a/features/withdrawals/request/form/options/lido-option.tsx +++ b/features/withdrawals/request/form/options/lido-option.tsx @@ -11,7 +11,6 @@ import { trackMatomoEvent } from 'utils/track-matomo-event'; import { FormatTokenStyled, - LidoIcon, LidoOptionContainer, LidoOptionValue, LidoOptionInlineLoader, @@ -64,8 +63,7 @@ export const LidoOption = () => { return ( - - Lido + You will receive {amountLoading && } {!amountLoading && ( diff --git a/features/withdrawals/request/form/options/options-picker.tsx b/features/withdrawals/request/form/options/options-picker.tsx index 141455209..5c1dbe90c 100644 --- a/features/withdrawals/request/form/options/options-picker.tsx +++ b/features/withdrawals/request/form/options/options-picker.tsx @@ -1,22 +1,12 @@ import { useWatch } from 'react-hook-form'; -import { parseEther } from 'viem'; import { Tooltip } from '@lidofinance/lido-ui'; import { MATOMO_CLICK_EVENTS_TYPES } from 'consts/matomo'; -import { DATA_UNAVAILABLE } from 'consts/text'; -import { TOKENS_TO_WITHDRAWLS } from 'features/withdrawals/types/tokens-withdrawable'; import { useWaitingTime } from 'features/withdrawals/hooks/useWaitingTime'; -import { useTvlError } from 'features/withdrawals/hooks/useTvlError'; -import { RequestFormInputType } from 'features/withdrawals/request/request-form-context'; -import { - getDexConfig, - useWithdrawalRates, -} from 'features/withdrawals/request/withdrawal-rates'; -import { useStETHByWstETH } from 'modules/web3'; +import { RequestFormInputType } from 'features/withdrawals/request/request-form-context'; -import { formatBalance } from 'utils/formatBalance'; import { trackMatomoEvent } from 'utils/track-matomo-event'; import { @@ -29,6 +19,7 @@ import { OptionsPickerRow, OptionsPickerSubLabel, InlineQuestion, + CowSwapIcon, } from './styles'; type OptionButtonProps = { @@ -36,27 +27,15 @@ type OptionButtonProps = { isActive?: boolean; }; -const DEFAULT_VALUE_FOR_RATE = parseEther('1'); - const LidoButton: React.FC = ({ isActive, onClick }) => { - const [amount, token] = useWatch({ - name: ['amount', 'token'], + const [amount] = useWatch({ + name: ['amount'], }); - const isSteth = token === TOKENS_TO_WITHDRAWLS.stETH; const { isCongested } = useWaitingTime(null); const { value: waitingTime, isLoading: isWaitingTimeLoading } = useWaitingTime(amount, { isApproximate: true, }); - const { data: wstethAsSteth, isLoading: isWstethAsStethLoading } = - useStETHByWstETH(DEFAULT_VALUE_FOR_RATE); - - const ratioLoading = !isSteth && isWstethAsStethLoading; - const ratio = isSteth - ? '1 : 1' - : wstethAsSteth - ? `1 : ${formatBalance(wstethAsSteth).trimmed}` - : DATA_UNAVAILABLE; return ( = ({ isActive, onClick }) => { - - Rate: - {ratioLoading ? : ratio} - Waiting time:  @@ -90,26 +65,7 @@ const LidoButton: React.FC = ({ isActive, onClick }) => { ); }; -const toFloor = (num: number): string => - (Math.floor(num * 10000) / 10000).toString(); - const DexButton: React.FC = ({ isActive, onClick }) => { - const { balanceDiffSteth } = useTvlError(); - const isPausedByTvlError = balanceDiffSteth !== undefined; - const { isLoading, bestRate, enabledDexes } = useWithdrawalRates({ - isPaused: isPausedByTvlError, - fallbackValue: DEFAULT_VALUE_FOR_RATE, - }); - const isAnyDexEnabled = enabledDexes.length > 0; - const bestRateFloored = bestRate !== null && toFloor(bestRate); - const bestRateValue = - !isPausedByTvlError && - isAnyDexEnabled && - bestRateFloored && - bestRateFloored !== '0' - ? `1 : ${bestRateFloored}` - : '—'; - return ( = ({ isActive, onClick }) => { onClick={onClick} > - Use DEXs + Use DEX - {enabledDexes.map((dexKey) => { - const Icon = getDexConfig(dexKey).icon; - return ; - })} + - - Best Rate: - {isLoading && !isPausedByTvlError ? ( - - ) : ( - bestRateValue - )} - Waiting time:{' '} - {isAnyDexEnabled ? <>~ 1-5 minutes : '—'} + <>~ 30 seconds ); @@ -155,7 +100,6 @@ export const OptionsPicker: React.FC = ({ { e.preventDefault(); trackMatomoEvent(MATOMO_CLICK_EVENTS_TYPES.withdrawalUseLido); diff --git a/features/withdrawals/request/form/options/styles.tsx b/features/withdrawals/request/form/options/styles.tsx index f1b77de48..d02421739 100644 --- a/features/withdrawals/request/form/options/styles.tsx +++ b/features/withdrawals/request/form/options/styles.tsx @@ -3,6 +3,7 @@ import { InlineLoader, Question } from '@lidofinance/lido-ui'; import { FormatToken } from 'shared/formatters'; import Lido from 'assets/icons/lido.svg'; +import CowSwap from 'assets/partner/cow-swap.svg'; // ICONS @@ -13,6 +14,13 @@ export const LidoIcon = styled.img.attrs({ display: block; `; +export const CowSwapIcon = styled.img.attrs({ + src: CowSwap, + alt: '', +})` + display: block; +`; + export const OptionAmountRow = styled.div` display: flex; align-items: center; @@ -32,7 +40,6 @@ export const InlineLoaderSmall = styled(InlineLoader)` export const LidoOptionContainer = styled.div` width: 100%; - min-height: 82px; background-color: var(--custom-background-secondary); border-radius: ${({ theme }) => theme.borderRadiusesMap.lg}px; diff --git a/features/withdrawals/request/form/request-form.tsx b/features/withdrawals/request/form/request-form.tsx index a3a445490..d3e202c53 100644 --- a/features/withdrawals/request/form/request-form.tsx +++ b/features/withdrawals/request/form/request-form.tsx @@ -13,39 +13,47 @@ import { TokenAmountInputRequest } from './controls/token-amount-input-request'; import { InputGroupRequest } from './controls/input-group-request'; import { RequestsInfo } from './requests-info'; import { ModePickerRequest } from './controls/mode-picker-request'; -import { DexOptions } from './options/dex-options'; +import { DexOptionWithErrorBoundary } from './options/dex-option'; import { LidoOption } from './options/lido-option'; import { SubmitButtonRequest, useRequestSubmitButtonProps, } from './controls/submit-button-request'; import { TransactionInfo } from './transaction-info'; +import { Hidden } from 'shared/components/hidden'; +import { useConfig } from 'config'; export const RequestForm = () => { const { isBunker, isPaused } = useWithdrawals(); + // conditional render breaks useFormState, so it can't be inside SubmitButton const submitButtonProps = useRequestSubmitButtonProps(); - const mode = useWatch({ name: 'mode' }); + const modeForm = useWatch({ name: 'mode' }); + + const isDexEnabled = useConfig().externalConfig.withdrawalDex.enabled; + + const mode = isDexEnabled ? modeForm : 'lido'; return ( {isPaused && } {isBunker && } - - - - - {mode === 'lido' && } - - {mode === 'lido' && ( - <> - - - - - )} - {mode === 'dex' && } + {isDexEnabled && } + + {/* Lido options is hidden visually to prevent mounting/unmounting of form controllers */} + + + + + + + + + + + + {isDexEnabled && mode === 'dex' && } ); diff --git a/features/withdrawals/request/withdrawal-rates/icons.tsx b/features/withdrawals/request/withdrawal-rates/icons.tsx deleted file mode 100644 index 3bcc227c2..000000000 --- a/features/withdrawals/request/withdrawal-rates/icons.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import styled from 'styled-components'; -import OpenOcean from 'assets/icons/open-ocean.svg'; -import Velora from 'assets/icons/velora.svg'; -import Oneinch from 'assets/icons/oneinch-circle.svg'; -import Bebop from 'assets/icons/bebop.svg'; -import Jumper from 'assets/icons/jumper.svg'; - -export const OpenOceanIcon = styled.img.attrs({ - src: OpenOcean, - alt: 'openOcean', -})` - display: block; -`; - -export const VeloraIcon = styled.img.attrs({ - src: Velora, - alt: 'velora', -})` - display: block; -`; - -export const OneInchIcon = styled.img.attrs({ - src: Oneinch, - alt: '1inch', -})` - display: block; -`; - -export const BebopIcon = styled.img.attrs({ - src: Bebop, - alt: 'Bebop', -})` - display: block; -`; - -export const JumperIcon = styled.img.attrs({ - src: Jumper, - alt: 'Jumper', -})` - display: block; -`; diff --git a/features/withdrawals/request/withdrawal-rates/index.ts b/features/withdrawals/request/withdrawal-rates/index.ts deleted file mode 100644 index 16a99745b..000000000 --- a/features/withdrawals/request/withdrawal-rates/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export type { useWithdrawalRatesOptions } from './use-withdrawal-rates'; -export type { GetWithdrawalRateResult, DexWithdrawalApi } from './types'; - -export { useWithdrawalRates } from './use-withdrawal-rates'; -export { getDexConfig } from './integrations'; diff --git a/features/withdrawals/request/withdrawal-rates/integrations.ts b/features/withdrawals/request/withdrawal-rates/integrations.ts deleted file mode 100644 index 21d75a18d..000000000 --- a/features/withdrawals/request/withdrawal-rates/integrations.ts +++ /dev/null @@ -1,211 +0,0 @@ -import { formatEther, getAddress } from 'viem'; -import { CHAINS } from '@lidofinance/lido-ethereum-sdk/common'; - -import { OPEN_OCEAN_REFERRAL_ADDRESS } from 'consts/external-links'; -import { MATOMO_CLICK_EVENTS_TYPES } from 'consts/matomo'; -import { getTokenAddress } from 'config/networks/token-address'; - -import { getOneInchRate } from 'utils/get-one-inch-rate'; -import { getBebopRate } from 'utils/get-bebop-rate'; -import { getOpenOceanRate } from 'utils/get-open-ocean-rate'; -import { getJumperRate } from 'utils/get-jumper-rate'; -import { standardFetcher } from 'utils/standardFetcher'; -import { calculateRateReceive } from 'utils/calculate-rate-to-receive'; - -import { - BebopIcon, - OneInchIcon, - OpenOceanIcon, - VeloraIcon, - JumperIcon, -} from './icons'; - -import type { - DexWithdrawalApi, - DexWithdrawalIntegrationMap, - GetRateType, -} from './types'; -import { TOKENS_TO_WITHDRAWLS } from '../../types/tokens-withdrawable'; - -const getOpenOceanWithdrawalRate: GetRateType = async ({ amount, token }) => { - if (amount && amount > 0n) { - try { - const result = await getOpenOceanRate(amount, token, 'ETH'); - return result; - } catch (e) { - console.warn( - '[getOpenOceanWithdrawalRate] Failed to receive withdraw rate', - e, - ); - } - } - - return { - rate: null, - toReceive: null, - }; -}; - -type ParaSwapPriceResponsePartial = { - priceRoute: { - srcAmount: string; - destAmount: string; - }; -}; - -const getParaSwapWithdrawalRate: GetRateType = async ({ amount, token }) => { - try { - if (amount > 0n) { - const api = `https://apiv5.paraswap.io/prices`; - const query = new URLSearchParams({ - srcToken: getTokenAddress(CHAINS.Mainnet, token) as string, - srcDecimals: '18', - destToken: '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE', - destDecimals: '18', - side: 'SELL', - excludeDirectContractMethods: 'true', - userAddress: '0x0000000000000000000000000000000000000000', - amount: amount.toString(), - network: '1', - partner: 'lido', - }); - - const url = `${api}?${query.toString()}`; - const data: ParaSwapPriceResponsePartial = - await standardFetcher(url); - const toReceive = BigInt(data.priceRoute.destAmount); - - const rate = calculateRateReceive( - amount, - BigInt(data.priceRoute.srcAmount), - toReceive, - ).rate; - - return { - rate, - toReceive: BigInt(data.priceRoute.destAmount), - }; - } - } catch (e) { - console.warn( - '[getParaSwapWithdrawalRate] Failed to receive withdraw rate', - e, - ); - } - - return { - rate: null, - toReceive: null, - }; -}; - -const getOneInchWithdrawalRate: GetRateType = async (params) => { - const fallback = { rate: null, toReceive: null }; - - try { - if (params.amount > 0n) { - return (await getOneInchRate(params)) ?? fallback; - } - } catch (e) { - console.warn( - '[getOneInchWithdrawalRate] Failed to receive withdraw rate', - e, - ); - } - - return fallback; -}; - -const getBebopWithdrawalRate: GetRateType = async ({ amount, token }) => { - try { - if (amount > 0n) { - return await getBebopRate(amount, token, 'ETH'); - } - } catch (e) { - console.warn( - '[getOneInchWithdrawalRate] Failed to receive withdraw rate', - e, - ); - } - return { - rate: null, - toReceive: null, - }; -}; - -const getJumperWithdrawalRate: GetRateType = async (params) => { - try { - if (params.amount > 0n) { - return await getJumperRate(params); - } - } catch (e) { - console.warn( - '[getJumperWithdrawalRate] Failed to receive withdraw rate', - e, - ); - } - return { - rate: null, - toReceive: null, - }; -}; - -const dexWithdrawalMap: DexWithdrawalIntegrationMap = { - 'open-ocean': { - title: 'OpenOcean', - fetcher: getOpenOceanWithdrawalRate, - icon: OpenOceanIcon, - matomoEvent: MATOMO_CLICK_EVENTS_TYPES.withdrawalGoToOpenOcean, - link: (amount, token) => - `https://app.openocean.finance/classic?referrer=${OPEN_OCEAN_REFERRAL_ADDRESS}&amount=${formatEther( - amount, - )}#/ETH/${token}/ETH`, - }, - paraswap: { - title: 'Velora', - icon: VeloraIcon, - fetcher: getParaSwapWithdrawalRate, - matomoEvent: MATOMO_CLICK_EVENTS_TYPES.withdrawalGoToParaswap, - link: (amount, token) => - `https://app.velora.xyz/?referrer=Lido&takeSurplus=true#/swap/${ - getTokenAddress(CHAINS.Mainnet, token) as string - }-0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE/${formatEther( - amount, - )}/SELL?version=6.2&network=ethereum`, - }, - 'one-inch': { - title: '1inch', - fetcher: getOneInchWithdrawalRate, - icon: OneInchIcon, - matomoEvent: MATOMO_CLICK_EVENTS_TYPES.withdrawalGoTo1inch, - link: (amount, token) => - `https://1inch.com/swap?src=1:${token === TOKENS_TO_WITHDRAWLS.stETH ? 'stETH' : 'wstETH'}&dst=1:ETH&srcAmount=${formatEther( - amount, - )}`, - }, - bebop: { - title: 'Bebop', - icon: BebopIcon, - fetcher: getBebopWithdrawalRate, - matomoEvent: MATOMO_CLICK_EVENTS_TYPES.withdrawalGoToBebop, - link: (amount, token) => - `https://bebop.xyz/trade?network=ethereum&buy=0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE&sell=${getAddress( - getTokenAddress(CHAINS.Mainnet, token) as string, - )}&sellAmounts=${formatEther(amount)}&source=lido`, - }, - jumper: { - title: 'Jumper', - icon: JumperIcon, - fetcher: getJumperWithdrawalRate, - matomoEvent: MATOMO_CLICK_EVENTS_TYPES.withdrawalGoToJumper, - link: (amount, token) => - `https://jumper.exchange/?fromAmount=${formatEther( - amount, - )}&fromChain=1&fromToken=${ - getTokenAddress(CHAINS.Mainnet, token) as string - }&toChain=1&toToken=0x0000000000000000000000000000000000000000`, - }, -} as const; - -export const getDexConfig = (dexKey: DexWithdrawalApi) => - dexWithdrawalMap[dexKey]; diff --git a/features/withdrawals/request/withdrawal-rates/types.ts b/features/withdrawals/request/withdrawal-rates/types.ts deleted file mode 100644 index 50454c4e6..000000000 --- a/features/withdrawals/request/withdrawal-rates/types.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { MATOMO_CLICK_EVENTS_TYPES } from 'consts/matomo'; -import { TOKENS_TO_WITHDRAWLS } from 'features/withdrawals/types/tokens-withdrawable'; - -export type GetWithdrawalRateParams = { - amount: bigint; - token: TOKENS_TO_WITHDRAWLS; - dexes: DexWithdrawalApi[]; -}; - -export type SingleWithdrawalRateResult = { - rate: number | null; - toReceive: bigint | null; -}; - -export type DexWithdrawalApi = - | 'paraswap' - | 'open-ocean' - | 'one-inch' - | 'bebop' - | 'jumper'; - -export type DexWithdrawalIntegration = { - title: string; - fetcher: GetRateType; - icon: React.FC; - matomoEvent: MATOMO_CLICK_EVENTS_TYPES; - link: (amount: bigint, token: TOKENS_TO_WITHDRAWLS) => string; -}; - -export type DexWithdrawalIntegrationMap = Record< - DexWithdrawalApi, - DexWithdrawalIntegration ->; - -export type GetRateType = ( - params: GetWithdrawalRateParams, -) => Promise; - -export type GetWithdrawalRateResult = (SingleWithdrawalRateResult & - DexWithdrawalIntegration)[]; diff --git a/features/withdrawals/request/withdrawal-rates/use-withdrawal-rates.ts b/features/withdrawals/request/withdrawal-rates/use-withdrawal-rates.ts deleted file mode 100644 index 41a651b83..000000000 --- a/features/withdrawals/request/withdrawal-rates/use-withdrawal-rates.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { useMemo } from 'react'; -import { useWatch } from 'react-hook-form'; -import { useQuery } from '@tanstack/react-query'; - -import { useConfig } from 'config'; -import { STRATEGY_LAZY } from 'consts/react-query-strategies'; -import { useDebouncedValue } from 'shared/hooks/useDebouncedValue'; - -import type { RequestFormInputType } from '../request-form-context'; - -import { getDexConfig } from './integrations'; -import type { GetWithdrawalRateParams, GetWithdrawalRateResult } from './types'; - -export type useWithdrawalRatesOptions = { - fallbackValue?: bigint; - isPaused?: boolean; -}; - -const getWithdrawalRates = async ( - params: GetWithdrawalRateParams, -): Promise => { - const rates = await Promise.all( - params.dexes.map((dexKey) => { - const dex = getDexConfig(dexKey); - return dex.fetcher(params).then((result) => ({ - ...dex, - ...result, - })); - }), - ); - - if (rates.length > 1) { - // sort by rate, then alphabetic - rates.sort((r1, r2) => { - const rate1 = r1.rate ?? 0; - const rate2 = r2.rate ?? 0; - if (rate1 == rate2) { - return r1.title.toLowerCase() > r2.title.toLowerCase() ? 1 : -1; - } - return rate2 - rate1; - }); - } - - return rates; -}; - -export const useWithdrawalRates = ({ - fallbackValue = 0n, - isPaused, -}: useWithdrawalRatesOptions = {}) => { - const [token, amount] = useWatch({ - name: ['token', 'amount'], - }); - const enabledDexes = useConfig().externalConfig.enabledWithdrawalDexes; - const fallbackedAmount = amount ?? fallbackValue; - const debouncedAmount = useDebouncedValue(fallbackedAmount, 1000); - - const queryResult = useQuery({ - queryKey: [ - 'withdrawal-rates', - debouncedAmount.toString(), - token, - enabledDexes, - ], - ...STRATEGY_LAZY, - enabled: - !isPaused && - debouncedAmount != null && - typeof debouncedAmount === 'bigint' && - enabledDexes.length > 0, - queryFn: () => - getWithdrawalRates({ - amount: debouncedAmount, - token, - dexes: enabledDexes, - }), - }); - - const bestRate = useMemo(() => { - return queryResult.data?.[0]?.rate ?? null; - }, [queryResult.data]); - - return { - amount: fallbackedAmount, - bestRate, - enabledDexes, - selectedToken: token, - data: queryResult.data, - get isLoading() { - return queryResult.isLoading || debouncedAmount !== fallbackedAmount; - }, - get isFetching() { - return queryResult.isFetching || debouncedAmount !== fallbackedAmount; - }, - get error() { - return queryResult.error; - }, - update: queryResult.refetch, - }; -}; diff --git a/features/withdrawals/withdrawals-faq/list/convert-steth-to-eth.tsx b/features/withdrawals/withdrawals-faq/list/convert-steth-to-eth.tsx index ed1ac6115..055a53466 100644 --- a/features/withdrawals/withdrawals-faq/list/convert-steth-to-eth.tsx +++ b/features/withdrawals/withdrawals-faq/list/convert-steth-to-eth.tsx @@ -7,11 +7,15 @@ import { LocalLink } from 'shared/components/local-link'; export const ConvertSTETHtoETH: FC = () => { return ( -

- Yes. Stakers can transform their stETH to ETH 1:1 using the{' '} - Request and{' '} - Claim tabs. -

+

Yes, stakers can:

+
    +
  1. + transform their stETH to ETH 1:1 using the{' '} + Request and{' '} + Claim tabs. +
  2. +
  3. swap stETH to ETH instantly via CowSwap
  4. +
); }; diff --git a/features/withdrawals/withdrawals-faq/list/convert-wsteth-to-eth.tsx b/features/withdrawals/withdrawals-faq/list/convert-wsteth-to-eth.tsx index e84a927d3..aa2714d46 100644 --- a/features/withdrawals/withdrawals-faq/list/convert-wsteth-to-eth.tsx +++ b/features/withdrawals/withdrawals-faq/list/convert-wsteth-to-eth.tsx @@ -7,13 +7,17 @@ import { LocalLink } from 'shared/components/local-link'; export const ConvertWSTETHtoETH: FC = () => { return ( -

- Yes. You can transform your wstETH to ETH using the{' '} - Request and{' '} - Claim tabs. Note - that, under the hood, wstETH will unwrap to stETH first, so your request - will be denominated in stETH. -

+

Yes, you can:

+
    +
  1. + transform your wstETH to ETH using the{' '} + Request and{' '} + Claim tabs. In + that case note that, under the hood, wstETH will unwrap to stETH + first, so your request or swap will be denominated in stETH +
  2. +
  3. swap wstETH via CowSwap
  4. +
); }; diff --git a/features/withdrawals/withdrawals-faq/list/how-do-i-swap.tsx b/features/withdrawals/withdrawals-faq/list/how-do-i-swap.tsx new file mode 100644 index 000000000..3ef3fc96c --- /dev/null +++ b/features/withdrawals/withdrawals-faq/list/how-do-i-swap.tsx @@ -0,0 +1,16 @@ +import { AccordionNavigatable } from 'shared/components/accordion-navigatable'; + +export const HowDoISwap: React.FC = () => { + return ( + +

+ In the Lido UI, select the DEX option powered by CowSwap, choose the + token you want to receive, and confirm the transaction in your wallet. + The swap will be executed without a withdrawal waiting period. +

+
+ ); +}; diff --git a/features/withdrawals/withdrawals-faq/list/how-does-withdrawals-work.tsx b/features/withdrawals/withdrawals-faq/list/how-does-withdrawals-work.tsx index 52510c0e9..2ba64c40e 100644 --- a/features/withdrawals/withdrawals-faq/list/how-does-withdrawals-work.tsx +++ b/features/withdrawals/withdrawals-faq/list/how-does-withdrawals-work.tsx @@ -4,7 +4,7 @@ import { NoBr } from '../styles'; export const HowDoesWithdrawalsWork: FC = () => { return ( - +

The withdrawal process is simple and has two steps:

  1. diff --git a/features/withdrawals/withdrawals-faq/list/how-long-to-withdraw.tsx b/features/withdrawals/withdrawals-faq/list/how-long-to-withdraw.tsx index c4fc2e4d2..626e63b42 100644 --- a/features/withdrawals/withdrawals-faq/list/how-long-to-withdraw.tsx +++ b/features/withdrawals/withdrawals-faq/list/how-long-to-withdraw.tsx @@ -7,7 +7,7 @@ import { WITHDRAWALS_REQUEST_PATH } from 'consts/urls'; export const HowLongToWithdraw: FC = () => { return ( - +

    On{' '} Request tab{' '} diff --git a/features/withdrawals/withdrawals-faq/list/rewards-after-withdraw.tsx b/features/withdrawals/withdrawals-faq/list/rewards-after-withdraw.tsx index 7ddb55b83..5bdf5b4d0 100644 --- a/features/withdrawals/withdrawals-faq/list/rewards-after-withdraw.tsx +++ b/features/withdrawals/withdrawals-faq/list/rewards-after-withdraw.tsx @@ -2,10 +2,10 @@ import { Accordion } from '@lidofinance/lido-ui'; export const RewardsAfterWithdraw: React.FC = () => { return ( - +

    - No. After you request a withdrawal, the stETH/wstETH submitted for - unstaking will not receive staking rewards on top of your submitted + No. After you request a withdrawal or execute a swap, the stETH/wstETH + submitted will not receive staking rewards on top of your submitted balance.

    diff --git a/features/withdrawals/withdrawals-faq/list/unstake-amount-boundaries.tsx b/features/withdrawals/withdrawals-faq/list/unstake-amount-boundaries.tsx index 7b0ec5251..1053b5db6 100644 --- a/features/withdrawals/withdrawals-faq/list/unstake-amount-boundaries.tsx +++ b/features/withdrawals/withdrawals-faq/list/unstake-amount-boundaries.tsx @@ -17,7 +17,7 @@ export const UnstakeAmountBoundaries: React.FC = () => { ); return ( - +

    Request size should be at least {minAmountDisplay} wei (in stETH), and at most {maxAmountDisplay} stETH. diff --git a/features/withdrawals/withdrawals-faq/list/what-are-my-options.tsx b/features/withdrawals/withdrawals-faq/list/what-are-my-options.tsx new file mode 100644 index 000000000..deb976add --- /dev/null +++ b/features/withdrawals/withdrawals-faq/list/what-are-my-options.tsx @@ -0,0 +1,22 @@ +import { AccordionNavigatable } from 'shared/components/accordion-navigatable'; + +export const WhatAreMyOptions: React.FC = () => { + return ( + +

    Users have two ways to exit their staked position:

    +
      +
    1. + Withdraw via Lido Withdrawals - unstake and receive ETH at a + 1:1 ratio after the withdrawal waiting period +
    2. +
    3. + Swap via CowSwap - instantly exchange stETH or wstETH into + other tokens directly using CowSwap through the Lido UI +
    4. +
    + + ); +}; diff --git a/features/withdrawals/withdrawals-faq/list/what-is-the-difference.tsx b/features/withdrawals/withdrawals-faq/list/what-is-the-difference.tsx new file mode 100644 index 000000000..fac4bd4d2 --- /dev/null +++ b/features/withdrawals/withdrawals-faq/list/what-is-the-difference.tsx @@ -0,0 +1,30 @@ +import { AccordionNavigatable } from 'shared/components/accordion-navigatable'; + +export const WhatIsTheDifference: React.FC = () => { + return ( + +

    + Withdrawal via Lido: +

    +
      +
    1. ETH at a fixed 1:1 rate
    2. +
    3. Requires waiting time (typically 1–5 days)
    4. +
    5. No price impact
    6. +
    7. Subject to queue and protocol conditions
    8. +
    +

    + Swap via CowSwap: +

    +
      +
    1. Instant execution
    2. +
    3. No waiting period
    4. +
    5. Market-based rate (may differ from 1:1)
    6. +
    7. Access to multiple assets
    8. +
    9. Powered by CowSwap
    10. +
    +
    + ); +}; diff --git a/features/withdrawals/withdrawals-faq/list/which-assets.tsx b/features/withdrawals/withdrawals-faq/list/which-assets.tsx new file mode 100644 index 000000000..1e89119ea --- /dev/null +++ b/features/withdrawals/withdrawals-faq/list/which-assets.tsx @@ -0,0 +1,15 @@ +import { AccordionNavigatable } from 'shared/components/accordion-navigatable'; + +export const WhichAssets: React.FC = () => { + return ( + +

    + When using CowSwap, you can swap your stETH or wstETH into: ETH, WETH, + USDC, USDT, USDS and WBTC. +

    +
    + ); +}; diff --git a/features/withdrawals/withdrawals-faq/list/why-steth.tsx b/features/withdrawals/withdrawals-faq/list/why-steth.tsx index f8459fde8..47e6c23b4 100644 --- a/features/withdrawals/withdrawals-faq/list/why-steth.tsx +++ b/features/withdrawals/withdrawals-faq/list/why-steth.tsx @@ -2,7 +2,7 @@ import { Accordion } from '@lidofinance/lido-ui'; export const WhySTETH: React.FC = () => { return ( - +

    When you request to withdraw wstETH, it is automatically unwrapped into stETH, which then gets transformed into ETH. The main withdrawal period diff --git a/features/withdrawals/withdrawals-faq/list/why-waiting-time-changed.tsx b/features/withdrawals/withdrawals-faq/list/why-waiting-time-changed.tsx index 66d121fdf..a1cda5bdc 100644 --- a/features/withdrawals/withdrawals-faq/list/why-waiting-time-changed.tsx +++ b/features/withdrawals/withdrawals-faq/list/why-waiting-time-changed.tsx @@ -4,7 +4,7 @@ import Link from 'next/link'; export const WhyWaitingTimeChanged: React.FC = () => { return ( - +

    The waiting time could be changed due to{' '} several factors{' '} diff --git a/features/withdrawals/withdrawals-faq/list/withdrawaal-fee.tsx b/features/withdrawals/withdrawals-faq/list/withdrawaal-fee.tsx index e91b9c169..e7985d04b 100644 --- a/features/withdrawals/withdrawals-faq/list/withdrawaal-fee.tsx +++ b/features/withdrawals/withdrawals-faq/list/withdrawaal-fee.tsx @@ -2,11 +2,12 @@ import { Accordion } from '@lidofinance/lido-ui'; export const WithdrawalFee: React.FC = () => { return ( - +

    There’s no withdrawal fee, but as with any Ethereum interaction, there will be a network gas fee. Lido does not collect a fee when you request - a withdrawal. + a withdrawal. Swaps via CowSwap may include: market pricing differences, + solver/execution fees, Lido fee, and Ethereum gas fees.

    ); diff --git a/features/withdrawals/withdrawals-faq/request-faq.tsx b/features/withdrawals/withdrawals-faq/request-faq.tsx index b6d9a3dae..f22b8be4b 100644 --- a/features/withdrawals/withdrawals-faq/request-faq.tsx +++ b/features/withdrawals/withdrawals-faq/request-faq.tsx @@ -26,6 +26,10 @@ import { HowToAddNFT } from './list/add-nft'; import { NFTNotChange } from './list/nft-not-change'; import { WhyWaitingTimeChanged } from './list/why-waiting-time-changed'; import { RisksOfEngagingWithLido } from './list/risks-of-engaging-with-lido'; +import { WhatAreMyOptions } from './list/what-are-my-options'; +import { WhatIsTheDifference } from './list/what-is-the-difference'; +import { WhichAssets } from './list/which-assets'; +import { HowDoISwap } from './list/how-do-i-swap'; // TODO: Replace this link when it will be finalized // const LEARN_MORE_LINK = @@ -38,8 +42,12 @@ export const RequestFaq: React.FC = () => {
    + + + + @@ -48,7 +56,7 @@ export const RequestFaq: React.FC = () => { - + diff --git a/networks/hoodi.json b/networks/hoodi.json index 5eaf90a7e..27caf5c37 100644 --- a/networks/hoodi.json +++ b/networks/hoodi.json @@ -1,6 +1,7 @@ { "contracts": { "lidoLocator": "0xe2EF9536DAAAEBFf5b1c130957AB3E80056b06D8", + "daoAgent": "0x0534aA41907c9631fae990960bCC72d75fA7cfeD", "stakingRouter": "0xCc820558B39ee15C7C45B59390B503b83fb499A8", "withdrawalQueue": "0xfe56573178f1bcdf53F01A6E9977670dcBBD9186", "lido": "0x3508A952176b3c15387C97BE809eaffB1982176a", diff --git a/networks/mainnet.json b/networks/mainnet.json index d212e3399..721d84814 100644 --- a/networks/mainnet.json +++ b/networks/mainnet.json @@ -1,6 +1,7 @@ { "contracts": { "lidoLocator": "0xC1d0b3DE6792Bf6b4b37EccdcC24e45978Cfd2Eb", + "daoAgent": "0x3e40d73eb977dc6a537af587d48316fee66e9c8c", "aggregatorEthUsdPriceFeed": "0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419", "aggregatorStEthUsdPriceFeed": "0xcfe54b5cd566ab89272946f602d76ea879cab4a8", "stethCurve": "0xDC24316b9AE028F1497c275EB9192a3Ea0f67022", @@ -61,7 +62,14 @@ "usdCollector": "0x40DA86d29AF2fe980733bD54E364e7507505b41B", "usdc": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", - "usdt": "0xdac17f958d2ee523a2206206994597c13d831ec7" + "usdt": "0xdac17f958d2ee523a2206206994597c13d831ec7", + "usds": "0xdc035d45d973e3ec169d2276ddab16f1e407384f", + "wbtc": "0x2260fac5e5542a773aa44fbcfedf7c193bc2c599", + + "aggregatorUsdcUsdPriceFeed": "0x8fFfFfd4AfB6115b954Bd326cbe7B4BA576818f6", + "aggregatorUsdtUsdPriceFeed": "0x3E7d1eAB13ad0104d2750B8863b489D65364e32D", + "aggregatorDaiUsdPriceFeed": "0xAed0c38402a5d19df6E4c03F4E2DceD6e29c1ee9", + "aggregatorBtcUsdPriceFeed": "0xF4030086522a5bEEa4988F8cA5B36dbC97BeE88c" }, "api": {} } diff --git a/networks/sepolia.json b/networks/sepolia.json index a03f2c94f..4bdf7e633 100644 --- a/networks/sepolia.json +++ b/networks/sepolia.json @@ -1,6 +1,7 @@ { "contracts": { "lidoLocator": "0x8f6254332f69557A72b0DA2D5F0Bc07d4CA991E7", + "daoAgent": "0x32A0E5828B62AAb932362a4816ae03b860b65e83", "stakingRouter": "0x4F36aAEb18Ab56A4e380241bea6ebF215b9cb12c", "withdrawalQueue": "0x1583C7b3f4C3B008720E6BcE5726336b0aB25fdd", "lido": "0x3e3FE7dBc6B4C189E7128855dD526361c49b40Af", diff --git a/next.config.mjs b/next.config.mjs index 7c94732d1..40623439d 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -154,6 +154,11 @@ export default withBundleAnalyzer({ source: '/manifest.json', headers: [{ key: 'Access-Control-Allow-Origin', value: '*' }], }, + { + // required for CoW widget iframe (swap.cow.fi) to fetch token lists cross-origin as a fallback + source: '/token-lists/:path*', + headers: [{ key: 'Access-Control-Allow-Origin', value: '*' }], + }, ...CACHE_CONTROL_PAGES.map((page) => ({ source: page, headers: [{ key: CACHE_CONTROL_HEADER, value: CACHE_CONTROL_VALUE }], diff --git a/package.json b/package.json index 8f3efb3d0..85fddbde6 100644 --- a/package.json +++ b/package.json @@ -22,8 +22,9 @@ "dependencies": { "@base-org/account": "^2.5.1", "@coinbase/wallet-sdk": "^4.3.6", + "@cowprotocol/widget-react": "^2.0.2", "@gemini-wallet/core": "~0.3.1", - "@lidofinance/analytics-matomo": "^0.56.0", + "@lidofinance/analytics-matomo": "^0.58.0", "@lidofinance/api-metrics": "^0.48.0", "@lidofinance/api-rpc": "^0.48.0", "@lidofinance/eth-api-providers": "^0.48.0", @@ -39,7 +40,7 @@ "@safe-global/safe-apps-provider": "~0.18.6", "@safe-global/safe-apps-sdk": "^9.1.0", "@tanstack/react-query": "^5.85.6", - "@walletconnect/ethereum-provider": "^2.21.1", + "@walletconnect/ethereum-provider": "^2.23.8", "copy-to-clipboard": "^3.3.1", "cors": "^2.8.5", "echarts": "^6.0.0", @@ -78,7 +79,7 @@ "@lidofinance/eslint-config": "^0.34.0", "@next/bundle-analyzer": "^13.2.4", "@next/eslint-plugin-next": "^13.4.13", - "@playwright/test": "^1.49.1", + "@playwright/test": "^1.55.1", "@svgr/webpack": "^8.0.1", "@types/jest": "28.1.6", "@types/js-cookie": "^3.0.0", @@ -111,7 +112,7 @@ "jest": "^29.5.0", "jsonschema": "^1.4.1", "lint-staged": "^13.2.3", - "playwright": "^1.49.1", + "playwright": "^1.55.1", "prettier": "^3.0.1", "string-replace-loader": "^3.3.0", "ts-jest": "^29.1.0", diff --git a/pages/_app.tsx b/pages/_app.tsx index 8bc91a41e..43cea1adc 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -3,8 +3,11 @@ import { ErrorBoundary } from 'react-error-boundary'; import type { AppProps } from 'next/app'; import 'nprogress/nprogress.css'; import Head from 'next/head'; - import { ToastContainer } from '@lidofinance/lido-ui'; +import { z } from 'zod'; +// Prevents Zod for calling `new Function("")` and causing an CSP error +// needs to run before importing any other module that imports Zod +z.config({ jitless: true }); import { config } from 'config'; import { withCsp } from 'config/csp'; @@ -13,16 +16,12 @@ import { Providers } from 'providers'; import { BackgroundGradient } from 'shared/components/background-gradient'; import { ErrorBoundaryFallback } from 'shared/components/error-boundary'; import { nprogress } from 'utils'; -import { z } from 'zod'; import { AddressValidationFile } from 'utils/address-validation'; // Visualize route changes nprogress(); -// Prevents Zod for calling `new Function("")` and causing an CSP error -z.config({ jitless: true }); - const App = (props: AppProps) => { const { Component, pageProps } = props; diff --git a/pages/rewards.tsx b/pages/rewards.tsx index fc3bd3153..ec936fa4e 100644 --- a/pages/rewards.tsx +++ b/pages/rewards.tsx @@ -10,7 +10,6 @@ import { AprDisclaimer, LegalDisclaimer, } from 'shared/components'; - import { getDefaultStaticProps } from 'utilsApi/get-default-static-props'; const Rewards: FC = () => { diff --git a/pages/withdrawals/[mode].tsx b/pages/withdrawals/[mode].tsx index dc62d2f96..a43ceaaeb 100644 --- a/pages/withdrawals/[mode].tsx +++ b/pages/withdrawals/[mode].tsx @@ -5,6 +5,7 @@ import Head from 'next/head'; import { WithdrawalsTabs } from 'features/withdrawals'; import { WithdrawalsProvider } from 'features/withdrawals/contexts/withdrawals-context'; import { Layout, DisclaimerSection, LegalDisclaimer } from 'shared/components'; +import { DexDisclaimer } from 'features/withdrawals/request/dex-disclaimer'; import { getDefaultStaticProps } from 'utilsApi/get-default-static-props'; const Withdrawals: FC = ({ mode }) => { @@ -20,6 +21,7 @@ const Withdrawals: FC = ({ mode }) => { + {mode === 'request' && } diff --git a/providers/address-validation-provider.tsx b/providers/address-validation-provider.tsx index 7951aaf1c..97613f234 100644 --- a/providers/address-validation-provider.tsx +++ b/providers/address-validation-provider.tsx @@ -16,23 +16,21 @@ import { import { useApiAddressValidation } from 'shared/hooks/use-api-address-validation'; import { Address } from 'viem'; -const AddressValidationContext = createContext<{ +type AddressValidationContextType = { isValidAddress: boolean; setIsValidAddress: (show: boolean) => void; validateAddress: (address?: Address) => Promise; -}>({ - isValidAddress: true, - setIsValidAddress: () => {}, - validateAddress: async () => { - return true; - }, -}); + isMarkedInvalid: boolean; +}; + +const AddressValidationContext = + createContext(null); AddressValidationContext.displayName = 'AddressValidationContext'; export const useAddressValidation = () => { const value = useContext(AddressValidationContext); invariant( - value !== null, + value, 'useAddressValidation was used used outside of AddressValidationProvider', ); return value; @@ -155,7 +153,10 @@ export const AddressValidationProvider = ({ validationFile?: AddressValidationFile; }) => { const validateAddressAPI = useApiAddressValidation(); + // Tracks UI state, can be reset const [isValidAddress, setIsValidAddress] = useState(true); + // Track address state, cannot be reset by user action + const [isMarkedInvalid, setIsMarkedInvalid] = useState(false); const queryClient = useQueryClient(); // File validation query (works independently of API settings) @@ -202,7 +203,7 @@ export const AddressValidationProvider = ({ // API responded successfully - use API result if (apiResult !== null && apiResult.isValid !== undefined) { setIsValidAddress(apiResult.isValid); - + setIsMarkedInvalid(!apiResult.isValid); return apiResult.isValid; } @@ -210,19 +211,21 @@ export const AddressValidationProvider = ({ if (apiResult === null && validationFile) { const fileResult = await validateAddressFile(addressToValidate); setIsValidAddress(fileResult.isValid); - + setIsMarkedInvalid(!fileResult.isValid); return fileResult.isValid; } } else if (validationFile) { // Case 2: API is disabled - use file validation when available const fileResult = await validateAddressFile(addressToValidate); setIsValidAddress(fileResult.isValid); + setIsMarkedInvalid(!fileResult.isValid); return fileResult.isValid; } // Default to valid if no validation data available setIsValidAddress(true); + setIsMarkedInvalid(false); return true; }, [validateAddressAPI, validateAddressFile, validationFile], @@ -233,6 +236,7 @@ export const AddressValidationProvider = ({ value={{ isValidAddress, setIsValidAddress, + isMarkedInvalid, validateAddress, }} > diff --git a/public/token-lists/withdrawals-dex-buy-tokenlist.json b/public/token-lists/withdrawals-dex-buy-tokenlist.json new file mode 100644 index 000000000..2e4a4be6f --- /dev/null +++ b/public/token-lists/withdrawals-dex-buy-tokenlist.json @@ -0,0 +1,60 @@ +{ + "name": "Lido withdrawals buy", + "timestamp": "2026-03-20T18:00:00.000Z", + "version": { + "major": 1, + "minor": 0, + "patch": 0 + }, + "keywords": [], + "tokens": [ + { + "chainId": 1, + "address": "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "name": "Ether", + "symbol": "ETH", + "decimals": 18, + "logoURI": "https://files.cow.fi/token-lists/images/1/0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee/logo.png" + }, + { + "symbol": "WETH", + "name": "Wrapped Ether", + "address": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", + "decimals": 18, + "chainId": 1, + "logoURI": "https://files.cow.fi/token-lists/images/1/0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2/logo.png" + }, + { + "symbol": "USDC", + "name": "USD Coin", + "address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + "decimals": 6, + "chainId": 1, + "logoURI": "https://files.cow.fi/token-lists/images/1/0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48/logo.png" + }, + { + "symbol": "USDT", + "name": "Tether USD", + "address": "0xdac17f958d2ee523a2206206994597c13d831ec7", + "decimals": 6, + "chainId": 1, + "logoURI": "https://files.cow.fi/token-lists/images/1/0xdac17f958d2ee523a2206206994597c13d831ec7/logo.png" + }, + { + "address": "0xdc035d45d973e3ec169d2276ddab16f1e407384f", + "symbol": "USDS", + "name": "USDS Stablecoin", + "decimals": 18, + "chainId": 1, + "logoURI": "https://files.cow.fi/token-lists/images/1/0xdc035d45d973e3ec169d2276ddab16f1e407384f/logo.png" + }, + { + "symbol": "WBTC", + "name": "Wrapped BTC", + "address": "0x2260fac5e5542a773aa44fbcfedf7c193bc2c599", + "decimals": 8, + "chainId": 1, + "logoURI": "https://files.cow.fi/token-lists/images/1/0x2260fac5e5542a773aa44fbcfedf7c193bc2c599/logo.png" + } + ] +} diff --git a/public/token-lists/withdrawals-dex-sell-tokenlist.json b/public/token-lists/withdrawals-dex-sell-tokenlist.json new file mode 100644 index 000000000..e283e1f64 --- /dev/null +++ b/public/token-lists/withdrawals-dex-sell-tokenlist.json @@ -0,0 +1,28 @@ +{ + "name": "Lido withdrawals sell", + "timestamp": "2026-03-20T18:00:00.000Z", + "version": { + "major": 1, + "minor": 0, + "patch": 0 + }, + "keywords": [], + "tokens": [ + { + "address": "0x7f39c581f595b53c5cb19bd0b3f8da6c935e2ca0", + "symbol": "wstETH", + "name": "Wrapped liquid staked Ether 2.0", + "decimals": 18, + "chainId": 1, + "logoURI": "https://files.cow.fi/token-lists/images/1/0x7f39c581f595b53c5cb19bd0b3f8da6c935e2ca0/logo.png" + }, + { + "address": "0xae7ab96520de3a18e5e111b5eaab095312d7fe84", + "symbol": "stETH", + "name": "Staked ETH by LIDO", + "decimals": 18, + "chainId": 1, + "logoURI": "https://files.cow.fi/token-lists/images/1/0xae7ab96520de3a18e5e111b5eaab095312d7fe84/logo.png" + } + ] +} diff --git a/shared/banners/amount-banners/amount-banner.tsx b/shared/banners/amount-banners/amount-banner.tsx new file mode 100644 index 000000000..062ea15a5 --- /dev/null +++ b/shared/banners/amount-banners/amount-banner.tsx @@ -0,0 +1,71 @@ +import { FC, PropsWithChildren } from 'react'; +import { Close } from '@lidofinance/lido-ui'; + +import { + trackAmountBannerCtaClick, + type AmountBannerPlacement, +} from './matomo'; +import { useAmountBannerOnConnectVisibility } from './use-amount-banner-on-connect-visibility'; + +import { + Wrapper, + HeaderStyled, + DescriptionStyled, + CtaGroup, + CtaLink, + CloseButton, +} from './styles'; + +type AmountBannerProps = { + isModal?: boolean; + marginTop?: number; + isDismissible?: boolean; + initialBalance?: bigint; + placement: AmountBannerPlacement; +}; + +export const AmountBanner: FC> = ({ + isModal, + marginTop, + children, + isDismissible = false, + initialBalance, + placement, +}) => { + const { shouldShow, bannerConfig, dismiss } = + useAmountBannerOnConnectVisibility({ initialBalance, isDismissible }); + + if (!shouldShow || !bannerConfig) return children; + + return ( + + {isDismissible && ( + + + + )} + {bannerConfig.heading} + {bannerConfig.body} + + {bannerConfig.ctas.map((cta) => ( + + trackAmountBannerCtaClick( + cta.text, + bannerConfig.variant, + placement, + ) + } + > + {cta.text} + + ))} + + + ); +}; diff --git a/shared/banners/amount-banners/consts.ts b/shared/banners/amount-banners/consts.ts new file mode 100644 index 000000000..3093fd9af --- /dev/null +++ b/shared/banners/amount-banners/consts.ts @@ -0,0 +1,27 @@ +import { parseEther } from 'viem'; + +// Threshold values (in ETH/stETH/wstETH, denominated in wei) +// Tier 1: 150 – ≤500 +// Tier 2: >500 – ≤1,000 +// Tier 3: >1,000 +export const AMOUNT_BANNER_THRESHOLD_1 = parseEther('150'); +export const AMOUNT_BANNER_THRESHOLD_2 = parseEther('500'); +export const AMOUNT_BANNER_THRESHOLD_3 = parseEther('1000'); + +export const AMOUNT_BANNER_LINKS = { + GET_IN_TOUCH: 'https://share-eu1.hsforms.com/1B8pLtartQYWwLXLw8K8oOw2dywmt', + CONTACT_ME: 'https://share-eu1.hsforms.com/1H4FscQB8T5i_8t0rNYUkDg2dywmt', + BOOK_A_CALL: 'https://meetings-eu1.hubspot.com/dominic-m/discovery', +} as const; + +export const AMOUNT_BANNER_AB_STORAGE_KEY = 'lido-amount-banner-ab-variant'; +export const AMOUNT_BANNER_DISMISSED_STORAGE_KEY = + 'lido-amount-banner-dismissed'; + +export const AMOUNT_BANNER_BODY_TEXT = + 'Connect with Lido contributors for opportunities across Lido Earn, V3, and institutional staking.'; + +export const AMOUNT_BANNER_HEADINGS = { + A: 'Dedicated support for large stakers', + B: 'Looking to do more with your stETH?', +} as const; diff --git a/shared/banners/amount-banners/index.ts b/shared/banners/amount-banners/index.ts new file mode 100644 index 000000000..ffd8b9704 --- /dev/null +++ b/shared/banners/amount-banners/index.ts @@ -0,0 +1,5 @@ +export { AmountBanner } from './amount-banner'; +export { useAmountBanner } from './use-amount-banner'; +export { useAmountBannerOnConnectVisibility } from './use-amount-banner-on-connect-visibility'; +export { useAmountBannerABVariant } from './use-amount-banner-ab-variant'; +export type { AmountBannerConfig, AmountBannerABVariant } from './types'; diff --git a/shared/banners/amount-banners/matomo.ts b/shared/banners/amount-banners/matomo.ts new file mode 100644 index 000000000..68ad91e22 --- /dev/null +++ b/shared/banners/amount-banners/matomo.ts @@ -0,0 +1,65 @@ +import { trackEvent } from '@lidofinance/analytics-matomo'; +import { overrideWithQAMockBoolean } from 'utils/qa'; + +import type { AmountBannerABVariant } from './types'; + +export type AmountBannerPlacement = + | 'after_stake' + | 'disconnect_wallet' + | 'connect_wallet'; + +const CATEGORY = 'Ethereum_Staking_Widget_Insti'; + +const VARIANT_LABELS: Record = { + A: 'Dedicated support', + B: 'Looking to do more', +}; + +const PLACEMENT_LABELS: Record = { + after_stake: 'after stake', + disconnect_wallet: 'disconnect wallet', + connect_wallet: 'connect wallet', +}; + +// [ctaText][variant] → slug для event name +const CTA_VARIANT_SLUGS: Record< + string, + Record +> = { + 'Get in touch': { + A: 'get_in_touch_dedicated_support', + B: 'get_in_touch_looking_to_do_more', + }, + 'Contact me': { + A: 'contact_me_dedicated_support', + B: 'contact_me_looking_to_do_more', + }, + 'Book a call': { + A: 'book_a_call_dedicated_support_more', + B: 'book_a_call_looking_to_do_more', + }, +}; + +export const trackAmountBannerCtaClick = ( + ctaText: string, + variant: AmountBannerABVariant, + placement: AmountBannerPlacement, +) => { + const slug = CTA_VARIANT_SLUGS[ctaText]?.[variant]; + if (!slug) return; + const action = `Click on "${ctaText}" ${VARIANT_LABELS[variant]} ${PLACEMENT_LABELS[placement]}`; + + const enableLogging = overrideWithQAMockBoolean( + false, + 'mock-qa-helpers-matomo-logging', + ); + if (enableLogging) { + console.info( + '%cTracking Matomo event:', + 'background:#3152A0;color:#fff;padding:2px 4px;border-radius:2px', + [CATEGORY, action, `eth_widget_${slug}_${placement}`].join(', '), + ); + } + + trackEvent(CATEGORY, action, `eth_widget_${slug}_${placement}`); +}; diff --git a/shared/banners/amount-banners/styles.tsx b/shared/banners/amount-banners/styles.tsx new file mode 100644 index 000000000..c043949f9 --- /dev/null +++ b/shared/banners/amount-banners/styles.tsx @@ -0,0 +1,137 @@ +import styled, { css } from 'styled-components'; +import { devicesHeaderMedia } from 'styles/global'; + +const modalStyles = css` + width: 100%; + max-width: 680px; + margin: 0 auto; + background: radial-gradient( + ellipse 70% 90% at 50% 50%, + #00a3ff -60%, + rgba(0, 163, 255, 0) 80% + ), + var(--lido-color-accent); +`; + +export const Wrapper = styled.div<{ $isModal?: boolean; $marginTop?: number }>` + position: relative; + display: flex; + flex-direction: column; + padding: ${({ theme }) => theme.spaceMap.md}px 20px; + border-radius: 16px; + background: linear-gradient(-115deg, #00a3ff -10%, rgba(0, 163, 255, 0) 60%), + var(--lido-color-accent); + color: #fff; + width: 240px; + text-align: left; + + ${({ $isModal }) => $isModal && modalStyles} + margin-top: ${({ $marginTop }) => ($marginTop ? `${$marginTop}px` : 0)}; + + @media ${devicesHeaderMedia.mobile} { + ${modalStyles} + margin-top: ${({ $marginTop }) => ($marginTop ? `${$marginTop}px` : 0)}; + } +`; + +export const HeaderStyled = styled.span` + font-size: ${({ theme }) => theme.fontSizesMap.sm}px; + line-height: 24px; + font-weight: 700; +`; + +export const DescriptionStyled = styled.span` + font-size: ${({ theme }) => theme.fontSizesMap.xxs}px; + line-height: 20px; + font-weight: 400; + margin-top: 4px; +`; + +export const CtaGroup = styled.div<{ $isModal?: boolean }>` + display: flex; + gap: 12px; + flex-wrap: wrap; + margin-top: 20px; + + ${({ $isModal }) => + $isModal && + css` + flex-wrap: nowrap; + `} + + @media ${devicesHeaderMedia.mobile} { + flex-wrap: nowrap; + } +`; + +export const CtaLink = styled.a<{ $isModal?: boolean }>` + display: inline-flex; + align-items: center; + justify-content: center; + padding: 8px 16px; + font-size: ${({ theme }) => theme.fontSizesMap.xs}px; + font-weight: 700; + line-height: 24px; + border-radius: ${({ theme }) => theme.borderRadiusesMap.sm}px; + color: #273852; + background-color: #fff; + text-decoration: none; + cursor: pointer; + width: 100%; + transition: background-color 0.15s; + order: 2; + + &:visited { + color: #273852; + } + + &:hover { + color: #273852; + background-color: rgba(225, 225, 225, 1); + } + + &:nth-child(2) { + background-color: unset; + color: #fff; + border: 1px solid #fff; + + &:hover { + background-color: rgba(255, 255, 255, 0.1); + } + } + + ${({ $isModal }) => + $isModal && + css` + &:nth-child(2) { + order: 1; + } + `} + + @media ${devicesHeaderMedia.mobile} { + width: 100%; + + &:last-child { + order: 1; + } + } +`; + +export const CloseButton = styled.button` + position: absolute; + top: 16px; + right: 16px; + display: flex; + align-items: center; + justify-content: center; + padding: 0; + border: none; + background: transparent; + cursor: pointer; + color: var(--lido-color-accentContrast); + opacity: 0.7; + + &:hover { + opacity: 1; + } +`; diff --git a/shared/banners/amount-banners/types.ts b/shared/banners/amount-banners/types.ts new file mode 100644 index 000000000..6a8a2e860 --- /dev/null +++ b/shared/banners/amount-banners/types.ts @@ -0,0 +1,16 @@ +export type AmountBannerABVariant = 'A' | 'B'; + +export type AmountBannerThresholdLevel = 1 | 2 | 3; + +export type AmountBannerCta = { + text: string; + href: string; +}; + +export type AmountBannerConfig = { + level: AmountBannerThresholdLevel; + variant: AmountBannerABVariant; + heading: string; + body: string; + ctas: AmountBannerCta[]; +}; diff --git a/shared/banners/amount-banners/use-amount-banner-ab-variant.ts b/shared/banners/amount-banners/use-amount-banner-ab-variant.ts new file mode 100644 index 000000000..f561c69af --- /dev/null +++ b/shared/banners/amount-banners/use-amount-banner-ab-variant.ts @@ -0,0 +1,21 @@ +import { useEffect } from 'react'; +import { useLocalStorage } from 'shared/hooks/use-local-storage'; +import { AMOUNT_BANNER_AB_STORAGE_KEY } from './consts'; +import type { AmountBannerABVariant } from './types'; + +export const useAmountBannerABVariant = (): AmountBannerABVariant => { + const [variant, setVariant] = useLocalStorage( + AMOUNT_BANNER_AB_STORAGE_KEY, + null, + ); + + useEffect(() => { + if (variant !== 'A' && variant !== 'B') { + const assigned: AmountBannerABVariant = Math.random() < 0.5 ? 'A' : 'B'; + setVariant(assigned); + } + }, [variant, setVariant]); + + // Return 'A' as default while variant is being assigned on the client + return variant === 'B' ? 'B' : 'A'; +}; diff --git a/shared/banners/amount-banners/use-amount-banner-on-connect-visibility.ts b/shared/banners/amount-banners/use-amount-banner-on-connect-visibility.ts new file mode 100644 index 000000000..8bef73459 --- /dev/null +++ b/shared/banners/amount-banners/use-amount-banner-on-connect-visibility.ts @@ -0,0 +1,61 @@ +import { useCallback } from 'react'; +import { useRouter } from 'next/router'; + +import { useConfig } from 'config'; +import { useDappStatus, useStethBalance } from 'modules/web3'; +import { useLocalStorage } from 'shared/hooks/use-local-storage'; + +import { AMOUNT_BANNER_DISMISSED_STORAGE_KEY } from './consts'; +import { useAmountBanner } from './use-amount-banner'; +import type { AmountBannerConfig } from './types'; + +type UseAmountBannerOnConnectVisibility = ({ + initialBalance, + isDismissible, +}: { + initialBalance?: bigint; + isDismissible?: boolean; +}) => { + shouldShow: boolean; + bannerConfig: AmountBannerConfig | null; + dismiss: () => void; +}; + +export const useAmountBannerOnConnectVisibility: UseAmountBannerOnConnectVisibility = + ({ initialBalance, isDismissible }) => { + const { address } = useDappStatus(); + const { query, pathname } = useRouter(); + const { featureFlags } = useConfig().externalConfig; + const { data: stethBalance } = useStethBalance(); + const bannerConfig = useAmountBanner(stethBalance, initialBalance); + + const isReferralUser = Boolean(query.ref); + // Prevent showing banner on earn vault pages + const isVaultPage = pathname === '/earn/[vault]/[action]'; + + const [isDismissed, setDismissed] = useLocalStorage( + AMOUNT_BANNER_DISMISSED_STORAGE_KEY, + false, + ); + + const dismiss = useCallback(() => { + setDismissed(true); + }, [setDismissed]); + + const notDismissedOrNotDismissible = !isDismissible || !isDismissed; + + const shouldShow = + featureFlags.amountBannerEnabled === true && + !!address && + !isReferralUser && + stethBalance !== undefined && + notDismissedOrNotDismissible && + !isVaultPage && + bannerConfig !== null; + + return { + shouldShow, + bannerConfig, + dismiss, + }; + }; diff --git a/shared/banners/amount-banners/use-amount-banner.ts b/shared/banners/amount-banners/use-amount-banner.ts new file mode 100644 index 000000000..35346b40c --- /dev/null +++ b/shared/banners/amount-banners/use-amount-banner.ts @@ -0,0 +1,99 @@ +import { useMemo } from 'react'; + +import { overrideWithQAMockEther } from 'utils/qa'; + +import { useAmountBannerABVariant } from './use-amount-banner-ab-variant'; +import { + AMOUNT_BANNER_THRESHOLD_1, + AMOUNT_BANNER_THRESHOLD_2, + AMOUNT_BANNER_THRESHOLD_3, + AMOUNT_BANNER_LINKS, + AMOUNT_BANNER_BODY_TEXT, + AMOUNT_BANNER_HEADINGS, +} from './consts'; +import type { AmountBannerConfig } from './types'; + +const QA_AMOUNT_MOCK_KEY = 'mockAmountBannerStethBalance'; +const QA_AMOUNT_THRESHOLD_1_MOCK_KEY = 'mockAmountBannerStethBalanceThreshold1'; +const QA_AMOUNT_THRESHOLD_2_MOCK_KEY = 'mockAmountBannerStethBalanceThreshold2'; +const QA_AMOUNT_THRESHOLD_3_MOCK_KEY = 'mockAmountBannerStethBalanceThreshold3'; + +export const useAmountBanner = ( + amount: bigint | undefined, + initialBalance?: bigint, +): AmountBannerConfig | null => { + const variant = useAmountBannerABVariant(); + const effectiveAmount = overrideWithQAMockEther( + amount ?? 0n, + QA_AMOUNT_MOCK_KEY, + ); + const amountThreshold1 = overrideWithQAMockEther( + AMOUNT_BANNER_THRESHOLD_1, + QA_AMOUNT_THRESHOLD_1_MOCK_KEY, + ); + const amountThreshold2 = overrideWithQAMockEther( + AMOUNT_BANNER_THRESHOLD_2, + QA_AMOUNT_THRESHOLD_2_MOCK_KEY, + ); + const amountThreshold3 = overrideWithQAMockEther( + AMOUNT_BANNER_THRESHOLD_3, + QA_AMOUNT_THRESHOLD_3_MOCK_KEY, + ); + + return useMemo(() => { + if (effectiveAmount === undefined || effectiveAmount === 0n) return null; + + const heading = AMOUNT_BANNER_HEADINGS[variant]; + const body = AMOUNT_BANNER_BODY_TEXT; + + // If the initial balance is greater than the threshold, don't show the banner + if (initialBalance && initialBalance >= amountThreshold1) return null; + + if (effectiveAmount >= amountThreshold3) { + return { + level: 3, + variant, + heading, + body, + ctas: [ + { text: 'Contact me', href: AMOUNT_BANNER_LINKS.CONTACT_ME }, + { text: 'Book a call', href: AMOUNT_BANNER_LINKS.BOOK_A_CALL }, + ], + }; + } + + if (effectiveAmount >= amountThreshold2) { + return { + level: 2, + variant, + heading, + body, + ctas: [ + { text: 'Contact me', href: AMOUNT_BANNER_LINKS.CONTACT_ME }, + { text: 'Book a call', href: AMOUNT_BANNER_LINKS.BOOK_A_CALL }, + ], + }; + } + + if (effectiveAmount >= amountThreshold1) { + return { + level: 1, + variant, + heading, + body, + ctas: [ + { text: 'Get in touch', href: AMOUNT_BANNER_LINKS.GET_IN_TOUCH }, + ], + }; + } + + return null; + }, [ + amountThreshold1, + amountThreshold2, + amountThreshold3, + effectiveAmount, + initialBalance, + variant, + ]); +}; diff --git a/shared/components/hidden/hidden.tsx b/shared/components/hidden/hidden.tsx new file mode 100644 index 000000000..48f2f369b --- /dev/null +++ b/shared/components/hidden/hidden.tsx @@ -0,0 +1,12 @@ +import { PropsWithChildren } from 'react'; +import styled from 'styled-components'; + +const HiddenBlock = styled.div.attrs({ 'aria-hidden': 'true' })` + display: none; +`; + +export const Hidden = ({ + show, + children, +}: PropsWithChildren<{ show: boolean }>) => + show ? <>{children} : {children}; diff --git a/shared/components/hidden/index.ts b/shared/components/hidden/index.ts new file mode 100644 index 000000000..1c184f0ac --- /dev/null +++ b/shared/components/hidden/index.ts @@ -0,0 +1 @@ +export * from './hidden'; diff --git a/shared/components/layout/header/components/header-wallet.tsx b/shared/components/layout/header/components/header-wallet.tsx index d187a3105..80f2cc0c9 100644 --- a/shared/components/layout/header/components/header-wallet.tsx +++ b/shared/components/layout/header/components/header-wallet.tsx @@ -6,6 +6,7 @@ import { CHAINS } from '@lidofinance/lido-ethereum-sdk/common'; import { config } from 'config'; import { useUserConfig } from 'config/user-config'; import { IPFSInfoBox } from 'features/ipfs/ipfs-info-box'; +import { AmountBanner } from 'shared/banners/amount-banners'; import { useDappStatus } from 'modules/web3'; import { Button, Connect } from 'shared/wallet'; import NoSSRWrapper from 'shared/components/no-ssr-wrapper'; @@ -15,6 +16,7 @@ import { HeaderWalletChainStyle, DotStyle, IPFSInfoBoxOnlyDesktopWrapper, + AmountBannerOnlyDesktopWrapper, } from '../styles'; import { ChainSwitcher } from './chain-switcher/chain-switcher'; @@ -60,6 +62,9 @@ const HeaderWallet: FC = () => { )} + + + ); }; diff --git a/shared/components/layout/header/styles.tsx b/shared/components/layout/header/styles.tsx index 4d30f051c..bfc5da311 100644 --- a/shared/components/layout/header/styles.tsx +++ b/shared/components/layout/header/styles.tsx @@ -59,3 +59,15 @@ export const IPFSInfoBoxOnlyDesktopWrapper = styled.div` display: none; } `; + +export const AmountBannerOnlyDesktopWrapper = styled.div` + position: absolute; + right: 0; + top: calc(100% + 15px); + width: 255px; + z-index: 3; + + @media ${devicesHeaderMedia.mobile} { + display: none; + } +`; diff --git a/shared/components/layout/layout.tsx b/shared/components/layout/layout.tsx index a8fc55fe6..aed14e59c 100644 --- a/shared/components/layout/layout.tsx +++ b/shared/components/layout/layout.tsx @@ -4,6 +4,7 @@ import { ContainerProps } from '@lidofinance/lido-ui'; import { config } from 'config'; import { IPFSInfoBox } from 'features/ipfs/ipfs-info-box'; +import { AmountBanner } from 'shared/banners/amount-banners'; import { Header } from './header/header'; import { Footer } from './footer/footer'; import { Main } from './main/main'; @@ -11,6 +12,7 @@ import { LayoutTitleStyle, LayoutSubTitleStyle, IPFSInfoBoxOnlyMobileAndPortableWrapper, + AmountBannerOnlyMobileWrapper, } from './styles'; import { HolidaysDecorFooter } from '../holiday-decor'; @@ -34,6 +36,9 @@ export const Layout: FC> = (props) => { )} + + + {title} {subtitle} {children} diff --git a/shared/components/layout/styles.tsx b/shared/components/layout/styles.tsx index 164d6f1cc..279d27164 100644 --- a/shared/components/layout/styles.tsx +++ b/shared/components/layout/styles.tsx @@ -40,3 +40,13 @@ export const IPFSInfoBoxOnlyMobileAndPortableWrapper = styled.div` margin-bottom: 40px; } `; + +export const AmountBannerOnlyMobileWrapper = styled.div` + display: none; + + @media ${devicesHeaderMedia.mobile} { + display: block; + margin-top: -6px; + margin-bottom: 40px; + } +`; diff --git a/shared/hooks/useDGWarningStatus.ts b/shared/hooks/useDGWarningStatus.ts index 2ac949bfd..842de6470 100644 --- a/shared/hooks/useDGWarningStatus.ts +++ b/shared/hooks/useDGWarningStatus.ts @@ -10,6 +10,14 @@ import { export type DGWarningState = 'Blocked' | 'Warning' | 'Unknown' | 'Normal'; +// Severity order: Normal < Unknown < Warning < Blocked +const DG_STATE_ORDER: DGWarningState[] = [ + 'Normal', + 'Unknown', + 'Warning', + 'Blocked', +]; + export const useDGWarningStatus = ( triggerPercent = 33, ): { @@ -23,10 +31,14 @@ export const useDGWarningStatus = ( // Use feature flags for testing states const { featureFlags } = useConfig().externalConfig; - const isDGBannerEnabled = overrideWithQAMockBoolean( - Boolean(featureFlags.dgBannerEnabled), - 'mock-qa-helpers-dg-banner-enabled', - ); + // SECURITY: QA overrides below can only escalate warnings, never suppress + // them. This prevents hiding governance alerts if QA localStorage keys + // leak to production. + + // QA can only enable the banner, not disable it + const isDGBannerEnabled = + Boolean(featureFlags.dgBannerEnabled) || + overrideWithQAMockBoolean(false, 'mock-qa-helpers-dg-banner-enabled'); const queryResult = useQuery({ queryKey: ['dgWarningStatus', triggerPercent], @@ -40,15 +52,27 @@ export const useDGWarningStatus = ( }); const dgStatus = queryResult.data; - const dgWarningState = dgStatus?.state ?? 'Unknown'; - const dgWarningStateOverriden = overrideWithQAMockString( - featureFlags.dgWarningState ? 'Warning' : dgWarningState, + const realState: DGWarningState = featureFlags.dgWarningState + ? 'Warning' + : (dgStatus?.state ?? 'Unknown'); + // QA can only escalate severity (e.g. Normal→Warning OK, Warning→Normal NO) + const qaState = overrideWithQAMockString( + realState, 'mock-qa-helpers-dg-state', ) as DGWarningState; + const dgWarningStateOverriden = + DG_STATE_ORDER.indexOf(qaState) >= DG_STATE_ORDER.indexOf(realState) + ? qaState + : realState; - const vetoSupportPercent = overrideWithQAMockNumber( - dgStatus?.currentVetoSupportPercent ?? 0, - 'mock-qa-helpers-dg-current-veto-support-percent', + // QA can only increase veto support (show more alarm), not decrease + const realPercent = dgStatus?.currentVetoSupportPercent ?? 0; + const vetoSupportPercent = Math.max( + realPercent, + overrideWithQAMockNumber( + realPercent, + 'mock-qa-helpers-dg-current-veto-support-percent', + ), ); const isWarningState = dgWarningStateOverriden === 'Warning'; diff --git a/shared/wallet/wallet-modal/wallet-modal.tsx b/shared/wallet/wallet-modal/wallet-modal.tsx index 9328969d3..8d91a654f 100644 --- a/shared/wallet/wallet-modal/wallet-modal.tsx +++ b/shared/wallet/wallet-modal/wallet-modal.tsx @@ -17,6 +17,7 @@ import { getEtherscanAddressLink } from 'utils/etherscan'; import { openWindow } from 'utils/open-window'; import { trackMatomoEvent } from 'utils/track-matomo-event'; import { MATOMO_CLICK_EVENTS_TYPES } from 'consts/matomo'; +import { AmountBanner } from 'shared/banners/amount-banners'; import { WalletModalContentStyle, @@ -119,6 +120,7 @@ export const WalletModal: ModalComponentType = ({ onClose, ...props }) => { + ); }; diff --git a/utils/get-bebop-rate.ts b/utils/get-bebop-rate.ts deleted file mode 100644 index aaf4e4fc3..000000000 --- a/utils/get-bebop-rate.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { getAddress } from 'viem'; - -import { config } from 'config'; -import { getTokenAddress } from 'config/networks/token-address'; -import { standardFetcher } from './standardFetcher'; -import { CHAINS } from '@lidofinance/lido-ethereum-sdk/common'; -import { type Token, type TokenSymbol } from 'consts/tokens'; - -type BebopGetQuotePartial = { - routes: { - quote: { - buyTokens: Record< - string, - { - amount: string; - amountBeforeFee: string; - } - >; - sellTokens: Record< - string, - { - amount: string; - priceBeforeFee: number; - } - >; - }; - }[]; -}; - -type RateCalculationResult = { rate: number; toReceive: bigint }; - -export const getBebopRate = async ( - amount: bigint, - fromToken: Token | TokenSymbol, - toToken: Token | TokenSymbol, -): Promise => { - const basePath = 'https://api.bebop.xyz/router/ethereum/v1/quote'; - - const sell_tokens = getAddress( - getTokenAddress(CHAINS.Mainnet, fromToken) as string, - ); - const buy_tokens = getAddress( - getTokenAddress(CHAINS.Mainnet, toToken) as string, - ); - - const params = new URLSearchParams({ - sell_tokens, - buy_tokens, - taker_address: config.ESTIMATE_ACCOUNT, - sell_amounts: amount.toString(), - approval_type: 'Standard', - source: 'lido', - }); - - const data = await standardFetcher( - `${basePath}?${params.toString()}`, - ); - - const bestRoute = data.routes.sort( - (r1, r2) => - r2.quote.sellTokens[sell_tokens].priceBeforeFee - - r1.quote.sellTokens[sell_tokens].priceBeforeFee, - )[0]; - - if ( - bestRoute && - bestRoute.quote.sellTokens[sell_tokens] && - bestRoute.quote.buyTokens[buy_tokens] - ) { - const rate = data.routes[0].quote.sellTokens[sell_tokens].priceBeforeFee; - - const toAmount = BigInt( - data.routes[0].quote.buyTokens[buy_tokens].amountBeforeFee, - ); - return { - rate, - toReceive: toAmount, - }; - } - throw new Error('[getBebopRate] Could not get quote, invalid response body'); -}; diff --git a/utils/get-jumper-rate.ts b/utils/get-jumper-rate.ts deleted file mode 100644 index 76a9c86c4..000000000 --- a/utils/get-jumper-rate.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { invariant } from '@lidofinance/lido-ethereum-sdk'; - -import { ETH_API_ROUTES, getEthApiPath } from 'consts/api'; -import { LIDO_TOKENS_VALUES } from 'consts/tokens'; - -import { standardFetcher } from './standardFetcher'; - -type GetOneInchRateParams = { - token: LIDO_TOKENS_VALUES; - amount?: bigint; -}; - -export const getJumperRate = async (params: GetOneInchRateParams) => { - const { token, amount } = params; - - const urlParams = new URLSearchParams({ token }); - if (amount) urlParams.append('amount', amount.toString()); - const url = getEthApiPath(ETH_API_ROUTES.SWAP_JUMPER, urlParams); - - invariant(url, 'Missing URL for Jumper rate request'); - - const data = await standardFetcher<{ - rate: number; - toReceive: string; - fromAmount: string; - }>(url); - - return { - rate: data.rate, - toReceive: BigInt(data.toReceive), - }; -}; diff --git a/utils/qa.ts b/utils/qa.ts index 3f81e883c..0e0d89865 100644 --- a/utils/qa.ts +++ b/utils/qa.ts @@ -1,4 +1,5 @@ import invariant from 'tiny-invariant'; +import { parseEther } from 'viem'; import { config } from 'config'; export const overrideWithQAMockBoolean = (value: boolean, key: string) => { @@ -45,6 +46,21 @@ export const overrideWithQAMockBigInt = (value: bigint, key: string) => { return value; }; +// Accepts ETH value as a decimal string (e.g. "200") for QA ergonomics +export const overrideWithQAMockEther = (value: bigint, key: string): bigint => { + if (config.enableQaHelpers && typeof window !== 'undefined') { + const mock = localStorage.getItem(key); + if (mock) { + try { + return parseEther(mock); + } catch { + invariant(false, `Invalid ETH QA mock for key "${key}": "${mock}"`); + } + } + } + return value; +}; + export const overrideWithQAMockArray = ( value: TArrayElement[], key: string, diff --git a/utilsApi/contractAddressesMetricsMap.ts b/utilsApi/contractAddressesMetricsMap.ts index 851101327..5392e4056 100644 --- a/utilsApi/contractAddressesMetricsMap.ts +++ b/utilsApi/contractAddressesMetricsMap.ts @@ -72,6 +72,7 @@ export const METRIC_CONTRACT_ABIS = { // Lido [CONTRACT_NAMES.lidoLocator]: LidoLocatorAbi, [CONTRACT_NAMES.lido]: StethAbi, + [CONTRACT_NAMES.daoAgent]: [], [CONTRACT_NAMES.wsteth]: WstethABI, [CONTRACT_NAMES.withdrawalQueue]: WithdrawalQueueAbi, [CONTRACT_NAMES.L2stETH]: rebasableL2StethAbi, diff --git a/utilsApi/fetch-external-manifest.ts b/utilsApi/fetch-external-manifest.ts index 586efe4a1..8cf86332e 100644 --- a/utilsApi/fetch-external-manifest.ts +++ b/utilsApi/fetch-external-manifest.ts @@ -4,7 +4,11 @@ import { responseTimeExternalMetricWrapper } from './fetchApiWrapper'; import { standardFetcher } from 'utils/standardFetcher'; import { config } from 'config'; -import { isManifestValid, type Manifest } from 'config/external-config'; +import { + isManifestValid, + getManifestKey, + type Manifest, +} from 'config/external-config'; import FallbackLocalManifest from 'IPFS.json'; @@ -42,7 +46,10 @@ export const fetchExternalManifest = async () => { if ( !data || typeof data !== 'object' || - !isManifestValid(data, config.defaultChain) + !isManifestValid( + data, + getManifestKey(config.defaultChain, config.manifestOverride), + ) ) { throw new Error(`invalid config received: ${data}`); } diff --git a/yarn.lock b/yarn.lock index 16f4cc59a..d8b4026a1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -30,6 +30,11 @@ "@jridgewell/gen-mapping" "^0.3.0" "@jridgewell/trace-mapping" "^0.3.9" +"@assemblyscript/loader@^0.9.4": + version "0.9.4" + resolved "https://registry.yarnpkg.com/@assemblyscript/loader/-/loader-0.9.4.tgz#a483c54c1253656bb33babd464e3154a173e1577" + integrity sha512-HazVq9zwTVwGmqdwYzu7WyQ6FQVZ7SwET0KKQuKm55jD0IfUpZgN0OPIiZG3zV1iSrVYcN0bdwLRXI/VNCYsUA== + "@babel/code-frame@^7.0.0", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.23.5": version "7.23.5" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.23.5.tgz#9009b69a8c602293476ad598ff53e4562e15c244" @@ -1439,6 +1444,256 @@ "@types/conventional-commits-parser" "^5.0.0" chalk "^5.3.0" +"@cowprotocol/cow-sdk@8.0.3": + version "8.0.3" + resolved "https://registry.yarnpkg.com/@cowprotocol/cow-sdk/-/cow-sdk-8.0.3.tgz#28a46eae9a9e4a7976957bda0e29ac04f142bc35" + integrity sha512-DOigHaD76OUnja/dZjqFYJBWr84zdV8C2NZmGrF162iRUShKZWcZ1CBNu0dZLmuZIpqILCWC0G6Sghr13NtC7w== + dependencies: + "@cowprotocol/sdk-app-data" "4.6.11" + "@cowprotocol/sdk-common" "0.8.2" + "@cowprotocol/sdk-config" "1.1.2" + "@cowprotocol/sdk-contracts-ts" "2.1.2" + "@cowprotocol/sdk-order-book" "2.0.3" + "@cowprotocol/sdk-order-signing" "0.2.2" + "@cowprotocol/sdk-trading" "1.1.2" + +"@cowprotocol/cow-sdk@8.1.0": + version "8.1.0" + resolved "https://registry.yarnpkg.com/@cowprotocol/cow-sdk/-/cow-sdk-8.1.0.tgz#4274f8cb2ddccfae8f313115a27d327fb9be8e4e" + integrity sha512-hoA9ZNPnjoFu0FbNexafFbu+M1yiGryVB1h3l2ay1qh4NCsEMZiySearfegK95M8kSvICoBkL4xO8ILMRdj2rw== + dependencies: + "@cowprotocol/sdk-app-data" "4.7.0" + "@cowprotocol/sdk-common" "0.10.1" + "@cowprotocol/sdk-config" "1.2.0" + "@cowprotocol/sdk-contracts-ts" "2.5.0" + "@cowprotocol/sdk-order-book" "2.1.0" + "@cowprotocol/sdk-order-signing" "0.3.0" + "@cowprotocol/sdk-trading" "1.3.0" + +"@cowprotocol/currency@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@cowprotocol/currency/-/currency-1.0.0.tgz#617df8a1b7265280eb8558627611a9897aaf0e5e" + integrity sha512-KXOw/OZ3+WZNvFMfThjLJTBgvh1sM4Yzj9KauomqgzaHC91aNMf2D+V+wk6baOwLNsbCnP218OavvcfxzLcPfQ== + dependencies: + "@cowprotocol/cow-sdk" "8.0.3" + jsbi "^3.1.4" + +"@cowprotocol/events@^4.2.1": + version "4.2.1" + resolved "https://registry.yarnpkg.com/@cowprotocol/events/-/events-4.2.1.tgz#38931da1bef0ffe57eb869e0ac68a272dde47080" + integrity sha512-+qxCkUk9alHZbejdMW+/MGMeIKklucmepSD13zXULQEfDHXjzBzoOQWnbEYHZ8e4IdDEqzGi8DeJdAOzkvYzBg== + dependencies: + "@cowprotocol/cow-sdk" "8.1.0" + "@cowprotocol/sdk-bridging" "3.4.0" + "@cowprotocol/types" "^4.2.1" + +"@cowprotocol/iframe-transport@^2.2.1": + version "2.2.1" + resolved "https://registry.yarnpkg.com/@cowprotocol/iframe-transport/-/iframe-transport-2.2.1.tgz#bf2291bef489f84c72cd2bbf0ac88af3e857262c" + integrity sha512-BqM5gJXzGRozPMKuvQqw93GtSFyRvOiyKdinbbsLWGRm6VqX+FOnGDw2AOQx34CkKD7c/b3y6Mqxb+XnXWo1hw== + dependencies: + "@cowprotocol/types" "^4.2.1" + eventemitter3 "^4.0.0" + +"@cowprotocol/sdk-app-data@4.6.11": + version "4.6.11" + resolved "https://registry.yarnpkg.com/@cowprotocol/sdk-app-data/-/sdk-app-data-4.6.11.tgz#c2f2d4b527051c18cc3523be8e9baeb7eaf4beb2" + integrity sha512-0i2vrTghdkzleuaiM7M83WPhWXi1aFsfe1xB8CBhk+tHTN9tOv0N4imxu8ZzB9kx2bKQgHOBOm59zdf/t1TT6g== + dependencies: + "@cowprotocol/sdk-common" "0.8.2" + ajv "^8.11.0" + cross-fetch "^3.1.5" + ipfs-only-hash "^4.0.0" + json-stringify-deterministic "^1.0.8" + multiformats "^9.6.4" + +"@cowprotocol/sdk-app-data@4.7.0": + version "4.7.0" + resolved "https://registry.yarnpkg.com/@cowprotocol/sdk-app-data/-/sdk-app-data-4.7.0.tgz#9a0f3375aebe0a347faded431ec851293f8510bd" + integrity sha512-HgVsZ9mTnGIOLm9RMkPyDCRedP5/uGih5UtvJYM0DKXRz7yHKlWx2HUTslbEQc6ReDA6uxbLsmhVQ6tBcsOS1Q== + dependencies: + "@cowprotocol/sdk-common" "0.10.1" + ajv "^8.11.0" + cross-fetch "^3.1.5" + ipfs-only-hash "^4.0.0" + json-stringify-deterministic "^1.0.8" + multiformats "^9.6.4" + +"@cowprotocol/sdk-bridging@3.4.0": + version "3.4.0" + resolved "https://registry.yarnpkg.com/@cowprotocol/sdk-bridging/-/sdk-bridging-3.4.0.tgz#c6a4dedc028c3dabbd0d79e13c05a8f8c7c13356" + integrity sha512-s4kxbfWGCZO+SNYgCS9gjeJY+oavG9D9sg1F45z3+ymVKkqtYOxrFSteG4+l5sUpn/dfaVGsXIUXwAL6IZEmHg== + dependencies: + "@cowprotocol/sdk-app-data" "4.7.0" + "@cowprotocol/sdk-common" "0.10.1" + "@cowprotocol/sdk-config" "1.2.0" + "@cowprotocol/sdk-contracts-ts" "2.5.0" + "@cowprotocol/sdk-cow-shed" "0.3.6" + "@cowprotocol/sdk-order-book" "2.1.0" + "@cowprotocol/sdk-trading" "1.3.0" + "@cowprotocol/sdk-weiroll" "0.1.29" + "@defuse-protocol/one-click-sdk-typescript" "0.1.1-0.2" + json-stable-stringify "^1.3.0" + +"@cowprotocol/sdk-common@0.10.1": + version "0.10.1" + resolved "https://registry.yarnpkg.com/@cowprotocol/sdk-common/-/sdk-common-0.10.1.tgz#78004d1a0e43efbccf3450775692bd5af6da839a" + integrity sha512-2EYpgXDwD0F5KR9W+jT2Vr/vzaJ8meAvEPm4fGjJUVr2otrUkhUG7ayx68L1AgKZWYzDtNrlM/+1EeyEjjEaWg== + dependencies: + "@cowprotocol/sdk-config" "1.2.0" + +"@cowprotocol/sdk-common@0.8.2": + version "0.8.2" + resolved "https://registry.yarnpkg.com/@cowprotocol/sdk-common/-/sdk-common-0.8.2.tgz#360addd68bfde5317a04b92d1e3f519e1e0da1d7" + integrity sha512-ybuSCuhLXA4SRiBcrWXos0sVKkznkCsCZ/e9b46irtJ5QhOHerFQQ1y+jUlsDncAlOjvrWLBLymAiiG3bnkjug== + dependencies: + "@cowprotocol/sdk-config" "1.1.2" + +"@cowprotocol/sdk-config@1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@cowprotocol/sdk-config/-/sdk-config-1.1.2.tgz#974cc02a039215420bb68e0938c186eb0059ecd5" + integrity sha512-ERjGavCdoDTBmL56sFf0uMPDd0Ku/5C8wSZpVVWYb2rrEoTSRgy1FTmCcVPPK8Igzy2m4BRBUp3itTv0GcQL2w== + dependencies: + exponential-backoff "^3.1.1" + limiter "^2.1.0" + +"@cowprotocol/sdk-config@1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@cowprotocol/sdk-config/-/sdk-config-1.2.0.tgz#7259772f8053f23ebd770e522279146fb66c5764" + integrity sha512-upZ2OPycc+rikwAnfNt58qBMlYb+7zpFPzG6zT/sfE50FS1f2ba4SMtw37HVq+v0e9R32o/rWZFcDZT5Ty5yyQ== + dependencies: + exponential-backoff "^3.1.1" + limiter "^2.1.0" + +"@cowprotocol/sdk-contracts-ts@2.1.2": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@cowprotocol/sdk-contracts-ts/-/sdk-contracts-ts-2.1.2.tgz#c04f533b1b743d94234eb75deb4bee068e6dcc86" + integrity sha512-DT/zwAQZIJ3ARePKX1kwjWrAWV3C/y6G9HGwCnKi376ccr0hadyG6S17M/kHfhYNYF0T9DZb+9t6KMXskhgMwA== + dependencies: + "@cowprotocol/sdk-common" "0.8.2" + "@cowprotocol/sdk-config" "1.1.2" + +"@cowprotocol/sdk-contracts-ts@2.5.0": + version "2.5.0" + resolved "https://registry.yarnpkg.com/@cowprotocol/sdk-contracts-ts/-/sdk-contracts-ts-2.5.0.tgz#125dfb8591a76d02aeca27da6f78bb8d038a746f" + integrity sha512-UZSmqUO+XPwxQ/w75nwkLBK/sjUHdYgWBPhA4EV5x8y5XdBm5Dk8UqrLUemgAAVKwuXaWcoqhkoJWKrV2/gqGQ== + dependencies: + "@cowprotocol/sdk-common" "0.10.1" + "@cowprotocol/sdk-config" "1.2.0" + +"@cowprotocol/sdk-cow-shed@0.3.6": + version "0.3.6" + resolved "https://registry.yarnpkg.com/@cowprotocol/sdk-cow-shed/-/sdk-cow-shed-0.3.6.tgz#e9685ad293c4051e9ceb18828fffc510ae693bdf" + integrity sha512-dIDjS5UnO4qAOEBw3vs+/NiEH3XQ1lEgzzcRSD3u0I3de28N9V5J2rbomxOjjEpcEZi4kohu4Io6t/AAZ+0a+Q== + dependencies: + "@cowprotocol/sdk-common" "0.10.1" + "@cowprotocol/sdk-config" "1.2.0" + "@cowprotocol/sdk-contracts-ts" "2.5.0" + +"@cowprotocol/sdk-order-book@2.0.3": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@cowprotocol/sdk-order-book/-/sdk-order-book-2.0.3.tgz#f9218c6f504b9ccccfa81945b0a01b4e6e6a40f2" + integrity sha512-/50AbBlBpcE2Hq3XGc5+XxKpbMV9H3d28OU7wP1YaJD4jAR+PdC6c9GRtzWw6+q4ZRyvnTFKrUTvzE6OLQfx3Q== + dependencies: + "@cowprotocol/sdk-common" "0.8.2" + "@cowprotocol/sdk-config" "1.1.2" + cross-fetch "^3.2.0" + exponential-backoff "^3.1.2" + limiter "^3.0.0" + +"@cowprotocol/sdk-order-book@2.1.0": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@cowprotocol/sdk-order-book/-/sdk-order-book-2.1.0.tgz#0af7f0888b350fe28d7de35eb502813f0a4c6912" + integrity sha512-mbWNDV2hRnd9wXT7n5/HOw8RufprusLVOW+Du/u/3syLApDaPUjtUvqTzEg3n3qxeqccAz99OeCW2i8BojUL9A== + dependencies: + "@cowprotocol/sdk-common" "0.10.1" + "@cowprotocol/sdk-config" "1.2.0" + cross-fetch "^3.2.0" + exponential-backoff "^3.1.2" + limiter "^3.0.0" + +"@cowprotocol/sdk-order-signing@0.2.2": + version "0.2.2" + resolved "https://registry.yarnpkg.com/@cowprotocol/sdk-order-signing/-/sdk-order-signing-0.2.2.tgz#55e57e5066f70bcae72b9bbe11d3173a2403486a" + integrity sha512-0fa8c1NetEe8avG56HfXFtANO2+XzVSCF8X2i8+2eeiTUjsZiuPBrTOcci3uC8FIpJ0Ko7tciYg1Rhib7O5TeA== + dependencies: + "@cowprotocol/sdk-common" "0.8.2" + "@cowprotocol/sdk-config" "1.1.2" + "@cowprotocol/sdk-contracts-ts" "2.1.2" + "@cowprotocol/sdk-order-book" "2.0.3" + +"@cowprotocol/sdk-order-signing@0.3.0": + version "0.3.0" + resolved "https://registry.yarnpkg.com/@cowprotocol/sdk-order-signing/-/sdk-order-signing-0.3.0.tgz#e0b0a68051e227b18ac92a5417e650c61df793d3" + integrity sha512-7+SVwsxW7tMipXhu+Se1pzvKm9XfSvHMNZbF28t0VCKigA39bUY9n5LZ6KLF9GTejswK6Rn/BKo6f7sl0RjKFw== + dependencies: + "@cowprotocol/sdk-common" "0.10.1" + "@cowprotocol/sdk-config" "1.2.0" + "@cowprotocol/sdk-contracts-ts" "2.5.0" + "@cowprotocol/sdk-order-book" "2.1.0" + +"@cowprotocol/sdk-trading@1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@cowprotocol/sdk-trading/-/sdk-trading-1.1.2.tgz#a138f209f6851062a8d5760476678f5fff8eace5" + integrity sha512-NAgPT+cEYa8MZHGbgzaG5wa+wTVDAx2DbIxTrDiEYCLNhNBGBX/M1yukhCu3cVzNKbGVTF3A023vWUD3nbFmTA== + dependencies: + "@cowprotocol/sdk-app-data" "4.6.11" + "@cowprotocol/sdk-common" "0.8.2" + "@cowprotocol/sdk-config" "1.1.2" + "@cowprotocol/sdk-contracts-ts" "2.1.2" + "@cowprotocol/sdk-order-book" "2.0.3" + "@cowprotocol/sdk-order-signing" "0.2.2" + deepmerge "^4.3.1" + +"@cowprotocol/sdk-trading@1.3.0": + version "1.3.0" + resolved "https://registry.yarnpkg.com/@cowprotocol/sdk-trading/-/sdk-trading-1.3.0.tgz#d3bbcd417d837ba0096d8530825353f092bbba64" + integrity sha512-xzBwRWCSTPoKDyc2DjgjMhvE2vUMkSkag3hGNTLDb8smJD0zTVhk0oruEQiX7T0MrgGgwgenxeF4ZpywtWhAZg== + dependencies: + "@cowprotocol/sdk-app-data" "4.7.0" + "@cowprotocol/sdk-common" "0.10.1" + "@cowprotocol/sdk-config" "1.2.0" + "@cowprotocol/sdk-contracts-ts" "2.5.0" + "@cowprotocol/sdk-order-book" "2.1.0" + "@cowprotocol/sdk-order-signing" "0.3.0" + deepmerge "^4.3.1" + +"@cowprotocol/sdk-weiroll@0.1.29": + version "0.1.29" + resolved "https://registry.yarnpkg.com/@cowprotocol/sdk-weiroll/-/sdk-weiroll-0.1.29.tgz#52ac03597f6f7e7db75ec8fcfbacd89a4a87adbf" + integrity sha512-e9qyZpqrG1GGI1I61VrH8lO/6EmaMBK7NV7z+AvW5K06g5DjSbyG8MEQsKNEX6FvJxNz58pUvdI2JWkjdu9jeA== + dependencies: + "@cowprotocol/sdk-common" "0.10.1" + "@cowprotocol/sdk-config" "1.2.0" + +"@cowprotocol/types@^4.2.1": + version "4.2.1" + resolved "https://registry.yarnpkg.com/@cowprotocol/types/-/types-4.2.1.tgz#558b7cc5908ea29d4c60f0d3756bad880cfbd84f" + integrity sha512-2mut7QxAbzJLL8OtjehTIAfZqLxzikNqT0DJuuaRMQdjHrA+Z+zKynnyGXKwMpw1RB8FZlrYFAnKqxIs8qX0jA== + dependencies: + "@cowprotocol/cow-sdk" "8.1.0" + "@cowprotocol/currency" "^1.0.0" + "@cowprotocol/sdk-bridging" "3.4.0" + eventemitter3 "^4.0.0" + +"@cowprotocol/widget-lib@^3.1.1": + version "3.1.1" + resolved "https://registry.yarnpkg.com/@cowprotocol/widget-lib/-/widget-lib-3.1.1.tgz#2f0b18be7ad6e3c7fd458d2ddb407cf63be88cc3" + integrity sha512-rTHcYyACt5hfnhi9g/gO7XnL+j6wKsdpkdTes+cJiNj0QAD9aojIgHWuKIbqbpcI9aIVzGOHunAYERAXKPqhiA== + dependencies: + "@cowprotocol/cow-sdk" "8.1.0" + "@cowprotocol/events" "^4.2.1" + "@cowprotocol/iframe-transport" "^2.2.1" + +"@cowprotocol/widget-react@^2.0.2": + version "2.0.2" + resolved "https://registry.yarnpkg.com/@cowprotocol/widget-react/-/widget-react-2.0.2.tgz#385e6ebf92b26ade899704bc241b3d72a04a34db" + integrity sha512-gxQ/MbcwC1Pg+tLgYTrBp4UyOFWKTEiHfGHT7XJck6SKb9bA8FE4KY5JN2+rbFXpKhIbC3243OeUXOGKuLLRoA== + dependencies: + "@cowprotocol/events" "^4.2.1" + "@cowprotocol/types" "^4.2.1" + "@cowprotocol/widget-lib" "^3.1.1" + "@dabh/diagnostics@^2.0.2": version "2.0.3" resolved "https://registry.yarnpkg.com/@dabh/diagnostics/-/diagnostics-2.0.3.tgz#7f7e97ee9a725dffc7808d93668cc984e1dc477a" @@ -1448,6 +1703,14 @@ enabled "2.0.x" kuler "^2.0.0" +"@defuse-protocol/one-click-sdk-typescript@0.1.1-0.2": + version "0.1.1-0.2" + resolved "https://registry.yarnpkg.com/@defuse-protocol/one-click-sdk-typescript/-/one-click-sdk-typescript-0.1.1-0.2.tgz#ffab6f7bc90b8514273ac8c9183c0b271ce22a09" + integrity sha512-Jgt8uZlB5hQAo3UpyHH9XcXKNT6Vsqd7TTPy/vLEwuOvQ88Pag9qUxpU9Z2jYMD8SqOpxzaJrtgx+FSDb4lQ9A== + dependencies: + axios "^1.6.8" + form-data "^4.0.0" + "@ecies/ciphers@^0.2.5": version "0.2.5" resolved "https://registry.yarnpkg.com/@ecies/ciphers/-/ciphers-0.2.5.tgz#754ff2f821645f0465d18a1a68198eb15d16c2a0" @@ -2350,10 +2613,10 @@ bignumber.js "^9.1.2" rxjs "^7.8.1" -"@lidofinance/analytics-matomo@^0.56.0": - version "0.56.0" - resolved "https://registry.yarnpkg.com/@lidofinance/analytics-matomo/-/analytics-matomo-0.56.0.tgz#48ab44457635f878783c52e2b80f8ce98a09e149" - integrity sha512-uUcNauwkOVCYkwbUyLm6PnSEBQ2H9b+XAiRz6fMm2mrpoxyoZLmbfxCU2Ekg1hpnpFWcFlNW/G9bTQeopFhAbA== +"@lidofinance/analytics-matomo@^0.58.0": + version "0.58.0" + resolved "https://registry.yarnpkg.com/@lidofinance/analytics-matomo/-/analytics-matomo-0.58.0.tgz#3e614c9fc9e909742c92805dd70313ee329d0e29" + integrity sha512-8bitqKDKL3JBwHC869ZABg4rbBaQ1fCaG5VBWvk8fkCeJuTnqQUpsYmtB9om6RUxdd5AO2PrIoQXJ9v+yixQlw== "@lidofinance/api-metrics@^0.48.0": version "0.48.0" @@ -2654,6 +2917,11 @@ resolved "https://registry.yarnpkg.com/@msgpack/msgpack/-/msgpack-3.1.3.tgz#c4bff2b9539faf0882f3ee03537a7e9a4b3a7864" integrity sha512-47XIizs9XZXvuJgoaJUIE2lFoID8ugvc0jzSHP+Ptfk8nTbnR8g788wv48N03Kx0UkAv559HWRQ3yzOgzlRNUA== +"@multiformats/base-x@^4.0.1": + version "4.0.1" + resolved "https://registry.yarnpkg.com/@multiformats/base-x/-/base-x-4.0.1.tgz#95ff0fa58711789d53aefb2590a8b7a4e715d121" + integrity sha512-eMk0b9ReBbV23xXU693TAIrLyeO5iTgBZGSJfpqriG8UkYvr/hC9u9pyMlAakDNHWmbhMZCDs6KQO0jzKD8OTw== + "@next/bundle-analyzer@^13.2.4": version "13.5.6" resolved "https://registry.yarnpkg.com/@next/bundle-analyzer/-/bundle-analyzer-13.5.6.tgz#3c73f2e15ff5507317b37b87ce984bac5a5d7ad0" @@ -2836,18 +3104,71 @@ resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== -"@playwright/test@^1.49.1": - version "1.49.1" - resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.49.1.tgz#55fa360658b3187bfb6371e2f8a64f50ef80c827" - integrity sha512-Ky+BVzPz8pL6PQxHqNRW1k3mIyv933LML7HktS8uik0bUXNCdPhoS/kLihiO1tMf/egaJb4IutXd7UywvXEW+g== +"@playwright/test@^1.55.1": + version "1.58.2" + resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.58.2.tgz#b0ad585d2e950d690ef52424967a42f40c6d2cbd" + integrity sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA== dependencies: - playwright "1.49.1" + playwright "1.58.2" "@polka/url@^1.0.0-next.20": version "1.0.0-next.24" resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.24.tgz#58601079e11784d20f82d0585865bb42305c4df3" integrity sha512-2LuNTFBIO0m7kKIQvvPHN6UE63VjpmL9rnEEaOOaiSPbZK+zUOYIzBAWcED+3XYzhYsd/0mD57VdxAEqqV52CQ== +"@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz#9b8b0cc663d669a7d8f6f5d0893a14d348f30fbf" + integrity sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ== + +"@protobufjs/base64@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@protobufjs/base64/-/base64-1.1.2.tgz#4c85730e59b9a1f1f349047dbf24296034bb2735" + integrity sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg== + +"@protobufjs/codegen@^2.0.4": + version "2.0.4" + resolved "https://registry.yarnpkg.com/@protobufjs/codegen/-/codegen-2.0.4.tgz#7ef37f0d010fb028ad1ad59722e506d9262815cb" + integrity sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg== + +"@protobufjs/eventemitter@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz#355cbc98bafad5978f9ed095f397621f1d066b70" + integrity sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q== + +"@protobufjs/fetch@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/fetch/-/fetch-1.1.0.tgz#ba99fb598614af65700c1619ff06d454b0d84c45" + integrity sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ== + dependencies: + "@protobufjs/aspromise" "^1.1.1" + "@protobufjs/inquire" "^1.1.0" + +"@protobufjs/float@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@protobufjs/float/-/float-1.0.2.tgz#5e9e1abdcb73fc0a7cb8b291df78c8cbd97b87d1" + integrity sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ== + +"@protobufjs/inquire@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/inquire/-/inquire-1.1.0.tgz#ff200e3e7cf2429e2dcafc1140828e8cc638f089" + integrity sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q== + +"@protobufjs/path@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@protobufjs/path/-/path-1.1.2.tgz#6cc2b20c5c9ad6ad0dccfd21ca7673d8d7fbf68d" + integrity sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA== + +"@protobufjs/pool@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/pool/-/pool-1.1.0.tgz#09fd15f2d6d3abfa9b65bc366506d6ad7846ff54" + integrity sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw== + +"@protobufjs/utf8@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570" + integrity sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw== + "@reef-knot/connect-wallet-modal@7.3.0-alpha.5": version "7.3.0-alpha.5" resolved "https://registry.yarnpkg.com/@reef-knot/connect-wallet-modal/-/connect-wallet-modal-7.3.0-alpha.5.tgz#167254a6b5d89e4cd4118dc62f6279cb7465dd23" @@ -3953,11 +4274,6 @@ dependencies: "@tanstack/query-core" "5.85.6" -"@trysound/sax@0.2.0": - version "0.2.0" - resolved "https://registry.yarnpkg.com/@trysound/sax/-/sax-0.2.0.tgz#cccaab758af56761eb7bf37af6f03f326dd798ad" - integrity sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA== - "@types/babel__core@^7.1.14": version "7.20.5" resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.20.5.tgz#3df15f27ba85319caa07ba08d0721889bb39c017" @@ -4079,11 +4395,21 @@ resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.17.23.tgz#c1bb06db218acc8fc232da0447473fc2fb9d9841" integrity sha512-RDvF6wTulMPjrNdCoYRC8gNR880JNGT8uB+REUpC2Ns4pRqQJhGz90wh7rgdXDPpCczF3VGktDuFGVnz8zP7HA== +"@types/long@^4.0.1": + version "4.0.2" + resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.2.tgz#b74129719fc8d11c01868010082d483b7545591a" + integrity sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA== + "@types/memory-cache@0.2.2": version "0.2.2" resolved "https://registry.yarnpkg.com/@types/memory-cache/-/memory-cache-0.2.2.tgz#f8fb6d8aa0eb006ed44fc659bf8bfdc1a5cc57fa" integrity sha512-xNnm6EkmYYhTnLiOHC2bdKgcYY5qjjrq5vl9KXD2nh0em0koZoFS500EL4Q4V/eW+A3P7NC7P7GIYzNOSQp7jQ== +"@types/minimist@^1.2.0": + version "1.2.5" + resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.5.tgz#ec10755e871497bcd83efe927e43ec46e8c0747e" + integrity sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag== + "@types/ms@*": version "2.1.0" resolved "https://registry.yarnpkg.com/@types/ms/-/ms-2.1.0.tgz#052aa67a48eccc4309d7f0191b7e41434b90bb78" @@ -4101,6 +4427,13 @@ dependencies: undici-types "~5.26.4" +"@types/node@>=13.7.0": + version "25.6.0" + resolved "https://registry.yarnpkg.com/@types/node/-/node-25.6.0.tgz#4e09bad9b469871f2d0f68140198cbd714f4edca" + integrity sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ== + dependencies: + undici-types "~7.19.0" + "@types/node@^12.12.54": version "12.20.55" resolved "https://registry.yarnpkg.com/@types/node/-/node-12.20.55.tgz#c329cbd434c42164f846b909bd6f85b5537f6240" @@ -4432,10 +4765,10 @@ events "3.3.0" uint8arrays "3.1.1" -"@walletconnect/core@2.23.4": - version "2.23.4" - resolved "https://registry.yarnpkg.com/@walletconnect/core/-/core-2.23.4.tgz#f51cad2749cf3141d95372011830559ea9381a45" - integrity sha512-qkzNvRfibl+r2GoPqKl+2MJLYA7ApEWyCmECJoK6IExeWyjKawAUC6Eo4cN0geCBefk9VSFRFEIVQ17vYWp0jQ== +"@walletconnect/core@2.23.8": + version "2.23.8" + resolved "https://registry.yarnpkg.com/@walletconnect/core/-/core-2.23.8.tgz#50813b589a4f367e01a3335d973232f4c86b3468" + integrity sha512-559+fA6Hh9CkEIOtrWKdDWoa3HL47glDF7D75LbqQzv4v325KXq24KEsjzDPBYr7pI49gQo7P2HpPnY1ax+8Aw== dependencies: "@walletconnect/heartbeat" "1.2.2" "@walletconnect/jsonrpc-provider" "1.0.14" @@ -4448,8 +4781,8 @@ "@walletconnect/relay-auth" "1.1.0" "@walletconnect/safe-json" "1.0.2" "@walletconnect/time" "1.0.2" - "@walletconnect/types" "2.23.4" - "@walletconnect/utils" "2.23.4" + "@walletconnect/types" "2.23.8" + "@walletconnect/utils" "2.23.8" "@walletconnect/window-getters" "1.0.1" es-toolkit "1.44.0" events "3.3.0" @@ -4462,10 +4795,10 @@ dependencies: tslib "1.14.1" -"@walletconnect/ethereum-provider@^2.21.1": - version "2.23.4" - resolved "https://registry.yarnpkg.com/@walletconnect/ethereum-provider/-/ethereum-provider-2.23.4.tgz#9477a2582e85b4cc50c0bb94d1ad4219be1e1fbc" - integrity sha512-HvbFoydhv8Zl1ey3RuMkV0UtPbZ9jRoZ/WyQHXMQLXHLMxsgweDBJmKdeNDrNTCxIPDv0Br4BGamk0TUZ3Q3SQ== +"@walletconnect/ethereum-provider@^2.23.8": + version "2.23.8" + resolved "https://registry.yarnpkg.com/@walletconnect/ethereum-provider/-/ethereum-provider-2.23.8.tgz#39a08a131b2a5229701bc37a0ee9d9a2f336ee75" + integrity sha512-nRUWmx8ihUmqVGwQD2PHlJ7+TFT4AZPSR5ssghosVHBt8kpjgtUbE7ej/bYAmmnerDKLyYGrqWtHwjaNBPJHqA== dependencies: "@reown/appkit" "1.8.17-wc-circular-dependencies-fix.0" "@walletconnect/jsonrpc-http-connection" "1.0.8" @@ -4474,10 +4807,10 @@ "@walletconnect/jsonrpc-utils" "1.0.8" "@walletconnect/keyvaluestorage" "1.1.1" "@walletconnect/logger" "3.0.2" - "@walletconnect/sign-client" "2.23.4" - "@walletconnect/types" "2.23.4" - "@walletconnect/universal-provider" "2.23.4" - "@walletconnect/utils" "2.23.4" + "@walletconnect/sign-client" "2.23.8" + "@walletconnect/types" "2.23.8" + "@walletconnect/universal-provider" "2.23.8" + "@walletconnect/utils" "2.23.8" events "3.3.0" "@walletconnect/events@1.0.1", "@walletconnect/events@^1.0.1": @@ -4600,19 +4933,19 @@ "@walletconnect/utils" "2.23.2" events "3.3.0" -"@walletconnect/sign-client@2.23.4": - version "2.23.4" - resolved "https://registry.yarnpkg.com/@walletconnect/sign-client/-/sign-client-2.23.4.tgz#d10bf3718c83e5b73158a0f2e05ee0ce86dab101" - integrity sha512-Q3hM8YmO+RHdT3R0MWyRBmekK5SNwpeheQQ+rWbu2dZFm9NyqfxJqwr6ZEhrZltFGTzCHrajTaFPrQnMb/LxuA== +"@walletconnect/sign-client@2.23.8": + version "2.23.8" + resolved "https://registry.yarnpkg.com/@walletconnect/sign-client/-/sign-client-2.23.8.tgz#c28e16a87ae7339597d89db64b3a4a648e18a3aa" + integrity sha512-7DtFDQZwOK4E9q+TKWL819d01dpNHA3jMcntSsQqSLNU34orbkDB/BJzW4nyWZ6H9DuGHRvibJA9wvfXjOCWBw== dependencies: - "@walletconnect/core" "2.23.4" + "@walletconnect/core" "2.23.8" "@walletconnect/events" "1.0.1" "@walletconnect/heartbeat" "1.2.2" "@walletconnect/jsonrpc-utils" "1.0.8" "@walletconnect/logger" "3.0.2" "@walletconnect/time" "1.0.2" - "@walletconnect/types" "2.23.4" - "@walletconnect/utils" "2.23.4" + "@walletconnect/types" "2.23.8" + "@walletconnect/utils" "2.23.8" events "3.3.0" "@walletconnect/time@1.0.2", "@walletconnect/time@^1.0.2": @@ -4634,10 +4967,10 @@ "@walletconnect/logger" "3.0.2" events "3.3.0" -"@walletconnect/types@2.23.4": - version "2.23.4" - resolved "https://registry.yarnpkg.com/@walletconnect/types/-/types-2.23.4.tgz#6b08a47fb9efa54a6cda9b88762798966dad94eb" - integrity sha512-6M+9JEXbZdnvdPc4tleXOFTV9al1brKojBpL5uzNCbzCxqvq273wWtW/eqPgTqH7BJam1nDVK8D02o63ECIooQ== +"@walletconnect/types@2.23.8": + version "2.23.8" + resolved "https://registry.yarnpkg.com/@walletconnect/types/-/types-2.23.8.tgz#cd68f15ec1f7ea41184289ce9e9110b52c1353b8" + integrity sha512-OI/0Z7/8r11EDU9bBPy5nixYgsk6SrTcOvWe9r7Nf2WvkMcPLgV7aS8rb6+nInRmDPfXuyTgzdAox0rtmfJMzg== dependencies: "@walletconnect/events" "1.0.1" "@walletconnect/heartbeat" "1.2.2" @@ -4664,10 +4997,10 @@ es-toolkit "1.39.3" events "3.3.0" -"@walletconnect/universal-provider@2.23.4": - version "2.23.4" - resolved "https://registry.yarnpkg.com/@walletconnect/universal-provider/-/universal-provider-2.23.4.tgz#9db30938d9ca7dc7cab840e6ddfc956d5d8e8948" - integrity sha512-f/w2pIEMcuEdVbfRuBZ2MYwvom5EP9I6rFSnLHWTmyfR7V0wY1LViVRcxR4mwCqcX//69lj8MiWnP4iQarP83w== +"@walletconnect/universal-provider@2.23.8": + version "2.23.8" + resolved "https://registry.yarnpkg.com/@walletconnect/universal-provider/-/universal-provider-2.23.8.tgz#ddee3caa0192a068ec88b59f5e5b33c1d82291de" + integrity sha512-TFn2TNhp5vlbV2HqPU/LfkEIYZEop4WDCTTZKw/RU4DbM1e1+etmvTr5JA+8dkZU7ee48mVUDodY0zQedP+KZA== dependencies: "@walletconnect/events" "1.0.1" "@walletconnect/jsonrpc-http-connection" "1.0.8" @@ -4676,9 +5009,9 @@ "@walletconnect/jsonrpc-utils" "1.0.8" "@walletconnect/keyvaluestorage" "1.1.1" "@walletconnect/logger" "3.0.2" - "@walletconnect/sign-client" "2.23.4" - "@walletconnect/types" "2.23.4" - "@walletconnect/utils" "2.23.4" + "@walletconnect/sign-client" "2.23.8" + "@walletconnect/types" "2.23.8" + "@walletconnect/utils" "2.23.8" es-toolkit "1.44.0" events "3.3.0" @@ -4708,10 +5041,10 @@ ox "0.9.3" uint8arrays "3.1.1" -"@walletconnect/utils@2.23.4": - version "2.23.4" - resolved "https://registry.yarnpkg.com/@walletconnect/utils/-/utils-2.23.4.tgz#d6bc935991d32f7b048356af57a863f97f6e227a" - integrity sha512-J2QTS1rga3/FE+REJUAk1HNnZZ2ubAA3VRZehsT3bfOn+OzT2+skQXVzU6ZFaiwPWsLtIOF3aSodZoc5bUrLyw== +"@walletconnect/utils@2.23.8": + version "2.23.8" + resolved "https://registry.yarnpkg.com/@walletconnect/utils/-/utils-2.23.8.tgz#8c6b34fabcbd6844e6163f061999c010ff601a5b" + integrity sha512-vJrRrZFZANWmnEEnWnfVSnpQ+jdjqBb5fqSgp0VGeRX3pNr2KAHJ0TwNnEN+fbhR76JxuFrpcY7HJUT7DHDJ7w== dependencies: "@msgpack/msgpack" "3.1.3" "@noble/ciphers" "1.3.0" @@ -4725,11 +5058,10 @@ "@walletconnect/relay-auth" "1.1.0" "@walletconnect/safe-json" "1.0.2" "@walletconnect/time" "1.0.2" - "@walletconnect/types" "2.23.4" + "@walletconnect/types" "2.23.8" "@walletconnect/window-getters" "1.0.1" "@walletconnect/window-metadata" "1.0.1" blakejs "1.2.1" - bs58 "6.0.0" detect-browser "5.3.0" ox "0.9.3" uint8arrays "3.1.1" @@ -5031,6 +5363,11 @@ arraybuffer.prototype.slice@^1.0.2: is-array-buffer "^3.0.2" is-shared-array-buffer "^1.0.2" +arrify@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" + integrity sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA== + ast-types-flow@^0.0.8: version "0.0.8" resolved "https://registry.yarnpkg.com/ast-types-flow/-/ast-types-flow-0.0.8.tgz#0a85e1c92695769ac13a428bb653e7538bea27d6" @@ -5125,6 +5462,15 @@ axios@^1.6.5: form-data "^4.0.0" proxy-from-env "^1.1.0" +axios@^1.6.8: + version "1.15.0" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.15.0.tgz#0fcee91ef03d386514474904b27863b2c683bf4f" + integrity sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q== + dependencies: + follow-redirects "^1.15.11" + form-data "^4.0.5" + proxy-from-env "^2.1.0" + axobject-query@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-3.2.1.tgz#39c378a6e3b06ca679f29138151e45b2b32da62a" @@ -5288,9 +5634,18 @@ bl@^4.1.0: inherits "^2.0.4" readable-stream "^3.4.0" -blakejs@1.2.1: +bl@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/bl/-/bl-5.1.0.tgz#183715f678c7188ecef9fe475d90209400624273" + integrity sha512-tv1ZJHLfTDnXE6tMHv73YgSJaWR2AFuPwMntBe7XL/GBFHnT0CLnsHMogfk5+GzCDC5ZWarSCYaIGATZt9dNsQ== + dependencies: + buffer "^6.0.3" + inherits "^2.0.4" + readable-stream "^3.4.0" + +blakejs@1.2.1, blakejs@^1.1.0: version "1.2.1" - resolved "https://registry.yarnpkg.com/blakejs/-/blakejs-1.2.1.tgz#5057e4206eadb4a97f7c0b6e197a505042fc3814" + resolved "https://registry.npmjs.org/blakejs/-/blakejs-1.2.1.tgz#5057e4206eadb4a97f7c0b6e197a505042fc3814" integrity sha512-QXUSXI3QVc/gJME0dBpXrag1kbzOqCjCX8/b54ntNyW6sjtoqxqRk3LTmXzaJoh71zMsDCjM+47jS7XiwN/+fQ== bn.js@^4.11.9: @@ -5484,6 +5839,15 @@ camelcase-css@^2.0.1: resolved "https://registry.yarnpkg.com/camelcase-css/-/camelcase-css-2.0.1.tgz#ee978f6947914cc30c6b44741b6ed1df7f043fd5" integrity sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA== +camelcase-keys@^6.2.2: + version "6.2.2" + resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-6.2.2.tgz#5e755d6ba51aa223ec7d3d52f25778210f9dc3c0" + integrity sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg== + dependencies: + camelcase "^5.3.1" + map-obj "^4.0.0" + quick-lru "^4.0.1" + camelcase@^5.0.0, camelcase@^5.3.1: version "5.3.1" resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" @@ -5500,9 +5864,9 @@ camelize@^1.0.0: integrity sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ== caniuse-lite@^1.0.30001406, caniuse-lite@^1.0.30001580: - version "1.0.30001713" - resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001713.tgz" - integrity sha512-wCIWIg+A4Xr7NfhTuHdX+/FKh3+Op3LBbSp2N5Pfx6T/LhdQy3GTyoTg48BReaW/MyMNZAkTadsBtai3ldWK0Q== + version "1.0.30001780" + resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001780.tgz" + integrity sha512-llngX0E7nQci5BPJDqoZSbuZ5Bcs9F5db7EtgfwBerX9XGtkkiO4NwfDDIRzHTTwcYC8vC7bmeUEPGrKlR/TkQ== chalk@5.3.0: version "5.3.0" @@ -5573,6 +5937,16 @@ ci-info@^3.2.0, ci-info@^3.8.0: resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.9.0.tgz#4279a62028a7b1f262f3473fc9605f5e218c59b4" integrity sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ== +cids@^1.0.0, cids@^1.1.5, cids@^1.1.6: + version "1.1.9" + resolved "https://registry.yarnpkg.com/cids/-/cids-1.1.9.tgz#402c26db5c07059377bcd6fb82f2a24e7f2f4a4f" + integrity sha512-l11hWRfugIcbGuTZwAM5PwpjPPjyb6UZOGwlHSnOBV5o07XhQ4gNpBN67FbODvpjyHtd+0Xs6KNvUcGBiDRsdg== + dependencies: + multibase "^4.0.1" + multicodec "^3.0.1" + multihashes "^4.0.1" + uint8arrays "^3.0.0" + cipher-base@^1.0.1: version "1.0.6" resolved "https://registry.yarnpkg.com/cipher-base/-/cipher-base-1.0.6.tgz#8fe672437d01cd6c4561af5334e0cc50ff1955f7" @@ -5878,9 +6252,9 @@ create-jest@^29.7.0: jest-util "^29.7.0" prompts "^2.0.1" -cross-fetch@^3.1.4: +cross-fetch@^3.1.4, cross-fetch@^3.2.0: version "3.2.0" - resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.2.0.tgz#34e9192f53bc757d6614304d9e5e6fb4edb782e3" + resolved "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.2.0.tgz#34e9192f53bc757d6614304d9e5e6fb4edb782e3" integrity sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q== dependencies: node-fetch "^2.7.0" @@ -6031,7 +6405,15 @@ debug@~4.4.1: dependencies: ms "^2.1.3" -decamelize@^1.2.0: +decamelize-keys@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/decamelize-keys/-/decamelize-keys-1.1.1.tgz#04a2d523b2f18d80d0158a43b895d56dff8d19d8" + integrity sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg== + dependencies: + decamelize "^1.1.0" + map-obj "^1.0.0" + +decamelize@^1.1.0, decamelize@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" integrity sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA== @@ -6359,6 +6741,11 @@ env-paths@^2.2.1: resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-2.2.1.tgz#420399d416ce1fbe9bc0a07c62fa68d67fd0f8f2" integrity sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A== +err-code@^3.0.0, err-code@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/err-code/-/err-code-3.0.1.tgz#a444c7b992705f2b120ee320b09972eef331c920" + integrity sha512-GiaH0KJUewYok+eeY05IIgjtAe4Yltygk9Wqp1V5yVWLdhf0hYZchRjNIT9bb0mSwRcIusT3cx7PJUf3zEIfUA== + error-ex@^1.3.1: version "1.3.2" resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" @@ -6909,6 +7296,11 @@ expect@^29.7.0: jest-message-util "^29.7.0" jest-util "^29.7.0" +exponential-backoff@^3.1.1, exponential-backoff@^3.1.2: + version "3.1.3" + resolved "https://registry.yarnpkg.com/exponential-backoff/-/exponential-backoff-3.1.3.tgz#51cf92c1c0493c766053f9d3abee4434c244d2f6" + integrity sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA== + extension-port-stream@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/extension-port-stream/-/extension-port-stream-3.0.0.tgz#00a7185fe2322708a36ed24843c81bd754925fef" @@ -7036,9 +7428,9 @@ flat-cache@^3.0.4: rimraf "^3.0.2" flatted@^3.2.9: - version "3.2.9" - resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.9.tgz#7eb4c67ca1ba34232ca9d2d93e9886e611ad7daf" - integrity sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ== + version "3.4.2" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.4.2.tgz#f5c23c107f0f37de8dbdf24f13722b3b98d52726" + integrity sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA== fn.name@1.x.x: version "1.1.0" @@ -7050,6 +7442,11 @@ follow-redirects@^1.14.0, follow-redirects@^1.15.4, follow-redirects@^1.15.6: resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.6.tgz#7f815c0cda4249c74ff09e95ef97c23b5fd0399b" integrity sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA== +follow-redirects@^1.15.11: + version "1.16.0" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.16.0.tgz#28474a159d3b9d11ef62050a14ed60e4df6d61bc" + integrity sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw== + for-each@^0.3.3: version "0.3.3" resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e" @@ -7083,7 +7480,7 @@ form-data@^4.0.0: hasown "^2.0.2" mime-types "^2.1.12" -form-data@^4.0.4: +form-data@^4.0.4, form-data@^4.0.5: version "4.0.5" resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.5.tgz#b49e48858045ff4cbf6b03e1805cebcad3679053" integrity sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w== @@ -7352,9 +7749,9 @@ gzip-size@^6.0.0: duplexer "^0.1.2" h3@^1.15.5: - version "1.15.5" - resolved "https://registry.yarnpkg.com/h3/-/h3-1.15.5.tgz#e2f28d4a66a249973bb050eaddb06b9ab55506f8" - integrity sha512-xEyq3rSl+dhGX2Lm0+eFQIAzlDN6Fs0EcC4f7BNUmzaRX/PTzeuM+Tr2lHB8FoXggsQIeXLj8EDVgs5ywxyxmg== + version "1.15.10" + resolved "https://registry.yarnpkg.com/h3/-/h3-1.15.10.tgz#defe650df7b70cf585d2020c4146fb580cfb0d42" + integrity sha512-YzJeWSkDZxAhvmp8dexjRK5hxziRO7I9m0N53WhvYL5NiWfkUkzssVzY9jvGu0HBoLFW6+duYmNSn6MaZBCCtg== dependencies: cookie-es "^1.2.2" crossws "^0.3.5" @@ -7366,6 +7763,19 @@ h3@^1.15.5: ufo "^1.6.3" uncrypto "^0.1.3" +hamt-sharding@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/hamt-sharding/-/hamt-sharding-2.0.1.tgz#f45686d0339e74b03b233bee1bde9587727129b6" + integrity sha512-vnjrmdXG9dDs1m/H4iJ6z0JFI2NtgsW5keRkTcM85NGak69Mkf5PHUqBz+Xs0T4sg0ppvj9O5EGAJo40FTxmmA== + dependencies: + sparse-array "^1.3.1" + uint8arrays "^3.0.0" + +hard-rejection@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/hard-rejection/-/hard-rejection-2.1.0.tgz#1c6eda5c1685c63942766d79bb40ae773cecd883" + integrity sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA== + has-bigints@^1.0.1, has-bigints@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.2.tgz#0871bd3e3d51626f6ca0966668ba35d5602d6eaa" @@ -7472,15 +7882,22 @@ hoist-non-react-statics@^3.0.0, hoist-non-react-statics@^3.3.0: react-is "^16.7.0" hono@^4.10.3: - version "4.11.7" - resolved "https://registry.yarnpkg.com/hono/-/hono-4.11.7.tgz#f5b8d0b0b503ef0d913a246012dda52ea23dbe53" - integrity sha512-l7qMiNee7t82bH3SeyUCt9UF15EVmaBvsppY2zQtrbIhl/yzBTny+YUxsVjSjQ6gaqaeVtZmGocom8TzBlA4Yw== + version "4.12.8" + resolved "https://registry.yarnpkg.com/hono/-/hono-4.12.8.tgz#5f3a9c0d5339ff460b2c652a4c64dd79059930ad" + integrity sha512-VJCEvtrezO1IAR+kqEYnxUOoStaQPGrCmX3j4wDTNOcD1uRPFpGlwQUIW8niPuvHXaTUxeOUl5MMDGrl+tmO9A== hosted-git-info@^2.1.4: version "2.8.9" resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9" integrity sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw== +hosted-git-info@^4.0.1: + version "4.1.0" + resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-4.1.0.tgz#827b82867e9ff1c8d0c4d9d53880397d2c86d224" + integrity sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA== + dependencies: + lru-cache "^6.0.0" + html-escaper@^2.0.0: version "2.0.2" resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453" @@ -7602,6 +8019,15 @@ inquirer@^9.2.15: wrap-ansi "^6.2.0" yoctocolors-cjs "^2.1.2" +interface-ipld-format@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/interface-ipld-format/-/interface-ipld-format-1.0.1.tgz#bee39c70c584a033e186ff057a2be89f215963e3" + integrity sha512-WV/ar+KQJVoQpqRDYdo7YPGYIUHJxCuOEhdvsRpzLqoOIVCqPKdMMYmsLL1nCRsF3yYNio+PAJbCKiv6drrEAg== + dependencies: + cids "^1.1.6" + multicodec "^3.0.1" + multihashes "^4.0.2" + internal-slot@^1.0.5: version "1.0.6" resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.6.tgz#37e756098c4911c5e912b8edbf71ed3aa116f930" @@ -7618,6 +8044,55 @@ invariant@2: dependencies: loose-envify "^1.0.0" +ipfs-only-hash@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/ipfs-only-hash/-/ipfs-only-hash-4.0.0.tgz#b3bd60a244d9eb7394961aa9d812a2e5ac7c04d6" + integrity sha512-TE1DZCvfw8i3gcsTq3P4TFx3cKFJ3sluu/J3XINkJhIN9OwJgNMqKA+WnKx6ByCb1IoPXsTp1KM7tupElb6SyA== + dependencies: + ipfs-unixfs-importer "^7.0.1" + meow "^9.0.0" + +ipfs-unixfs-importer@^7.0.1: + version "7.0.3" + resolved "https://registry.yarnpkg.com/ipfs-unixfs-importer/-/ipfs-unixfs-importer-7.0.3.tgz#b850e831ca9647d589ef50bc33421f65bab7bba6" + integrity sha512-qeFOlD3AQtGzr90sr5Tq1Bi8pT5Nr2tSI8z310m7R4JDYgZc6J1PEZO3XZQ8l1kuGoqlAppBZuOYmPEqaHcVQQ== + dependencies: + bl "^5.0.0" + cids "^1.1.5" + err-code "^3.0.1" + hamt-sharding "^2.0.0" + ipfs-unixfs "^4.0.3" + ipld-dag-pb "^0.22.2" + it-all "^1.0.5" + it-batch "^1.0.8" + it-first "^1.0.6" + it-parallel-batch "^1.0.9" + merge-options "^3.0.4" + multihashing-async "^2.1.0" + rabin-wasm "^0.1.4" + uint8arrays "^2.1.2" + +ipfs-unixfs@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/ipfs-unixfs/-/ipfs-unixfs-4.0.3.tgz#7c43e5726052ade4317245358ac541ef3d63d94e" + integrity sha512-hzJ3X4vlKT8FQ3Xc4M1szaFVjsc1ZydN+E4VQ91aXxfpjFn9G2wsMo1EFdAXNq/BUnN5dgqIOMP5zRYr3DTsAw== + dependencies: + err-code "^3.0.1" + protobufjs "^6.10.2" + +ipld-dag-pb@^0.22.2: + version "0.22.3" + resolved "https://registry.yarnpkg.com/ipld-dag-pb/-/ipld-dag-pb-0.22.3.tgz#6d5af28b5752236a5cb0e0a1888c87dd733b55cd" + integrity sha512-dfG5C5OVAR4FEP7Al2CrHWvAyIM7UhAQrjnOYOIxXGQz5NlEj6wGX0XQf6Ru6or1na6upvV3NQfstapQG8X2rg== + dependencies: + cids "^1.0.0" + interface-ipld-format "^1.0.0" + multicodec "^3.0.1" + multihashing-async "^2.0.0" + protobufjs "^6.10.2" + stable "^0.1.8" + uint8arrays "^2.0.5" + iron-webcrypto@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/iron-webcrypto/-/iron-webcrypto-1.2.1.tgz#aa60ff2aa10550630f4c0b11fd2442becdb35a6f" @@ -7703,6 +8178,13 @@ is-core-module@^2.11.0, is-core-module@^2.13.0, is-core-module@^2.13.1: dependencies: hasown "^2.0.0" +is-core-module@^2.5.0: + version "2.16.1" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.16.1.tgz#2a98801a849f43e2add644fbb6bc6229b19a4ef4" + integrity sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w== + dependencies: + hasown "^2.0.2" + is-date-object@^1.0.1, is-date-object@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.5.tgz#0841d5536e724c25597bf6ea62e1bd38298df31f" @@ -7799,6 +8281,16 @@ is-path-inside@^3.0.3: resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== +is-plain-obj@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e" + integrity sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg== + +is-plain-obj@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287" + integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA== + is-regex@^1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.4.tgz#eef5663cd59fa4c0ae339505323df6854bb15958" @@ -7990,6 +8482,28 @@ istanbul-reports@^3.1.3: html-escaper "^2.0.0" istanbul-lib-report "^3.0.0" +it-all@^1.0.5: + version "1.0.6" + resolved "https://registry.yarnpkg.com/it-all/-/it-all-1.0.6.tgz#852557355367606295c4c3b7eff0136f07749335" + integrity sha512-3cmCc6Heqe3uWi3CVM/k51fa/XbMFpQVzFoDsV0IZNHSQDyAXl3c4MjHkFX5kF3922OGj7Myv1nSEUgRtcuM1A== + +it-batch@^1.0.8, it-batch@^1.0.9: + version "1.0.9" + resolved "https://registry.yarnpkg.com/it-batch/-/it-batch-1.0.9.tgz#7e95aaacb3f9b1b8ca6c8b8367892171d6a5b37f" + integrity sha512-7Q7HXewMhNFltTsAMdSz6luNhyhkhEtGGbYek/8Xb/GiqYMtwUmopE1ocPSiJKKp3rM4Dt045sNFoUu+KZGNyA== + +it-first@^1.0.6: + version "1.0.7" + resolved "https://registry.yarnpkg.com/it-first/-/it-first-1.0.7.tgz#a4bef40da8be21667f7d23e44dae652f5ccd7ab1" + integrity sha512-nvJKZoBpZD/6Rtde6FXqwDqDZGF1sCADmr2Zoc0hZsIvnE449gRFnGctxDf09Bzc/FWnHXAdaHVIetY6lrE0/g== + +it-parallel-batch@^1.0.9: + version "1.0.11" + resolved "https://registry.yarnpkg.com/it-parallel-batch/-/it-parallel-batch-1.0.11.tgz#f889b4e1c7a62ef24111dbafbaaa010b33d00f69" + integrity sha512-UWsWHv/kqBpMRmyZJzlmZeoAMA0F3SZr08FBdbhtbe+MtoEBgr/ZUAKrnenhXCBrsopy76QjRH2K/V8kNdupbQ== + dependencies: + it-batch "^1.0.9" + iterator.prototype@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/iterator.prototype/-/iterator.prototype-1.1.2.tgz#5e29c8924f01916cb9335f1ff80619dcff22b0c0" @@ -8436,9 +8950,9 @@ js-cookie@^3.0.1: resolved "https://registry.yarnpkg.com/js-cookie/-/js-cookie-3.0.5.tgz#0b7e2fd0c01552c58ba86e0841f94dc2557dcdbc" integrity sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw== -js-sha3@0.8.0: +js-sha3@0.8.0, js-sha3@^0.8.0: version "0.8.0" - resolved "https://registry.yarnpkg.com/js-sha3/-/js-sha3-0.8.0.tgz#b9b7a5da73afad7dedd0f8c463954cbde6818840" + resolved "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz#b9b7a5da73afad7dedd0f8c463954cbde6818840" integrity sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q== "js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: @@ -8461,6 +8975,11 @@ js-yaml@^4.1.0: dependencies: argparse "^2.0.1" +jsbi@^3.1.4: + version "3.2.5" + resolved "https://registry.yarnpkg.com/jsbi/-/jsbi-3.2.5.tgz#b37bb90e0e5c2814c1c2a1bcd8c729888a2e37d6" + integrity sha512-aBE4n43IPvjaddScbvWRA2YlTzKEynHzu7MqOyTipdHucf/VxS63ViCjxYRg86M8Rxwbt/GfzHl1kKERkt45fQ== + jsesc@^2.5.1: version "2.5.2" resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" @@ -8501,6 +9020,22 @@ json-stable-stringify-without-jsonify@^1.0.1: resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== +json-stable-stringify@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-1.3.0.tgz#8903cfac42ea1a0f97f35d63a4ce0518f0cc6a70" + integrity sha512-qtYiSSFlwot9XHtF9bD9c7rwKjr+RecWT//ZnPvSmEjpV5mmPOCN4j8UjY5hbjNkOwZ/jQv3J6R1/pL7RwgMsg== + dependencies: + call-bind "^1.0.8" + call-bound "^1.0.4" + isarray "^2.0.5" + jsonify "^0.0.1" + object-keys "^1.1.1" + +json-stringify-deterministic@^1.0.8: + version "1.0.13" + resolved "https://registry.yarnpkg.com/json-stringify-deterministic/-/json-stringify-deterministic-1.0.13.tgz#45f5eb75a8058b25c9ca65071a0744fe0729808e" + integrity sha512-Gm2hkhXlhD69QplkQGmY30S8Ps5noSFzruJKbgmD/nkYIXimS/Q0P1YIbJkWe2nbWDIzERmtp+hWGzC3Vk3bZg== + json-stringify-safe@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" @@ -8527,6 +9062,11 @@ jsonfile@^6.0.1: optionalDependencies: graceful-fs "^4.1.6" +jsonify@^0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.1.tgz#2aa3111dae3d34a0f151c63f3a45d995d9420978" + integrity sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg== + jsonparse@^1.2.0: version "1.3.1" resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280" @@ -8547,6 +9087,11 @@ jsonschema@^1.4.1: object.assign "^4.1.4" object.values "^1.1.6" +just-performance@4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/just-performance/-/just-performance-4.3.0.tgz#cc2bc8c9227f09e97b6b1df4cd0de2df7ae16db1" + integrity sha512-L7RjvtJsL0QO8xFs5wEoDDzzJwoiowRw6Rn/GnvldlchS2JQr9wFYPiwZcDfrbbujEKqKN0tvENdbjXdYhDp5Q== + keccak@^3.0.3: version "3.0.4" resolved "https://registry.yarnpkg.com/keccak/-/keccak-3.0.4.tgz#edc09b89e633c0549da444432ecf062ffadee86d" @@ -8568,6 +9113,11 @@ keyvaluestorage-interface@^1.0.0: resolved "https://registry.yarnpkg.com/keyvaluestorage-interface/-/keyvaluestorage-interface-1.0.0.tgz#13ebdf71f5284ad54be94bd1ad9ed79adad515ff" integrity sha512-8t6Q3TclQ4uZynJY9IGr2+SsIGwK9JHcO6ootkHCGA0CrQCRy+VkouYNO2xicET6b9al7QKzpebNow+gkpCL8g== +kind-of@^6.0.3: + version "6.0.3" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" + integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== + kleur@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" @@ -8613,6 +9163,18 @@ lilconfig@^3.0.0: resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-3.1.2.tgz#e4a7c3cb549e3a606c8dcc32e5ae1005e62c05cb" integrity sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow== +limiter@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/limiter/-/limiter-2.1.0.tgz#d38d7c5b63729bb84fb0c4d8594b7e955a5182a2" + integrity sha512-361TYz6iay6n+9KvUUImqdLuFigK+K79qrUtBsXhJTLdH4rIt/r1y8r1iozwh8KbZNpujbFTSh74mJ7bwbAMOw== + dependencies: + just-performance "4.3.0" + +limiter@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/limiter/-/limiter-3.0.0.tgz#03556b76d1a81f547caeecc6b83ecc6f24495715" + integrity sha512-hev7DuXojsTFl2YwyzUJMDnZ/qBDd3yZQLSH3aD4tdL1cqfc3TMnoecEJtWFaQFdErZsKoFMBTxF/FBSkgDbEg== + lines-and-columns@^1.1.6: version "1.2.4" resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" @@ -8801,6 +9363,11 @@ logform@^2.3.2, logform@^2.4.0: safe-stable-stringify "^2.3.1" triple-beam "^1.3.0" +long@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/long/-/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28" + integrity sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA== + loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" @@ -8821,9 +9388,9 @@ lru-cache@^10.2.0: integrity sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ== lru-cache@^11.2.0: - version "11.2.5" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-11.2.5.tgz#6811ae01652ae5d749948cdd80bcc22218c6744f" - integrity sha512-vFrFJkWtJvJnD5hg+hJvVE8Lh/TcMzKnTgCWmtBipwI5yLX/iX+5UB2tfuyODF5E7k9xEzMdYgGqaSb1c0c5Yw== + version "11.2.7" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-11.2.7.tgz#9127402617f34cd6767b96daee98c28e74458d35" + integrity sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA== lru-cache@^5.1.1: version "5.1.1" @@ -8858,6 +9425,16 @@ makeerror@1.0.12: dependencies: tmpl "1.0.5" +map-obj@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d" + integrity sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg== + +map-obj@^4.0.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-4.3.0.tgz#9304f906e93faae70880da102a9f1df0ea8bb05a" + integrity sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ== + math-intrinsics@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9" @@ -8901,6 +9478,31 @@ meow@^12.0.1: resolved "https://registry.yarnpkg.com/meow/-/meow-12.1.1.tgz#e558dddbab12477b69b2e9a2728c327f191bace6" integrity sha512-BhXM0Au22RwUneMPwSCnyhTOizdWoIEPU9sp0Aqa1PnDMR5Wv2FGXYDjuzJEIX+Eo2Rb8xuYe5jrnm5QowQFkw== +meow@^9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/meow/-/meow-9.0.0.tgz#cd9510bc5cac9dee7d03c73ee1f9ad959f4ea364" + integrity sha512-+obSblOQmRhcyBt62furQqRAQpNyWXo8BuQ5bN7dG8wmwQ+vwHKp/rCFD4CrTP8CsDQD1sjoZ94K417XEUk8IQ== + dependencies: + "@types/minimist" "^1.2.0" + camelcase-keys "^6.2.2" + decamelize "^1.2.0" + decamelize-keys "^1.1.0" + hard-rejection "^2.1.0" + minimist-options "4.1.0" + normalize-package-data "^3.0.0" + read-pkg-up "^7.0.1" + redent "^3.0.0" + trim-newlines "^3.0.0" + type-fest "^0.18.0" + yargs-parser "^20.2.3" + +merge-options@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/merge-options/-/merge-options-3.0.4.tgz#84709c2aa2a4b24c1981f66c179fe5565cc6dbb7" + integrity sha512-2Sug1+knBjkaMsMgf1ctR1Ujx+Ayku4EdJN4Z+C2+JzoeF7A3OZ9KM2GY0CpQS51NR61LTurMJrRKPhSs3ZRTQ== + dependencies: + is-plain-obj "^2.1.0" + merge-stream@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" @@ -8987,9 +9589,18 @@ minimatch@^9.0.4: dependencies: brace-expansion "^2.0.1" -minimist@^1.2.0, minimist@^1.2.6, minimist@^1.2.8: +minimist-options@4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/minimist-options/-/minimist-options-4.1.0.tgz#c0655713c53a8a2ebd77ffa247d342c40f010619" + integrity sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A== + dependencies: + arrify "^1.0.1" + is-plain-obj "^1.1.0" + kind-of "^6.0.3" + +minimist@^1.2.0, minimist@^1.2.5, minimist@^1.2.6, minimist@^1.2.8: version "1.2.8" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" + resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== "minipass@^5.0.0 || ^6.0.2 || ^7.0.0", minipass@^7.1.2: @@ -9017,11 +9628,52 @@ ms@^2.0.0, ms@^2.1.1, ms@^2.1.3: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== -multiformats@^9.4.2: +multibase@^4.0.1: + version "4.0.6" + resolved "https://registry.yarnpkg.com/multibase/-/multibase-4.0.6.tgz#6e624341483d6123ca1ede956208cb821b440559" + integrity sha512-x23pDe5+svdLz/k5JPGCVdfn7Q5mZVMBETiC+ORfO+sor9Sgs0smJzAjfTbM5tckeCqnaUuMYoz+k3RXMmJClQ== + dependencies: + "@multiformats/base-x" "^4.0.1" + +multicodec@^3.0.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/multicodec/-/multicodec-3.2.1.tgz#82de3254a0fb163a107c1aab324f2a91ef51efb2" + integrity sha512-+expTPftro8VAW8kfvcuNNNBgb9gPeNYV9dn+z1kJRWF2vih+/S79f2RVeIwmrJBUJ6NT9IUPWnZDQvegEh5pw== + dependencies: + uint8arrays "^3.0.0" + varint "^6.0.0" + +multiformats@^9.4.2, multiformats@^9.6.4: version "9.9.0" - resolved "https://registry.yarnpkg.com/multiformats/-/multiformats-9.9.0.tgz#c68354e7d21037a8f1f8833c8ccd68618e8f1d37" + resolved "https://registry.npmjs.org/multiformats/-/multiformats-9.9.0.tgz#c68354e7d21037a8f1f8833c8ccd68618e8f1d37" integrity sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg== +multihashes@^4.0.1, multihashes@^4.0.2: + version "4.0.3" + resolved "https://registry.yarnpkg.com/multihashes/-/multihashes-4.0.3.tgz#426610539cd2551edbf533adeac4c06b3b90fb05" + integrity sha512-0AhMH7Iu95XjDLxIeuCOOE4t9+vQZsACyKZ9Fxw2pcsRmlX4iCn1mby0hS0bb+nQOVpdQYWPpnyusw4da5RPhA== + dependencies: + multibase "^4.0.1" + uint8arrays "^3.0.0" + varint "^5.0.2" + +multihashing-async@^2.0.0, multihashing-async@^2.1.0: + version "2.1.4" + resolved "https://registry.yarnpkg.com/multihashing-async/-/multihashing-async-2.1.4.tgz#26dce2ec7a40f0e7f9e732fc23ca5f564d693843" + integrity sha512-sB1MiQXPSBTNRVSJc2zM157PXgDtud2nMFUEIvBrsq5Wv96sUclMRK/ecjoP1T/W61UJBqt4tCTwMkUpt2Gbzg== + dependencies: + blakejs "^1.1.0" + err-code "^3.0.0" + js-sha3 "^0.8.0" + multihashes "^4.0.1" + murmurhash3js-revisited "^3.0.0" + uint8arrays "^3.0.0" + +murmurhash3js-revisited@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/murmurhash3js-revisited/-/murmurhash3js-revisited-3.0.0.tgz#6bd36e25de8f73394222adc6e41fa3fac08a5869" + integrity sha512-/sF3ee6zvScXMb1XFJ8gDsSnY+X8PbOyjIuBhtgis10W2Jx4ZjIhikUCIF9c4gpJxVnQIsPAFrSwTCuAjicP6g== + mute-stream@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-1.0.0.tgz#e31bd9fe62f0aed23520aa4324ea6671531e013e" @@ -9150,6 +9802,16 @@ normalize-package-data@^2.5.0: semver "2 || 3 || 4 || 5" validate-npm-package-license "^3.0.1" +normalize-package-data@^3.0.0: + version "3.0.3" + resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-3.0.3.tgz#dbcc3e2da59509a0983422884cd172eefdfa525e" + integrity sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA== + dependencies: + hosted-git-info "^4.0.1" + is-core-module "^2.5.0" + semver "^7.3.4" + validate-npm-package-license "^3.0.1" + normalize-path@^3.0.0, normalize-path@~3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" @@ -9365,6 +10027,20 @@ ox@0.11.3: abitype "^1.2.3" eventemitter3 "5.0.1" +ox@0.14.7: + version "0.14.7" + resolved "https://registry.yarnpkg.com/ox/-/ox-0.14.7.tgz#efe6770f7a138f823fb2df26001b679c2565b0a4" + integrity sha512-zSQ/cfBdolj7U4++NAvH7sI+VG0T3pEohITCgcQj8KlawvTDY4vGVhDT64Atsm0d6adWfIYHDpu88iUBMMp+AQ== + dependencies: + "@adraffy/ens-normalize" "^1.11.0" + "@noble/ciphers" "^1.3.0" + "@noble/curves" "1.9.1" + "@noble/hashes" "^1.8.0" + "@scure/bip32" "^1.7.0" + "@scure/bip39" "^1.6.0" + abitype "^1.2.3" + eventemitter3 "5.0.1" + ox@0.6.9: version "0.6.9" resolved "https://registry.yarnpkg.com/ox/-/ox-0.6.9.tgz#da1ee04fa10de30c8d04c15bfb80fe58b1f554bd" @@ -9614,17 +10290,17 @@ pkg-dir@^4.2.0: dependencies: find-up "^4.0.0" -playwright-core@1.49.1: - version "1.49.1" - resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.49.1.tgz#32c62f046e950f586ff9e35ed490a424f2248015" - integrity sha512-BzmpVcs4kE2CH15rWfzpjzVGhWERJfmnXmniSyKeRZUs9Ws65m+RGIi7mjJK/euCegfn3i7jvqWeWyHe9y3Vgg== +playwright-core@1.58.2: + version "1.58.2" + resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.58.2.tgz#ac5f5b4b10d29bcf934415f0b8d133b34b0dcb13" + integrity sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg== -playwright@1.49.1, playwright@^1.49.1: - version "1.49.1" - resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.49.1.tgz#830266dbca3008022afa7b4783565db9944ded7c" - integrity sha512-VYL8zLoNTBxVOrJBbDuRgDWa3i+mfQgDTrL8Ah9QXZ7ax4Dsj0MSq5bYgytRnDVVe+njoKnfsYkH3HzqVj5UZA== +playwright@1.58.2, playwright@^1.55.1: + version "1.58.2" + resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.58.2.tgz#afe547164539b0bcfcb79957394a7a3fa8683cfd" + integrity sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A== dependencies: - playwright-core "1.49.1" + playwright-core "1.58.2" optionalDependencies: fsevents "2.3.2" @@ -9805,6 +10481,25 @@ prop-types@^15.6.2, prop-types@^15.8.1: object-assign "^4.1.1" react-is "^16.13.1" +protobufjs@^6.10.2: + version "6.11.5" + resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-6.11.5.tgz#01b714e6b711089219c7c60c514a40d0a249007a" + integrity sha512-OKjVH3hDoXdIZ/s5MLv8O2X0s+wOxGfV7ar6WFSKGaSAxi/6gYn3px5POS4vi+mc/0zCOdL7Jkwrj0oT1Yst2A== + dependencies: + "@protobufjs/aspromise" "^1.1.2" + "@protobufjs/base64" "^1.1.2" + "@protobufjs/codegen" "^2.0.4" + "@protobufjs/eventemitter" "^1.1.0" + "@protobufjs/fetch" "^1.1.0" + "@protobufjs/float" "^1.0.2" + "@protobufjs/inquire" "^1.1.0" + "@protobufjs/path" "^1.1.2" + "@protobufjs/pool" "^1.1.0" + "@protobufjs/utf8" "^1.1.0" + "@types/long" "^4.0.1" + "@types/node" ">=13.7.0" + long "^4.0.0" + proxy-compare@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/proxy-compare/-/proxy-compare-3.0.1.tgz#3262cff3a25a6dedeaa299f6cf2369d6f7588a94" @@ -9815,6 +10510,11 @@ proxy-from-env@^1.1.0: resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== +proxy-from-env@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-2.1.0.tgz#a7487568adad577cfaaa7e88c49cab3ab3081aba" + integrity sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA== + pump@^3.0.0: version "3.0.3" resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.3.tgz#151d979f1a29668dc0025ec589a455b53282268d" @@ -9867,6 +10567,23 @@ quick-format-unescaped@^4.0.3: resolved "https://registry.yarnpkg.com/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz#93ef6dd8d3453cbc7970dd614fad4c5954d6b5a7" integrity sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg== +quick-lru@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-4.0.1.tgz#5b8878f113a58217848c6482026c73e1ba57727f" + integrity sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g== + +rabin-wasm@^0.1.4: + version "0.1.5" + resolved "https://registry.yarnpkg.com/rabin-wasm/-/rabin-wasm-0.1.5.tgz#5b625ca007d6a2cbc1456c78ae71d550addbc9c9" + integrity sha512-uWgQTo7pim1Rnj5TuWcCewRDTf0PEFTSlaUjWP4eY9EbLV9em08v89oCz/WO+wRxpYuO36XEHp4wgYQnAgOHzA== + dependencies: + "@assemblyscript/loader" "^0.9.4" + bl "^5.0.0" + debug "^4.3.1" + minimist "^1.2.5" + node-fetch "^2.6.1" + readable-stream "^3.6.0" + radix3@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/radix3/-/radix3-1.1.2.tgz#fd27d2af3896c6bf4bcdfab6427c69c2afc69ec0" @@ -10057,6 +10774,14 @@ real-require@^0.2.0: resolved "https://registry.yarnpkg.com/real-require/-/real-require-0.2.0.tgz#209632dea1810be2ae063a6ac084fee7e33fba78" integrity sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg== +redent@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/redent/-/redent-3.0.0.tgz#e557b7998316bb53c9f1f56fa626352c6963059f" + integrity sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg== + dependencies: + indent-string "^4.0.0" + strip-indent "^3.0.0" + reef-knot@8.0.0-alpha.1: version "8.0.0-alpha.1" resolved "https://registry.yarnpkg.com/reef-knot/-/reef-knot-8.0.0-alpha.1.tgz#59a5b07cbe8decac90253ce59f6e249a6013b032" @@ -10336,6 +11061,11 @@ safe-stable-stringify@^2.3.1: resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== +sax@^1.5.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/sax/-/sax-1.6.0.tgz#da59637629307b97e7c4cb28e080a7bc38560d5b" + integrity sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA== + scheduler@^0.23.0: version "0.23.0" resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.0.tgz#ba8041afc3d30eb206a487b6b384002e4e61fdfe" @@ -10382,6 +11112,11 @@ semver@^6.3.0, semver@^6.3.1: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== +semver@^7.3.4: + version "7.7.4" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.4.tgz#28464e36060e991fa7a11d0279d2d3f3b57a7e8a" + integrity sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA== + semver@^7.3.5, semver@^7.3.7, semver@^7.5.3, semver@^7.5.4: version "7.5.4" resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" @@ -10534,9 +11269,9 @@ socket.io-client@^4.5.1: socket.io-parser "~4.2.4" socket.io-parser@~4.2.4: - version "4.2.5" - resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-4.2.5.tgz#3f41b8d369129a93268f2abecba94b5292850099" - integrity sha512-bPMmpy/5WWKHea5Y/jYAP6k74A+hvmRCQaJuJB6I/ML5JZq/KfNieUVo/3Mh7SAqn7TyFdIo6wqYHInG1MU1bQ== + version "4.2.6" + resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-4.2.6.tgz#19156bf179af3931abd05260cfb1491822578a6f" + integrity sha512-asJqbVBDsBCJx0pTqw3WfesSY0iRX+2xzWEWzrpcH7L6fLzrhyF8WPI8UaeM4YCuDfpwA/cgsdugMsmtz8EJeg== dependencies: "@socket.io/component-emitter" "~3.1.0" debug "~4.4.1" @@ -10549,9 +11284,9 @@ sonic-boom@^3.7.0: atomic-sleep "^1.0.0" sonic-boom@^4.0.1: - version "4.2.0" - resolved "https://registry.yarnpkg.com/sonic-boom/-/sonic-boom-4.2.0.tgz#e59a525f831210fa4ef1896428338641ac1c124d" - integrity sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww== + version "4.2.1" + resolved "https://registry.yarnpkg.com/sonic-boom/-/sonic-boom-4.2.1.tgz#28598250df4899c0ac572d7e2f0460690ba6a030" + integrity sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q== dependencies: atomic-sleep "^1.0.0" @@ -10573,6 +11308,11 @@ source-map@^0.6.0, source-map@^0.6.1: resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== +sparse-array@^1.3.1: + version "1.3.2" + resolved "https://registry.yarnpkg.com/sparse-array/-/sparse-array-1.3.2.tgz#0e1a8b71706d356bc916fe754ff496d450ec20b0" + integrity sha512-ZT711fePGn3+kQyLuv1fpd3rNSkNF8vd5Kv2D+qnOANeyKs3fx6bUMGWRPvgTTcYV64QMqZKZwcuaQSP3AZ0tg== + spdx-correct@^3.0.0: version "3.2.0" resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.2.0.tgz#4f5ab0668f0059e34f9c00dce331784a12de4e9c" @@ -10609,6 +11349,11 @@ sprintf-js@~1.0.2: resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g== +stable@^0.1.8: + version "0.1.8" + resolved "https://registry.yarnpkg.com/stable/-/stable-0.1.8.tgz#836eb3c8382fe2936feaf544631017ce7d47a3cf" + integrity sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w== + stack-trace@0.0.x: version "0.0.10" resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-0.0.10.tgz#547c70b347e8d32b4e108ea1a2a159e5fdde19c0" @@ -10879,17 +11624,17 @@ svg-parser@^2.0.4: integrity sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ== svgo@^3.0.2: - version "3.2.0" - resolved "https://registry.yarnpkg.com/svgo/-/svgo-3.2.0.tgz#7a5dff2938d8c6096e00295c2390e8e652fa805d" - integrity sha512-4PP6CMW/V7l/GmKRKzsLR8xxjdHTV4IMvhTnpuHwwBazSIlw5W/5SmPjN8Dwyt7lKbSJrRDgp4t9ph0HgChFBQ== + version "3.3.3" + resolved "https://registry.yarnpkg.com/svgo/-/svgo-3.3.3.tgz#8246aee0b08791fde3b0ed22b5661b471fadf58e" + integrity sha512-+wn7I4p7YgJhHs38k2TNjy1vCfPIfLIJWR5MnCStsN8WuuTcBnRKcMHQLMM2ijxGZmDoZwNv8ipl5aTTen62ng== dependencies: - "@trysound/sax" "0.2.0" commander "^7.2.0" css-select "^5.1.0" css-tree "^2.3.1" css-what "^6.1.0" csso "^5.0.5" picocolors "^1.0.0" + sax "^1.5.0" tailwindcss@^3.3.1: version "3.4.10" @@ -11056,6 +11801,11 @@ tr46@~0.0.3: resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== +trim-newlines@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-3.0.1.tgz#260a5d962d8b752425b32f3a7db0dcacd176c144" + integrity sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw== + triple-beam@^1.3.0: version "1.4.1" resolved "https://registry.yarnpkg.com/triple-beam/-/triple-beam-1.4.1.tgz#6fde70271dc6e5d73ca0c3b24e2d92afb7441984" @@ -11134,6 +11884,11 @@ type-detect@4.0.8: resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== +type-fest@^0.18.0: + version "0.18.1" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.18.1.tgz#db4bc151a4a2cf4eebf9add5db75508db6cc841f" + integrity sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw== + type-fest@^0.20.2: version "0.20.2" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" @@ -11239,6 +11994,13 @@ uint8arrays@3.1.1, uint8arrays@^3.0.0: dependencies: multiformats "^9.4.2" +uint8arrays@^2.0.5, uint8arrays@^2.1.2: + version "2.1.10" + resolved "https://registry.yarnpkg.com/uint8arrays/-/uint8arrays-2.1.10.tgz#34d023c843a327c676e48576295ca373c56e286a" + integrity sha512-Q9/hhJa2836nQfEJSZTmr+pg9+cDJS9XEAp7N2Vg5MzL3bK/mkMVfjscRGYruP9jNda6MAdf4QD/y78gSzkp6A== + dependencies: + multiformats "^9.4.2" + unbox-primitive@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.2.tgz#29032021057d5e6cdbd08c5129c226dff8ed6f9e" @@ -11264,6 +12026,11 @@ undici-types@~5.26.4: resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== +undici-types@~7.19.0: + version "7.19.2" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-7.19.2.tgz#1b67fc26d0f157a0cba3a58a5b5c1e2276b8ba2a" + integrity sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg== + unicode-canonical-property-names-ecmascript@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz#301acdc525631670d39f6146e0e77ff6bbdebddc" @@ -11412,12 +12179,22 @@ valtio@2.1.7: dependencies: proxy-compare "^3.0.1" +varint@^5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/varint/-/varint-5.0.2.tgz#5b47f8a947eb668b848e034dcfa87d0ff8a7f7a4" + integrity sha512-lKxKYG6H03yCZUpAGOPOsMcGxd1RHCu1iKvEHYDPmTyq2HueGhD73ssNBqqQWfvYs04G9iUFRvmAVLW20Jw6ow== + +varint@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/varint/-/varint-6.0.0.tgz#9881eb0ce8feaea6512439d19ddf84bf551661d0" + integrity sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg== + vary@^1: version "1.1.2" resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== -viem@2.45.0, viem@>=2.37.9, viem@^2.1.1, viem@^2.21.26, viem@^2.27.2, viem@^2.31.7: +viem@2.45.0, viem@^2.1.1, viem@^2.21.26, viem@^2.27.2, viem@^2.31.7: version "2.45.0" resolved "https://registry.yarnpkg.com/viem/-/viem-2.45.0.tgz#5f7f28b639193b783b7fbb6bef7575e9cc8d6f86" integrity sha512-iVA9qrAgRdtpWa80lCZ6Jri6XzmLOwwA1wagX2HnKejKeliFLpON0KOdyfqvcy+gUpBVP59LBxP2aKiL3aj8fg== @@ -11431,6 +12208,20 @@ viem@2.45.0, viem@>=2.37.9, viem@^2.1.1, viem@^2.21.26, viem@^2.27.2, viem@^2.31 ox "0.11.3" ws "8.18.3" +viem@>=2.37.9: + version "2.47.6" + resolved "https://registry.yarnpkg.com/viem/-/viem-2.47.6.tgz#0249c166d5e0555074ffd3d320f2be2057551796" + integrity sha512-zExmbI99NGvMdYa7fmqSTLgkwh48dmhgEqFrUgkpL4kfG4XkVefZ8dZqIKVUhZo6Uhf0FrrEXOsHm9LUyIvI2Q== + dependencies: + "@noble/curves" "1.9.1" + "@noble/hashes" "1.8.0" + "@scure/bip32" "1.7.0" + "@scure/bip39" "1.6.0" + abitype "1.2.3" + isows "1.0.7" + ox "0.14.7" + ws "8.18.3" + wagmi@3.4.1: version "3.4.1" resolved "https://registry.yarnpkg.com/wagmi/-/wagmi-3.4.1.tgz#c2f9771802d15c4379e25b67e0696231ab6fa7ae" @@ -11738,6 +12529,11 @@ yargs-parser@^18.1.2: camelcase "^5.0.0" decamelize "^1.2.0" +yargs-parser@^20.2.3: + version "20.2.9" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" + integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== + yargs-parser@^21.0.1, yargs-parser@^21.1.1: version "21.1.1" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35"